# Buy from the x402 Trinity Marketplace Paste this whole file into your coding agent — Claude Code, Cursor, whatever you build with. It has everything needed to buy from the marketplace, APIs and files alike, and it never spends more than the human ordered. --- ## What you are setting up Every listing has an endpoint that looks like `https://x402trinity.com/m/lst_…`. Calling it returns **HTTP 402** with a price. Your agent pays that price from its own wallet and gets the goods back in the same response. There is no checkout page and no session. Two kinds of listing: - **API** — paid **per request**. - **File** — a digital product, bought **once**. You get the file, plus a receipt link to download it again without paying. --- ## 1. The order **If this file starts with an order, use it exactly and go to step 2.** The human chose it on the listing; do not ask again and do not change it. Otherwise, ask the human for: 1. **What to buy** — the listing's link. 2. **How many requests** — for an API. Do not decide this yourself. The easiest way for them is to open the listing on the marketplace, choose the amount, and press **Copy for your agent** — then paste that here instead. If you must fill the order yourself: call the listing once with a plain `fetch`. The 402 body has a `price`. Give the order any `id`, set `maxPayment` to that price, `total` to price × requests, `bulkAfter` to 100, and leave `bulk` false unless the human asked to prepay in bulk. --- ## 2. Install npm install x402-trinity --- ## 3. The wallet — generate a fresh one This is the default and the right answer. An agent that spends on its own should hold only what you are willing for it to spend. Do not paste in the key of a wallet you keep money in. 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` - never print the key, never paste it into a chat, never put it in `.env` - tell the human the address, what the order costs, and that it needs **USDC on Base** — not Ethereum mainnet - it needs **no ETH**. Your agent only ever signs; it never pays gas. If the order came with a **buyer key**, save it to `.x402/buyer.key`. It puts the purchases in the human's Library; it cannot spend anything. **If `.x402/wallet.key` is missing but `.x402/budget.json` exists, stop.** A funded wallet was lost. Say the key needs restoring rather than generating a replacement. --- ## 4. Write this module A new order replaces `ORDER` and nothing else. ```js // x402.mjs import { createX402Fetch } from 'x402-trinity'; import { createFileBudgetStore, createFileFeeStore } from 'x402-trinity/budget-file'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; // What the human ordered. Never raise these yourself. export const ORDER = { id: '[ORDER_ID]', total: '[TOTAL]', // the most this order may spend, atomic USDC apis: { api: { listing: 'https://x402trinity.com/m/[LISTING_ID]', requests: [REQUESTS], // the most requests of this API bulk: [BULK], // true: after the first bulkAfter, prepay the rest in one payment bulkAfter: [BULK_AFTER], maxPayment: '[MAX_PAYMENT]', // its largest single payment, atomic USDC }, }, }; // The state folder must exist before the stores touch it - and it will not if the key came from // X402_KEY instead of the wallet generator. mkdirSync('.x402', { recursive: true }); export const budget = createFileBudgetStore('.x402/budget.json'); // The budget file counts everything this wallet has ever spent, so each order's cap is fixed // the first time it runs: what was spent before it, plus its total. Running it again - after a // crash, say - can never spend more than the order. const ORDERS = '.x402/orders.json'; const orders = existsSync(ORDERS) ? JSON.parse(readFileSync(ORDERS, 'utf8')) : {}; orders[ORDER.id] ??= { spentBefore: String(budget.spent()) }; writeFileSync(ORDERS, JSON.stringify(orders)); /** What this order has spent so far, atomic USDC. */ export const orderSpent = () => BigInt(budget.spent()) - BigInt(orders[ORDER.id].spentBefore); export const pay = createX402Fetch({ privateKey: process.env.X402_KEY ?? readFileSync('.x402/wallet.key', 'utf8').trim(), // Checked BEFORE anything is signed. These are refusals, not warnings. policy: { maxAmountPerRequest: String(Object.values(ORDER.apis).reduce((m, a) => (BigInt(a.maxPayment) > m ? BigInt(a.maxPayment) : m), 0n)), totalBudget: String(BigInt(orders[ORDER.id].spentBefore) + BigInt(ORDER.total)), allowHosts: ['x402trinity.com'], // Every marketplace payment goes to this one contract, which pays the seller and the fee // in the same transaction. Pinning it means nothing else can ever be paid. allowPayTo: ['0xde946d3f483c1b1b3feb63e2557e72afa618f007'], allowNetworks: ['base'], }, // DURABLE, and not optional. An in-memory budget resets when the process restarts, which // is no limit at all. Without it the library declines mainnet payments outright. budgetStore: budget, surcharge: { store: createFileFeeStore('.x402/fees.json') }, onDecline: (d) => console.warn('[x402] refused', d.url, '-', d.reason), }); // Puts each purchase in the human's Library. Without it, buying works exactly the same. const buyerKey = process.env.MARKETPLACE_KEY || (existsSync('.x402/buyer.key') ? readFileSync('.x402/buyer.key', 'utf8').trim() : ''); export const withKey = buyerKey ? { headers: { 'x-marketplace-key': buyerKey } } : {}; // The on-chain payment behind a response - the link the human gets. A single request is // answered as soon as it is delivered and settled a moment later, so this waits for it. export async function txOf(res) { try { return JSON.parse(atob(res.headers.get('payment-response'))).transaction; } catch {} const id = res.headers.get('x-marketplace-payment'); if (!id) return null; const origin = new URL(Object.values(ORDER.apis)[0].listing).origin; for (let i = 0; i < 30; i++) { const p = await (await fetch(`${origin}/v1/payments/${id}`)).json().catch(() => ({})); if (p.status === 'settled') return p.transaction; if (p.status === 'failed') return null; await new Promise((r) => setTimeout(r, 1000)); } return null; } const FIRST = Object.keys(ORDER.apis)[0]; /** * Call an API of the order - wherever the human's work needs it, with whatever input it takes: * callApi('?city=Chicago') * callApi('', { method: 'POST', headers: { 'content-type': 'application/json' }, body }) * callApi('?q=x402', {}, 'search') // a bundle: name the API * Each successful call is one request of that API. It stops at the order's limit for it. */ export async function callApi(input = '', init = {}, name = FIRST) { const api = ORDER.apis[name]; if (!api) throw new Error(`this order has no API called "${name}" - it has: ${Object.keys(ORDER.apis).join(', ')}`); const o = orders[ORDER.id]; o.used ??= {}; const used = o.used[name] ?? 0; if (api.requests != null && used >= api.requests) { return new Response(JSON.stringify({ error: `this order is out of ${name} requests - top up from the listing` }), { status: 429 }); } // A bulk order, once past the first requests, prepays everything still to come. const bulk = api.bulk && used >= api.bulkAfter ? { 'x-marketplace-bulk': String(api.requests - used) } : {}; // A time pass: the first request buys it, the rest are served free until it ends. const pass = api.pass ? { 'x-marketplace-pass': api.pass } : {}; const res = await pay(api.listing + input, { ...init, headers: { ...withKey.headers, ...init.headers, ...bulk, ...pass } }); if (res.ok) { o.used[name] = used + 1; writeFileSync(ORDERS, JSON.stringify(orders)); } return res; } /** One API of a bundle, as a function: const search = useApi('search'); await search('?q=x402') */ export const useApi = (name) => (input, init) => callApi(input, init, name); /** What has been bought on this order, and what it cost - for telling the human. */ export const usage = () => { const byApi = orders[ORDER.id].used ?? {}; return { used: Object.values(byApi).reduce((n, v) => n + v, 0), byApi, spent: '$' + (Number(orderSpent()) / 1e6).toFixed(4), }; }; ``` --- ## 5. Use an API An API order is a **limit**, not a purchase: nothing is paid up front, each request is paid when it is used, and the order stops at its limit. Do not call it in a loop to use it up. Make one call now to check it works: ```js // try-api.mjs // One call, to check it works and show the human what comes back. // Pass the API's input if it takes one: node try-api.mjs "?city=Chicago" import { callApi, usage, txOf } from './x402.mjs'; const res = await callApi(process.argv[2] ?? ''); console.log(res.status, await res.text()); const tx = await txOf(res); const u = usage(); console.log(`${u.used} bought so far, for ${u.spent}`); if (tx) console.log(`https://basescan.org/tx/${tx}`); ``` Then use `callApi` wherever the human's work needs this API. It works like `fetch`: it pays, checks every payment against the order, and hands back the seller's response. Use that response in the work - it is what the human paid for. --- ### A time pass If the order has a `pass`, the first call buys it - one payment - and every call after that is served free until the pass ends (or reaches its request cap). Use `callApi` exactly the same way. When the pass is over, the order's limit stops a second one being bought: tell the human it has ended and that they can top up from the listing. Each response says when the pass ends in its `x-marketplace-pass-ends` header. --- ## 6. Use a bundle A bundle order has several APIs and a `workflow` - the job they do together. Do not make a check call. Tell the human it is ready and ask what to run it on, then follow the workflow each time they ask, calling each API by name: ```js // run-bundle.mjs - the shape of a run. Write the real one from ORDER.workflow. import { useApi, usage, ORDER } from './x402.mjs'; console.log(ORDER.workflow); for (const name of Object.keys(ORDER.apis)) console.log(name, '- up to', ORDER.apis[name].requests, 'requests'); // e.g. const search = useApi('search'); const results = await (await search('?q=' + topic)).json(); console.log(usage()); ``` Each API is paid separately, only when it delivers, and each stops at its own limit. --- ## 7. Buy a file ```js // buy-file.mjs import { writeFileSync } from 'node:fs'; import { pay, withKey, txOf, ORDER } from './x402.mjs'; const res = await pay(Object.values(ORDER.apis)[0].listing, withKey); if (res.ok) { const name = /filename="([^"]+)"/.exec(res.headers.get('content-disposition') ?? '')?.[1] ?? 'download'; writeFileSync(name, Buffer.from(await res.arrayBuffer())); // Keep this. It downloads the file again - up to 25 times - with no payment. const receipt = res.headers.get('x-marketplace-download'); writeFileSync(name + '.receipt.txt', receipt ?? ''); console.log('saved', name); const tx = await txOf(res); if (tx) console.log(`https://basescan.org/tx/${tx}`); } else { console.log(res.status, await res.text()); } ``` **Do not buy the same file twice to download it again.** Fetch the receipt link with a plain `fetch` — it is free. --- ## 8. Tell the human Keep it short and plain: - **a file:** that it is saved, where, what it cost, the transaction link, and that it is in their Library to download again - **a bundle:** what it did, the result, what each API was used for and what it all cost - then what they can ask you to run it on next - **an API:** that it is ready, what the first call returned, how many requests have been bought and what they cost (each is paid only when used), the transaction link, and that it shows in their Library. Only mention the order's limit if it has been reached. - if something stopped: why, and what to do (for example, add USDC) Then, for an API or a bundle, tell them **what they can now ask you for** - in their words, not the API's. Take it from the order's **what it does** line and what the first call returned, and give one example they could say, for example: *"You can ask me to check any website - try 'is example.com up?'"*. Say they can just ask whenever their work needs it; you will use the API for them, and each request is paid only when it is used. --- ## What things cost The price on the card is the seller's price. | | you pay | |---|---| | A file | **exactly the listed price** | | An API, your first 100 requests | the price **plus a small fee** — 1% or $0.005, whichever is greater | | An API, after that | **the listed price**, flat — unless it costs under $0.02, where the fee stays on top | | Bulk, after the first 100 | **exactly the listed price × the requests**, in one payment of at least $0.50 | **Bulk** is only ever bought when the human's order says so. Those requests are then served without paying again, and any left unused stay with the human for next time. --- ## Responses you may get back | response | what it means | |---|---| | `200` | Paid and delivered. | | `401` "buyer key is not valid" | The buyer key is wrong or was turned off. Get a new order from the listing, or delete `.x402/buyer.key`. **Nothing was charged.** | | `402` "not enough USDC" | The wallet has run out. Stop, tell the human how much is left and ask them to add USDC on Base. **Nothing was charged.** | | `404` | That listing does not exist, or the seller took it down. | | a refusal in `onDecline` | The order's own limits said no. Nothing was signed. Tell the human; do not raise the limits. | | `429` "out of requests" | Every request in the order has been used. Tell the human they can top up from the listing. | | a refusal after a pass ends | The pass is over and the order does not buy another. Tell the human they can top up from the listing. | | `424` "the seller could not deliver" | The seller's API failed this time. **Nothing was charged** and it does not count against the order. Try again later, or tell the human. | | another `4xx` from the seller | The seller refused the input (for example, a bad URL). It says why. **Nothing was charged.** | --- ## Rules 1. **Buy exactly the order.** Never raise `maxPayment` or `total`, never add bulk on your own. 2. **Do not hand-roll the 402.** Do not parse the challenge, build a signature, or set a payment header yourself. The client does all of it. 3. **Keep `allowHosts` and `allowPayTo`.** Without them any 402 from anywhere is payable. 4. **Prices are atomic-unit strings.** `'20000'` is two cents. Never use floats. 5. **Read spend from `budget.spent()`**, not from `pay.stats()` — the store survives restarts, the stats do not. 6. **Before telling the human it is done, check one real payment on basescan.org.** A `200` is not proof that money moved.