x402 — paid actions
Every other surface bills a human. This one bills the caller, in the same request that does the work.
An agent calls an endpoint, gets 402 Payment Required with machine-readable
terms, signs a payment authorization, retries with an X-PAYMENT header, and the
action runs — no invoice, no plan, no billing relationship set up in advance.
There is still a key, because the action operates a specific customer's identity;
see Payment is metering, not authentication.
This is x402, an open protocol. Incarna is a payee.
Network eip155:84532 (Base Sepolia)
Asset USDC
Facilitator https://x402.org/facilitator
Pay to 0xCa1fBb1900e1C17Cc443e34f312720960E72a83F
Testnet. Prices are denominated in real dollars but settle in Base Sepolia USDC today. Mainnet is a separate decision, not a flag flip.
Payment is metering, not authentication
Worth stating plainly, because the opposite assumption is a security hole:
Paying does not grant you someone else's account.
An identity action operates a specific customer's identity, so the paid routes still resolve a principal. Authentication answers may this identity act; the payment answers who pays for it. They are independent, and both are required.
Discovering prices
curl $BASE/x402/tools
{
"enabled": true,
"network": "eip155:84532",
"tools": {
"identity.x.post": "$0.05",
"identity.email.send": "$0.02",
"identity.email.inbox": "$0.01",
"identity.github.act": "$0.05",
"identity.reddit.act": "$0.05"
}
}
A tool not listed here cannot be charged for — which is what keeps a newly added endpoint from silently shipping as free.
GET /x402/quote/{tool} returns the full 402 document for one tool without needing
a real request, so a payer can inspect terms ahead of time.
Paid endpoints
| Endpoint | Tool | Price |
|---|---|---|
POST /x402/agents/{id}/accounts/{acct}/tweet |
identity.x.post |
$0.05 |
POST /x402/agents/{id}/email/send |
identity.email.send |
$0.02 |
POST /x402/agents/{id}/inbox |
identity.email.inbox |
$0.01 |
POST /x402/agents/{id}/accounts/{acct}/github |
identity.github.act |
$0.05 |
POST /x402/agents/{id}/accounts/{acct}/reddit |
identity.reddit.act |
$0.05 |
Bodies are identical to the unpaid equivalents in the REST reference.
The exchange
1 — Call without payment
curl -X POST $BASE/x402/agents/$AGENT/email/send \
-H 'Content-Type: application/json' \
-d '{"to":"someone@example.com","subject":"Hi","body":"..."}'
{
"x402Version": 2,
"error": "payment required",
"resource": {
"url": "https://api.incarna.io/x402/agents/{agent_id}/email/send",
"description": "Send email from the agent's own inbox. Body: {\"to\", \"subject\", \"body\"}. POST with an Authorization: Bearer key for the org that owns the agent — payment meters the action, it does not authorise it.",
"mimeType": "application/json",
"serviceName": "Incarna",
"tags": ["identity", "agents", "email"]
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:84532",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"amount": "20000",
"payTo": "0xCa1fBb1900e1C17Cc443e34f312720960E72a83F",
"maxTimeoutSeconds": 120
}
]
}
amount is in the asset's own decimals — USDC has 6, so 20000 is $0.02.
The resource block doubles as the service's directory listing, which is how an
agent that has never heard of Incarna finds it — so url is the path it will call
next. It is a template: {agent_id} and {account_id} are the caller's own, and
x402's ResourceInfo has no schema field to declare them, which is why the
description names them and says a bearer token is still required.
2 — Sign and retry
from x402 import x402Client
from x402.schemas.payments import PaymentRequired
required = PaymentRequired.model_validate(r.json())
payload = await client.create_payment_payload(required)
header = base64.b64encode(
json.dumps(payload.model_dump(mode="json", by_alias=True)).encode()).decode()
r2 = await http.post(url, headers={**headers, "X-PAYMENT": header}, json=body)
The authorization is EIP-3009 transferWithAuthorization — a signature, not a
transfer. Nothing moves until settlement.
3 — Receipt
A successful call returns 200 with the action's normal body, plus a base64
X-PAYMENT-RESPONSE header:
{
"success": true,
"transaction": "0xd8b176786a3bbbf7b5f2b9ee66512e9b0ecec8be43f075c2057f1d551579403a",
"network": "eip155:84532"
}
Verify → act → settle
The ordering is the design decision worth knowing about.
verify payment ──► run the action ──► settle payment
│ │ │
reject if if this fails, money moves
invalid nothing was only after the
charged work happened
Payment is verified before the action and settled after it. A caller whose payment is invalid is rejected before any work is done; a caller whose action fails is not charged. The window where we have done the work and not yet been paid is ours, not the customer's — which is the right way round.
maxTimeoutSeconds is 120 so that a slow real-world write (a post through a
residential connection) completes well inside the authorization's validity. Too
short and payers find their authorization expired at settlement.
Ledger
curl "$BASE/x402/payments?agent_id=$AGENT&limit=50" \
-H "Authorization: Bearer $INCARNA_KEY"
Every settled payment is recorded with its on-chain transaction. Amounts are stored as strings — money is never a float. Settlement is idempotent on the transaction hash, so a duplicated receipt cannot double-count.
Recording a payment never fails a request. By the time the ledger is written the
action has happened and the money has moved; turning a bookkeeping error into a
client-visible 500 would make a successful, paid-for action look failed.
A complete run
Real output against production, paying for an email that was actually sent:
payer 0x9c0071bc0F70C45565d42a9469C05bad1dCEDc75 USDC 1.990000
payee 0xCa1fBb1900e1C17Cc443e34f312720960E72a83F USDC 0.010000
[1] unpaid → HTTP 402 terms: 20000 USDC on eip155:84532
[2] signed → X-PAYMENT 1296 bytes
[3] paid → HTTP 200 {"message_id":"<...@email.amazonses.com>"}
[4] settled → tx 0x57453d22acbb24bb4f7bce3e0f004cacc534087b942d271265314b8f976d6f0a
[5] payer USDC 1.990000 → 1.970000
payee USDC 0.010000 → 0.030000
The receipt is a claim; the balance is the evidence.