Deposit USDG on Robinhood Chain and mint a key with a spending limit. Your agent pays x402 endpoints from the tab. It never holds the money.
An agent that pays for things needs a key that can move money. The usual answer is a wallet with a small balance, and the balance is the only limit.
Agents now buy things by the request: a quote, a page of research, a model call, another agent's time. x402 made that simple on the wire. An endpoint answers 402, the client retries with a signed payment, and a facilitator settles it on chain. What it did not solve is where the money sits while the agent runs.
If the agent's own wallet holds it, a prompt injection, a leaked environment variable or a bad loop can spend the whole balance in one burst. If you top it up in small amounts you spend your day topping up. If a provider holds prepaid credits, the agent can only buy from that provider.
A tab is the third option. Your wallet keeps the money in a contract. The agent gets a key, and the key gets a policy: the most it may pay per request, the most it may pay per day, optionally who it may pay, and when it expires. Every payment is checked against that policy by the contract itself, before any token moves. The facilitator can say no. It cannot say more.
The agent never sees your wallet key. A leaked agent key costs at most one day's cap until you revoke it, which is one transaction. Withdrawing is always yours and never the agent's.
Ten messages between five parties. Only the first one is yours. The rest happen inside a single HTTP request.
A throwaway key is generated in your browser and signs a real payment against the live vault. Verify and settle are simulated on this page, because the throwaway key has no tab, and are marked as such.
POST /research HTTP/1.1
Host: research.example
Content-Type: application/json
{"q": "NVDA guidance, last two quarters"}
Press run. Each step prints what actually crosses the wire.
A key is an address with a policy. The policy is checked by the contract on every payment, not by a server you have to trust.
No single request can cost more than this, whatever the endpoint asks for. A seller that raises its price past the limit gets a refusal, not a payment.
The window opens at the key's first payment and lasts 24 hours. Spend inside it cannot exceed the cap. Replacing the policy keeps the window, so a cap cannot be reset by re-minting.
Optional. When it is set, the key can only pay the addresses you named. Useful for an agent that should buy from three providers and nobody else.
A key stops working at a time you choose. Leave it unset for keys that run until you revoke them. A job that should run for a weekend gets a key that dies on Monday.
One transaction. Payments the key signed but had not settled fail from then on. Nothing moves, nothing needs sweeping.
The owner can pull the balance at any time. Keys cannot. There is no function in the contract that lets a key move funds anywhere except to the payee in a payment it signed.
A key is bound to a token when it is minted. A payment in any other token fails. Most tabs will hold USDG, but any ERC-20 on the chain works, including launchpad tokens.
Each row is a thing that will happen eventually. The middle column is what the contract does about it without anyone acting.
| Event | What happens on its own | What you can do |
|---|---|---|
| The agent key leaks | The thief can spend at most the daily cap per day, only in the key's token, only to allowed payees if a list is set. | Revoke the key. One transaction. Mint a new one. |
| The facilitator goes down | Sellers cannot verify, so requests get 402s. No balance moves. Nothing is lost. | Any facilitator can take over. The owner or the seller can also call pay() themselves with the signed payment. |
| A seller takes payment and returns junk | With settle before serve, that one call is paid. The loss is bounded by max per payment. | Drop the seller from the payee list or lower the per-payment limit. Sellers that settle after serve carry this risk themselves. |
| The balance runs out | Verify fails with InsufficientBalance. The agent gets a 402 that says so and stops. | Deposit more. Keys and policies are untouched. |
| The key expires mid-job | Payments fail with Expired. Work already paid for is unaffected. | Set a new policy on the same key. The daily window carries over. |
| The same payment is submitted twice | The second attempt reverts with NonceUsed. The seller is paid once. | Nothing. |
| A seller quotes above the limit | The client refuses to sign. No request is sent, no fee is paid. | Raise the limit for that key if the price is fair. |
The buyer wraps fetch. The seller wraps a route. Neither one needs an account with anybody.
import { createPayFetch } from 'tab'
const payFetch = createPayFetch({
agentKey: process.env.AGENT_KEY,
owner: '0xYourWallet',
maxAmount: '250000', // refuse anything over 0.25 USDG
})
const res = await payFetch('https://example.com/research', {
method: 'POST',
body: JSON.stringify({ q: 'NVDA guidance' }),
})
console.log(res.paid, res.payment.txHash)
A 402 is answered by signing with the key and retrying once. Anything else passes through untouched. The key is a plain private key, so the same wrapper works from a cron job, a Claude tool, or an MCP server.
import { createPaywall } from 'tab'
const wall = createPaywall({
facilitator: 'https://tab.example/x402',
payTo: '0xYourWallet',
asset: USDG, vault: VAULT, chainId: 4663,
price: '10000', // 0.01 USDG per call
})
app.post('/research', wall.middleware(async (req, res) => {
res.json(await research(req.body.q))
}))
The route settles before it runs by default, so the work is paid for before it starts. Set settle to after if you would rather serve first.
const wall = createPaywall({
...common,
// price is computed per request: 0.002 USDG per
// 100 characters of input, floor 0.005
price: req => {
const chars = Number(req.headers['content-length'] || 0)
return String(Math.max(5000, Math.ceil(chars / 100) * 2000))
},
})
Price can be a function of the request. The 402 states the exact amount, the client checks it against its own limit, and the signature covers that amount and nothing else.
const res = await fetch(url)
if (res.status === 402) {
const { accepts } = await res.json()
const offer = accepts.find(a => a.scheme === 'tab-vault')
const payload = await signPayment({ agentKey, owner, ...offer })
return fetch(url, { headers: {
'X-PAYMENT': btoa(JSON.stringify({
x402Version: 1, scheme: 'tab-vault',
network: offer.network, payload,
})),
}})
}
The wire format is two JSON documents and one EIP-712 signature. A client in any language can produce it. The protocol page has every field.
Three ways to let an agent spend money, and what each one gives up.
| Agent holds a wallet | Prepaid credits with one provider | A tab | |
|---|---|---|---|
| Who holds the money | The agent | The provider | You, in a contract |
| Per-payment limit | None | None | Yes |
| Daily cap | None | None | Yes |
| Buys from any x402 seller | Yes | No | Yes, when the seller supports the scheme |
| If the key leaks | The whole balance | The credits | One day's cap |
| Stop it | Sweep the wallet, if you can | Rotate the API key | Revoke, one transaction |
| Withdraw | Only if the agent cooperates | Usually not | Any time |
What is real today and what is not.
| Chain | Robinhood Chain, id 4663 |
|---|---|
| Token | USDG. The vault accepts any ERC-20, and a key is bound to one token. |
| Settlement | One transaction per payment, submitted by the facilitator, which pays the gas. |
| Fee | 0 basis points at launch, charged on top so the seller receives the full price. The contract caps it at 500. |
| Contract | Live on Robinhood Chain since block 56350964: 0x5e97c3557f8d4094A7a0f7F2815414974becf171. 18 tests. Fee set to 0. |
| SDK | Written. Buyer, seller and signing helpers in one module. Not on npm yet. |
| App | Working against a local chain: deposit, withdraw, mint, revoke and the receipt are tested end to end in a browser. |
| Facilitator | Written and tested end to end with 19 checks including replay, tampering, a burst over the cap, and revoke. The public instance runs at https://tab-sb5f.onrender.com/x402 with a minimum price of 0.10 USDG per call. |
| MCP | Written. Two tools, pay_fetch and tab_status, so a Claude or any MCP client can pay with a key it was given. |
| Next | Public facilitator on the node service, a minimum price per call so gas is covered, then npm publish. |
| Source | github.com/ElizenDevVini/tab |