Skip to main content
Every route is nested under an identity. {identity} accepts an idn_… id or the identity’s name. An identity has at most one live wallet per network. Amounts are integer micro-USD (1 USD = 1000000); USDC’s six decimals map onto it one-to-one. Networks are CAIP-2: eip155:84532 (Base Sepolia, the default) and eip155:8453 (Base mainnet, enabled per organization by its owner). Every route below selects the wallet with ?network= (POST …/wallet names it in the body); the testnet wallet is selected when it is omitted. A route never answers with another network’s wallet: reading, funding or paying on a network the identity has no wallet on is 404 not_found, and a value that is not one of the two networks is validation_failed with param: network.
On a deployment with no custody provider bound, every route below answers 501 feature_not_configured.

Read the wallet

GET /v1/identities/{identity}/wallet — scope agents:read Answers the selected network’s wallet with its live balance_micro_usd and spent_today_micro_usd, or 404 not_found when the identity has none on that network. A swept wallet is retired and no longer answers here.
The response never carries a private key or a custody reference; the provider keeps the key and Vetta holds only the address, the policy and the ledger.

Create the wallet

POST /v1/identities/{identity}/wallet — scope billing:write
Both fields are optional. The policy is attached at the custody provider before the wallet exists, so a wallet without a cap is never observable. Answers 201, or 200 with the identity’s existing wallet on that network — a wallet on the other network neither blocks nor is returned, so one identity may hold a testnet and a mainnet wallet side by side. A wallet’s network never changes after creation.

Policy, freeze, unfreeze

PATCH /v1/identities/{identity}/wallet/policy — scope billing:write POST /v1/identities/{identity}/wallet/freeze — scope billing:write POST /v1/identities/{identity}/wallet/unfreeze — scope billing:write A cap change is pushed to the provider first. freeze pushes a per-transaction cap of 0; every spend against a frozen wallet answers state_conflict.

Fund, transfer, sweep

POST /v1/identities/{identity}/wallet/fund — scope billing:write { "amount_micro_usd": 1000000, "source": "faucet" }faucet is testnet-only and grants the faucet’s fixed amount (1 USDC per request; the recorded transaction carries what actually arrived). treasury draws amount_micro_usd from the organization’s treasury wallet and answers 501 feature_not_configured until the owner has provisioned one. Answers the wallet_transaction. Honours Idempotency-Key.
POST /v1/identities/{identity}/wallet/transfer — scope billing:write { "to": "0x…", "amount_micro_usd": 250000, "purpose": "API credit" }to is an address or a sibling identity’s idn_… in the same organization (the sibling’s ledger records the matching in transaction). A sibling is resolved to its wallet on the sender’s network; a sibling with no wallet there is validation_failed (param: to), never a cross-network transfer. purpose is required. Capped by the per-transaction and daily caps (validation_failed), refused when the balance is short (validation_failed, insufficient_balance). Honours Idempotency-Key. POST /v1/identities/{identity}/wallet/sweep — scope billing:write Drains the balance to { "to": "0x…" } (or to the organization’s treasury when omitted — 501 feature_not_configured until one is provisioned) and retires the wallet as swept. A retired wallet does not block POST …/wallet from provisioning a fresh one.

Transactions

GET /v1/identities/{identity}/wallet/transactions — scope agents:read A paginated list of the selected network’s wallet_transaction objects. Filters: network, kind (fund | transfer | pay | sweep), direction (in | out), origin, since. x402 receipts are ?kind=pay.

Pay for a paywalled resource

POST /v1/identities/{identity}/wallet/quote — scope agents:read { "url": "https://api.example.com/report", "method": "GET" } — fetches the URL and answers a wallet_quote describing what it asks for, or payable: false. Zero side effects: nothing is signed and nothing is recorded. Only public hosts are fetched and redirects are never followed; a private, loopback or link-local address is validation_failed.
POST /v1/identities/{identity}/wallet/pay — scope billing:write { "url": "https://api.example.com/report", "max_amount_micro_usd": 250000 } — pays the smaller of max_amount_micro_usd and the per-transaction cap, reserves the daily budget, retries the request once with the payment attached and answers a wallet_payment with the response. A short balance answers validation_failed (insufficient_balance); the organization’s credits are never used. Honours Idempotency-Key. response_body is truncated to the first 64 KiB.
GET …/wallet/transactions?kind=pay lists the receipts; each carries the resource’s origin.

Agent tools

A session whose identity owns the wallet gets wallet.balance, wallet.transactions, wallet.receipts, wallet.quote, wallet.pay and wallet.transfer. The four reads run under the agent’s ordinary tool policy; wallet.pay and wallet.transfer move money and default to ask, so the operator approves each one before it is signed. Provisioning, policy, funding and sweeping are operator routes with no tool.

Errors

not_found for an identity with no wallet; validation_failed for caps, a short balance, faucet on mainnet, a self-transfer, or a requirement the wallet cannot meet; forbidden for a mainnet wallet the organization has not enabled or a transfer to an identity outside it; state_conflict for a write against a frozen or swept wallet; rate_limited when the faucet is exhausted; feature_not_configured when custody or the treasury is not bound. See Errors.