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#
# 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 --helpKeys 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{
"mcpServers": {
"paravion": {
"command": "node",
"args": [
"/path/to/paravion/packages/agent/bin/paravion.mjs",
"mcp"
],
"env": {
"PARAVION_NETWORK": "mainnet",
"PARAVION_PASSPHRASE": "a-long-passphrase-only-this-config-knows",
"PARAVION_MAX_AMOUNT": "USDG=50,ETH=0.01"
}
}
}
}{
"mcpServers": {
"paravion": {
"command": "node",
"args": [
"/path/to/paravion/packages/agent/bin/paravion.mjs",
"mcp"
],
"env": {
"PARAVION_NETWORK": "mainnet",
"PARAVION_PASSPHRASE": "${PARAVION_PASSPHRASE}",
"PARAVION_MAX_AMOUNT": "USDG=50,ETH=0.01"
}
}
}
}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#
| Tool | What it does | Annotations | CLI equivalent |
|---|---|---|---|
| paravion_network | Chain, relay, paymaster, current epoch, difficulty, budget, guardrails and honest limits. | read-only, network | paravion network |
| paravion_create_account | Generates a key on this machine, stores it encrypted, returns the address only. Parameters: label (optional). | additive, local only | paravion account new [--label NAME] |
| paravion_list_accounts | Addresses in the local keystore for this network. Reads nothing from the network. | read-only, local only | paravion account list |
| paravion_get_account | One account: ETH and token balances and delegation state, read through the relay. Parameters: account (optional). | read-only, network | paravion account show [ADDRESS] |
| paravion_list_assets | ETH and the allowlisted tokens this network sponsors, with the jurisdiction notice. Parameters: query (optional), kind (optional), limit (optional). | read-only, local only | paravion assets [--query Q] [--kind K] [--limit N] |
| paravion_quote_send | Checks 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, network | paravion quote --to ADDRESS --amount AMOUNT --asset SYMBOL [--account ADDRESS] [--postage stamp] |
| paravion_send | Executes a quote: mines the proof of work, submits through the relay, waits for the receipt. Parameters: quoteId, maxWaitSeconds (optional). | destructive, network | paravion send --quote QUOTE_ID |
| paravion_get_operation | Status and receipt of an operation by userOpHash, with the explorer link. Parameters: userOpHash. | read-only, network | paravion status USER_OP_HASH |
| paravion_claim_test_tokens | Test 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_stamps | Buys 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 only | paravion stamps buy --count N --payer ADDRESS |
| paravion_list_stamps | Usable and expired stamps of the local carnet, by expiry, plus pending booklets and payer accounts. | additive, network, networks with a PostOffice only | paravion 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 bypayer, 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 underPARAVION_PASSPHRASEwith 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_stampsshows 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.
| Command | What it does |
|---|---|
| paravion network | The network, epoch, difficulty, budget, guardrails and honest limits. |
| paravion account new [--label NAME] | Generate a key, store it encrypted, print the address. |
| paravion account list | Accounts 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-key | Print 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 ADDRESS | Buy a booklet of prepaid stamps in ETH from a payer account (blind-signed). |
| paravion stamps list | The local carnet: usable stamps by expiry, pending booklets, payer accounts. |
| paravion status USER_OP_HASH | Status and receipt of an operation. |
| paravion mcp | Start the MCP server on stdio (for Claude Code, Claude Desktop, Cursor…). |
$ 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… --jsonA 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#
| Variable | Required | Meaning |
|---|---|---|
| PARAVION_NETWORK | yes | mainnet or local. Never inferred: nothing runs until it is set. |
| PARAVION_PASSPHRASE | yes | Encrypts and decrypts the keystore and the carnet of stamps (at least 8 characters). Needed to create accounts and to send. |
| PARAVION_HOME | no | Keystore and quote directory (default ~/.paravion; created 0700, files 0600). |
| PARAVION_RELAY_URL | no | Relay override (mainnet default: the relay in the SDK deployment record; local: the stack descriptor). |
| PARAVION_RPC_URL | no | Chain RPC override, used only for address-free reads (the epoch cross-check). |
| PARAVION_STACK_FILE | no | Local only: the e2e stack descriptor (default packages/e2e/.stack.json). |
| PARAVION_MAX_AMOUNT | no | Per-send caps by symbol, e.g. USDG=50,ETH=0.01; *=0 blocks every asset not listed. |
| PARAVION_ALLOWED_RECIPIENTS | no | Comma-separated recipient allowlist; when set, sends to any other address are refused. |
| PARAVION_MINER_THREADS | no | Mining threads (default: CPU cores − 1). |
- The network is explicit. Nothing runs until
PARAVION_NETWORKis set;localreads the e2e stack descriptor.PARAVION_RELAY_URLandPARAVION_RPC_URLaccept https, or plain http on a loopback host only. - Caps per send.
PARAVION_MAX_AMOUNT=USDG=50,ETH=0.01caps each send by symbol, in the asset's own decimals;*=0blocks 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.
| Method | Behaviour |
|---|---|
| eth_chainId, eth_supportedEntryPoints | Answered locally (EntryPoint v0.9 only). |
| stamp_getIssuer | The 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_sendUserOperation | A 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_getUserOperationByHash | Served from chain logs (UserOperationEvent) plus the ledger for pending ops, so they work while the bundler is down. |
| pow_getUserOperationStatus | held / submitted / included / rejected / expired. |
| pow_getEpoch | Current 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_estimateUserOperationGas | Debugging and third-party tooling only; the SDK never calls it. Rate-limited, 4 concurrent, bodies ≤ 8 KB. |
| reason | JSON-RPC code | Meaning |
|---|---|---|
| POW_INVALID | -32501 | Mined stamp: the proof or a mirrored on-chain check failed; failMask says which. |
| EPOCH_STALE | -32503 | The op's epoch is older than the grace window after a roll. The SDK rebuilds. |
| EPOCH_EXPIRED | -32503 | Less than 60 s left in the op's validity window. The SDK rebuilds. |
| NONCE_KEY | -32501 | The EntryPoint nonce key is not 0. |
| AUTH_INVALID | -32501 | The EIP-7702 authorization is wrong (delegate, chain id, nonce or signer). |
| DELEGATE_MISMATCH | -32501 | No authorization, and the sender is not delegated to Simple7702Account v0.9. |
| TEMPLATE / POLICY | -32501 | The call is not one of the sponsored templates, or the op fails the paymaster policy. |
| GAS_PROFILE | -32501 | Gas limits differ from the published profile. |
| PVG_TOO_LOW / PVG_TOO_HIGH | -32501 | preVerificationGas is outside the quote bounds. The SDK rebuilds on TOO_LOW. |
| FEE_MISMATCH / FEE_TOO_LOW | -32501 | Fees differ from the epoch's rule, or the bundler's floor moved. |
| RECIPIENT / INSUFFICIENT_BALANCE | -32501 | An ETH send to a contract, the sender itself or a reserved address; or more ETH than the sender holds. |
| SIGNATURE_INVALID | -32501 | The account signature does not recover to the sender, so the op could never validate. |
| PENDING_LIMIT | -32501 | The sender already has 4 ops pending. |
| BUDGET_EXHAUSTED / PACING | -32501 | Mined stamp: the epoch budget is used up, or paced; retryAt says when to try again. |
| HOLD_LIMIT | -32501 | Held ops already reserve their share of the budget; retryAt is the next held op's forward time. |
| STAMP_INVALID | -32501 | The 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 | -32501 | The paymaster already recorded this stamp's nullifier (cancelledAt ≠ 0). |
| STAMP_IN_FLIGHT | -32501 | The same stamp already pays for an operation the relay has not seen land yet. |
| STAMP_EXPIRED | -32501 | The stamp's validity ends within the relay's 60 s margin. |
| STAMP_LANE_CLOSED | -32501 | The epoch has no STAMPS flag, or this relay redeems no stamps. |
| ISSUER_UNAVAILABLE | -32501 | This relay sells no stamps, or no issuer key is active yet. |
| ORDER_UNKNOWN / ORDER_PENDING | -32501 | stamp_issue: the purchase is not mined, or not confirmed yet. Retry shortly. |
| ORDER_MISMATCH | -32602 | stamp_issue: the transaction bought nothing, reverted, or the blinded messages, count or window are not the ones it paid for. |
| BUNDLER_REJECTED | bundler's | The bundler refused the op after co-signing. data carries bundlerCode (and aaCode); its text is never forwarded. |
| BUNDLER_UNAVAILABLE | -32603 | The bundler did not confirm the op. It may still land; the reservation is kept until it resolves. |