Overview

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.

Sandbox and production

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.

The seller settles through a third-party testnet facilitator. If it is unavailable the seller reports it on /health, which returns 503 rather than pretending to be up. A failure there is the testnet, not your integration or your key.
Authentication

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.

Pay for a resource

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.

What the agent can read

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" }
  ]
}
Read `governed` carefully. `false` does not mean unlimited. It means no policy is being enforced, and this rail fails closed — payments will be refused until a policy exists. An agent that treats `false` as permission will simply never pay.

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.

Spending limits

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 }
  ]
}
scope_type

global, chain, protocol, agent, merchant. Everything except global requires scope_value. Merchant scope matches the resource URL’s hostname, lowercased.

period

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.

A note on `lifetime`. It works, and it is the right shape for “$50 in total, ever”. The cost is that its window is unbounded, so evaluating it scans that app’s entire spend history. Fine for a low-volume agent; think before attaching one to a high-volume app.

GET /v1/policieslists them, PATCH /v1/policies/:id updates name, enabled or limits.

Dry-run a policy

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.

Refusals and the ledger

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. Optional from / to ISO timestamps.
  • · GET /v1/apps/:appId/payments — the payment list, filterable by disposition.
  • · GET /v1/pay/executions/:id — one attempt in full, by the executionId a 403 or 502 gave you.
The trap worth knowing. Rate limiting (default 60 requests per minute per app) produces 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.

Error codes

What a failure means

codeHTTPmeaning
policy_limit_exceeded403A limit refused it. `breach` says which.
policy_required403No policy is enabled. This rail fails closed rather than allowing.
sandbox_cannot_pay_mainnet502A sandbox key tried to pay on a live network. Refused before signing.
rate_limited429Too many requests for this app. Not a spending limit.
unauthorized401Missing, unknown or inactive API key.
x402_rail_disabled404The rail is off for this deployment.
paused503We have paused payments deliberately.
signer_not_configured502No signing key for this app. Nothing was attempted.
sandbox_signer_not_configured502Same, for sandbox. Sandbox apps never fall back to a mainnet signer.
settlement_ambiguous502Settlement did not return cleanly and was not retried. Read the chain before paying again.
What this does not do yet

The edges, stated rather than discovered

We hold signing keys

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.

Denylist, not allowlist

A merchant limit of zero blocks that host. “Only these hosts, refuse everything else” does not exist yet.

No human approval step

Limits are enforced without anyone in the loop. Escalating to a human above a threshold is planned and not built.

One asset, one protocol

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.