Automate spending#
Everything an agent or a script needs to pay x402 endpoints from a wallet without a person in the loop, over plain HTTP or over MCP. The two surfaces are the same rail: same wallets, same ledger, same refusals.
The shape of it#
- A person opens a wallet in the dashboard, ticks the not-storage line and funds it. The wallet's key is on its card. Acknowledging, funding a first dollar, setting a burn address and revealing a key are a person's and stay that way.
- From then on the key does the spending: read the budget, pay a URL, read the events. An account key can open a fresh wallet per job and burn it when the job is done; a branch key can open wallets in its branch. No key can reveal anything or move money anywhere but to another of your wallets or the burn address a person set.
- When a call needs one of the person's steps, the answer says so:
needs_personwith the step, why, and the dashboard link.
The base URL is https://api.mechawallet.com. Send the key as
Authorization: Bearer mw_wallet_… (or a branch or account key). A
mw_test_… key can rehearse and read but never spend.
Name a wallet in a path by its id, its label, its address, or its chain when you have one wallet on that chain. A wallet key needs no name at all: it can only mean its own wallet.
Over HTTP#
Read the budget first#
curl -s https://api.mechawallet.com/v1/spending-wallets/research/budget \
-H "Authorization: Bearer $MW_KEY"
{"wallet": {"id": "…", "label": "research", "chain": "base", "awake": true},
"budget": "the wallet's balance; pass max_usd on a call to cap that call only",
"fee": {"free_per_month": 1000, "free_left_this_month": 997, "floor_usd": "0.002"},
"costs": {"…": "the price list, the same as GET /v1/costs"}}
and the balance when the price matters:
curl -s https://api.mechawallet.com/v1/spending-wallets/research/balance \
-H "Authorization: Bearer $MW_KEY"
available_usd is what can be spent: the chain balance minus wallet fees
owed.
Pay a URL#
curl -s -X POST https://api.mechawallet.com/v1/spending-wallets/research/pay \
-H "Authorization: Bearer $MW_KEY" \
-H "content-type: application/json" \
-d '{"url": "https://weather.example.net/x402/now?city=Lisbon", "max_usd": "0.05"}'
{"paid": true, "url": "…",
"goods": {"temp_c": 24.1, "…": "whatever the endpoint returned"},
"receipt": {"tx_hash": "0x…", "amount_usd": "0.02"},
"fee": {"fee_usd": "0.000", "why": "inside your free payments this month"}}
urlis any x402 endpoint, ours or anyone's. The wallet reads the 402, checks the price against the balance, signs, retries, and returns the body asgoods.max_usdis optional and caps this one call. The balance is the ceiling either way.params(an object) is appended to the URL as a query string.methodandbody: a POST-only endpoint (the directory row saysmethod: POST) is paid with"method": "POST"and its JSONbody.- To pay through a branch's rule instead of a named wallet:
POST /v1/spending-branches/{branch}/paywith the same body.
A refusal is 402 (or 403 for a frozen account) with the reason in one
sentence, and the refusal is recorded on the wallet:
{"detail": "insufficient balance: this wallet holds $0.10 spendable. Send USDC to 0x… on base"}
{"detail": "insufficient balance: this wallet holds $0.60 and sleeps until it has held $1.00 once. Send USDC to 0x… on base to wake it; rehearsals need no funds."}
Handle a refusal as "ask the person to fund", never as "retry". 502 is
the endpoint failing after payment was attempted; the reason names it.
Rehearse for free#
Every checkout has a /test face that settles with a synthetic receipt and
no funds, and pay accepts it from an empty, sleeping wallet:
curl -s https://api.mechawallet.com/v1/rehearsal-checkout \
-H "Authorization: Bearer $MW_KEY"
returns a URL to rehearse against. Wire the whole loop on it before any money is in the wallet.
Find something to buy#
curl -s "https://api.mechawallet.com/v1/directory?q=weather&chain=base&max_price_usd=0.05"
No key needed. Every row carries url, pay (the call that buys it) and
reviews from accounts that paid it; third-party prices are re-read from
the endpoint's own 402 on a timer. After paying a row, leave a review with
POST /v1/directory/{entry_id}/reviews.
Read what happened#
curl -s https://api.mechawallet.com/v1/spending-wallets/research/events \
-H "Authorization: Bearer $MW_KEY"
Newest first: spent (amount, URL, tx), refused (reason), rehearsed,
funded, transferred, burned, with who did it. /events.csv exports
it and /stats totals it.
Wallets per job#
GET /v1/spending-walletslists the account's wallets by branch.POST /v1/spending-walletsopens one ({"chain", "label", "branch?"}), with an account key or a branch key for its own branch, once the person has acknowledged.GET /v1/spending-wallets/{wallet}/funding?amount=5.00is the wallet's Send money to link;POST /{wallet}/transfermoves money to another of your wallets.POST /v1/spending-wallets/{wallet}/burnreturns what is left to the burn address and closes the wallet; it refuses without one, naming the step.
The whole loop in Python#
import os, requests
API = "https://api.mechawallet.com"
H = {"Authorization": f"Bearer {os.environ['MW_KEY']}"}
W = "research"
balance = requests.get(f"{API}/v1/spending-wallets/{W}/balance", headers=H).json()
if float(balance["available_usd"]) < 0.05:
raise SystemExit("ask the person to fund the wallet")
rows = requests.get(f"{API}/v1/directory", params={"q": "weather", "chain": "base",
"max_price_usd": "0.05"}).json()["rows"]
url = rows[0]["url"]
r = requests.post(f"{API}/v1/spending-wallets/{W}/pay", headers=H,
json={"url": url, "max_usd": "0.05"})
if r.status_code in (402, 403):
raise SystemExit(f"needs a person: {r.json()['detail']}")
r.raise_for_status()
out = r.json()
print(out["goods"], out["receipt"]["tx_hash"])
The mw CLI (clients/mw.py) wraps the account side; run mw --help for
what it offers today (mw pay, mw events).
Over MCP#
Connect with the key (claude mcp add --transport http mechawallet
https://api.mechawallet.com/mcp --header "Authorization: Bearer mw_wallet_…")
or paste the MCP configuration from the dashboard into any client. The
tools, in the order an agent uses them:
| Tool | Does | Needs |
|---|---|---|
search_endpoints |
What can I buy: live endpoints with verified prices and reviews, {query?, chain?, max_price_usd?, category?} |
nothing |
spending_budget |
Which wallet this key pays from, ready or still loading its first dollar, the fee rule, free payments left | key |
pay_x402 |
Pay any x402 URL, {url, max_usd?, wallet?, branch?, params?, method?, body?} |
wallet, branch or live key |
spending_wallet_balance |
The chain balance and what is spendable | key |
spending_wallet_events, spending_stats |
The receipts list; totals per wallet and branch | key |
list_spending_wallets, spending_branches |
The wallets by branch, with addresses | key |
spending_funding_link, spending_transfer |
The Send money to link; move money between your wallets | key |
spending_open_wallet, spending_open_branch |
A fresh wallet or branch for a job | live or branch key, person acknowledged |
spending_burn, spending_burn_branch |
Return what is left to the burn address and close | live key (or a wallet key, its own wallet), burn address set |
review_endpoint |
Rate an endpoint the account paid | key, a receipt for that URL |
support_open_thread |
Ask a person | key |
whoami |
The account, its role, and profile_url |
key |
There is no reveal tool and no withdraw tool, and there never will be: the
credential that spends must never be the credential that can take the key
out. When a tool needs a person it answers needs a person: <step>, with
why and the dashboard link; relay it and stop.
Guardrails to design around#
- The wallet is the budget. It spends what it holds and nothing more; a refusal names the balance. Build the retry policy on the reason text, not on the status alone.
- One wallet per job. Cheap and instant; burn it when the job is done and the receipts stay with the account.
- A policy narrows the budget. A person can allow-list hosts, cap one
call, or set a line above which a call needs them.
spending_budgetandGET /{wallet}/policyshow it; a refusal it causes starts withpolicy:orneeds a person:. Relay the second to the person and stop. - Fees are quoted before signing. The price list
is in
spending_budgetandspending_costs, andfeecomes back on every paid call. No gas, ever. - Public page. The wallet's address is on the account's page at
/u/{handle}, and a seller can check any payer atGET /v1/payers/{address}. Burned wallets are listed by default.
See the API reference for every endpoint and MCP tools for every tool.