← Back

← Harnesses

Standard Purchase Harness

For buying from anyone who takes x402 — a paid API, a dataset, a file, another agent’s service. You give your agent the link and what it may spend; it pays in USDC on Base and hands you back what you bought. No account with the seller, no card, no subscription. Buying from a second seller later is one line: add their hostname.

Youlink

The endpoint you want, the most per purchase, the most in total.

Agentprice

Calls it once without paying and tells you the price, before a wallet is funded.

Agentbuys

Pays once, saves what came back, and shows you the payment on Base.

Required

AGENT.md

Paste this into Claude Code, Cursor or any coding agent. It covers the install, the wallet, the limits, the purchase itself and a test to run before the wallet is funded.

AGENT.md buyer · standard purchase
Let me buy things that charge with x402 - any endpoint that answers HTTP 402, whoever runs it.
Paid APIs, data, files, another agent's service. One purchase at a time, from my wallet, with
hard limits I set. No account with the seller, no card, no checkout page, no subscription.

ASK ME THIS FIRST, IN ONE MESSAGE, BEFORE YOU WRITE ANY CODE. Skip what I already told you.

  1. WHAT AM I BUYING? The link, or at least its hostname. Only hosts I name are ever payable.
  2. THE MOST PER PURCHASE, in dollars.
  3. THE MOST IN TOTAL, for the life of this wallet.

  "You pick" means $0.05 a purchase and $2.50 in total - say that is what you used.

  Then call it ONCE WITHOUT PAYING and tell me the price it asks and what it says it sells.
  If my limit is under that price, 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 this back at me.

INSTALL

  npm install x402-trinity

THE WALLET - MAKE A FRESH ONE. It holds only what I am willing to spend. Do not offer to use
a wallet I already keep money in; if I insist, use it, say once why a separate 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, say it needs USDC ON BASE and 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, my side only signs

  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

  // buyer.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 WHAT I SPENT.
  export const budget = createFileBudgetStore('.x402/budget.json');
  export const TOTAL_BUDGET = '2500000';        // answer 3, atomic units ($2.50)

  export const pay = createX402Fetch({
    privateKey: process.env.X402_KEY ?? readFileSync('.x402/wallet.key', 'utf8').trim(),

    // Checked BEFORE anything is signed, so a refused purchase can never become a transfer
    // later. These are refusals, not warnings - they are the whole safety story.
    policy: {
      maxAmountPerRequest: '50000',             // answer 2, atomic units, 6 decimals
      totalBudget: TOTAL_BUDGET,
      allowHosts: ['api.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 script
    // run twice would spend its "lifetime" budget twice. 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),
  });

BUY IT. `pay` is fetch - same arguments, same response - so use whatever the seller's own
documentation says, GET or POST, query or body:

  // buy.mjs
  import { writeFileSync } from 'node:fs';
  import { pay, budget, TOTAL_BUDGET } from './buyer.mjs';

  const res = await pay('https://api.example.com/thing?ticker=AAPL');
  if (!res.ok) { console.log(res.status, await res.text()); process.exit(1); }

  // A file comes back as bytes with a name; anything else is text or JSON. Save it either
  // way - I paid for it, so it should not live only in a terminal buffer - and PRINT it,
  // because what I bought is the point, not the file path.
  const name = /filename="([^"]+)"/.exec(res.headers.get('content-disposition') ?? '')?.[1];
  if (name) {
    const bytes = Buffer.from(await res.arrayBuffer());
    writeFileSync(name, bytes);
    console.log('saved', name, '-', bytes.length, 'bytes');
  } else {
    const text = await res.text();
    writeFileSync('result.json', text);
    console.log(text.slice(0, 4000));
    if (text.length > 4000) console.log('... (rest in result.json)');
  }

  // The payment behind it, for me to check.
  try { console.log('tx', JSON.parse(atob(res.headers.get('payment-response'))).transaction); } catch {}
  const spent = budget.spent();
  console.log('spent $' + (Number(spent) / 1e6).toFixed(4),
              '| left $' + (Number(BigInt(TOTAL_BUDGET) - spent) / 1e6).toFixed(4));

  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,
  so free and paid endpoints work through the same call.

BUYING FROM SOMEONE ELSE LATER. Add their hostname to allowHosts. That is the only change -
not a new integration, not a new account. A seller I have not named is refused.

WHEN A PURCHASE 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.

  - 400 or 422: wrong parameters, usually ours. Read their docs or discovery document
    (openapi.json, llms.txt) and fix the call. Their error message usually names the field.
  - 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 push a purchase through - ask me.
  - "not enough USDC": the wallet is short. Tell me what it holds and what the purchase costs.

WHAT I SPENT

  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. Buy what I asked for, once. No loops, no "while I am here" extra calls, no buying the
     same thing again to look at it twice - save the first response instead.
  3. 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, and there is no signChallenge().
  4. No wallet-connect, no session, no payments table, no database. If you are writing one,
     stop and ask me.
  5. Never log the private key, and never print it on a crash.
  6. Prices are atomic-unit STRINGS. '50000' is five cents. Never use floats.

WHEN IT IS FUNDED, make the purchase and verify that payment on basescan.org before telling
me it worked. A 200 from someone else's server is not evidence that money moved.

  SHOW ME WHAT I BOUGHT, not just where you put it. Put it in your reply: the answer, the
  rows, the summary - whatever it is - trimmed to what is useful, with the file path under
  it. A file I cannot read in a chat (an image, a zip, an audio file) is the exception: say
  what it is, how big, and where it is saved. Then the transaction, what it cost, and what
  is left.

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.