x402 Python Tutorial: Let Your AI Agent Pay Per Call for Web Search
Most web APIs want you to sign up, copy an API key into a .env file and pick a monthly plan before your agent can make a single request. x402 flips that around. The server answers an unpaid request with HTTP 402 Payment Required and a price, your client signs a USDC payment and retries, and the response comes back. No account, no key, no plan.
This guide shows how to do that from Python, end to end, with the AgentSearch direct x402 API (docs; base URL https://api.agentsearchhq.com):
- read a real 402 for free, so you can see the exact terms before paying anything
- pay per call with the official
x402Python SDK andhttpx - wrap web search and page extract as agent tools with a hard spend budget
- plug those tools into an OpenAI-style tool-calling loop
If you want the TypeScript version, see the x402 tutorial for AI agents. If you'd rather not write payment code at all, skip to the package route.
What it costs
The direct API settles in USDC on Base mainnet (eip155:8453):
-
Search:
POST /x402/v1/search, $0.008 per call, up to 10 ranked results (default 5) -
Extract:
POST /x402/v1/extract, $0.007 per call, any public URL to clean markdown -
Render:
POST /x402/v1/render, for JavaScript-heavy pages that Extract can't read (see Render) -
Search + Read:
POST /x402/v1/search_and_read, $0.015 for the top pages with their markdown in one call
Payment is verified first and settled only after a successful answer. Invalid input, blocked URLs, upstream failures, empty search results and empty extractions are never charged; the signed authorization just expires unused. At these prices $1 of USDC covers 125 searches or about 140 extracts. Current numbers are always on the pricing page.
Step 0: A small, dedicated wallet
Create a new EVM wallet just for your agent and put a few dollars of USDC on Base in it. You don't need ETH: the Coinbase CDP facilitator pays the gas. Keep the private key in an environment variable, never in code:
export WALLET_PRIVATE_KEY=0x... # the agent wallet only, not your main wallet
The wallet balance is your real ceiling. If the agent goes into a loop, the worst case is what's in that wallet. The wallet setup guide walks through funding it.
Install the SDK:
pip install "x402[httpx,evm]" eth-account
Step 1: Look at the 402 without paying
An unpaid request is free and tells you exactly what a paid one would cost. The terms come back base64-encoded in the PAYMENT-REQUIRED header:
import base64, json, httpx
r = httpx.post(
"https://api.agentsearchhq.com/x402/v1/search",
json={"query": "x402 python", "max_results": 5},
timeout=30,
)
print(r.status_code) # 402: payment required, nothing charged
terms = json.loads(base64.b64decode(r.headers["PAYMENT-REQUIRED"]))
offer = terms["accepts"][0]
print(offer["network"], offer["amount"], offer["payTo"])
# eip155:8453 8000 0xC49B...C337 (8000 atomic USDC = $0.008)
Every 402 also carries extensions.bazaar with JSON Schemas and examples for the input and output, which is how x402 directories and agents discover what the endpoint does. Before you trust any payee, compare payTo with the published list at /.well-known/x402.
Step 2: Pay per call from Python
The x402 SDK registers a payment scheme for an EVM signer, and x402HttpxClient handles the 402, the signature and the retry for you:
import asyncio, os
from eth_account import Account
from x402 import x402Client
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact import register_exact_evm_client
from x402.http.clients import x402HttpxClient
API = "https://api.agentsearchhq.com/x402/v1"
async def main():
account = Account.from_key(os.environ["WALLET_PRIVATE_KEY"])
client = x402Client()
register_exact_evm_client(client, EthAccountSigner(account))
async with x402HttpxClient(client, timeout=60) as http:
r = await http.post(f"{API}/search",
json={"query": "x402 payment protocol", "max_results": 5})
r.raise_for_status()
for hit in r.json()["results"]:
print(hit["score"], hit["url"])
asyncio.run(main())
A search response looks like this (trimmed):
{
"query": "x402 payment protocol",
"result_count": 5,
"results": [
{"title": "x402 - payments protocol for the internet",
"url": "https://www.x402.org/",
"content": "x402 is an open payment standard built on HTTP 402 ...",
"score": 0.91, "domain": "x402.org", "published_date": null}
]
}
The receipt for each paid call comes back in the PAYMENT-RESPONSE header if you want to log it.
Step 3: Turn it into agent tools with a budget
Agents call tools in loops, so put the spending rules in code instead of in the prompt. This small class exposes web_search and read_page, counts what was actually spent, and refuses a call that would go over budget. Because failed and empty calls aren't settled, it only counts a call when it returns 200.
import os
from eth_account import Account
from x402 import x402Client
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact import register_exact_evm_client
from x402.http.clients import x402HttpxClient
API = "https://api.agentsearchhq.com/x402/v1"
PRICE_USD = {"search": 0.008, "extract": 0.007}
class BudgetExceeded(Exception):
pass
class WebTools:
def __init__(self, budget_usd: float = 0.25):
account = Account.from_key(os.environ["WALLET_PRIVATE_KEY"])
client = x402Client()
register_exact_evm_client(client, EthAccountSigner(account))
self.http = x402HttpxClient(client, timeout=60)
self.budget = budget_usd
self.spent = 0.0
async def _call(self, endpoint: str, body: dict) -> dict:
price = PRICE_USD[endpoint]
if self.spent + price > self.budget:
raise BudgetExceeded(f"{endpoint} would exceed ${self.budget:.2f}")
r = await self.http.post(f"{API}/{endpoint}", json=body)
if r.status_code != 200:
# 400/502/empty are not settled, so nothing to count
return {"error": f"HTTP {r.status_code}", "detail": r.text[:300]}
self.spent += price
return r.json()
async def web_search(self, query: str, max_results: int = 5) -> dict:
data = await self._call("search", {"query": query, "max_results": max_results})
if "error" in data:
return data
return {"results": [
{"title": h["title"], "url": h["url"], "snippet": h.get("content", "")[:300]}
for h in data.get("results", [])
]}
async def read_page(self, url: str, max_chars: int = 8000) -> dict:
data = await self._call("extract", {"url": url})
if "error" in data:
return data
md = data.get("markdown") or ""
return {"url": data.get("url", url), "title": data.get("title"),
"markdown": md[:max_chars], "empty": not md.strip()}
async def aclose(self):
await self.http.aclose()
Two things worth copying even if you change everything else:
- Trim what goes back to the model. Snippets at 300 characters and pages at 8,000 keep the context window (and your model bill) under control.
-
Return errors as data. The agent sees
{"error": "HTTP 400"}and can choose another URL, instead of the whole run crashing.
Step 4: Wire the tools into an agent loop
Here's a minimal tool-calling loop with the OpenAI Python SDK. Any model or framework that supports tool calls works the same way: describe the tools, run whatever the model asks for, feed the results back.
import asyncio, json
from openai import AsyncOpenAI
TOOLS = [
{"type": "function", "function": {
"name": "web_search",
"description": "Search the live web. Returns up to 5 results with title, url, snippet.",
"parameters": {"type": "object", "properties": {
"query": {"type": "string"}}, "required": ["query"]}}},
{"type": "function", "function": {
"name": "read_page",
"description": "Read one URL as clean markdown. Use on the 1-2 best search results.",
"parameters": {"type": "object", "properties": {
"url": {"type": "string"}}, "required": ["url"]}}},
]
SYSTEM = ("Answer with sources. Search once, read at most two pages, "
"then answer and cite the URLs you read.")
async def run(question: str):
llm = AsyncOpenAI()
tools = WebTools(budget_usd=0.10)
messages = [{"role": "system", "content": SYSTEM},
{"role": "user", "content": question}]
try:
for _ in range(6): # hard cap on tool rounds
resp = await llm.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=TOOLS)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
print(msg.content)
break
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
try:
fn = getattr(tools, call.function.name)
result = await fn(**args)
except BudgetExceeded as e:
result = {"error": str(e), "hint": "Answer with what you have."}
messages.append({"role": "tool", "tool_call_id": call.id,
"content": json.dumps(result)})
finally:
print(f"spent ${tools.spent:.3f}")
await tools.aclose()
asyncio.run(run("What changed in the x402 v2 spec?"))
A typical run is one search and two page reads: $0.008 + $0.007 + $0.007 = $0.022. With budget_usd=0.10 the worst case per question is ten cents, and the six-round cap stops runaway loops before the budget even matters.
When Extract comes back empty
Some pages are single-page apps that ship an empty HTML shell and draw everything with JavaScript. Extract returns little or no markdown for those, and since empty extractions aren't settled, you don't pay for the miss. For those URLs, call POST /x402/v1/render with the same {"url": ...} body: it loads the page in headless Chromium and returns markdown. Add it as a third tool, or fall back to it inside read_page when empty is true. Pricing for Render is on the Render page, and there's a full walkthrough in reading JavaScript pages from an AI agent.
One call instead of three: Search + Read
If your agent nearly always reads the top results anyway, POST /x402/v1/search_and_read returns the top 1 to 5 results with each page's markdown in a single paid call ($0.015, settled only when at least one page has text):
r = await http.post(f"{API}/search_and_read",
json={"query": "x402 v2 spec changes", "num_results": 3})
for page in r.json()["results"]:
print(page["url"], page["chars"], page["extract_status"])
That's one payment instead of four for a search plus three reads. More on when to use it in search and read in one paid call.
Prefer not to write payment code?
You don't have to. The AgentSearch packages pay the direct x402 API by default, so you just set a wallet key:
-
LangChain (Python):
pip install "langchain-agentsearch[paid]", setAGENTSEARCH_WALLET_PRIVATE_KEYon a small dedicated wallet, then useAgentSearchTool(mode="free")for search andAgentSearchExtractTool()for page reads. Walkthrough: LangChain agent: search, then read the page. -
MCP (Claude, Cursor, Cline, VS Code):
npx -y @agentsearchhq/agentsearch-mcp, withAGENTSEARCH_WALLET_PRIVATE_KEYin the server's env for Extract and Render. Setup: MCP server.
Both also include a free search tier: up to 25 searches a day per IP with no key and no wallet, up to 5 results each. That's a good way to build and test your agent at $0 before you fund anything.
Wallet checklist
- Use a new wallet that only holds what the agent may spend.
- Keep the key in an environment variable or a secret manager, never in a repo or a prompt.
- Cap spend in code (budget per task) and cap tool rounds per run.
- Check
payTo, amount, token and network against the 402 terms and /.well-known/x402. - Never sign a Permit, Permit2 or
approverequest to pay for a call. An x402exactpayment is a single transfer of the listed amount with a short validity window.
FAQ
Do I need an AgentSearch account or API key?
No. The 402 response is the price list, and the signed payment is the authorization.
Which network and token?
USDC on Base mainnet (eip155:8453), x402 v2 exact scheme, settled through the Coinbase CDP facilitator. No ETH needed for gas.
What happens if a call fails?
Nothing is settled. Payment is verified first and only settled after a successful, non-empty answer. A reused payment gets HTTP 409.
Can I use this from TypeScript instead?
Yes, with @x402/fetch or @x402/axios. Copy-paste examples are in the direct x402 API docs.
Is AgentSearch also on Pocket Network?
Yes. Search, Extract and Render are also available through the Pocket Network portal, and the packages can switch to it with AGENTSEARCH_PAYMENT_ROUTE=pocket. The direct API is the default.
Get started
- Run the free 402 check from Step 1 to see the live terms.
- Fund a small agent wallet with a few dollars of USDC on Base.
- Drop
WebToolsinto your agent and set a budget.
Full endpoint reference: docs/x402. Machine-readable: OpenAPI.
More from the blog
- x402 tutorial: pay-per-call APIs for AI agents (TypeScript)
- AgentSearch now speaks native x402 on Base
- Error handling for AI agent web tools
Originally published on the AgentSearch blog.
Top comments (0)