For crawlers, data pipelines and agents that meet an HTTP 402 paywall and have to resolve it themselves. The client reads the price, checks it against limits you set, pays in USDC on Base, and returns the response — inside a single fetch call.
Payment Required, with the price, asset and chain in the header.
Price checked against your limits, then an authorization is signed. No gas, no ETH.
The response your code was waiting for, from the same call.
npm install x402-trinityPaste this into Claude Code, Cursor or any coding agent. It covers the install, the wallet, the spend limits, the durable state and a test to run before the wallet is funded.
Make this crawler pay for the data it gets paywalled on, by itself, with hard limits on what
it can spend. For headless things that meet HTTP 402 and decide alone - scrapers, LLM data
crawlers, agent swarms, metered APIs. No account, no checkout page, no human watching.
ASK ME THIS FIRST, IN ONE MESSAGE, BEFORE YOU WRITE ANY CODE. Skip what I already told you.
1. WHAT AM I BUYING? The endpoint, or at least its hostname. Only hosts I name are ever
payable - the difference between buying my data and paying any 402 it stumbles into.
2. THE MOST PER CALL, in dollars.
3. THE MOST IN TOTAL, for the life of this wallet.
"You pick" means $0.005 a call and $2.50 in total - say that is what you used.
Then call the endpoint ONCE WITHOUT PAYING and tell me the price it asks. If my per-call
limit is under that price, every call will be refused: say so now, not after I have funded
a wallet.
KEEP WHAT YOU SAY TO ME SHORT. A few plain lines: what you need, what it costs, what
happened. No lectures, no walls of caveats, no restating the brief back at me.
INSTALL
npm install x402-trinity
THE WALLET - MAKE A FRESH ONE. This crawler spends unattended, so it holds only what I am
willing to lose. Do not offer to use a wallet I already keep money in; if I insist, use it,
say once why a dedicated one is safer, and leave it there.
node -e "Promise.all([import('x402-trinity'),import('node:fs')]).then(([x,fs])=>{const b=crypto.getRandomValues(new Uint8Array(32));const k='0x'+[...b].map(v=>v.toString(16).padStart(2,'0')).join('');fs.mkdirSync('.x402',{recursive:true});fs.writeFileSync('.x402/wallet.key',k,{mode:0o600});console.log('fund this address on Base:',x.__internals.addressOf(x.__internals.toBig(x.fromHex(k))))})"
Then, immediately:
- add .x402/ to .gitignore, and check it is not already committed
- never print the key, never paste it in a chat, never put it in .env
- give me the address and say it needs USDC ON BASE, not Ethereum mainnet - a
wrong-network send is silent and gone - and how much covers what I am buying
- it needs NO ETH: a facilitator pays the gas, this crawler only signs
- in a container or CI, read the key from CRAWLER_KEY so it never touches disk
IF .x402/wallet.key IS MISSING BUT .x402/budget.json EXISTS, STOP. A funded wallet was
lost. Say the key needs restoring rather than quietly making a replacement.
WRITE THIS MODULE - with MY answers in it, not the examples
// crawler.mjs
import { createX402Fetch } from 'x402-trinity';
import { createFileBudgetStore, createFileFeeStore } from 'x402-trinity/budget-file';
import { readFileSync } from 'node:fs';
// Keep the store: it is the durable limit AND the only honest place to read the total
// back from - see REPORTING.
const budget = createFileBudgetStore('.x402/budget.json');
const TOTAL_BUDGET = '2500000'; // answer 3, atomic units ($2.50)
const pay = createX402Fetch({
privateKey: process.env.CRAWLER_KEY ?? readFileSync('.x402/wallet.key', 'utf8').trim(),
// Checked BEFORE anything is signed, so a refused call can never become a transfer
// later. These are refusals, not warnings - they are the whole safety story.
policy: {
maxAmountPerRequest: '5000', // answer 2, atomic units, 6 decimals
totalBudget: TOTAL_BUDGET,
allowHosts: ['data.example.com'], // answer 1 - these hosts and nothing else
allowNetworks: ['base'],
},
// DURABLE, and not optional. An in-memory budget resets with the process, so a crawler
// in a crash loop spends its "lifetime" budget once per restart - no limit at all, and
// it ends in an empty wallet rather than an error. Without a store the library DECLINES
// mainnet payments outright.
budgetStore: budget,
// The protocol fee accrues here between settlements, durable for the same reason.
surcharge: { store: createFileFeeStore('.x402/fees.json') },
onDecline: (d) => console.warn('[x402] refused', d.url, '-', d.reason),
});
USE IT AS FETCH. That is the entire integration:
const res = await pay('https://data.example.com/v1/report');
if (res.ok) process(await res.json());
It sees the 402, reads the price, checks it against my limits, signs an EIP-3009
authorization and retries - internally. Anything that is not a 402 passes straight through.
WHEN A CALL FAILS, SAY WHOSE SIDE IT IS ON. Only a 402 costs money, so none of these charged
me. Say what happened, whose fault it is, that nothing was charged - then stop, do not loop.
- 400 or 422: wrong parameters, usually ours. Read their docs or discovery document
(openapi.json, llms.txt) and fix the call.
- 404: the route moved. Find the current one before trying again.
- 429, 5xx, or an error naming THEIR upstream ("out of searches"): their problem. Tell me,
and suggest another endpoint or seller.
- a refusal in onDecline: MY limits said no. Tell me which limit and what the price was.
Never raise a limit to make a call go through.
REPORTING - READ THE STORE, NOT THE CLIENT
budget.spent() is the money, in atomic units, and it survives restarts.
pay.stats() is the fee tally and the live pool; its .spent counts only this process, so
after a restart it says "nothing spent" when the truth may be "the budget is gone".
RULES
1. allowHosts is not optional. Without it any 402 is payable - including one from a
redirect I did not expect.
2. Do not hand-roll the 402: no parsing the challenge, no signature, no payment header, no
re-issuing the request. The client IS the retry. Touching `payment-signature` is a wrong
turn, and there is no signChallenge() to call.
3. No wallet-connect, no session, no payments table, no webhook listener, no database. If
you are writing one, stop and ask me.
4. Never log the private key, and never print it on a crash.
5. Prices are atomic-unit STRINGS. '5000' is half a cent. Never use floats.
6. One state directory per crawler. Two sharing a budget.json each count the other's spend.
WHEN IT IS LIVE AND FUNDED, make ONE real call to a host I named and verify that payment on
basescan.org before telling me it works. A 200 from someone else's server is not evidence
that money moved. Tell me the transaction, what it cost, and what is left of the budget.
COST. The library takes a small protocol fee from the payer, disclosed in its README. The fee
store above is what lets it settle correctly. Do not remove it.