Agent payments API
This rail lets an agent pay for a resource that answers HTTP 402 with an x402 v2 offer, under a spending limit the agent cannot raise. You send us a URL. We read the seller’s offer, check it against your policy, sign an EIP-3009 authorization if it fits, hand it to the seller, and return what the seller sent back.
The agent never holds a wallet and never sees a key. Everything below is one API key and JSON.
These are separate docs from gas sponsorship, and deliberately so: the two products make different promises. Most importantly, gas sponsorship holds no keys and this rail does. See what this does not do yet.
Two environments, one code path
Signing up issues a sandbox key. It pays real x402 resources on Base Sepolia with real authorizations and real on-chain settlement. It is not a simulation and there is no separate mock rail — the settlement code is the same code mainnet uses, which is the point.
- · A sandbox key is refused on mainnet by the rail, before anything is signed. The error is
sandbox_cannot_pay_mainnet. - · Sandbox apps are created with $1.00 per payment and $5.00 per day.
- · We fund the sandbox testnet wallet. You need no test tokens.
- · Production keys pay USDC on Base mainnet and require a signer you or we configure.
Something to pay against
We run a real x402 seller on Base Sepolia at https://seller.tollbeam.com. Nothing about it is simulated: the 402s are real, the authorization your key signs is real, and settlement happens on chain. Only the content it sells is invented.
- ·
/priced/quote— $0.01. The happy path. - ·
/priced/report— $0.25. Twenty of these fill the $5.00 daily cap. - ·
/priced/audit— $2.50. Priced above your $1.00 per-payment cap on purpose.
Pay a cent. You need no test tokens — we fund the sandbox wallet.
curl -s "$TOLLBEAM_API/v1/pay/fetch" \
-H "authorization: Bearer $TOLLBEAM_KEY" \
-H "content-type: application/json" \
-d '{"url": "https://seller.tollbeam.com/priced/quote"}'Now the part worth seeing. Ask for the $2.50 endpoint, which your sandbox app’s $1.00 per-payment limit will not allow:
curl -s "$TOLLBEAM_API/v1/pay/fetch" \
-H "authorization: Bearer $TOLLBEAM_KEY" \
-H "content-type: application/json" \
-d '{"url": "https://seller.tollbeam.com/priced/audit"}'
# 403
# {
# "error": "policy_limit_exceeded",
# "breach": {
# "scope_type": "global",
# "period": "transaction",
# "limit_minor": "100",
# "attempted_minor": "250"
# }
# }Nothing was signed and nothing moved. The refusal happened before the authorization existed, and it is recorded — that record is the product, not the payment.
/health, which returns 503 rather than pretending to be up. A failure there is the testnet, not your integration or your key.One app key per agent fleet
Send the app API key as a bearer token, or in x-api-key. Both work. Requests are attributed to the app that owns the key.
# $TOLLBEAM_API is your API base URL. export TOLLBEAM_API="..." export TOLLBEAM_KEY="..." curl -s "$TOLLBEAM_API/v1/budget" \ -H "authorization: Bearer $TOLLBEAM_KEY"
Optionally send x-tollbeam-agent (or agentId in the body) to label which agent is spending. Per-agent limits and per-agent reporting key off it. It is a free-form string, up to 128 characters — we do not verify it, so treat it as a label rather than an identity.
POST /v1/pay/fetch
The only endpoint an agent needs. Give it a URL; it returns the resource, or a refusal with a reason.
POST /v1/pay/fetch
{
"url": "https://seller.example/priced/report", // required
"method": "GET", // optional, default GET
"headers": { "accept": "application/json" }, // optional
"body": null, // optional, string, max 1 MB
"agentId": "research-agent-3" // optional label
}Paid
200 OK
{
"paid": true,
"executionId": "382f9894-005a-4f94-ac41-63c4b1422aac",
"settlement": {
"status": "settled",
"txHash": "0x1356...e2a7",
"network": "eip155:84532"
},
"resource": { "status": 200, "body": "..." }
}Nothing to pay — the resource was free
200 OK
{ "paid": false, "resource": { "status": 200, "body": "..." } }Refused by your policy
403 Forbidden
{
"error": "policy_limit_exceeded",
"message": "global per-transaction limit of 100 exceeded by an attempted 250",
"executionId": "e3c7addc-88dc-4624-98e3-c66768ba19ea",
"breach": {
"scope_type": "global",
"scope_value": null,
"period": "transaction",
"limit_minor": "100",
"current_minor": "0",
"attempted_minor": "250"
}
}A 403 means we refused. A 502 means something upstream failed — the seller, or its settlement — and carries an executionId you can look up. The distinction matters: a 403 is a decision and safe to treat as final; a 502 may be ambiguous about whether money moved, which is why we never retry settlement automatically.
All amounts everywhere in this API are USD minor units — cents. 250 is $2.50.
GET /v1/budget
An agent can ask what it has left. It cannot change it — editing a policy needs a signed-in human. This is deliberate: an agent that can raise its own ceiling does not have a ceiling.
200 OK
{
"currency": "usd",
"unit": "minor",
"governed": true,
"as_of": "2026-08-29T20:28:56.085Z",
"policy": { "id": "...", "name": "Sandbox defaults" },
"limits": [
{ "scope_type": "global", "period": "transaction", "limit_minor": "100", "spent_minor": "0" },
{ "scope_type": "global", "period": "daily", "limit_minor": "500", "spent_minor": "1" }
]
}as_of is there because no lock is taken on a read. A concurrent payment can invalidate these figures immediately. Use it to decide whether a read is fresh enough to plan against, and do not treat headroom as reserved — it is reserved at payment time, not at read time.
POST /v1/policies
A policy is a named set of limits attached to an app. One policy is enabled per app at a time; creating a new enabled policy retires the previous one rather than failing.
POST /v1/policies
{
"app_id": "84135c11-df09-4a22-92e9-b2a043774c4d",
"name": "Research agents",
"enabled": true,
"limits": [
{ "scope_type": "global", "period": "transaction", "limit_minor": 100 },
{ "scope_type": "global", "period": "daily", "limit_minor": 2000 },
{ "scope_type": "agent", "scope_value": "research-agent-3", "period": "daily", "limit_minor": 500 },
{ "scope_type": "merchant", "scope_value": "seller.example", "period": "daily", "limit_minor": 0 }
]
}global, chain, protocol, agent, merchant. Everything except global requires scope_value. Merchant scope matches the resource URL’s hostname, lowercased.
transaction, hourly, daily, monthly, lifetime. Windows are calendar-aligned in UTC, not rolling: a daily limit resets at 00:00 UTC, it is not a trailing 24 hours.
Limits stack. Every applicable limit must pass, so the tightest one decides. Up to 50 limits per policy. A limit of 0 blocks that scope outright, which is how you deny a specific merchant today.
GET /v1/policieslists them, PATCH /v1/policies/:id updates name, enabled or limits.
POST /v1/apps/:appId/policies/simulate
Replay a candidate set of limits against what this app actually spent, before saving them. Answers “would this have broken anything?” without finding out in production.
POST /v1/apps/:appId/policies/simulate
{
"limits": [ { "scope_type": "global", "period": "daily", "limit_minor": 200 } ],
"days": 30
}Up to 20 limits, 1–365 days, default 30. The replay models the thing people get wrong by hand: a refused payment consumes no budget, so everything after it sees the headroom it really would have had.
What we stopped, not just what we paid
Every attempt is recorded, including the ones that never became payments. That record is the part of this product that is hard to rebuild.
- ·
GET /v1/apps/:appId/refusals/summary— prevented spend, grouped by agent and merchant. Optionalfrom/toISO timestamps. - ·
GET /v1/apps/:appId/payments— the payment list, filterable by disposition. - ·
GET /v1/pay/executions/:id— one attempt in full, by theexecutionIda 403 or 502 gave you.
error_code = rate_limited, and our classifier files that under refused — so in the payments list it looks exactly like a spending refusal. Read error_code, not the refusal count, or you will conclude your limits are biting when you are simply calling too fast.Some refusals have no amount. The mainnet guard fires before the seller’s asset is looked up, so there is no priced amount to record — those rows count toward refusal counts but contribute nothing to prevented spend. Reports label them rather than hiding them, so the totals reconcile.
What a failure means
| code | HTTP | meaning |
|---|---|---|
policy_limit_exceeded | 403 | A limit refused it. `breach` says which. |
policy_required | 403 | No policy is enabled. This rail fails closed rather than allowing. |
sandbox_cannot_pay_mainnet | 502 | A sandbox key tried to pay on a live network. Refused before signing. |
rate_limited | 429 | Too many requests for this app. Not a spending limit. |
unauthorized | 401 | Missing, unknown or inactive API key. |
x402_rail_disabled | 404 | The rail is off for this deployment. |
paused | 503 | We have paused payments deliberately. |
signer_not_configured | 502 | No signing key for this app. Nothing was attempted. |
sandbox_signer_not_configured | 502 | Same, for sandbox. Sandbox apps never fall back to a mainnet signer. |
settlement_ambiguous | 502 | Settlement did not return cleanly and was not retried. Read the chain before paying again. |
The edges, stated rather than discovered
Paying with x402 means signing an EIP-3009 authorization, so something must hold a key. Ours are in AWS KMS and the private material never leaves it — we ask KMS to sign a specific digest. This is the opposite of gas sponsorship, where we hold nothing. Bring your own key on Enterprise.
A merchant limit of zero blocks that host. “Only these hosts, refuse everything else” does not exist yet.
Limits are enforced without anyone in the loop. Escalating to a human above a threshold is planned and not built.
USDC, x402 v2, exact scheme, on Base mainnet and Base Sepolia. If the thing your agent needs does not speak x402, this cannot buy it.
Reviewed August 29, 2026. If something here does not match what the API does, the API is right and we want to know — tell us.