# 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](https://www.x402.org/) 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](https://agentsearchhq.com/docs/x402); 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 `x402` Python SDK and `httpx`
- 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](https://agentsearchhq.com/blog/x402-pay-per-call-api-for-ai-agents?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search). If you'd rather not write payment code at all, skip to [the package route](#prefer-not-to-write-payment-code).

## 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](https://agentsearchhq.com/render?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search))
- **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](https://agentsearchhq.com/pricing?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search).

## 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:

```bash
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](https://agentsearchhq.com/wallet-setup?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search) walks through funding it.

Install the SDK:

```bash
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:

```python
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](https://api.agentsearchhq.com/.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:

```python
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):

```json
{
  "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.

```python
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:

1. **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.
2. **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.

```python
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](https://agentsearchhq.com/render?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search), and there's a full walkthrough in [reading JavaScript pages from an AI agent](https://agentsearchhq.com/blog/read-javascript-pages-from-ai-agent?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search).

## 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):

```python
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](https://agentsearchhq.com/blog/search-and-read-one-paid-call?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search).

## 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]"`, set `AGENTSEARCH_WALLET_PRIVATE_KEY` on a small dedicated wallet, then use `AgentSearchTool(mode="free")` for search and `AgentSearchExtractTool()` for page reads. Walkthrough: [LangChain agent: search, then read the page](https://agentsearchhq.com/blog/langchain-agent-search-then-read-page?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search).
- **MCP (Claude, Cursor, Cline, VS Code):** `npx -y @agentsearchhq/agentsearch-mcp`, with `AGENTSEARCH_WALLET_PRIVATE_KEY` in the server's env for Extract and Render. Setup: [MCP server](https://agentsearchhq.com/mcp?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search).

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](https://api.agentsearchhq.com/.well-known/x402).
- Never sign a Permit, Permit2 or `approve` request to pay for a call. An x402 `exact` payment 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](https://agentsearchhq.com/docs/x402?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search).

### 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

1. Run the free 402 check from Step 1 to see the live terms.
2. Fund a small agent wallet with a few dollars of USDC on Base.
3. Drop `WebTools` into your agent and set a budget.

Full endpoint reference: [docs/x402](https://agentsearchhq.com/docs/x402?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search). Machine-readable: [OpenAPI](https://api.agentsearchhq.com/openapi.json).

### More from the blog

- [x402 tutorial: pay-per-call APIs for AI agents (TypeScript)](https://agentsearchhq.com/blog/x402-pay-per-call-api-for-ai-agents?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search)
- [AgentSearch now speaks native x402 on Base](https://agentsearchhq.com/blog/agentsearch-native-x402-on-base?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search)
- [Error handling for AI agent web tools](https://agentsearchhq.com/blog/error-handling-ai-agent-web-tools?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search)

---

*Originally published on the* [*AgentSearch blog*](https://agentsearchhq.com/blog/x402-python-ai-agent-web-search?utm_source=hashnode&utm_medium=referral&utm_campaign=x402-python-ai-agent-web-search)*.*
