oshi-mcp 0.1¶
Oshi API Rewards lets an app pay its users in real bitcoin. oshi-mcp is an MCP server for it: plug it into Claude Code, Cursor, Codex, or any other MCP client, and an AI agent can look up your campaigns, rewards, and users, and grant or void rewards once you allow it to.
I shipped v0.1.0 on October 18 and kept going the same day, through v0.1.4. v0.1.5 followed on October 24 to pick up two TLS library fixes. It's a single static binary with no runtime dependencies, for macOS, Linux, and Windows.
It's an independent project, not affiliated with or endorsed by Oshi.
The Problem¶
An MCP server is an agent with a wallet the moment it exposes a write tool. Most servers expose everything the API can do and leave it to the client to ask before acting. For an API that moves money, I wanted the default to be the opposite.
Read-Only Until You Say Otherwise¶
Five tools are on out of the box: list and get campaigns, get or look up a reward, and get a user. Four are off:
grant_rewardspends sats.void_rewardis irreversible.update_campaignchanges payouts and budgets.mint_portal_linkcreates a link that signs someone into a user's wallet.
To turn any of them on, you list it by name in OSHI_MCP_ENABLE_TOOLS. There's no all shortcut, and an unknown name stops the server from starting, so a typo can't quietly change what's exposed. Tools you haven't enabled aren't in the tool list at all, and calls to them are refused with a note telling the agent to ask you rather than work around it.
A Human Approves Every Write¶
Turning a write tool on doesn't hand the agent a blank check. As of 0.1.2, each call to an enabled write tool pauses for your approval in your MCP client's own UI, using MCP elicitation:
Approve paying 100 sats from campaign "App Referral" to user "user_123" for action "action_456"?
The server writes that prompt from validated arguments and Oshi's own data (the campaign amount, the event being voided, before and after values for a campaign edit), not from anything the agent wrote. Values like campaign names are quoted and truncated so a crafted name can't pass itself off as part of the prompt. It fails closed: declining, cancelling, an unticked box, a five-minute timeout, or a client that can't show the prompt all mean no, and nothing reaches Oshi. For unattended use you can turn approval off with OSHI_MCP_CONFIRM=off, but you have to say so on purpose.
A Record of Every Write¶
As of 0.1.4, every call to a write tool leaves one JSON line on stderr, whatever happened: the tool, identifiers, sats, whether you approved it, and the outcome (ok, invalid_input, not_approved, failed, or outcome_unknown). The API key, metadata, and portal links never go in it. The README has a small wrapper script that saves those lines to a file for any client, plus jq recipes for reviewing every write, totaling sats granted, and finding calls to reconcile.
Portal Links Are Credentials¶
A portal link signs whoever opens it into a user's wallet, so it should never end up in an agent's context by accident. In 0.1.0 the server redacted the link from grant responses. In 0.1.1 that became one choke point: every tool result and every API error body is walked at any depth, and portal-link fields, plus anything in free text shaped like a portal link, are redacted unless mint_portal_link is enabled. Regression tests call every listed tool against success and error responses stuffed with portal links and check that none escapes.
The Boring Parts That Matter¶
- Ids are percent-encoded per path segment, so a crafted user id like
x/../../events/<id>/voidcan't reach a different endpoint. Inputs are checked locally, and unknown arguments are rejected instead of dropped. - The API key is read from the environment, sent only as a bearer header to Oshi, never logged, and redacted from debug output. Redirects aren't followed, and the API base URL is compiled in.
- Each tool carries MCP read-only, destructive, idempotent, and open-world hints, so clients can prompt before writes.
- Failures say what actually happened. A refused connection means nothing was sent; a timeout, a 5xx, or an unreadable reply means the request may or may not have taken effect, and a failed grant tells the agent to check
lookup_rewardor retry with the sameexternal_id, never a new one, so a flaky network can't pay someone twice. Tests replay error responses recorded from the live API, including ones the spec doesn't list. - The server tells the agent to treat campaign names and metadata as data, not instructions.
- Release binaries come with SHA256SUMS and build-provenance attestations, and the install script checks the checksum. A release only builds if the tag matches the version, sits on
main, the test suite passes on Linux, macOS, and Windows, andcargo auditfinds no advisories.cargo auditalso runs weekly. 0.1.5 patches two RustSec advisories in the TLS stack that carries the API key to Oshi (RUSTSEC-2026-0382 inrustls-webpkiand RUSTSEC-2026-0381 inwebpki-roots, both about trusting distrusted certificate intermediates).
The repo also ships an Agent Skill covering the Oshi API's rules and flows, for agents writing integration code.
What I Learned¶
Least privilege for an agent isn't only about which tools exist. It's also about what comes back. The first version hid the dangerous tools but could still hand a wallet sign-in link to the agent inside an error message. Treating every response as something to scrub, in one place, was the fix that made the rest of the model hold up.
What's Next¶
The Shopify purchase-verification endpoints aren't covered yet, so grants on campaigns that require a real Shopify purchase may fail until they are. A built-in audit log file option is next, so you won't need the wrapper script. If you're building on Oshi and want to try it, start read-only and turn on write tools one at a time.
Links¶
An agent near money should start with its hands in its pockets.