# Plain fetch with x402: one paid call, step by step

Last updated: 2026-10-18

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 (`eip155:8453`), in USDC.

## Install

Node.js 20 or later.

```sh
npm init -y && npm pkg set type=module
npm install @x402/core@2.28.0 @x402/evm@2.28.0 viem@2.57.4
```

Set `ATTESTPAGE_X402_KEY` to the private key (`0x` and 64 hex digits) of a Base wallet holding a little USDC (real money). The key stays in this process.

## fetch-x402.mjs

```js
// 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://s2h6qt73jtzlgbgnnfacln2poq.srv.us').replace(/\/+$/, '');
const NETWORK = process.env.ATTESTPAGE_NETWORK || 'eip155:8453'; // Base; ATTESTPAGE_NETWORK picks another network the 402 lists
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

```sh
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 `400` before any offer and is never charged.
- **Settled only below 400.** The payment settles only when the route answers with a status below 400.
- **Spent signatures.** A payment signature pays for one call. Sending the same request with the same signature again gets the stored result back (marked `idempotent-replayed: true`), with no second charge. Sending that signature with a different request gets a 409 answer, not charged (see [errors](/docs/errors)); sign a new offer instead.
- **Shorter.** `wrapFetchWithPayment` from `@x402/fetch` does steps 1 to 3 inside `fetch`; [openai-agents-sdk.md](/docs/examples/openai-agents-sdk) 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](/docs/examples#routes-and-prices).

Prices are per call in USDC. The full list is at [`/pricing.json`](https://s2h6qt73jtzlgbgnnfacln2poq.srv.us/pricing.json).
