MoneySwitch MCP Server
README.md
# MoneySwitch
[](#license)
[](https://github.com/dongsheng123132/moneyswitch/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/moneyswitch)
**Give your AI an API key for money.**
Website: [moneyswitch.dev](https://moneyswitch.dev) · [中文说明 →](README.zh-CN.md)
An AI agent holds a `mk_live_xxx` **MoneyKey** — not a wallet private key —
and spends USDC through [x402](https://x402.org)-priced HTTP APIs on the
[Monad](https://monad.xyz) testnet. MoneySwitch checks policy (budgets,
per-request limits, an approval threshold, a host allowlist, an SSRF guard)
*before* every payment, then signs the USDC authorization itself with a
locally-held, low-balance wallet. **The agent never sees the private key.**
<!-- screenshot: dashboard-overview -->
## Why
The industry already taught AI agents one pattern: hold a string that starts
with a prefix (`sk-…`, `mk_live_…`), put it in an `Authorization` header,
and the rest is handled for you. MoneySwitch reuses exactly that pattern for
money instead of inventing a new one — no wallet extension, no seed phrase,
no signing UI for the agent to click through. The agent calls an HTTP API
with a bearer token; MoneySwitch is the thing standing between that token
and an on-chain USDC transfer, enforcing the same kind of limits an
API-key-issuing platform already enforces on rate and spend.
## 30 seconds
**Try it — fully offline, no real money** (Node.js 22+):
```bash
npx moneyswitch demo
```
A throwaway MoneySwitch starts on free local ports with a mock wallet, a demo
LLM channel, two MoneyKeys ("Claude Code", "Codex"), a toll booth in front of
a demo API and a few payments, and the Dashboard opens already signed in.
Send a message in the Playground ($0.01), try to buy a $5 report and watch
the per-request limit block it, then look at the toll booth's income. Every
screen says **DEMO · simulated settlement**; settlement goes through a local
mock facilitator and never touches a chain. Ctrl+C stops it and deletes the
data. (`moneyswitch demo` downloads the separate, AGPL-3.0-only
`moneyswitch-server` package; if your npm mirror has not synced it yet, add
`--registry=https://registry.npmjs.org/`.)
**Self-host the server + Dashboard** in one command:
```bash
npx moneyswitch-server # http://127.0.0.1:4020, data in ~/.moneyswitch/server
```
On first start it prints a one-time setup link (`http://127.0.0.1:4020/setup#ms_setup_…`)
that signs you in and walks you through wallet → channel → first key →
connecting an agent. `--data-dir`, `--port` and `--host` change the defaults.
**Connect your local Claude Code / Codex** to a MoneySwitch server with a MoneyKey:
```bash
npx moneyswitch connect --server http://127.0.0.1:4020 --key mk_live_xxx --apply
```
This detects Claude Code / Codex on your machine and wires up the
MoneySwitch MCP server (without `--apply` it only prints the planned
changes). `npx moneyswitch status --server … --key …` checks a MoneyKey's
remaining budget; `npx moneyswitch remove --apply` undoes it;
`npx moneyswitch ui` opens the local desktop console. The Dashboard's
*Connect agent* page and the "send to employee" message generate these
commands with the right address.
**Charge AI for your own API** (no server needed):
```bash
npx moneyswitch sell --upstream http://localhost:8000 --price 0.01 --pay-to 0xYourPublicAddress
```
From a checkout (contributors):
```bash
pnpm install
pnpm build
pnpm demo:local
```
`pnpm demo:local` starts a mock x402 facilitator, a demo x402 seller, and
the MoneySwitch server (with the Dashboard UI) from the repository, entirely
offline — no real payment happens. See [`docs/quickstart.md`](docs/quickstart.md)
for the real Monad-testnet path (`pnpm demo:testnet`) and the manual
step-by-step version.
## Architecture
```text
mk_live_xxx (MoneyKey, not a private key)
│
┌──────────────┐ MCP / OpenAI-compatible / REST ┌────────────────────┐
│ AI Agent │ ───────────────────────────────► │ MoneySwitch server │
│ (Claude Code, │ │ (apps/server) │
│ Codex, ...) │ ◄─────────────────────────────── │ policy → wallet │
└──────────────┘ result / usage / error │ (apps/dashboard) │
└─────────┬──────────┘
│ x402 (EIP-3009 USDC auth)
▼
┌────────────────────┐
│ x402 facilitator │
│ (settles on-chain) │
└─────────┬──────────┘
▼
┌────────────────────┐
│ Monad testnet │
│ USDC Transfer │
└─────────┬──────────┘
▼
┌────────────────────┐
│ x402 seller / │
│ priced API │
└────────────────────┘
```
The server holds the private key in an encrypted, low-balance local wallet
(`packages/wallet`); the agent process never has it. Every payment is
checked against policy (`packages/core`) inside a database transaction
*before* a signature is produced.
## Three ways to plug an agent in
1. **MCP** (`apps/mcp`, stdio) — `money_status`, `paid_fetch`,
`money_history` tools. `npx moneyswitch mcp`, or `moneyswitch connect
--apply` wires it into Claude Code / Codex automatically. See
[`docs/claude.md`](docs/claude.md), [`docs/codex.md`](docs/codex.md).
2. **OpenAI / NewAPI-compatible gateway** — set `Base URL =
http://<server>/v1`, `API Key = mk_live_xxx` in any OpenAI-SDK-compatible
client (openai SDK, Cherry Studio, Open WebUI, NewAPI upstream channel).
`GET /v1/models`, `POST /v1/chat/completions`, and the legacy OpenAI
billing endpoints are implemented — see [`SPEC-v0.2.md`](SPEC-v0.2.md)
and [`docs/money-api-v0.md`](docs/money-api-v0.md).
3. **REST** — `POST /v1/fetch { url, method?, headers?, body?, max_price? }`
fetches any x402-priced URL through the policy engine directly. See
[`docs/money-api-v0.md`](docs/money-api-v0.md).
## Get paid: toll booths (v0.5)
MoneySwitch can also **receive** money. Put a *toll booth* in front of an
API you already run: AI agents pay USDC per call over x402, the money goes
straight to your receiving address, and the Dashboard shows it like revenue.
**You need no secret to sell — only a public receiving address — and your
service does not change a single line.**
### The three things (read this first)
| Thing | Think of it as | Give it to | In MoneySwitch |
|---|---|---|---|
| Private key | the key to the safe | **nobody** | never shown anywhere; it only lives (decrypted) in the server's memory |
| MoneyKey `mk_live_…` | a capped company card for an employee | **only your own AI** | 🔒 amber, "Secret: whoever holds it can spend within its limits — never send it to a seller" |
| Receiving address `0x…` | your payment QR code | **anyone** | ✅ green, "Public: people pay you with it, safe to share" |
Paste a MoneyKey, admin token, private key or recovery phrase into any
receiving-address field and it is blocked and explained (the server refuses
it too with `INVALID_PAY_TO`); paste a `0x…` address into a key field and it
is blocked the same way. The receiving address defaults to this
MoneySwitch's own wallet (one wallet receives and pays); you can point it
at any other address you control, e.g. a cold wallet — MoneySwitch then
cannot spend that money for you.
### In the Dashboard
*Toll booths → New toll booth*: ① which service (`http://127.0.0.1:8000`,
with a free "test connection") ② how to charge (templates such as "the whole
service, $0.01 per call" or "`/v1/chat/completions` $0.01, everything else
free"; rules are `METHOD /path` or `/prefix/*`, the most specific wins; paths
that match no rule are charged a default price, passed through free, or
refused) ③ where the money goes. You get a public address
`https://<server>/t/<slug>/…` to hand to buyers, plus ready-made buyer
snippets. *Earnings* shows today / 7 days / all, per toll booth and per
rule, every payment (payer, route, amount, tx, upstream status) and a CSV
export.
```bash
# anyone: 402 + price + pay_to
curl -i http://127.0.0.1:4020/t/weather/v1/today
# a buyer, with THEIR OWN MoneyKey
curl -s http://127.0.0.1:4020/v1/fetch \
-H "Authorization: Bearer mk_live_xxx" -H "Content-Type: application/json" \
-d '{"url":"http://127.0.0.1:4020/t/weather/v1/today"}'
```
An OpenAI-compatible upstream behind a toll booth can be added as a
*channel* in another MoneySwitch (`Base URL = https://<server>/t/<slug>/v1`).
**Buyers are only charged when your service answers 2xx/3xx.** The payment
is verified first, the request is forwarded, and it is settled only on
success; on 4xx/5xx/timeouts it is cancelled and recorded as "not charged".
Your upstream receives `X-MoneySwitch-Payer`, `X-MoneySwitch-Amount` and
`X-MoneySwitch-Tollbooth`, never the buyer's `Authorization`/`Cookie` or
payment headers. Only `/t/*` has to be reachable by buyers — keep the admin
API and Dashboard private (see [`docs/security.md`](docs/security.md)); set
`MONEYSWITCH_PUBLIC_URL` to the address buyers use. API details:
[`docs/money-api-v0.md`](docs/money-api-v0.md#toll-booths-v05-spec-v05-2).
### Without a server: `moneyswitch sell`
```bash
npx moneyswitch sell \
--upstream http://localhost:8000 --price 0.01 --pay-to 0xYourPublicAddress \
--route "POST /v1/chat/completions=0.02" --route "GET /health=0"
```
A single process on your machine (official `@x402/express`), with the same
rule matching and forwarding code as the server's toll booths. It prints the
public address and your (public) receiving address, and refuses a MoneyKey
or private key as `--pay-to`.
## Roles
| Role | Holds | Uses | Can do |
|---|---|---|---|
| Admin (owner / finance) | `ms_admin_…` | Dashboard | wallet, channels, issue/revoke MoneyKeys, approve payments, see all usage |
| Employee | one or more `mk_live_…` | Dashboard "My Budget" view + desktop CLI | see own budget/history, Playground, one-command connect to their own local agent; cannot see others, cannot touch the wallet |
| Agent (Claude Code / Codex / Cherry Studio / …) | `mk_live_…` in env or MCP config | MCP or OpenAI-compatible interface | spend money, bounded by policy |
| Seller | only a **public** receiving address `0x…` (no secret) | a MoneySwitch toll booth, `moneyswitch sell`, or any x402 server | receive USDC; with a toll booth, see income under *Earnings* |
## Security model & guardrails
- **The agent never holds a private key.** Only a `mk_live_…` MoneyKey,
scoped by budgets/allowlist/approval — the same shape as any other API
key an agent already knows how to use.
- **Policy runs before signing**, inside one database transaction: per-request
limit → `max_price` cap → daily/total budget → approval threshold. Nothing
is signed until every check passes.
- **Host allowlist governs every outbound request**, including free ones —
`POST /v1/fetch` is fundamentally an outbound proxy. An empty allowlist
denies everything.
- **SSRF guard**: MoneySwitch always refuses to let an agent point it back
at its own listening address, in any common literal form
(`127.0.0.1`/`localhost`/`0.0.0.0`/`::1`). This is literal/hostname
matching, not a DNS-rebinding defense — see
[SECURITY.md](SECURITY.md) for the exact limitation.
- **Secrets never round-trip.** The full MoneyKey and admin token are shown
exactly once, at creation; only their SHA-256 hash is stored. The wallet
private key exists decrypted only in-process memory while unlocked, never
on disk, never logged, never returned by any API response.
- **`unknown` payment outcomes count as spent**, conservatively, rather than
risk silently exceeding a budget when an upstream result can't be
determined (timeout, dropped connection). Audit them manually via
`GET /v1/admin/usage`.
Full threat model: [`docs/security.md`](docs/security.md) /
[`SECURITY.md`](SECURITY.md).
## Monad testnet
MoneySwitch's x402 client and mock-facilitator test suite are built against
these testnet facts, checked against a live RPC as part of the repo's own
`pnpm test:testnet` (see [`SPEC.md`](SPEC.md) §1 for the full, dated table):
| | Value |
|---|---|
| CAIP-2 network | `eip155:10143` |
| RPC | `https://testnet-rpc.monad.xyz` |
| USDC (testnet) | `0x534b2f3A21130d7a60830c2Df862319e593943A3` (6 decimals) |
| Facilitator | `https://x402-facilitator.molandak.org` |
| Payment scheme | `exact` (EIP-3009 `transferWithAuthorization`) |
A real settlement (no mock) is a `paid_fetch`/`/v1/fetch` call that produces
a `tx_hash`, independently checkable by fetching that transaction's receipt
and finding a matching USDC `Transfer(from=wallet, to=seller, value=…)` log
— see `tools/m6/verify-transfer.mjs`. MoneySwitch never auto-funds a wallet
or auto-requests testnet tokens; that's always a manual, out-of-band step
(see [`docs/quickstart.md`](docs/quickstart.md)).
As of 2026-09-26, 11 real (non-mock) x402 payments have been settled on the
Monad testnet this way — paid fetches, OpenAI-SDK chat completions and an
approval-gated payment — all visible on-chain; 4 of them (first payment,
Claude Code via MCP, OpenAI SDK chat, approval-gated) were checked
Transfer-by-Transfer with [`tools/m6/verify-transfer.mjs`](tools/m6/verify-transfer.mjs). All were gas-free for the agent's wallet (the facilitator relays and pays
gas for `exact`/EIP-3009 settlement), first tx `0x1c83a45d…4d4d` (block
65595248).
## Roadmap
- **v0.1** (done): MoneyKey, policy engine, local wallet, x402 client,
MCP, Dashboard, offline (T1/T2) and testnet-read (T3) test suites.
- **v0.2** (done): OpenAI/NewAPI-compatible gateway (`/v1/chat/completions`,
`/v1/models`, billing endpoints), channels, Playground.
- **v0.3** (done): employee-facing Dashboard view, one-command desktop
connect (`moneyswitch-connect` → the `moneyswitch` npm package).
- **v0.4** (done): child MoneyKeys (multi-level delegation) and the local desktop console (`moneyswitch ui`).
- **v0.5** (done): toll booths — sell any API to AI for USDC (`/t/<slug>`, Earnings, `moneyswitch sell`);
v0.5.1: `npx moneyswitch demo` (offline tour) and `npx moneyswitch-server` (one-command self-host).
- **Next**: per-token pricing (x402 `upto`), MetaMask / OKX wallet drivers,
multi-user organizations with department budgets and approval flows.
- Not planned (see [`SPEC.md`](SPEC.md) §12 for the full list and why):
wallet-extension import, mainnet-by-default, fiat on-ramp.
## License
MoneySwitch is dual-licensed by component: the parts an agent or seller
*embeds in their own process* are permissive; the self-hosted server stays
copyleft so that hosted forks give improvements back.
| Component | License |
|---|---|
| `apps/mcp`, `apps/connect`, `apps/cli` (the `moneyswitch` npm package) — client-side code | [Apache-2.0](apps/mcp/LICENSE) |
| `apps/server-pkg` (the `moneyswitch-server` npm package: server + Dashboard bundled for `npx`) | [AGPL-3.0-only](apps/server-pkg/LICENSE) |
| `apps/demo-seller` (x402 seller example) | [Apache-2.0](apps/demo-seller/LICENSE) |
| `packages/tollbooth` (toll booth rules, forwarding, pay-to checks — shared by the server and `moneyswitch sell`) | [Apache-2.0](packages/tollbooth/LICENSE) |
| Everything else (`apps/server`, `apps/dashboard`, the other `packages/*`) | [AGPL-3.0-only](LICENSE) |
The two npm packages are published separately: `moneyswitch` (Apache-2.0)
never contains server code; `moneyswitch demo` only *runs*
`moneyswitch-server` (AGPL-3.0-only) through `npx` as a separate process.
To embed the server in a closed-source product, open an issue to discuss a
commercial license.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). Contributions require agreeing to
[CLA.md](CLA.md) (so the project can keep offering the dual-license terms
above). Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) first.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues