Dev.to AI 🤖 Ai 👁 0 📖 3 min read

How I put a data product behind x402 so AI agents can buy it with USDC

I sell verified B2B lead packs. For a one-off campaign, a $49/month data seat is a terrible deal — and for an AI agent, a subscription signup is a non-starter. Agents can't fill out billing forms. So I put the product be

I sell verified B2B lead packs. For a one-off campaign, a $49/month data seat is a terrible deal — and for an AI agent, a subscription signup is a non-starter. Agents can't fill out billing forms. So I put the product behind x402: HTTP 402 "Payment Required", resurrected as a machine checkout. Here's exactly how it works, with the real shapes and flows.

What x402 actually is

x402 (from Coinbase) turns the 402 status code into a payment handshake an agent can complete without human help:

  1. The agent requests a paid resource with no payment attached.
  2. The server responds 402 with machine-readable payment terms in the body.
  3. The agent pays (USDC on a supported network) and retries the request with an X-Payment header carrying proof of payment.
  4. The server verifies the payment on-chain and serves the resource.

No accounts, no API keys, no OAuth dance for the buyer — the wallet is the identity, and the retry loop is the session.

The shape of a 402 response

The 402 body isn't an error page. It's an offer. Here's the actual structure I return:

{
  "x402Version": 1,
  "error": "Payment required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0xYourTreasuryWallet",
      "maxAmountRequired": "9000000",
      "maxTimeoutSeconds": 300,
      "resource": "https://scoutpacks-tunnel-1.loca.lt/buy/pack-25",
      "description": "25 verified B2B leads, JSON",
      "mimeType": "application/json"
    }
  ]
}

The fields that matter:

  • payTo — your treasury wallet. The agent pays this address directly.
  • maxAmountRequired — in the asset's base units. USDC has 6 decimals, so 9000000 = $9.00. This is the single most common bug I see: people write dollars and charge micro-cents.
  • asset — the token contract (above is USDC on Base).
  • network — base here. Match your payTo wallet to a chain you actually monitor.
  • scheme — exact means exact-amount settlement.
  • resource — which endpoint the terms apply to.

I also serve a machine-readable manifest at /.well-known/x402 so agents can discover the catalog and terms before touching a paid endpoint — cheaper than learning via 402s.

The pay-and-retry flow

Client side (the agent's wallet code):

import requests

URL = "https://scoutpacks-tunnel-1.loca.lt/buy/pack-25"

resp = requests.get(URL)
if resp.status_code == 402:
    terms = resp.json()["accepts"][0]
    # Build + sign the USDC transfer per the scheme, then:
    payment_payload = sign_transfer(terms)  # base64, per the x402 spec
    resp = requests.get(URL, headers={"X-Payment": payment_payload})

resp.raise_for_status()
leads = resp.json()  # 200 — the goods

Server side (FastAPI-style):

@app.get("/buy/{pack_id}")
def buy(pack_id: str, x_payment: str | None = Header(default=None)):
    pack = CATALOG[pack_id]
    if x_payment is None:
        return JSONResponse(status_code=402, content=payment_terms(pack))
    verify_onchain(x_payment, pack)  # re-check amount, asset, recipient, settlement
    return deliver(pack)             # JSON leads: instant for the 25-pack

Three things worth internalizing:

  1. Re-verify on every retry. The X-Payment header is untrusted input. Check recipient, amount, asset, and that the transaction actually settled — or use a facilitator service.
  2. Timeouts are terms, not suggestions. maxTimeoutSeconds bounds how long the quote is valid; re-quote after it lapses.
  3. Keep the catalog single-sourced. The 402 handler, the /.well-known/x402 manifest, and delivery should all read the same catalog object. If price lives in three places, it will drift.

The MCP wrapper: let agents browse before they buy

Raw HTTP is fine, but agents live in tool-calling land. The repo ships an MCP server with two tools:

  • list_packs() — returns the catalog: pack sizes, prices, and exactly what each pack contains. No payment needed; this is the menu.
  • buy_pack(pack_id) — runs the pay-and-retry flow above and returns the leads JSON.

The MCP server reads the same catalog the 402 endpoint uses, so the menu and the checkout can never disagree. The pattern generalizes: list_* (free, discoverable) + buy_* (402-gated) is a good shape for any agent-sold data product.

The working example: Scout Packs

Scout Packs is the live implementation of everything above: verified B2B lead packs for AI agents — 25 leads for $9, 50 for $15, 100 for $25. Every lead ships as JSON: company, contact, title, published email, plus the source URL it was verified against.

If you're selling anything to agents — data, compute, API access — stop minting API keys and start returning 402s. The wallet is the account.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.