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

  1. Request. The agent calls the resource as usual, e.g. POST /agenten/signal/preflight with a JSON body.
  2. 402 + offer. The server answers 402 with header PAYMENT-REQUIRED: base64 of a JSON object {x402Version: 2, resource, accepts: [ … ]}. Each entry in accepts names scheme, network (CAIP-2, Base mainnet is eip155:8453), asset, amount (atomic units, USDC has 6 decimals), payTo, maxTimeoutSeconds and extra (the token's EIP-712 domain name and version).
  3. Signed retry. For the exact scheme on EVM the agent signs an EIP-3009 TransferWithAuthorization for exactly amount to payTo, then repeats the same request with header PAYMENT-SIGNATURE: base64 of {x402Version: 2, resource, accepted, payload: {signature, authorization}}, where accepted is the chosen accepts entry, unchanged.
  4. 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:

  1. Asset is the official token contract for the network (USDC on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913), not a lookalike with the same symbol.
  2. Network is the one you hold funds on (eip155:8453 for Base mainnet).
  3. Amount in atomic units is what the listing advertised and below your per-call cap.
  4. payTo matches what the seller publishes (/.well-known/x402, OpenAPI, earlier receipts).
  5. 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.
  6. resource.url is on the host you called; no cross-host redirect.
  7. 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

ResponseMeaningFix
402, error starts with payment_missingNo 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_exhaustedThis 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_exhaustedThe 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: falseThe body failed validation; nothing was charged, even with a payment header.Read message, fix the body, resend.
Client throws rejected by spendControls.maxAmountPerPaymentPrice above your client's cap ($1 by default).Raise maxAmountPerPayment (section 4).
402 again after a signed retryVerification 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.

PathUse it whenUSDC
/agenten/signal/preflightyou are about to pay an unknown 402 endpoint0.25
/agenten/signal/reconciliationyou need to confirm expected USDC payments arrived on Base0.25
/agenten/signal/registry-metricsyou want measured reliability of an x402 endpoint0.25
/agenten/signal/gas-windowyou are deciding whether to send a transaction now or wait0.25
/agenten/signal/input-guardyou are about to read untrusted web, email or tool content (guide)0.99
/agenten/signal/token-riskyou 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.