x402 Explained: HTTP-Native Micropayments for AI Agents (With Real Code)
When building autonomous AI agents, traditional SaaS billing models present a significant engineering bottleneck. Traditional API access relies on pre-funded accounts, monthly subscriptions, or credit cards tied to API keys. For an autonomous agent traversing the web to gather data, execute tasks, and coordinate with other agents, this centralized paradigm is highly inefficient.
An agent cannot easily input a credit card, manage 50 different SaaS subscriptions, or handle the security risks of storing highly-privileged long-lived API keys.
The architectural solution is native to the web itself: HTTP Status Code 402 (Payment Required). While reserved for decades, the rise of low-latency Layer 2 blockchains and stablecoins makes programmatic, pay-as-you-go HTTP-native payments viable.
Below, we’ll explore the design pattern of x402—a standardized implementation of HTTP 402 for machine-to-machine (M2M) micropayments—and look at the production-ready code to implement both sides of the handshake.
The x402 Handshake Protocol
The x402 protocol shifts the payment responsibility from a pre-established off-chain relationship to an on-demand, request-response handshake.
┌──────────┐ GET /endpoint ┌──────────┐
│ │──────────────────────────────────────────────>│ │
│ │ <── 402 Payment Required ────────────────── │ │
│ │ X-402-Payment-Destination: 0x... │ │
│ AI │ X-402-Amount-USDC: 10000 (0.01 USDC) │ API │
│ Agent │ X-402-Chain-Id: 8453 (Base) │ Provider │
│ │ │ │
│ (Client) │ ─── GET /endpoint ────────────────────────> │ (Server) │
│ │ X-402-Payment-Proof: 0xTxHash... │ │
│ │ <── 200 OK (With payload) ───────────────── │ │
└──────────┘ └──────────┘
Enter fullscreen mode Exit fullscreen mode
- Initial Request: The agent makes an unauthenticated HTTP request to an endpoint.
-
Payment Challenge (402): The server responds with an
HTTP 402 Payment Requiredstatus. It embeds payment metadata in the headers: target wallet address, price (e.g., in USDC), and the target network (e.g., Base). - Settlement: The agent’s execution loop catches the 402, constructs an on-chain transaction matching the criteria, signs it with its operational wallet, and submits it to the network.
-
Resubmission with Proof: The agent retries the original request, attaching the transaction hash in the
X-402-Payment-Proofheader. - Execution (200): The server verifies the transaction against the blockchain ledger and serves the requested resource.
The Implementation
Let’s write a production-grade implementation using TypeScript, Hono (for the server-side), and viem (for the client-side agent execution). We will use USDC on the Base network (Chain ID: 8453) due to sub-cent gas fees and instant settlement.
1. Server-Side: Enforcing and Verifying x402
This middleware intercepts requests, checks for a valid payment proof, verifies it against an RPC provider, and returns a 402 if unpaid.
typescript
import { Hono } from 'hono';
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
const app = new Hono();
// Base USDC contract address
const USDC_BASE_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bda02913';
const RECIPIENT_WALLET = '0xYourMerchantWalletAddressHere...';
const ENDPOINT_COST_USDC = 10000n; // 0.01 USDC (USDC has 6 decimals)
const publicClient = createPublicClient({
chain: base,
transport: http('https://mainnet.base.org')
});
// Transfer event signature for ERC-20: Transfer(address indexed from, address indexed to, uint256 value)
const TRANSFER_EVENT_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
async function verifyPayment(txHash: `0x${string}`): Promise<boolean> {
try {
const receipt = await publicClient.getTransactionReceipt({ hash: txHash });
// Mitigate double-spending: Check block age to prevent replay of old transactions
const currentBlock = await publicClient.getBlockNumber();
if (currentBlock - receipt.blockNumber > 100n) {
return false; // Transaction is too old
}
// Parse logs for USDC transfer to our merchant address
for (const log of receipt.logs) {
if (log.address.toLowerCase() === USDC_BASE_ADDRESS.toLowerCase()) {
const isTransfer = log.topics[0] === TRANSFER_EVENT_TOPIC;
const toAddressMatched = log.topics[2] &&
('0x' + log.topics[2].slice(26)).toLowerCase() === RECIPIENT_WALLET.toLowerCase();
// Decode transfer value
if (isTransfer && toAddressMatched && log.data) {
const value = Big
Enter fullscreen mode Exit fullscreen mode