# How the x402 protocol works, with one real payment

> A walk through one paid HTTP call on Base: the 402 answer, the signed USDC authorization, the receipt, and what we learned when the first body was wrong.

Published 2026-10-16 · https://aiskills402.com/blog/x402-protocol-first-payment-example

The x402 protocol lets a program pay for an HTTP call with no account and no API key. The server answers an unpaid request with status 402 and a description of the price; the client signs a USDC authorization, repeats the same request with the signature attached, and gets the result together with a receipt. Below is one real call we made on 16 October 2026, step by step, so you can see every field a client has to read.

## Step 1: ask without paying

We sent a plain `POST` with a JSON body to one of our endpoints, `jsonl-repair`, which turns broken JSON Lines into valid records. No payment header, no key.

The answer was `402 Payment Required`. The requirements arrive twice, in the body and in a `PAYMENT-REQUIRED` response header. The header is base64 of the same UTF-8 JSON. Decoded, the first entry of `accepts` looked like this:

```json
{
  "scheme": "exact",
  "network": "eip155:8453",
  "amount": "3000",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "payTo": "0x8e37022edcf0f21cf3c9f93fee9d4d32519f36f4",
  "maxTimeoutSeconds": 60,
  "extra": { "name": "USD Coin", "version": "2" }
}
```

Read it field by field:

- `amount` is `3000`, counted in the tiniest unit of the token. USDC divides a dollar into 6 decimal places, so this is 0.003 USDC, three tenths of a cent.
- `network` is a CAIP-2 chain id. `eip155:8453` is Base mainnet.
- `asset` is the USDC contract on Base, and `payTo` is the wallet that receives the money.
- `maxTimeoutSeconds` is how long the signed authorization may stay valid, here 60 seconds.
- `extra` carries the token's EIP-712 `name` and `version`. A client needs both to build the typed message it signs; without them nobody can sign.

## Step 2: sign with the official client

The `exact` scheme uses [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009). The buyer signs a `transferWithAuthorization` message that allows exactly this amount to move to `payTo`. Nothing is broadcast by the buyer, so the buyer needs no gas; the seller's facilitator submits it on chain.

You do not need to build the message by hand. The project publishes packages for it; below is the code path we ran, with `@x402/core` and `@x402/evm` 2.28. The key comes from an environment variable and never from the source file.

```js
import { x402Client, x402HTTPClient } from "@x402/core/client";
import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

const signer = toClientEvmSigner(privateKeyToAccount(process.env.BUYER_KEY));
const http = new x402HTTPClient(
  x402Client.fromConfig({ schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(signer) }] })
);

const body = JSON.stringify({ input: "..." });
const first = await fetch(url, { method: "POST", headers, body });
const required = http.getPaymentRequiredResponse((n) => first.headers.get(n), await first.json());
const payload = await http.createPaymentPayload(required, url);
const paid = await fetch(url, {
  method: "POST",
  headers: { ...headers, ...http.encodePaymentSignatureHeader(payload) },
  body, // the same body as before
});
```

Import paths can move between versions, so check the package readme for the one you install. Two points matter more than the syntax. First, the second request carries the same body as the first, because the signature is tied to this one call. Second, the signature travels in the `PAYMENT-SIGNATURE` header, which is base64 of UTF-8 JSON as well.

That second point cost us a purchase on 30 September 2026. A facilitator rejected our first mainnet sale as an invalid payload, because a description containing a non-ASCII character had been decoded with a bare `atob`, which does not understand UTF-8. Decode every x402 header as bytes and then as UTF-8, on both the sending and the receiving side.

## Step 3: the result and the receipt

With the signature attached, the server answered `200`. The body held the result and a receipt:

```json
{
  "ok": true,
  "skill": "jsonl-repair",
  "result": {
    "jsonl": "{\"id\":1,\"ok\":true}\n{\"id\":2,\"ok\":null,\"price\":1.10}",
    "records": 2
  },
  "refusal": null,
  "receipt": {
    "payment_id": "c369a4d7-8518-4f85-80ed-4ae8bed1977b",
    "tx": "0xc741aa11e39c09bb0ec2975e7b6b738d7cea739d50b68524b2e05736c88bac8c",
    "network": "eip155:8453",
    "paid_usd": "0.003",
    "settled": true
  }
}
```

Two fields are trimmed here, the engine version and an input hash. The `tx` value is a public transaction on Base: `https://basescan.org/tx/0xc741aa11e39c09bb0ec2975e7b6b738d7cea739d50b68524b2e05736c88bac8c`. Anyone can open it and see 0.003 USDC move from the buyer to the seller. The result matched, byte for byte, what the same code produced locally.

## What we learned

We made three calls that day, and two of them taught us more than the clean one.

**Input is checked before money moves.** One attempt used a wrong body: it included an `output` field the endpoint does not accept. The server answered `400` and said that nothing was settled or charged. Whether a bad request costs anything is a choice each seller makes, so read the seller's documentation; on our endpoints the check comes first.

**On our endpoints a refusal is still a paid answer.** Our first test input had broken lines the engine could not read, and it returned `CANNOT REPAIR` with a reason. That call was settled and had its own receipt. We price it that way on purpose, because the engine did the work of examining the input. It is how this service is priced, not a rule of the protocol, so a client should look at the `refusal` field before it treats a 200 as success.

**A 402 is not an error to retry.** It is an offer, in plain words: this is the price, pay it or leave.  Show it to whatever decides whether to pay, whether that is a person, a budget rule or an agent policy, before the client signs anything.

## The six endpoints

The public list at `https://api.aiskills402.com/v1/run` shows six small deterministic endpoints, with no language model behind them. Each has a free `GET /v1/run/<slug>/example` page with its input schema. Prices in USDC per call, as listed on 16 October 2026:

| Endpoint | What it does | Price | Source |
|---|---|---|---|
| `jsonl-repair` | fixes broken JSON Lines | 0.003 | [list](https://api.aiskills402.com/v1/run) |
| `markdown-table-repair` | fixes malformed tables | 0.003 | [list](https://api.aiskills402.com/v1/run) |
| `bulgarian-transliteration` | Latin spelling of Bulgarian text | 0.003 | [list](https://api.aiskills402.com/v1/run) |
| `mojibake-repair` | undoes garbled encodings | 0.004 | [list](https://api.aiskills402.com/v1/run) |
| `csv-repair` | fixes broken CSV | 0.004 | [list](https://api.aiskills402.com/v1/run) |
| `cron-dialect-convert` | converts between cron syntaxes | 0.005 | [list](https://api.aiskills402.com/v1/run) |

The list is the live source of these prices; ours may change, the answer you receive when you call is what counts.

## When this does not apply

This is one afternoon on one network, and it shows a happy path.

- **Base mainnet only.** We did not try other chains or tokens, and the `exact` scheme is only one of the shapes the protocol allows.
- **Our wallets on both sides.** The buyer and the seller in this demo both belong to us, so the net movement was zero and we saw no price pressure, no failed settlement and no foreign client.
- **One day, protocol version 2.** The header names and fields above are what we saw on 16 October 2026. A later version may move them.
- **One client library.** We used the official packages; a hand-written client has to repeat every step, including the UTF-8 rule.
- **Your seller decides the rest.** Whether a 400 is free, whether a refusal is charged and how long a retry is free all vary by seller. Our statements cover our endpoints only.
