Dev.to · 5 min read

x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)

x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)

x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code) An autonomous AI agent often needs to pay for external services—LLM APIs, data feeds, or compute—without a human in the loop. The x402 specification turns ordinary HTTP responses into a payment negotiation channel, letting an agent settle a micro‑fee directly on‑chain before the server returns the payload. Below is a walk‑through of how x402 works, a minimal implementation in JavaScript/TypeScript, and the practical trade‑offs you’ll face when you adopt it. How x402 Turns 402 into a Payment Handshake HTTP already defines status code 402 Payment Required. x402 repurposes it as a negotiation signal: Client → Server: ordinary request (GET/POST). Server → Client: if the endpoint requires payment, it returns 402 plus a JSON body that describes the payment request (amount, token, chain, and a nonce). Client → Server: the client signs a transaction that pays the requested amount, includes the nonce to prevent replay, and sends the signed transaction hash (or a full ERC‑20 approval + transfer) in an X-Payment header on a retry. Server → Client: validates the payment on‑chain, then replies with 200 and the desired payload. Because the negotiation lives entirely in HTTP headers and bodies, no new protocol layer is needed—any HTTP client can be extended to understand x402. Minimal x402 Client (Node.js / TypeScript) The example below assumes you have an Ethereum wallet (private key or mnemonic) that can sign ERC‑20 transfers on Base. It uses viem for low‑level chain interaction and node-fetch for HTTP. // x402Client.ts import { createPublicClient, http, parseAbiItem } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { base } from "viem/chains"; import fetch from "node-fetch"; // --- CONFIG ---------------------------------------------------- const PRIVATE_KEY = process.env.BASE_PRIVATE_KEY!; // 0x-prefixed const ACCOUNT = privateKeyToAccount(PRIVATE_KEY); const PUBLIC_CLIENT = createPublicClient({ chain: base, transport: http() }); const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base const DECIMALS = 6; // Helper: build and send an ERC‑20 transfer async function pay(amount: number, nonce: string): Promise { const amountWei = amount * 10 ** DECIMALS; const data = encodeFunctionData({ abi: [parseAbiItem("function transfer(address to, uint256 amount) returns (bool)")], functionName: "transfer", args: [ACCOUNT.address, amountWei], }); const { request } = await PUBLIC_CLIENT.simulateContract({ address: USDC_ADDRESS as `0x${string}`, abi: [parseAbiItem("function transfer(address to, uint256 amount) returns (bool)")], functionName: "transfer", args: [ACCOUNT.address, amountWei], }); const hash = await PUBLIC_CLIENT.writeContract(request); return hash; } // Core request with x402 handling async function x402Get(url: string): Promise { let resp = await fetch(url); if (resp.status !== 402) return resp; // no payment needed const { amount, token, chainId, nonce } = await resp.json(); // Basic sanity checks – you may want stricter validation in prod if (token.toLowerCase() !== USDC_ADDRESS.toLowerCase()) throw new Error("Unexpected token"); if (chainId !== base.id) throw new Error("Wrong chain"); // Pay and retry const txHash = await pay(Number(amount), nonce); const retryHeaders = new Headers(resp.headers); retryHeaders.set("X-Payment", txHash); retryHeaders.set("X-Payment-Nonce", nonce); resp = await fetch(url, { method: "GET", headers: retryHeaders }); if (!resp.ok) throw new Error(`Payment failed: ${resp.status}`); return resp; } // Example usage (async () => { const resp = await x402Get("https://api.example.com/premium-data"); const json = await resp.json(); console.log("Paid data:", json); })(); What the code does Detects 402 – If the server asks for payment, we parse the JSON body. Validates the request – Checks token, chain, and amount to avoid paying the wrong contract. Executes an ERC‑20 transfer – Uses viem to simulate, then signs and sends the transaction on Base. Retries with proof – Sends the transaction hash (and nonce) in X-Payment / X-Payment-Nonce headers. Returns the payload – Only after the server verifies the payment on‑chain does it respond with 200. You can adapt the same pattern for POST/PUT bodies or for approval‑then‑transfer flows if the server prefers you to approve a spender first. Server‑Side Sketch (Express) The counterpart is tiny: detect missing/invalid payment, return 402 with a challenge, and verify on‑chain before responding. // x402Server.js import express from "express"; import { parseUnits } from "viem"; import { base } from "viem/chains"; import fetch from "node-fetch"; const app = express(); const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; const REQUIRED_AMOUNT = 0.01; // USDC app.get("/premium-data", async (req, res) => { const txHash = req.headers["x-payment"]; const nonce = req.headers["x-payment-nonce"]; if (!txHash || !nonce) { return res.status(402).json({ amount: REQUIRED_AMOUNT, token: USDC, chainId: base.id, nonce: Math.random().toString(36).substring(2), }); } // Verify transaction on Base try { const tx = await fetch(`https://api.basescan.org/api?module=transaction&action=gettxinfo&txhash=${txHash}&apikey=YOUR_KEY`); const txInfo = await tx.json(); if (txInfo.result.to.toLowerCase() !== USDC.toLowerCase()) throw new Error("Wrong recipient"); if (BigInt(txInfo.result.value) < parseUnits(String(REQUIRED_AMOUNT), 6)) throw new Error("Insufficient amount"); // Optional: check nonce against a short‑lived store to prevent replay } catch (e) { return res.status(402).json({ error: "Invalid payment", detail: e.message }); } // Payment OK – return actual resource res.json({ message: "Here is your premium data", timestamp: Date.now() }); }); app.listen(3000, () => console.log("x402 server listening on :3000")); Key points The server never holds funds; it only validates that a transaction with sufficient value reached the USDC contract. The nonce prevents replay attacks; in production you’d store used nonces for a few minutes (e.g., in Redis). If you want to avoid a full transfer each request, you could have the client approve a spender and let the server pull the amount via transferFrom; the core flow stays the same. Honest Trade‑offs Aspect Benefit Cost / Limitation Atomicity Payment and service delivery are inseparable; the agent never gets data without paying. Requires an on‑chain confirmation (≈2 s on Base) before the server can release the payload, adding latency. Universality Works with any HTTP client; no new ports, WebSocket, or gRPC needed. The client must hold a wallet and pay gas for each micro‑transaction; gas on Base is cheap (~$0.0001) but not zero. Granularity Enables true pay‑per‑call pricing (e.g., $0.01 per LLM token bundle). Very small amounts (

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News