Angel · Oroboros Design · reference · updated 14 November 2026 (v2)
x402 client integration guide: pay an HTTP 402 from an AI agent
x402 v2 turns 402 Payment Required into a machine-readable offer: the server says what it costs, the agent signs a USDC authorization, the request is repeated and answered. This guide shows the complete client side with code that runs as-is. Every example points at a live x402 endpoint (https://angel.oroboros-design.com/agenten/signal/…) so you can see real headers; swap the URL and the same code pays any x402 v2 seller.
Disclosure: the example endpoints are operated by the author of this guide and are paid services. Probing them (empty body) and the GET previews cost nothing; each agent also gets 3 free calls.
- 1. The flow in four messages
- 2. See a real 402 (curl, free)
- 3. Official client: @x402/fetch
- 4. The $1 default spend cap
- 5. No SDK: plain viem
- 6. Check a 402 before you sign
- 7. Paying inside MCP tool calls
- 8. Pay by transfer instead
- 9. Errors and what they mean
- 10. Endpoint reference
1. The flow in four messages
- Request. The agent calls the resource as usual, e.g.
POST /agenten/signal/preflightwith a JSON body. - 402 + offer. The server answers
402with headerPAYMENT-REQUIRED: base64 of a JSON object{x402Version: 2, resource, accepts: [ … ]}. Each entry inacceptsnamesscheme,network(CAIP-2, Base mainnet iseip155:8453),asset,amount(atomic units, USDC has 6 decimals),payTo,maxTimeoutSecondsandextra(the token's EIP-712 domain name and version). - Signed retry. For the
exactscheme on EVM the agent signs an EIP-3009TransferWithAuthorizationfor exactlyamounttopayTo, then repeats the same request with headerPAYMENT-SIGNATURE: base64 of{x402Version: 2, resource, accepted, payload: {signature, authorization}}, whereacceptedis the chosenacceptsentry, unchanged. - 200 + receipt. The server (through a facilitator) verifies and settles the authorization on-chain, answers the request and returns header
PAYMENT-RESPONSE: base64 JSON with the settlement transaction. The agent never pays gas; the facilitator submits the transfer.
2. See a real 402 (curl, free)
An empty JSON body is a probe: it returns the 402 offer without charging.
curl -s -D - -o /dev/null -X POST https://angel.oroboros-design.com/agenten/signal/preflight \
-H 'content-type: application/json' -d '{}' \
| tr -d '\r' | sed -n 's/^payment-required: //Ip' | base64 -d | jq .
Abridged output:
{
"x402Version": 2,
"resource": { "url": "https://angel.oroboros-design.com/agenten/signal/preflight", "mimeType": "application/json", … },
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"amount": "250000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x0CAc7D3c89be4f478E4c8f713C0D3D2D0954678D",
"maxTimeoutSeconds": 300,
"extra": { "name": "USD Coin", "version": "2" }
}],
"extensions": { "bazaar": { "info": { "input": { … }, "output": { … } } } }
}
"amount": "250000" is 0.25 USDC. The JSON body of the same 402 repeats the offer and adds a plain-language fix field with the exact signing parameters.
3. Official client: @x402/fetch
Tested with @x402/fetch, @x402/evm and @x402/core 2.31.0, Node 20+.
npm i @x402/fetch @x402/evm viem
// pay.mjs — node pay.mjs
import { wrapFetchWithPaymentFromConfig, decodePaymentResponseHeader } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY); // a hot wallet holding a little USDC on Base
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }],
// default spend cap is $1 per payment (section 4); every endpoint in this guide costs at most 0.99 USDC
});
const PREFLIGHT = process.env.X402_PREFLIGHT_URL ?? "https://angel.oroboros-design.com/agenten/signal/preflight";
const res = await fetchWithPayment(PREFLIGHT, {
method: "POST",
headers: { "content-type": "application/json", "X-Payer-Address": account.address },
body: JSON.stringify({ url: "https://api.example.com/v1/quote", method: "GET" }),
});
console.log(res.status, await res.json());
const receipt = res.headers.get("payment-response");
if (receipt) console.log(decodePaymentResponseHeader(receipt)); // settlement transaction on Base
X-Payer-Address is optional. On this endpoint it gives your wallet its own 3 free calls and its price tier; without it the free calls are counted per IP address.
4. The $1 default spend cap
@x402/core 2.31.0 refuses any payment above $1 unless you raise the cap. DEFAULT_MAX_AMOUNT_PER_PAYMENT is "$1". A 402 that asks for more fails before anything is signed, with:
Failed to create payment payload: All payment requirements were rejected by
spendControls.maxAmountPerPayment ($1, including USDC). Raise maxAmountPerPayment, …
Every endpoint in this guide is priced at or below 0.99 USDC, so the default works unchanged. For sellers above $1, set spendControls: { maxAmountPerPayment: "$3" } (or your own budget) in the config, or call client.setSpendControls({ maxAmountPerPayment: "$3" }) on an x402Client. false disables the cap; keep a cap for autonomous agents.
5. No SDK: plain viem
The same protocol in 40 lines, with your own checks before signing. This is the code used for the first paid calls against the endpoints above.
// fetch402.mjs — npm i viem
import { randomBytes } from "node:crypto";
import { privateKeyToAccount } from "viem/accounts";
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const MAX_ATOMIC = 1_000_000n; // your cap: 1.00 USDC per call
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);
export async function fetch402(url, init = {}) {
const first = await fetch(url, init);
if (first.status !== 402) return first;
const offer = JSON.parse(Buffer.from(first.headers.get("payment-required"), "base64").toString("utf8"));
const a = offer.accepts.find((x) =>
x.scheme === "exact" && x.network === "eip155:8453" && x.asset.toLowerCase() === USDC_BASE.toLowerCase());
if (!a) throw new Error("no USDC-on-Base offer");
if (BigInt(a.amount) > MAX_ATOMIC) throw new Error(`price ${a.amount} above cap`);
if (new URL(offer.resource.url).host !== new URL(url).host) throw new Error("offer is for another host");
const now = Math.floor(Date.now() / 1000);
const authorization = {
from: account.address, to: a.payTo, value: a.amount,
validAfter: String(now - 30), validBefore: String(now + 600),
nonce: `0x${randomBytes(32).toString("hex")}`,
};
const signature = await account.signTypedData({
domain: { name: a.extra?.name ?? "USD Coin", version: a.extra?.version ?? "2", chainId: 8453, verifyingContract: a.asset },
types: { TransferWithAuthorization: [
{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" } ] },
primaryType: "TransferWithAuthorization",
message: { ...authorization, value: BigInt(authorization.value),
validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) },
});
const header = Buffer.from(JSON.stringify({
x402Version: 2, resource: offer.resource, accepted: a, payload: { signature, authorization },
})).toString("base64");
return fetch(url, { ...init, headers: { ...(init.headers ?? {}), "PAYMENT-SIGNATURE": header } });
}
// usage
const r = await fetch402("https://angel.oroboros-design.com/agenten/signal/preflight", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ url: "https://api.example.com/v1/quote", method: "GET" }),
});
console.log(r.status, await r.json());
Notes: validBefore should stay short (60–3600 s); the nonce must be fresh random 32 bytes; accepted must be the offer entry exactly as received, or verification fails.
The same flow with the checks below built in: angel-agent-guard (MIT) — createX402Fetch({ privateKey, maxAmountAtomic }) signs only after the offer passes, and verifyBeforePay(url) runs the checks on their own.
6. Check a 402 before you sign
A signature is money. Before signing, an agent should confirm:
- Asset is the official token contract for the network (USDC on Base:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913), not a lookalike with the same symbol. - Network is the one you hold funds on (
eip155:8453for Base mainnet). - Amount in atomic units is what the listing advertised and below your per-call cap.
- payTo matches what the seller publishes (
/.well-known/x402, OpenAPI, earlier receipts). - extra.name / extra.version match the token's EIP-712 domain (
"USD Coin","2"for USDC on Base); a mismatch means an invalid signature or a different token. - resource.url is on the host you called; no cross-host redirect.
- Scheme is
exact: you authorize one transfer of one amount, never an open allowance.
The same checks, run server-side against any URL, plus latency and discovery files:
# free preview: verdict, 3 reasons, number of fixes
curl -s "https://angel.oroboros-design.com/agenten/signal/preflight?url=https://api.example.com/v1/quote"
# full report (0.25 USDC via x402; first 3 calls free per agent)
curl -s -X POST https://angel.oroboros-design.com/agenten/signal/preflight \
-H 'content-type: application/json' \
-d '{"url":"https://api.example.com/v1/quote","method":"GET"}'
The answer is verdict (pay, caution or do_not_pay), a 0–100 score, every reason, the cheapest verified option and the fixes a seller should make.
7. Paying inside MCP tool calls
The same endpoints are an MCP server (streamable HTTP): https://angel.oroboros-design.com/agenten/signal/mcp.
# Claude Code
claude mcp add --transport http angel https://angel.oroboros-design.com/agenten/signal/mcp
# clients that take a JSON config (Cursor, Windsurf, others)
{ "mcpServers": { "angel": { "url": "https://angel.oroboros-design.com/agenten/signal/mcp" } } }
An unpaid tools/call past the free calls returns isError: true with the 402 offer in structuredContent. Pay by repeating the call with the payment object in params._meta["x402/payment"] (the decoded JSON you would base64 into PAYMENT-SIGNATURE; a base64 string is accepted too). The receipt comes back in result._meta["x402/payment-response"].
{
"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {
"name": "preflight",
"arguments": { "url": "https://api.example.com/v1/quote", "method": "GET" },
"_meta": { "x402/payment": { "x402Version": 2, "resource": { … }, "accepted": { … },
"payload": { "signature": "0x…", "authorization": { … } } } }
}
}
8. Pay by transfer instead
Wallets that cannot sign EIP-712 can send the USDC first: transfer at least amount to payTo on Base in one transaction, then repeat the request with header X-PAYMENT-TX: <transaction hash>. Each hash is accepted once. For many calls, buy a prepaid pack once (POST /agenten/signal/pack, or by card at /buy) and send X-Bundle-Token: no per-call signing.
9. Errors and what they mean
| Response | Meaning | Fix |
|---|---|---|
402, error starts with payment_missing | No payment header and no free call left. | Sign and retry with PAYMENT-SIGNATURE, or use X-PAYMENT-TX / X-Bundle-Token. |
402, error_code: free_tier_exhausted | This agent used its 3 free calls (30 days). | Pay as above; the body carries the offer and a card checkout link. |
402, error_code: free_tier_daily_anonymous_exhausted | The shared daily pool for callers without wallet or key is used up. | Add header X-Payer-Address (or X-API-Key) to get your own 3 free calls. |
400 bad_request, charged: false | The body failed validation; nothing was charged, even with a payment header. | Read message, fix the body, resend. |
Client throws rejected by spendControls.maxAmountPerPayment | Price above your client's cap ($1 by default). | Raise maxAmountPerPayment (section 4). |
| 402 again after a signed retry | Verification failed: wrong accepted object, expired validBefore, reused nonce, or not enough USDC. | Re-read the new offer, check balance, sign a fresh authorization. |
10. Endpoint reference
All POST, JSON body, base https://angel.oroboros-design.com. The 402 always states the exact amount for your address.
| Path | Use it when | USDC |
|---|---|---|
/agenten/signal/preflight | you are about to pay an unknown 402 endpoint | 0.25 |
/agenten/signal/reconciliation | you need to confirm expected USDC payments arrived on Base | 0.25 |
/agenten/signal/registry-metrics | you want measured reliability of an x402 endpoint | 0.25 |
/agenten/signal/gas-window | you are deciding whether to send a transaction now or wait | 0.25 |
/agenten/signal/input-guard | you are about to read untrusted web, email or tool content (guide) | 0.99 |
/agenten/signal/token-risk | you are about to swap into or approve an ERC-20 (honeypot / rug-pull check) | 0.99 |
Machine-readable: llms.txt · OpenAPI · /.well-known/x402 · agent.md. Market context: State of x402 sellers, November 2026.