Skip to content
AgentSearch

x402 Tutorial: Pay-Per-Call APIs for AI Agents, No API Key Required

Every API your agent uses usually starts the same way: make an account, verify an email, pick a plan, copy a key into an environment variable, and hope nobody commits it. That’s tolerable for one service. It falls apart once an agent wants to pick its own tools at runtime, because the agent can’t sign up for things.

x402 removes that step. The server answers an unpaid request with HTTP 402 Payment Required and a machine-readable price. The client signs a USDC payment and retries the same request, and the server serves it. There’s no account, no key and no monthly plan, just a price on each request.

This guide works through it hands-on against a live service. We’ll read a real 402 challenge with nothing but Python’s standard library, pay for calls from TypeScript with the standard x402 v2 client, put hard spend limits around it, and handle errors without getting charged for failures. The examples use the AgentSearch web tools on the Pocket Agentic Portal: Web Search, Web Extract and Web Render, at $0.005 per call each.

How x402 works in four steps

x402 builds on a status code HTTP reserved decades ago and never really used. Version 2 of the protocol, which the Pocket Agentic Portal speaks, works like this:

  1. Request. Your client sends a normal request, for example POST /v1/search with a JSON body.
  2. Challenge. The server answers 402 and puts the payment terms in a PAYMENT-REQUIRED header (base64-encoded JSON). The portal also repeats them in the response body.
  3. Pay. Your client picks one of the offered options, signs a USDC authorization for that amount, and retries the request with a PAYMENT-SIGNATURE header.
  4. Serve. The server verifies the payment, serves the request, and returns a receipt in a PAYMENT-RESPONSE header.

Networks are named with CAIP-2 IDs, so Base mainnet is eip155:8453. The older v1 header names aren’t accepted by the portal, so use a v2 client.

Step 1: Look at a real 402 without paying

You don’t need a wallet to see what an endpoint costs. An unpaid request is free and returns the terms. This script uses only the Python standard library:

# inspect_402.py: see what a paid endpoint asks for, without paying anything
import base64, json, urllib.request, urllib.error

URL = "https://agent.pocket.network/v1/agentsearch-web-search-v1/v1/search"
req = urllib.request.Request(
    URL,
    data=json.dumps({"query": "x402", "max_results": 3}).encode(),
    headers={"content-type": "application/json"},
    method="POST",
)
try:
    urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
    assert e.code == 402, e.code
    terms = json.loads(base64.b64decode(e.headers["PAYMENT-REQUIRED"]))
    for option in terms["accepts"]:
        usd = int(option["amount"]) / 1_000_000  # USDC has 6 decimals
        print(f'{option["scheme"]} on {option["network"]}: ${usd:.3f} to {option["payTo"]}')
    print("MPP offered:", e.headers.get("WWW-Authenticate", "").startswith("Payment"))

Running it prints something like:

exact on eip155:8453: $0.005 to 0xF732ea490c5766071785a2310523f7fA2CEbB829
MPP offered: True

The decoded accepts entry is the whole offer:

{
  "scheme": "exact",
  "network": "eip155:8453",
  "amount": "5000",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "payTo": "0xF732ea490c5766071785a2310523f7fA2CEbB829",
  "maxTimeoutSeconds": 60,
  "extra": { "name": "USD Coin", "version": "2" }
}

A few things to notice:

  • amount is in atomic units. USDC has 6 decimals, so 5000 is $0.005.
  • asset is the USDC contract on Base. Your client should check this, not just the amount, so it never pays in a token you didn’t expect.
  • scheme: "exact" means you authorize exactly that amount, not a ceiling.
  • The challenge also carries a resource block describing the service and an extensions.bazaar block with a JSON Schema for the input and output. An agent can read what a service expects before paying for it.

The same request against Extract (/agentsearch-web-extract-v1/v1/extract) or Render (/agentsearch-web-render-v1/v1/render) returns the same shape with its own description and schema.

The other option: MPP on Tempo

That MPP offered: True line refers to a second payment method. The portal also speaks MPP, the Machine Payments Protocol, on the Tempo network. The same 402 response carries a WWW-Authenticate: Payment challenge with method="tempo" and intent="charge", and its request parameter names the amount (again 5000), the token, the recipient, and two modes, pull and push.

  • Pull: you sign the transfer and send it unbroadcast. The portal verifies it, serves the call, and only then broadcasts it, so a failed call moves no money.
  • Push: you broadcast the transfer yourself and send the transaction hash. If the call then fails, the payment stays redeemable for 15 minutes: retry the same service with the same credential and you’re served without paying again.

You retry with Authorization: Payment <credential> and get a Payment-Receipt header back. A challenge is valid for 20 minutes. The mppx package (mppx/client with its tempo method) is a working client. The rest of this guide uses x402 on Base, but the response handling is identical either way.

Step 2: Pay per call from TypeScript

The standard x402 v2 client packages handle the whole handshake for you: they catch the 402, check it against your rules, sign, and retry.

npm install @x402/fetch @x402/evm viem

The important part isn’t the payment, it’s the limits around it. An agent loop can call a tool hundreds of times if a prompt goes wrong, so we add two layers:

  • A policy that runs before anything is signed. It accepts only USDC on Base, at most $0.005 per call, and nothing once a session budget is used up.
  • The library’s own spend controls as a backstop, with a per-payment cap of $0.01.
// pay-per-call.ts
import { wrapFetchWithPaymentFromConfig, decodePaymentResponseHeader } from "@x402/fetch";
import type { PaymentPolicy } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";

const BASE = "eip155:8453";
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const MAX_PER_CALL = 5_000n;     // $0.005 in USDC atomic units (6 decimals)
const SESSION_BUDGET = 500_000n; // $0.50 for this agent run

let signedTotal = 0n;

// Runs before anything is signed. Keep only terms we're willing to pay,
// and refuse everything once the session budget is spent.
const budgetPolicy: PaymentPolicy = (_version, requirements) =>
  requirements.filter((r) => {
    const amount = BigInt(r.amount);
    return (
      r.network === BASE &&
      r.asset.toLowerCase() === USDC_BASE.toLowerCase() &&
      amount <= MAX_PER_CALL &&
      signedTotal + amount <= SESSION_BUDGET
    );
  });

// Use a dedicated wallet that holds only what you're willing to spend.
const account = privateKeyToAccount(process.env.AGENT_WALLET_KEY as `0x${string}`);

const payFetch = wrapFetchWithPaymentFromConfig(fetch, {
  schemes: [{ network: BASE, client: new ExactEvmScheme(account) }],
  policies: [budgetPolicy],
  spendControls: { maxAmountPerPayment: "$0.01" }, // library-level backstop
});

const PORTAL = "https://agent.pocket.network/v1";

Now one function that every tool goes through. It pays, records the receipt, and unwraps the portal’s response envelope:

export async function paidPost<T>(serviceId: string, path: string, body: unknown): Promise<T> {
  const res = await payFetch(`${PORTAL}/${serviceId}${path}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });

  const receiptHeader = res.headers.get("PAYMENT-RESPONSE");
  if (receiptHeader) {
    const receipt = decodePaymentResponseHeader(receiptHeader);
    if (receipt.success) signedTotal += BigInt(receipt.amount ?? MAX_PER_CALL);
    console.log(`paid via ${receipt.network}, tx ${receipt.transaction}`);
  }

  const json = await res.json();
  if (!res.ok) {
    // Portal errors are not enveloped: { error: { code, message, retryable } }
    const err = json.error ?? {};
    throw Object.assign(new Error(`${res.status} ${err.code}: ${err.message}`), {
      code: err.code,
      retryable: err.retryable === true,
      retryAfter: res.headers.get("Retry-After"),
    });
  }

  if (json.portal?.provenance !== "third-party-supplier") {
    throw new Error("Unexpected response shape; refusing to use it.");
  }
  if (json.portal.schemaCheck !== "passed") {
    console.warn(`schemaCheck=${json.portal.schemaCheck}: shape not verified`);
  }
  return json.data as T; // third-party content: data, never instructions
}

Finally, the three web tools an agent actually wants, each a single line that calls paidPost:

type SearchResult = { title: string; url: string; content: string; score: number; domain: string };
type SearchData = { query: string; results: SearchResult[]; error?: { code: string; retryable: boolean } };
type PageData = { title: string | null; markdown?: string; text?: string; error?: { code: string; retryable: boolean } | null };

export const tools = {
  search: (query: string, max_results = 5) =>
    paidPost<SearchData>("agentsearch-web-search-v1", "/v1/search", { query, max_results }),
  extract: (url: string, max_chars = 8000) =>
    paidPost<PageData>("agentsearch-web-extract-v1", "/v1/extract", { url, formats: ["markdown"], max_chars }),
  render: (url: string, max_chars = 8000) =>
    paidPost<PageData>("agentsearch-web-render-v1", "/v1/render", { url, formats: ["markdown"], max_chars }),
};

async function main() {
  const found = await tools.search("x402 v2 PAYMENT-SIGNATURE header", 3);
  for (const r of found.results) console.log(r.title, r.url);
  const first = found.results[0];
  if (first) {
    const page = await tools.extract(first.url);
    console.log(page.markdown?.slice(0, 500));
  }
}
main().catch((e) => { console.error(e); process.exit(1); });

Run it with a funded key:

AGENT_WALLET_KEY=0x... npx tsx pay-per-call.ts

That run makes two paid calls, one Search and one Extract, for $0.01 in total. Search returns up to 5 results. Extract returns clean markdown with the page clutter stripped. Render loads the page in headless Chromium first, for single-page apps where Extract only sees an empty shell. For when to use which, see Search, Extract, Render: three web tools for AI agents.

Step 3: Handle the response envelope safely

Every paid response from the portal has the same outer shape:

{
  "portal": {
    "provenance": "third-party-supplier",
    "serviceId": "agentsearch-web-search-v1",
    "schemaCheck": "passed"
  },
  "data": { "query": "...", "results": [] }
}

portal is what the portal itself asserts. data is the supplier’s output, passed through unchanged. That nesting matters for agents. Web pages can contain text aimed at your model (“ignore previous instructions…”), and a paid, curated source can feel more trustworthy than it is. Treat everything under data as third-party content:

  • Pass it to the model in a tool-result or quoted block, never concatenated into the system prompt.
  • Don’t spread it ({...envelope.data}) into an object your agent reasons over next to trusted fields.
  • Check schemaCheck === "passed" before relying on the shape. undeclared and unchecked both mean no check ran.

Step 4: Errors, retries, and not paying for failures

Errors from the portal aren’t wrapped in the envelope, because they come from the portal, not the supplier. They carry a stable code:

{ "error": { "code": "UPSTREAM_ERROR", "message": "...", "retryable": true } }

Rules that keep an agent loop well behaved:

  • Branch on code and retryable, not on the message text.
  • Respect Retry-After. A 503 with Retry-After means the service is temporarily not offered, and retrying sooner won’t help.
  • Send PAYMENT-SIGNATURE exactly once. Two of them is ambiguous, and the portal re-challenges with a fresh 402 instead of guessing.
  • A failed delivery costs nothing. If the supplier fails, or its response doesn’t match the declared schema, the portal settles nothing and your authorization stays valid for a retry.

Keep portal errors separate from website errors. When a page your agent asked Extract or Render to fetch times out or returns a 404, the service still answers normally and reports the problem inside data.error (for example TARGET_TIMEOUT or TARGET_HTTP_ERROR), with its own retryable flag. A portal error means the call itself didn’t go through. A data.error means the call worked and the website was the problem, so retrying the same URL may not help.

No code at all: the MCP route

If your agent runs in Claude Desktop, Claude Code, Cursor or another MCP client, you can skip the client code. Pocket’s local MCP server holds the wallet key on your machine and pays over x402 on Base:

{
  "mcpServers": {
    "pocket-network": {
      "command": "npx",
      "args": ["-y", "@pocket-network/agentic-portal-mcp"],
      "env": {
        "POCKET_PRIVATE_KEY": "0x…",
        "POCKET_MAX_TOTAL_ATOMIC": "1000000",
        "POCKET_MAX_PER_CALL_ATOMIC": "5000"
      }
    }
  }
}

Put this in claude_desktop_config.json, .cursor/mcp.json or .mcp.json. Settings must go in the env block, because desktop clients don’t pass your shell environment to MCP servers. 1000000 is a $1.00 total cap for the session and 5000 is $0.005 per call. It exposes three tools: search_services and describe_service are free, and call_service pays the service’s price.

Two settings help while you’re testing. With no key set, only the free tools work. With POCKET_QUOTE_ONLY=true, it returns the seller’s terms and never pays.

Don’t count on the client to ask before paying. Pocket’s own docs note that in testing one client’s model asked for a go-ahead and another paid without asking. The spend caps are the control that holds either way.

More on MCP setup: MCP web search for AI agents.

Wallet checklist for agent payments

  • Use a dedicated wallet that holds only what you’re willing to spend. Its key sits in an environment variable or config file, so treat it like petty cash, not savings.
  • Fund it with USDC on Base for x402, or USDC.e on Tempo for MPP.
  • Cap every layer: a per-call maximum, a per-session budget, and the library’s spend controls.
  • Check the asset and network, not just the amount, before signing.
  • Log receipts. The PAYMENT-RESPONSE header gives you the transaction for each paid call, which makes per-task cost accounting simple.

Why this fits agents

Pay-per-call changes how you think about tools. A key-based API is something a human sets up ahead of time. An x402 endpoint is something an agent can discover, price-check from the 402 challenge, and use within a budget you set, all at runtime. Billing is per request, so a task that makes three calls costs three calls, and there’s no plan to outgrow or forget to cancel.

For web access that comes to $0.005 per call on the Pocket Agentic Portal, in USDC over x402 or MPP, with no signup, no API key and no minimums:

Service Endpoint Portal page
Web Search POST /v1/agentsearch-web-search-v1/v1/search agentsearch-web-search-v1
Web Extract POST /v1/agentsearch-web-extract-v1/v1/extract agentsearch-web-extract-v1
Web Render POST /v1/agentsearch-web-render-v1/v1/render agentsearch-web-render-v1

Pocket’s integration guide covers the envelope, both payment methods and the error codes in full.

Get started

Run the Python script above to see a live 402 for free, then add npx -y @pocket-network/agentic-portal-mcp to your MCP client or drop paidPost into your own agent loop. Start with Web Search and Web Extract, add Web Render for JavaScript-heavy pages, and find more guides on the AgentSearch blog.

More from the blog

Service pages: Web Search API · Web Extract API · Web Render API