MCP and agent grants

Connect an AI agent to sats over MCP. The agent can read the wallet and file payment requests; only you can approve them.

sats agent serve <name> runs a stdio MCP server for one named agent, under the grant its token names. The server holds no key material and never prepares, signs, or broadcasts. The flow:

  1. The agent calls request_send. sats records the request as pending_approval, or denied if it crosses a grant limit.
  2. You run sats agent approve, review the payment, and enter your password. That command signs and broadcasts, in your process.
  3. The agent observes the result with check_request. It never retries or files again to make a payment happen.

Connect an agent

Create a grant on a funded wallet:

sats agent grant claude --budget 50k --for 24h --max-tx 10k --max-fee 1000

sats prints two setup commands that embed the same token as SATS_AGENT_TOKEN. Run the one for your client:

# ChatGPT desktop, Codex CLI, and the Codex IDE extension
codex mcp add sats --env SATS_AGENT_TOKEN=<token> -- <pinned sats command>

# Claude Code, scoped to the current project
claude mcp add --transport stdio --scope local sats --env SATS_AGENT_TOKEN=<token> -- <pinned sats command>

At startup the server checks that the grant exists and hasn't expired, that SATS_AGENT_TOKEN matches it, that a wallet exists on the network, and that the configuration parses. It doesn't select a provider until get_balance needs one, so a provider choice that can't be served never blocks the other tools.

Tools

ToolDoesTouches the chain
get_balanceSync and return the balanceYes
get_receive_addressReveal and persist the next receive addressNo
get_grantReturn the agent's own grantNo
request_sendFile a payment requestNo
check_requestRead one of the agent's own requestsNo

get_balance

{ "balance_sat": 118500, "pending_sat": 0, "synced": true, "network": "signet" }

synced: false means the chain was unreachable and the balance is cached. A provider choice the network can't serve returns an error, not a cached balance.

get_receive_address

{ "address": "tb1p...", "index": 3, "network": "signet" }

The tool is annotated as not read-only, because each call advances the wallet's derivation index.

get_grant

Returns budget, spent, remaining, caps, mode, recipient allowlist, and expiry. active: false means the grant was revoked, expired, or re-issued with a different token. It then carries no limits, and the message names the command a human runs to issue a new one.

request_send

Parameters:

The key is the agent's retry handle. Repeating a call with the same key, address, and amount returns the existing request instead of filing another, so a lost response never files twice. Reusing a key for a different send is the error idempotency_key_conflict. Keys are scoped to a grant: after a re-issue, the same key files a new request.

sats runs the grant's full authorization ladder with the fee unknown. A proposal inside every limit is the normal, successful result:

{
  "status": "pending_approval",
  "request_id": "r-8c1f0a2b9d3e4f57a1b2c3d4e5f60718",
  "recipient": "tb1p...",
  "amount_sat": 4500,
  "message": "filed for human review — the human approves with: sats agent approve r-8c1f0a2b9d3e4f57a1b2c3d4e5f60718; poll check_request to observe the result, and do not file it again"
}

A proposal outside a limit is recorded as denied, with a reason:

{
  "status": "denied",
  "request_id": "r-2e7d41c0a95b6f13c4d5e6f708192a3b",
  "reason": "over_max_tx",
  "recipient": "tb1p...",
  "amount_sat": 20000,
  "message": "outside the grant: requested 20,000 sat; max tx 10,000 sat — no approval lifts a grant boundary; only the human changing the grant can"
}

request_id is the server's id: r- plus 32 hex characters, unique across agents. Humans approve and dismiss by it, and the agent observes by it. It is not an idempotency key. Passing it back as one files a new request.

check_request

Takes a request_id and returns that request in the same shape as request_send. A sent result adds txid and fee_sat. The tool reads the local record only. It never contacts the chain or changes anything.

Request states

statusMeaningThe agent should
pending_approvalInside the grant, waiting for the humanRelay the id and observe
deniedOutside a grant limit (see reason); terminalReport once and stop
dismissedThe human declined; terminalStop; ask before proposing again
signingApproval is executing, or needs human reconciliationObserve; relay guidance if needs_reconciliation
sentBroadcast; has txidDone
broadcast_pendingSigned and saved; broadcast not confirmedObserve; the human rebroadcasts
unresolvedA signature may exist without a saved resultDon't file again; the human inspects
failedStopped before signing; see the diagnosticObserve; only the human can retry

What each state means for signing and budget is in Security.

Errors

An error is a failed call, not a request state. It never files a request. request_send and check_request return it as status: "error" with an error_code. get_balance and get_receive_address return the same status, error_code, and message as an isError tool result.

error_codeMeaning
invalid_agentThe agent name isn't 1–32 characters of a-z0-9_-
invalid_idempotency_keyThe key is missing or isn't 1–64 characters of A-Za-z0-9_-
invalid_addressThe address doesn't parse or is for another network
idempotency_key_conflictThe key was used for a different send; request_id names it
no_grantNo active grant: it was revoked or has expired. Nothing is read or written
unauthorizedThe token doesn't match this agent's grant, which was re-issued. Nothing is read or written
clock_unavailableThe system clock can't be read, so expiry can't be checked
store_errorThe request store couldn't be read or written

Denial reasons

Every reason is a grant limit. Report it to the human once and stop, because filing the same proposal again can't expand authority.

reasonMeaning
expiredThe grant has expired
observe_onlyThe grant is observe-only
amount_overflowAmount plus fee overflows
recipient_not_allowedThe recipient isn't on the grant's allowlist
over_max_txThe amount exceeds the per-transaction cap
over_max_feeThe real fee, known at approval, exceeds the fee cap
over_budgetAmount plus fee exceeds the remaining budget
revokedThe grant that created the request was revoked or re-issued

After filing

Nothing happens on the agent's side. The human finds the request with sats agent requests (or --watch) and approves or dismisses it; see Reviewing agent requests. Approval prepares the transaction on fresh chain state, verifies it matches the request, re-checks the grant with the real fee, and signs only after the human enters the wallet password.

If preparation fails because of a sync, fee, or provider problem, the request becomes failed and the human can try again. The agent keeps observing with check_request. It shouldn't poll the balance or file again.

Revocation and expiry

sats agent list
sats agent revoke claude

Demo

sh scripts/demo-mcp.sh runs the request loop end to end offline: an agent session over stdio and the human's CLI, through approval, denial, expiry, revocation, and a failed broadcast. It uses an isolated wallet and a mock chain, and asserts every outcome it prints.

Transport

MCP frames use stdout, and startup messages and diagnostics use stderr. Code inside the server must never print to stdout.