How to Build an AI Agent That Can Pay for Its Own APIs
An AI agent can find an API on the x402 Bazaar, check the price, pay in USDC from its own wallet and collect the result, with no account or API key set up by a human. This tutorial builds one in TypeScript that buys a voiceover inside a budget you set.

Every AI agent framework eventually hits the same wall, whether it is LangChain, CrewAI, the OpenAI Agents SDK or Claude with MCP: the agent needs to call an external API, and someone has to set up the credentials first.
That someone is a human. They create accounts, generate API keys, configure billing, set spending limits and inject secrets into the agent's environment, for every service the agent needs.
It is the bottleneck nobody talks about. Your agent can orchestrate a dozen services, but it cannot sign up for a single one.
This tutorial builds an agent that pays for what it uses by itself. It finds services on the x402 Bazaar, reads each price, checks it against a budget you set, pays in USDC from its own wallet and collects the result. The worked example buys a voiceover from Treza's speech endpoint for a few cents.
What you'll build
- A wallet the agent can spend from, with limits you control
- A
fetchthat pays402 Payment Requiredresponses automatically - A search of the x402 Bazaar for services that match a task and a price
- A purchase function that checks every price against a budget before paying, then waits for the result
Prerequisites
- Node.js 20 or later
- A wallet holding a few dollars of USDC on Base. It needs no ETH for gas.
- A new ES module project with the x402 client packages, viem and tsx:
npm init -y
npm pkg set type=module
npm install @x402/fetch @x402/evm viem tsxStep 1: Give the agent its own wallet
The agent pays from a wallet, and whoever holds a wallet's key can spend what is in it. That makes the wallet the budget. Create one just for the agent, fund it with only what the agent is allowed to spend, and keep everything else somewhere the agent cannot reach. If the agent misbehaves or its host is compromised, the most it can lose is that balance.
Keep the private key in your secrets manager or the process environment, never in the prompt, the agent's memory, or anywhere the model can read it back.
The wallet needs USDC and nothing else. On Base, an x402 payment is an EIP-3009 authorization that the facilitator submits on-chain, so the wallet never pays gas. A Solana wallet works the same way, and the docs show that setup.
Step 2: Create a fetch that pays
The x402 protocol adds payment to HTTP. When a server answers 402 Payment Required, the response carries everything a client needs to pay: the price, the currency, the network and the recipient. The x402 client packages wrap fetch so that happens automatically.
// pay.ts
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.AGENT_WALLET_KEY as `0x${string}`);
export const paidFetch = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(account) }],
// Never sign a single payment above $1, whatever a server asks for.
spendControls: { maxAmountPerPayment: '$1' },
});paidFetch works exactly like fetch. When a response is a 402, it:
- Reads the payment requirements from the response
- Signs a USDC payment for that amount with the agent's key
- Sends the request again with the payment in a
Payment-Signatureheader - Returns the paid response
spendControls is your first limit. The client refuses to sign any payment above the cap, whatever the server asks for. It defaults to $1, which is also why a buyer of pricier items has to raise it.
Step 3: Find services on the x402 Bazaar
An agent should not need a hard-coded list of APIs. The x402 Bazaar is a public index of services that accept x402 payments, run by Coinbase, and anyone can read it without a key. Each listing carries:
resource: the URL to calldescriptionandtags: what the service doesaccepts: the price on each network, in the token's smallest unit (USDC has 6 decimals, so20000is $0.02)extensions.bazaar.info.input: the method and an example request bodyquality: how many calls and unique payers the listing had in the last 30 days
// bazaar.ts
const DISCOVERY = 'https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources';
export interface Listing {
resource: string;
description?: string;
tags?: string[];
accepts: { network: string; amount: string }[];
extensions?: { bazaar?: { info?: { input?: { method?: string; body?: Record<string, unknown> } } } };
quality?: { l30DaysUniquePayers?: number };
}
/** Every listing in the index, a thousand per request. Load it once and reuse it. */
export async function loadBazaar(): Promise<Listing[]> {
const all: Listing[] = [];
for (let offset = 0; ; ) {
const res = await fetch(`${DISCOVERY}?limit=1000&offset=${offset}`);
if (!res.ok) throw new Error(`The Bazaar answered ${res.status}`);
const page = (await res.json()) as { items: Listing[]; pagination: { total: number } };
all.push(...page.items);
offset += page.items.length;
if (!page.items.length || offset >= page.pagination.total) return all;
}
}
/** POST services matching every word, payable on Base at or under maxUsd, most-used first. */
export function find(listings: Listing[], words: string[], maxUsd: number): Listing[] {
return listings
.filter((l) => {
const text = `${l.resource} ${l.description ?? ''} ${(l.tags ?? []).join(' ')}`.toLowerCase();
return words.every((w) => text.includes(w));
})
.filter((l) => l.extensions?.bazaar?.info?.input?.method === 'POST')
.filter((l) => l.accepts.some((a) => a.network === 'eip155:8453' && Number(a.amount) / 1e6 <= maxUsd))
.sort((a, b) => (b.quality?.l30DaysUniquePayers ?? 0) - (a.quality?.l30DaysUniquePayers ?? 0));
}The index holds more than 17,000 listings, so loading all of it takes about twenty seconds. Load it once per session, or cache it.
Sorting by unique payers puts services that other agents already rely on first. Treat a listing's price as a guide. A service priced by the request, such as a voiceover charged per character, quotes the exact figure only when you ask with your actual request, which is the next step.
Step 4: Check the price, then pay within a budget
The first request, without a payment, costs nothing and returns the exact price for exactly what you are about to ask for. That makes a budget easy to enforce: quote, compare, then pay.
// buy.ts
import { paidFetch } from './pay';
const BUDGET_USD = 2;
let spentUsd = 0;
/** What the endpoint charges for this exact request, read off its 402. Costs nothing. */
export async function quoteUsd(url: string, body: object): Promise<number> {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (res.status !== 402) throw new Error(`Expected a price quote, got ${res.status}: ${await res.text()}`);
const challenge = JSON.parse(Buffer.from(res.headers.get('payment-required') ?? '', 'base64').toString());
const offer = challenge.accepts.find((a: { network: string }) => a.network === 'eip155:8453');
if (!offer) throw new Error(`${url} does not take payment on Base`);
return Number(offer.amount) / 1e6;
}
/** Pay for one request if the budget allows it, and wait for the result. */
export async function buy(url: string, body: object): Promise<Record<string, any>> {
const price = await quoteUsd(url, body);
if (spentUsd + price > BUDGET_USD) {
throw new Error(`${url} costs $${price}, and $${(BUDGET_USD - spentUsd).toFixed(2)} of the budget is left`);
}
const res = await paidFetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`Purchase failed (${res.status}): ${await res.text()}`);
spentUsd += price;
// A long job ends the paid response with status "running" and a statusUrl to poll.
let run = await res.json();
const statusUrl = run.statusUrl;
while (run.status === 'running') {
await new Promise((resolve) => setTimeout(resolve, run.pollAfterMs ?? 15_000));
run = await (await fetch(statusUrl)).json();
}
return run;
}The polling loop follows Treza's response shape: the file comes back in the paid response, or a job that takes minutes ends with "status": "running" and a free statusUrl. Other sellers shape their responses differently, and each listing's output example shows how.
The budget check assumes one purchase at a time. An agent that buys in parallel should reserve the quoted amount before paying and release it if the purchase fails.
Step 5: Put it together
// agent.ts
import { writeFile } from 'node:fs/promises';
import { find, loadBazaar } from './bazaar';
import { buy } from './buy';
// 1. Find text-to-speech services that cost at most $0.10 a call.
const listings = await loadBazaar();
const candidates = find(listings, ['text-to-speech'], 0.1);
for (const l of candidates.slice(0, 5)) console.log(l.resource, l.quality?.l30DaysUniquePayers ?? 0);
// 2. Pick one. Here it is Treza's; a general agent would hand each listing's
// input schema to its model and let the model choose one and fill it in.
const service = candidates.find((l) => l.resource === 'https://www.trezalabs.com/api/x402/speech');
if (!service) throw new Error('No voiceover service found within budget');
// 3. Start from the listing's own example request and change what the task needs.
const example = service.extensions?.bazaar?.info?.input?.body ?? {};
const run = await buy(service.resource, { ...example, text: 'Your order has shipped. It should arrive on Thursday.' });
// 4. Collect the file, or say why there is none.
if (run.status === 'success') {
const file = await fetch(run.audio);
await writeFile('voiceover.mp3', Buffer.from(await file.arrayBuffer()));
console.log(`Paid $${run.paidUsd} (transaction ${run.transaction}), saved voiceover.mp3`);
} else {
// A failed render is not charged, and retryUrl tries again without paying twice.
console.log(run.status, run.message, run.retryUrl);
}Run it with the wallet's key in the environment:
AGENT_WALLET_KEY=0x... npx tsx agent.tsThe agent searches the index, quotes the request, sees three cents against a two-dollar budget, pays, and saves the file. Nobody created an account or pasted a key.
Swap the task and the same code buys other things. On Treza alone the agent could ask for an image, a music bed, a video clip, or a finished narrated short, each at its own endpoint with its own listing. The x402 integration post lists them with their prices.
The security model
An agent that can pay has to be one you can trust with money. The limits come in layers, and each one catches what the one before it misses:
| Layer | What it protects | How |
|---|---|---|
| Dedicated wallet | Everything else you own | The agent can spend only what you put in its wallet |
| Per-payment cap | Against one overpriced call | spendControls refuses to sign any payment above the cap |
| Budget in code | Against many small calls | Every quote is checked against what is left before paying |
| x402 facilitator | Against replays | Each signed payment settles once, and a replay is rejected |
| The seller's own checks | Against paying for nothing | On Treza, a request that cannot be filled is refused before the payment settles, and a failed render is not charged |
Compare that with the usual setup, where one API key tied to a company card sits in an environment variable. That key has no ceiling, no per-call limit and no record of which agent spent what. A dedicated wallet has all three by construction.
Scaling: many agents, one pattern
The pattern extends to fleets. Give each agent, or each task, its own wallet and its own budget, funded from an orchestrator wallet with an ordinary USDC transfer. When a task ends, sweep what is left back.
Every payment is an on-chain USDC transfer, so each wallet's history is a complete record of what that agent bought and what it paid, with no logging to build. Treza's responses also carry the transaction of the payment, so you can match each result to the transfer that paid for it.
Testing without spending much
The unpaid request is free and returns the exact price, so the search and the quotes in steps 3 and 4 cost nothing to run as often as you like. The voiceover in the example costs three cents, so a full end-to-end run of the agent costs less than a dime even with a few retries.
Why this matters for agent builders
The current model of agent-to-API integration does not scale. Every new service an agent needs means a human provisioning credentials, and as agents call more services, and other agents, that work keeps growing.
x402 turns it around. Services publish their prices. Agents pay per request, from wallets with limits you set, and settlement happens on-chain. No accounts, no API keys, no billing portals.
Next steps
- x402 Payment Integration: everything an agent can buy from Treza, with prices
- Treza x402 documentation: every endpoint's fields and responses
- Example buyer scripts: buy from Treza from a Base or Solana wallet
- x402 protocol docs: Coinbase's protocol reference
- x402 Bazaar: how the discovery index works


