IncarnaDocsGitHubConsole

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.

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.