Plain fetch with x402: one paid call, step by step
Last updated: 2026-10-10
One paid call to POST /v1/verify/quote (US$0.01) with plain fetch, showing each step of an x402 payment: read the offer, check it, sign it, send the request again, read the settlement, then check the receipt for free. Use this to wire AttestPage into any agent framework, or to see what an x402 client library does for you.
Payments run on Base Sepolia (eip155:84532), a test network, in test USDC.
Install
Node.js 20 or later.
npm init -y && npm pkg set type=module
npm install @x402/core @x402/evm viem
Set ATTESTPAGE_X402_KEY to the private key (0x and 64 hex digits) of a Base Sepolia wallet made only for testing, holding test USDC. The key stays in this process.
fetch-x402.mjs
// One paid AttestPage call with plain fetch and x402, step by step.
import { x402Client, x402HTTPClient } from '@x402/core/client';
import { registerExactEvmScheme } from '@x402/evm/exact/client';
import { privateKeyToAccount } from 'viem/accounts';
const BASE = (process.env.ATTESTPAGE_URL || 'https://vehcdj664efetfrsolne5umanq.srv.us').replace(/\/+$/, '');
const NETWORK = 'eip155:84532'; // Base Sepolia
const MAX_USD = 0.01; // the most this script will pay; verify/quote is listed at US$0.01
const client = new x402Client();
registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.ATTESTPAGE_X402_KEY) });
const x402 = new x402HTTPClient(client);
const url = `${BASE}/v1/verify/quote`;
const request = {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
url: process.argv[2] || 'https://www.iana.org/help/example-domains',
quote: process.argv[3] || 'These domains may be used as illustrative examples in documents',
}),
};
// 1. Send the request with no payment. A valid request gets 402, with the offer in PAYMENT-REQUIRED.
let res = await fetch(url, request);
if (res.status !== 402) throw new Error(`expected 402, got ${res.status}: ${await res.text()}`);
const offer = x402.getPaymentRequiredResponse((name) => res.headers.get(name), await res.json());
// 2. Check the offer before signing: right network, price within MAX_USD (USDC has 6 decimals).
const accepts = offer.accepts.filter((a) => a.network === NETWORK && Number(a.amount) <= MAX_USD * 1e6);
if (accepts.length === 0) throw new Error(`no acceptable offer: ${JSON.stringify(offer.accepts)}`);
console.log(`price: US$${Number(accepts[0].amount) / 1e6} on ${accepts[0].network}, pay to ${accepts[0].payTo}`);
// 3. Sign the offer and send the same request again with PAYMENT-SIGNATURE.
const payment = await x402.createPaymentPayload({ ...offer, accepts });
res = await fetch(url, { ...request, headers: { ...request.headers, ...x402.encodePaymentSignatureHeader(payment) } });
const answer = await res.json();
if (!res.ok) throw new Error(`${res.status} ${answer.error?.code}: ${answer.error?.message}`);
// 4. A settled call carries PAYMENT-RESPONSE: the transaction, payer and network.
const settlement = x402.getPaymentSettleResponse((name) => res.headers.get(name));
console.log('settled:', settlement.transaction, 'from', settlement.payer, 'on', settlement.network);
console.log('match:', answer.result.match, 'score:', answer.result.score, 'page:', answer.page.content_kind);
// 5. Check the receipt's signature for free.
const check = await fetch(`${BASE}/v1/receipt/verify`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ receipt: answer.receipt }),
}).then((r) => r.json());
console.log('receipt valid:', check.valid);
Run
ATTESTPAGE_X402_KEY=0x... node fetch-x402.mjs
ATTESTPAGE_X402_KEY=0x... node fetch-x402.mjs https://example.com 'Example Domain'
Notes
- Only the price. An unpaid request with an empty body (
{}) also gets the 402 offer, so a client can read a route's price before it has a real input. - Bad input is free. A request that fails validation gets
400before any offer and is never charged. - Settled only on success. The payment settles only when the route answers with a status below 400.
- Spent signatures. A payment signature pays for one call. Sending it again gets
409 payment_already_used; sign a new offer instead. - Shorter.
wrapFetchWithPaymentfrom@x402/fetchdoes steps 1 to 3 insidefetch; openai-agents-sdk.md uses it with a price limit. - Other routes. The same steps work for every paid route; only the body and the price change. See README.md.