# 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](#payment-is-metering-not-authentication).

This is [x402](https://x402.org), 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

```sh
curl $BASE/x402/tools
```

```json
{
  "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](rest.md).

## The exchange

### 1 — Call without payment

```sh
curl -X POST $BASE/x402/agents/$AGENT/email/send \
  -H 'Content-Type: application/json' \
  -d '{"to":"someone@example.com","subject":"Hi","body":"..."}'
```

```json
{
  "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

```python
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:

```json
{
  "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

```sh
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.
