What you will build
An agent built on the OpenAI Agents SDK that pays for the tools it calls, settled in USDC on Base, in about fifteen minutes. The agent loop does not change. You add one tool that pays, and the agent calls it like any other tool.
This is the task walkthrough. For the full compatibility matrix, handoff billing, and pricing detail, the companion reference is openai-agents-sdk-payments. The OpenAI Agents SDK integration page is the canonical reference, and the payment API product page covers the wider surface. The path you take depends only on whether you run the TypeScript or Python SDK.
The shared idea, whichever language you use, is that payment lives in the tool and nowhere else. The agent, its instructions, its model, and its run loop are untouched. You are giving one tool the ability to pay for what it calls, and the SDK keeps treating it as an ordinary tool. That is why this is additive: you can add it to a single tool on an existing agent, confirm it works, and leave everything else exactly as it was.
Prerequisites
- A Blockchain0x account with one agent created, so it has a wallet.
- A
sk_test_API key for this walkthrough. - Node 18+ (TypeScript SDK, or the Python proxy; the SDK targets
>=18), or Python plus Node for the proxy.
export B0X_API_KEY=sk_test_... # sk_test_ -> Base SepoliaTypeScript: wrap the client in a tool
If you run the TypeScript Agents SDK, wrap the x402 client directly in a tool. No extra process.
import { Agent, run, tool } from "@openai/agents";
import { createClient } from "@blockchain0x/node";
import { createX402Client } from "@blockchain0x/x402/client";
import { z } from "zod";
const sdk = createClient({ apiKey: process.env.B0X_API_KEY! });
const fetchWithPay = createX402Client({ sdk });
const getQuote = tool({
name: "get_quote_realtime",
description: "Fetch a real-time quote for a stock ticker.",
parameters: z.object({ ticker: z.string() }),
execute: async ({ ticker }) => {
const res = await fetchWithPay(`https://quotes.example.com/v1/quote?ticker=${ticker}`);
return res.ok ? await res.text() : `Lookup failed: ${res.status}`;
},
});
const researcher = new Agent({ name: "Researcher", instructions: "Equity research analyst.", tools: [getQuote] });On a 402 the client settles in USDC and retries, and the tool returns the result. The agent never sees the payment.
Python: call a Node proxy
If you run the Python Agents SDK, the x402 client is not available there, so run a small Node proxy (the same one used in the CrewAI guide) and call it from a function_tool.
from agents import Agent, function_tool
import requests
@function_tool
def get_quote_realtime(ticker: str) -> str:
"""Fetch a real-time quote for a stock ticker."""
upstream = f"https://quotes.example.com/v1/quote?ticker={ticker}"
res = requests.post("http://127.0.0.1:8787", json={"url": upstream}, timeout=30)
return res.text if res.status_code == 200 else f"Lookup failed: {res.status_code}"
researcher = Agent(name="Researcher", instructions="Equity research analyst.", tools=[get_quote_realtime])The proxy holds the wallet and answers the 402; the Python tool just calls localhost. Run one proxy per agent wallet.
Run the agent
Either way, run the agent normally. The paid tool behaves like any other.
const result = await run(researcher, "What is the live quote on TSLA?");
console.log(result.finalOutput);The first time the agent calls the tool, the upstream returns 402, the wallet settles, and the retried request returns the quote. From the agent's view it was one tool call that took a beat longer.
Verify the full loop
Before you trust the agent with anything, confirm the payment path end to end on testnet. There are three things worth checking, and they catch different failures.
First, the happy path: call the paid tool against a funded wallet and confirm you get a real result, not a 402. If you see a 402 reach your agent, the client or proxy is not wired in, because it should have answered the 402 itself.
Second, the unfunded path: drain or use a fresh wallet and confirm the call fails cleanly rather than hanging. A paid call against an empty wallet should return a non-2xx your tool turns into a sensible message, not a timeout.
Third, the logs: watch the settlement happen. On the TypeScript path, log around fetchWithPay; on the Python path, watch the proxy output. Seeing the 402, the settle, and the 200 in order is how you know the loop is real and not a cached or mocked response. Run all three on Base Sepolia, where mistakes cost test USDC, before a live key is anywhere near the code.
Handle a failed payment
A paid call will sometimes come back as a non-2xx: the wallet refused an over-limit payment, the balance ran out, or the upstream itself failed. The agent has to handle that, or it will loop on the same wall.
const res = await fetchWithPay(url);
if (!res.ok) {
// Do not retry the same call. Surface it so the planner can choose another action.
return `Paid call failed with status ${res.status}; skipping.`;
}
return await res.text();Then tell the agent, in its instructions, what a failed paid call means: do not retry the identical call, and treat a failure as a budget or availability signal rather than a transient blip. An agent that blindly retries a refused payment burns turns and, in your logs, looks exactly like a runaway. The instruction to stop is what keeps a failed payment calm.
Fund and set a spend limit
The agent pays from its wallet, so fund it. On sk_test_ that is Base Sepolia; send the wallet test USDC from a public faucet. Then set a spend limit in the dashboard and confirm it over the API:
const res = await fetch(
`https://api.blockchain0x.com/v1/agents/${agentId}/spend-permissions`,
{ headers: { Authorization: `Bearer ${process.env.B0X_API_KEY!}` } },
);
const permissions = await res.json(); // per_tx_wei, allowance_wei, period_seconds, ...The wallet refuses any call over the per-transaction cap or the period allowance before settling, so the limit is the real boundary. Set a tight per_tx_wei first; it is the cheapest protection against a single runaway payment.
Ship to mainnet
When the loop works on testnet, going live is a key swap. Replace the sk_test_ key with a sk_live_ one and the same code settles on Base mainnet (eip155:8453). Re-check the spend limit on the live agent, because limits are per agent, and confirm the key prefix at boot so a test key never runs in production; the server rejects a mismatch with apikey.network_mismatch.
Common pitfalls
Three traps.
Writing your own 402 handling. The client (or the proxy) already does the 402, settle, and retry. Adding your own branch makes you pay twice. Await the final response.
Sharing one wallet across a handoff graph. Give each agent its own wallet so a prompt-injected agent only spends its own allowance and attribution stays clean. The reference page covers this.
Exposing the Python proxy beyond localhost. It holds a wallet key. Bind it to 127.0.0.1 and treat it like a credential.
What to ship today
Pick your path by language, wire one paid tool, fund the test wallet, and watch a 402 settle on Base Sepolia. Set a dashboard spend limit, then swap to a sk_live_ key. For the full integration reference, handoff billing, and tier math, see openai-agents-sdk-payments. Pricing is on the pricing page.