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:
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "3000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x8e37022edcf0f21cf3c9f93fee9d4d32519f36f4",
"maxTimeoutSeconds": 60,
"extra": { "name": "USD Coin", "version": "2" }
}
Read it field by field:
amountis3000, 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.networkis a CAIP-2 chain id.eip155:8453is Base mainnet.assetis the USDC contract on Base, andpayTois the wallet that receives the money.maxTimeoutSecondsis how long the signed authorization may stay valid, here 60 seconds.extracarries the token's EIP-712nameandversion. 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. 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.
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:
{
"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 |
markdown-table-repair |
fixes malformed tables | 0.003 | list |
bulgarian-transliteration |
Latin spelling of Bulgarian text | 0.003 | list |
mojibake-repair |
undoes garbled encodings | 0.004 | list |
csv-repair |
fixes broken CSV | 0.004 | list |
cron-dialect-convert |
converts between cron syntaxes | 0.005 | list |
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
exactscheme 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.
Read this post as Markdown: /blog/x402-protocol-first-payment-example.md · Atom feed.
