{"slug": "i-built-an-api-that-ai-agents-pay-in-usdc-full-x402-walkthrough-27-endpoints", "title": "I Built an API That AI Agents Pay in USDC — Full x402 Walkthrough (27 Endpoints, Real Transactions)", "summary": "A developer built an Express API that allows AI agents to pay per call in USDC via the x402 protocol, with 27 paid endpoints live on Base mainnet. The implementation uses the dormant HTTP 402 status code for a payment handshake, eliminating the need for signups or API keys, and has processed real settled transactions.", "body_md": "I built an Express API that AI agents (or humans, or anything with `fetch`\n\n) can pay per call, in USDC, with no signup and no API key. It's live on Base mainnet with 27 paid endpoints, and I've run real settled transactions against it. This is the technical walkthrough — the code, the protocol, and the things that actually broke — not an \"agentic economy\" pitch.\n\n`x402`\n\nresurrects the dormant HTTP `402 Payment Required`\n\nstatus code as a real payment handshake. A client calls a paid route → the server replies `402`\n\nwith payment requirements (amount, asset, network) instead of the resource → the client signs a USDC transfer on Base and replays the request with a `PAYMENT`\n\nheader → a **facilitator** (a third party, or Coinbase's CDP service in production) verifies and settles the transfer on-chain → the server serves the response. No account creation, no API key issuance, no OAuth dance — the wallet address *is* the identity, and payment *is* the auth.\n\nThe server is plain Express. Each endpoint is a file in `endpoints/`\n\nexporting `{ path, method, price, handler }`\n\n; `server.js`\n\nloads them all, builds the x402 route table, and mounts one middleware:\n\n``` js\nimport { paymentMiddleware, x402ResourceServer } from \"@x402/express\";\nimport { ExactEvmScheme } from \"@x402/evm/exact/server\";\nimport { HTTPFacilitatorClient } from \"@x402/core/server\";\nimport { createFacilitatorConfig } from \"@coinbase/x402\";\n\nconst facilitatorConfig = config.isMainnet\n  ? createFacilitatorConfig(config.cdpApiKeyId, config.cdpApiKeySecret)\n  : { url: config.testnetFacilitatorUrl }; // https://x402.org/facilitator, no key\n\nconst facilitatorClient = new HTTPFacilitatorClient(facilitatorConfig);\nconst resourceServer = new x402ResourceServer(facilitatorClient).register(\n  config.caip2Network, // \"eip155:8453\" on mainnet\n  new ExactEvmScheme()\n);\n\nconst paidRoutes = {};\nfor (const ep of endpoints) {\n  if (ep.price == null) continue;\n  paidRoutes[`${ep.method} ${ep.path}`] = {\n    accepts: { scheme: \"exact\", price: ep.price, network: config.caip2Network, payTo: config.payToAddress },\n    resource: `${config.baseUrl}${ep.path}`,\n    description: ep.description,\n    mimeType: \"application/json\",\n  };\n}\n\napp.use(paymentMiddleware(paidRoutes, resourceServer));\n```\n\nThat's the entire payment layer. `@x402/express`\n\nhandles the 402 response and calls the facilitator's `verify`\n\n/`settle`\n\n— the endpoint handler never sees a wallet address or a signature, only a normal `req`\n\n/`res`\n\n.\n\nHere's a full endpoint, `GET /api/gas/base`\n\n(versions in use: `@x402/express`\n\n, `@x402/core`\n\n, `@x402/evm`\n\n, `@x402/extensions`\n\n, `@x402/fetch`\n\nall `2.24.0`\n\n, `@coinbase/x402`\n\n`2.1.0`\n\n— the scoped `@x402/*`\n\nline is current v2; the older unscoped `x402-express`\n\n/`x402-fetch`\n\nare deprecated, don't mix them):\n\n``` js\nimport { declareDiscoveryExtension } from \"@x402/extensions/bazaar\";\nimport { getGasPrice } from \"../lib/chains.js\";\nimport { cached } from \"../lib/cache.js\";\n\nexport const path = \"/api/gas/base\";\nexport const method = \"GET\";\nexport const price = \"$0.005\";\nexport const description =\n  \"Current gas price on Base (Ethereum L2), read live via a public RPC endpoint — no API key, no aggregator.\";\n\nexport const discovery = declareDiscoveryExtension({\n  input: {},\n  inputSchema: { properties: {}, required: [] },\n  output: { example: { chain: \"base\", gas_price_wei: \"6000000\", gas_price_gwei: 0.006 } },\n});\n\nexport async function handler(req, res) {\n  const gasPriceWei = await cached(\"gas-base\", 60_000, () => getGasPrice(\"base\"));\n  res.json({\n    chain: \"base\",\n    gas_price_wei: gasPriceWei.toString(),\n    gas_price_gwei: Number(gasPriceWei) / 1e9,\n    fetched_at: new Date().toISOString(),\n  });\n}\n```\n\nNo blockchain code in the handler — `viem`\n\nreads gas price from a public RPC. `price`\n\n/`discovery`\n\nare just metadata consumed elsewhere (the middleware, and the discovery documents below).\n\n`@x402/fetch`\n\nwraps `fetch`\n\nso it transparently handles the 402 → sign → replay cycle. A minimal buyer:\n\n``` js\nimport { wrapFetchWithPaymentFromConfig, decodePaymentResponseHeader } from \"@x402/fetch\";\nimport { ExactEvmScheme } from \"@x402/evm/exact/client\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\nconst account = privateKeyToAccount(BUYER_PRIVATE_KEY);\n\nconst fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {\n  schemes: [{ network: \"eip155:*\", client: new ExactEvmScheme(account) }],\n});\n\nconst response = await fetchWithPayment(\"https://x402-seller-0ay3.onrender.com/api/gas/base\");\nconst data = await response.json();\n\nconst receipt = decodePaymentResponseHeader(response.headers.get(\"PAYMENT-RESPONSE\"));\nconsole.log(receipt.transaction); // the on-chain settlement hash\n```\n\nUnder the hood: first call gets `402`\n\n+ payment requirements, `ExactEvmScheme`\n\nsigns a USDC transfer authorization for the exact price, the wrapper replays the request with a `PAYMENT`\n\nheader, the facilitator settles it, and the response carries a `PAYMENT-RESPONSE`\n\nheader with the receipt. This isn't a simulation — here's an actual settled transaction from a real run against the production server:\n\n```\nGET /api/gas/base — $0.005 USDC\ntx: 0x4c3c38d0a7732244caf895d32a46e5e1fa780abdf4216b3c3fa28bb496062646\nhttps://basescan.org/tx/0x4c3c38d0a7732244caf895d32a46e5e1fa780abdf4216b3c3fa28bb496062646\n```\n\nEvery settled payment is logged server-side (endpoint, payer address, amount, tx hash — nothing that isn't already public on-chain), which is where that hash comes from.\n\nPayment infra is useless if nothing finds your endpoints. Five discovery surfaces, in the order I wired them up:\n\n`GET /.well-known/x402.json`\n\n`payTo`\n\n, and input/output schema. There's no single official schema for this exact path; I followed the envelope from the IETF draft `x402Version`\n\n, `kind`\n\n, `resources[]`\n\n) and enriched each resource with the same Bazaar metadata used elsewhere.`GET /openapi.json`\n\n`npm run bazaar`\n\n) that pages through `facilitatorClient.extensions.bazaar.listResources()`\n\nand filters for our `payTo`\n\naddress to confirm we're actually indexed.`POST /api/x402/registry/register-origin {origin}`\n\ncall makes it crawl our `/openapi.json`\n\nand register everything in one shot.`POST core.x402arena.gg/register`\n\nagainst our `/api/gas/base`\n\nendpoint. No facilitator involvement, no on-chain proof required at registration time — it shows up `verified:true`\n\nin their public agent list (`GET core.x402arena.gg/agents`\n\n) after a health check against the live endpoint.`User-Agent`\n\n→ silent 403 from Cloudflare.`rdap.org`\n\n) and a couple of other upstream sources return a flat 403 to anonymous-looking requests. Fix was a one-line default: every outbound `fetch`\n\nin this project now sends a descriptive UA (`x402-seller/1.0 (+https://...)`\n\n). Confirmed by testing: 403 with no UA, 200 with one, same request otherwise.`GITHUB_TOKEN`\n\n(no scopes needed — it's all public repo data) raises that to 5000/hour.`.jsonl`\n\nfiles on disk for simplicity — which is fine until a redeploy wipes the disk and takes the logs with it. Not fixed yet (would mean a real datastore); flagged here as a known limitation, not something I'm going to gloss over.`POST /api/web/read`\n\nfetches an arbitrary URL and returns readable Markdown — an obvious target for hitting `169.254.169.254`\n\nor `localhost:6379`\n\n. It's guarded on both the initial hostname `ipaddr.js`\n\n, refuse anything that isn't `unicast`\n\n(public), reject literal `localhost`\n\n/loopback/link-local hostnames outright, and cap the download at 2 MB inside a 10 s timeout regardless.`x402.gitbook.io`\n\nstill reference some v1, unscoped package names in places; what's actually current and maintained is the scoped `@x402/*`\n\nline (`2.24.0`\n\nas of this writing). When something in the docs didn't match `node_modules`\n\n, I trusted the installed package and its own type definitions over the prose.The x402 ecosystem is small right now. Traffic on this server is basically me testing it, plus whatever probes discovery crawlers send. This post is a \"here's how the plumbing works and here's the code,\" not a revenue story — I have no evidence yet that agents are out there autonomously discovering and paying for API calls at any real scale. If that changes, that's a different post.\n\nBase URL: `https://x402-seller-0ay3.onrender.com`\n\n. All 27 endpoints below are real and callable; each costs a fraction of a cent, so poking at a few won't cost you anything meaningful. `GET /health`\n\n, `GET /stats`\n\n, and the two discovery documents are free.\n\n| Method | Path | Price |\n|---|---|---|\n| GET | `/api/price/eth-usd` |\n$0.005 |\n| GET | `/api/price/btc-usd` |\n$0.005 |\n| GET | `/api/price/sol-usd` |\n$0.005 |\n| GET | `/api/price/usdc-supply` |\n$0.005 |\n| GET | `/api/gas/base` |\n$0.005 |\n| GET | `/api/gas/ethereum` |\n$0.005 |\n| GET | `/api/chain/gas` |\n$0.005 |\n| GET | `/api/chain/block` |\n$0.005 |\n| GET | `/api/defi/price` |\n$0.005 |\n| GET | `/api/defi/tvl` |\n$0.005 |\n| GET | `/api/defi/tvl-chain` |\n$0.005 |\n| GET | `/api/defi/protocols` |\n$0.005 |\n| GET | `/api/defi/yields` |\n$0.005 |\n| GET | `/api/defi/stablecoins` |\n$0.005 |\n| GET | `/api/fx/rates` |\n$0.005 |\n| GET | `/api/github/repo` |\n$0.005 |\n| GET | `/api/npm/package` |\n$0.005 |\n| GET | `/api/hn/top` |\n$0.005 |\n| GET | `/api/wiki/summary` |\n$0.005 |\n| GET | `/api/dns/lookup` |\n$0.005 |\n| GET | `/api/rdap/domain` |\n$0.005 |\n| POST | `/api/web/read` |\n$0.005 |\n| POST | `/api/web/extract` |\n$0.02 |\n| POST | `/api/ai/summarize` |\n$0.01 |\n| POST | `/api/ai/classify` |\n$0.01 |\n| POST | `/api/ai/translate` |\n$0.01 |\n| POST | `/api/ai/extract` |\n$0.02 |\n\nTwo full examples with `@x402/fetch`\n\n(swap in your own funded Base wallet):\n\n```\n# GET, no body\nENDPOINT_PATH=\"/api/gas/base\" \\\nTARGET_URL=\"https://x402-seller-0ay3.onrender.com\" \\\nnode scripts/buyer-test.js\n# POST, with a JSON body\nENDPOINT_PATH=\"/api/web/read\" METHOD=POST \\\nBODY='{\"url\":\"https://en.wikipedia.org/wiki/HTTP_402\"}' \\\nTARGET_URL=\"https://x402-seller-0ay3.onrender.com\" \\\nnode scripts/buyer-test.js\n```\n\nBoth print the response JSON and the settlement receipt, hash included. `GET /.well-known/x402.json`\n\nand `GET /openapi.json`\n\nlist every route with full input/output schemas if you want to build a client instead of copy-pasting curl.", "url": "https://wpnews.pro/news/i-built-an-api-that-ai-agents-pay-in-usdc-full-x402-walkthrough-27-endpoints", "canonical_source": "https://dev.to/entreprisedaney33rgb/i-built-an-api-that-ai-agents-pay-in-usdc-full-x402-walkthrough-27-endpoints-real-transactions-44jn", "published_at": "2026-09-01 20:48:09+00:00", "updated_at": "2026-09-01 20:54:46.382794+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-infrastructure", "developer-tools", "ai-agents"], "entities": ["x402", "Coinbase", "Base", "USDC", "Express"], "alternates": {"html": "https://wpnews.pro/news/i-built-an-api-that-ai-agents-pay-in-usdc-full-x402-walkthrough-27-endpoints", "markdown": "https://wpnews.pro/news/i-built-an-api-that-ai-agents-pay-in-usdc-full-x402-walkthrough-27-endpoints.md", "text": "https://wpnews.pro/news/i-built-an-api-that-ai-agents-pay-in-usdc-full-x402-walkthrough-27-endpoints.txt", "jsonld": "https://wpnews.pro/news/i-built-an-api-that-ai-agents-pay-in-usdc-full-x402-walkthrough-27-endpoints.jsonld"}}