STAMP

DocsFor AI agents

Agents buy stamps, not gas.

An agent can create a fresh address, receive tokens there and send them on, with every gas fee paid by the Stamp paymaster: for a prepaid stamp from a booklet it bought from a payer account, or for a proof of work its own CPU mines. Nobody funds the address, so no funding transaction links it back to the operator's wallet. @paravion/agent packages this as an MCP server and a CLI.

Why it fits agents#

A wallet-holding agent normally needs ETH before it can do anything, and topping it up from the operator's wallet writes a public link between the two. Stamp removes that step: the paymaster pays for an operation of a fresh EIP-7702 address when it carries a stamp, co-signed by the relay. A prepaid stamp comes from a booklet the agent bought once, in ETH, from a payer account; the relay signed it blind, so it cannot tell which purchase a later operation's stamp came from. A mined stamp is 8 keccak sub-solutions, seconds of CPU on a laptop, paid from each epoch's small free budget. The agent's fresh addresses never hold ETH for gas.

What stays visible is everything else. The tokens an address receives link their sender to it, the relay sees the IP address and the time of each send, and every operation is public on chain. The limits are written into the server's instructions and into the descriptions of the tools that lead to a send, so the model reads them too.

Install#

shell
# From a checkout of the monorepo (Node 22+, pnpm)
$ cd /path/to/paravion
$ pnpm install
$ pnpm --filter @paravion/miner build   # the Node miner loads its WASM build
$ node packages/agent/bin/paravion.mjs --help

Keys are generated on the machine that runs the agent and stored encrypted in a local keystore: ~/.paravion (or PARAVION_HOME), directories 0700 and files 0600, sealed with AES-256-GCM under a scrypt key derived from PARAVION_PASSPHRASE. The MCP server has no tool that returns a key; only the CLI's account export prints one, after an explicit flag and a confirmation typed on a terminal.

Connect an MCP client#

The server speaks MCP over stdio (paravion mcp). The network is never chosen by the agent: set PARAVION_NETWORK to mainnet or local in the client's configuration.

$ claude mcp add --transport stdio --scope user \
    --env PARAVION_NETWORK=mainnet \
    --env PARAVION_PASSPHRASE="$PARAVION_PASSPHRASE" \
    --env PARAVION_MAX_AMOUNT='USDG=50,ETH=0.01' \
    paravion -- node /path/to/paravion/packages/agent/bin/paravion.mjs mcp
Replace /path/to/paravion with your checkout. Claude Desktop reads claude_desktop_config.json; Cursor reads ~/.cursor/mcp.json or a project's .cursor/mcp.json.

A typical session: paravion_network → paravion_buy_stamps (once, from a payer) → paravion_create_account → (the account receives assets) → paravion_quote_send with postage: "stamp" → paravion_send → paravion_get_operation. A send always needs a fresh quote: the quote is the dry run (amount, guardrails, recipient, balance, gas sponsored, work and time), and paravion_send executes exactly what it describes, once, within about 120 seconds. While it mines and waits for inclusion, the server sends notifications/progress to clients that ask for them, and a cancelled call stops mining.

Tools#

MCP tools
ToolWhat it doesAnnotationsCLI equivalent
paravion_networkChain, relay, paymaster, current epoch, difficulty, budget, guardrails and honest limits.read-only, networkparavion network
paravion_create_accountGenerates a key on this machine, stores it encrypted, returns the address only. Parameters: label (optional).additive, local onlyparavion account new [--label NAME]
paravion_list_accountsAddresses in the local keystore for this network. Reads nothing from the network.read-only, local onlyparavion account list
paravion_get_accountOne account: ETH and token balances and delegation state, read through the relay. Parameters: account (optional).read-only, networkparavion account show [ADDRESS]
paravion_list_assetsETH and the allowlisted tokens this network sponsors, with the jurisdiction notice. Parameters: query (optional), kind (optional), limit (optional).read-only, local onlyparavion assets [--query Q] [--kind K] [--limit N]
paravion_quote_sendChecks amount, guardrails, recipient and balance; estimates gas and work; returns a quoteId (about 2 min, one use). Parameters: account (optional), to, amount, asset, postage (optional).read-only, networkparavion quote --to ADDRESS --amount AMOUNT --asset SYMBOL [--account ADDRESS] [--postage stamp]
paravion_sendExecutes a quote: mines the proof of work, submits through the relay, waits for the receipt. Parameters: quoteId, maxWaitSeconds (optional).destructive, networkparavion send --quote QUOTE_ID
paravion_get_operationStatus and receipt of an operation by userOpHash, with the explorer link. Parameters: userOpHash.read-only, networkparavion status USER_OP_HASH
paravion_claim_test_tokensTest networks only: claims 10 mock tokens from the faucet with a sponsored operation. Parameters: account (optional), token (optional), postage (optional).additive, network, networks with a test faucet only (the local fork)paravion claim [--token mNVDA] [--account ADDRESS] [--postage stamp]
paravion_buy_stampsBuys 1 to 100 prepaid stamps from the PostOffice, paid in ETH by a payer account; blind-signed into the local carnet. Parameters: count, payer.destructive, network, networks with a PostOffice onlyparavion stamps buy --count N --payer ADDRESS
paravion_list_stampsUsable and expired stamps of the local carnet, by expiry, plus pending booklets and payer accounts.additive, network, networks with a PostOffice onlyparavion stamps list

Prepaid stamps#

  • Buying. paravion_buy_stamps (paravion stamps buy --count N --payer ADDRESS) buys 1 to 100 stamps from the PostOffice, paid in ETH by payer, an account of the local keystore that holds ETH. The serials are drawn and blinded on the agent's machine, the relay signs them blind, and the unblinded stamps are saved, encrypted. The tool spends ETH irreversibly and is marked destructive; stamps are never refunded.
  • The payer is public. The purchase names it on chain. The agent records every payer and refuses to use a stamp for a payer's own operations: that would tie the stamps to their purchase in plain sight.
  • Saved stamps. <PARAVION_HOME>/carnet/<network>.json, encrypted under PARAVION_PASSPHRASE with the SDK's format (PBKDF2-SHA-256 at 600,000 iterations, AES-256-GCM): a stamp file exported from the web app imports here, and back. Writes are serialized across processes. paravion_list_stamps shows usable stamps by expiry, pending booklets and payers, and finishes the issuance of a booklet that was paid but not issued yet.
  • Spending. postage: "stamp" (--postage stamp) in the quote makes the send use one saved stamp instead of mining. The stamp is removed once the paymaster has cancelled it on chain.
  • Privacy advice, in the tools too. Buy well before you send, buy several at once, and reach the relay over Tor or a VPN: it sees the IP address at purchase and at use.

Every tool returns structuredContent that matches its published output schema, and the same JSON as text. Errors are tool results with isError and a JSON { "error": { "code", "message", "details" } }, where code is stable: GUARDRAIL_CAP, RECIPIENT_REFUSED, INSUFFICIENT_BALANCE, QUOTE_EXPIRED, QUOTE_USED, RELAY_REFUSED (with the relay's reason and retryAt), RATE_LIMITED, and so on.

The CLI#

For agents that shell out instead of speaking MCP. Every command takes --json and then prints exactly one JSON object on stdout, {"ok":true,"result":…} or {"ok":false,"error":…}; progress goes to stderr. Exit codes: 0 success, 1 failure, 2 usage or configuration error, 130 interrupted.

paravion commands (all take --json)
CommandWhat it does
paravion networkThe network, epoch, difficulty, budget, guardrails and honest limits.
paravion account new [--label NAME]Generate a key, store it encrypted, print the address.
paravion account listAccounts of the local keystore for this network (no network read).
paravion account show [ADDRESS]Balances and delegation state of one account, read through the relay.
paravion account export ADDRESS --reveal-private-keyPrint the private key after an interactive confirmation on a terminal. CLI only: there is no MCP equivalent.
paravion assets [--query Q] [--kind K] [--limit N]Assets this network sponsors, with the jurisdiction notice.
paravion quote --to ADDRESS --amount AMOUNT --asset SYMBOL [--account ADDRESS] [--postage stamp]Dry run; prints a quoteId (about 2 minutes, one use). --postage stamp pays with a prepaid stamp.
paravion send --quote QUOTE_ID [--max-wait SECONDS] [--quiet]Mine, submit, wait for the receipt. Progress goes to stderr.
paravion claim [--token mNVDA] [--account ADDRESS] [--postage stamp]Test networks with a faucet only: claim 10 mock tokens.
paravion stamps buy --count N --payer ADDRESSBuy a booklet of prepaid stamps in ETH from a payer account (blind-signed).
paravion stamps listThe local carnet: usable stamps by expiry, pending booklets, payer accounts.
paravion status USER_OP_HASHStatus and receipt of an operation.
paravion mcpStart the MCP server on stdio (for Claude Code, Claude Desktop, Cursor…).
shell
$ alias paravion='node /path/to/paravion/packages/agent/bin/paravion.mjs'
$ export PARAVION_NETWORK=mainnet PARAVION_PASSPHRASE='…'
$ paravion account new --json
{"ok":true,"result":{"account":{"address":"0x8f3c…41aE","network":"mainnet",…}}}

# someone sends the account some USDG (that transfer links the sender to it, publicly)
$ paravion quote --to 0x2b1d…09fC --amount 12.5 --asset USDG --json
{"ok":true,"result":{"quoteId":"q_9b1e…","gas":{"sponsored":true,"paidBySenderWei":"0",…},"estimatedSeconds":41,…}}
$ paravion send --quote q_9b1e… --json        # progress on stderr
{"ok":true,"result":{"status":"included","success":true,"userOpHash":"0x…","transactionHash":"0x…",…}}
$ paravion status 0x… --json

A quote made by one paravion quote process is consumed by another paravion send process: quotes live as 0600 files in the keystore home and are claimed with an atomic rename, so two sends of one quote cannot both run.

Guardrails#

Environment
VariableRequiredMeaning
PARAVION_NETWORKyesmainnet or local. Never inferred: nothing runs until it is set.
PARAVION_PASSPHRASEyesEncrypts and decrypts the keystore and the carnet of stamps (at least 8 characters). Needed to create accounts and to send.
PARAVION_HOMEnoKeystore and quote directory (default ~/.paravion; created 0700, files 0600).
PARAVION_RELAY_URLnoRelay override (mainnet default: the relay in the SDK deployment record; local: the stack descriptor).
PARAVION_RPC_URLnoChain RPC override, used only for address-free reads (the epoch cross-check).
PARAVION_STACK_FILEnoLocal only: the e2e stack descriptor (default packages/e2e/.stack.json).
PARAVION_MAX_AMOUNTnoPer-send caps by symbol, e.g. USDG=50,ETH=0.01; *=0 blocks every asset not listed.
PARAVION_ALLOWED_RECIPIENTSnoComma-separated recipient allowlist; when set, sends to any other address are refused.
PARAVION_MINER_THREADSnoMining threads (default: CPU cores − 1).
  • The network is explicit. Nothing runs until PARAVION_NETWORK is set; local reads the e2e stack descriptor. PARAVION_RELAY_URL and PARAVION_RPC_URL accept https, or plain http on a loopback host only.
  • Caps per send. PARAVION_MAX_AMOUNT=USDG=50,ETH=0.01 caps each send by symbol, in the asset's own decimals; *=0 blocks every asset you did not list. A malformed value stops the agent instead of running without the cap.
  • Recipient allowlist. With PARAVION_ALLOWED_RECIPIENTS, any other recipient is refused.
  • The relay's recipient rule for every send. A recipient must be an address without code or an EOA delegated to Simple7702Account v0.9, never the sender itself or a reserved address. The relay enforces it for ETH; the agent applies it to token sends too, so an agent never sends tokens into a contract.
  • Checked twice. The quote checks the guardrails, the recipient and the balance; the send checks the guardrails again (a CLI quote and its send are separate processes).
  • No key in any output. Tool results, errors, progress messages and logs go through a scrubber that knows every key the process decrypted (by hash) and the passphrase; library errors are reduced to their short message, with URLs cut to their origin.

Honest limits#

  • Stamp is not a mixer: it pays gas for one operation at a time and never pools, holds or hides assets. Stamps pay gas only and are never refunded.
  • Assets an address receives link the sender to it, publicly and permanently.
  • The relay sees your IP address and when you send. It keeps no logs, but you are trusting that; run your own relay or use Tor if it matters.
  • Every operation is public on chain: sender, recipient, token, amount and time.
  • Robinhood stock tokens are not offered to persons in the US, Canada, the UK or Switzerland.

A few more facts an agent should know. Reads keyed to an address go to the relay only, one account at a time (paravion_list_accounts reads nothing, so the relay cannot see your accounts side by side), and the relay rate-limits them: 10 per minute for pow_getAccountState and pow_checkRecipient together, 60 for epoch and receipt reads. A quote reads the sender's balance, so paravion_send pauses a random time (up to 20 s) before mining or sending, as the SDK does after its own reads: the relay cannot time the send from that read. And proof of work is a rate limiter, not a guarantee: when the epoch's free budget is used up, mined sends wait for the next epoch; stamped sends do not draw on that budget.

Relay JSON-RPC#

The agent talks to the relay's public JSON-RPC API. Its machine-readable description is an OpenRPC document, /openrpc.json (OpenRPC 1.4), derived from the relay's code and checked against it by the agent's tests, and against a live relay by its end-to-end run.

Relay JSON-RPC methods
MethodBehaviour
eth_chainId, eth_supportedEntryPointsAnswered locally (EntryPoint v0.9 only).
stamp_getIssuerThe issuer key, the metadata (info), the window and the PostOffice price booklets are bought under now. Address-free; clients compare key and window with the published pins before blinding anything.
stamp_issue([{ orderTx, info, blinded }])Blind-signs the 1 to 100 messages a mined PostOffice purchase committed to. Deterministic: asking again returns the same signatures. Rate class issue (10/min).
stamp_sendUserOperation(op, ep, { stamp, delaySec? })A prepaid-stamp op (paymasterData version 2) with { info, serial, signature }: the relay verifies the stamp and that its nullifier is neither cancelled nor in flight, runs the same op checks without proof of work, co-signs userOpHash and the nullifier.
eth_sendUserOperationA mined stamp: full check order, then co-sign, then forward to the relay's own bundler. Errors: -32501 (paymaster rejected) or -32503 (time range) with data { reason, failMask?, retryAt? }.
pow_sendUserOperation(op, ep, { delaySec })Same checks and co-signature; the op is held for delaySec ≤ validUntil − now − 90 s, then forwarded. It counts in the ledger while held.
eth_getUserOperationReceipt, eth_getUserOperationByHashServed from chain logs (UserOperationEvent) plus the ledger for pending ops, so they work while the bundler is down.
pow_getUserOperationStatusheld / submitted / included / rejected / expired.
pow_getEpochCurrent epoch fields (flags, stampPostOpGasLimit included), acceptUntil, nextRollAt, headroomWei, budgetRemainingWei, gasProfiles, pvgQuotes, cosigner. Cached per block. Optional [{ wait: true }] only while waiting to send: an on-demand relay then rolls a due epoch.
pow_getAccountState(address)Delegation, EOA nonce, EntryPoint nonce (key 0), ETH and the non-zero balances of the listed tokens. Not logged. The SDK calls it only at import and after failures.
pow_checkRecipient(address)Whether an ETH send to this recipient would be sponsored (no code, or delegated to Simple7702Account v0.9; not a reserved address). The SDK asks it once before an ETH send, so a refused recipient costs nothing. Reads the recipient only; not logged.
eth_estimateUserOperationGasDebugging and third-party tooling only; the SDK never calls it. Rate-limited, 4 concurrent, bodies ≤ 8 KB.
Relay rejection reasons (error.data.reason)
reasonJSON-RPC codeMeaning
POW_INVALID-32501Mined stamp: the proof or a mirrored on-chain check failed; failMask says which.
EPOCH_STALE-32503The op's epoch is older than the grace window after a roll. The SDK rebuilds.
EPOCH_EXPIRED-32503Less than 60 s left in the op's validity window. The SDK rebuilds.
NONCE_KEY-32501The EntryPoint nonce key is not 0.
AUTH_INVALID-32501The EIP-7702 authorization is wrong (delegate, chain id, nonce or signer).
DELEGATE_MISMATCH-32501No authorization, and the sender is not delegated to Simple7702Account v0.9.
TEMPLATE / POLICY-32501The call is not one of the sponsored templates, or the op fails the paymaster policy.
GAS_PROFILE-32501Gas limits differ from the published profile.
PVG_TOO_LOW / PVG_TOO_HIGH-32501preVerificationGas is outside the quote bounds. The SDK rebuilds on TOO_LOW.
FEE_MISMATCH / FEE_TOO_LOW-32501Fees differ from the epoch's rule, or the bundler's floor moved.
RECIPIENT / INSUFFICIENT_BALANCE-32501An ETH send to a contract, the sender itself or a reserved address; or more ETH than the sender holds.
SIGNATURE_INVALID-32501The account signature does not recover to the sender, so the op could never validate.
PENDING_LIMIT-32501The sender already has 4 ops pending.
BUDGET_EXHAUSTED / PACING-32501Mined stamp: the epoch budget is used up, or paced; retryAt says when to try again.
HOLD_LIMIT-32501Held ops already reserve their share of the budget; retryAt is the next held op's forward time.
STAMP_INVALID-32501The stamp does not verify under its window's key, is not one of this issuer's windows, or its nullifier is not the op's.
STAMP_SPENT-32501The paymaster already recorded this stamp's nullifier (cancelledAt ≠ 0).
STAMP_IN_FLIGHT-32501The same stamp already pays for an operation the relay has not seen land yet.
STAMP_EXPIRED-32501The stamp's validity ends within the relay's 60 s margin.
STAMP_LANE_CLOSED-32501The epoch has no STAMPS flag, or this relay redeems no stamps.
ISSUER_UNAVAILABLE-32501This relay sells no stamps, or no issuer key is active yet.
ORDER_UNKNOWN / ORDER_PENDING-32501stamp_issue: the purchase is not mined, or not confirmed yet. Retry shortly.
ORDER_MISMATCH-32602stamp_issue: the transaction bought nothing, reverted, or the blinded messages, count or window are not the ones it paid for.
BUNDLER_REJECTEDbundler'sThe bundler refused the op after co-signing. data carries bundlerCode (and aaCode); its text is never forwarded.
BUNDLER_UNAVAILABLE-32603The bundler did not confirm the op. It may still land; the reservation is kept until it resolves.

Next steps#