DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

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

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

Why x402 matters for autonomous agents

Autonomous AI agents often need to consume paid services—API calls, data feeds, compute cycles—without human intervention. Traditional payment flows (OAuth, API keys, invoicing) require out‑of‑band setup, manual top‑ups, or trusted intermediaries, which break the “fire‑and‑forget” model agents rely on.

x402 is a lightweight extension to HTTP that turns the existing 402 Payment Required status code into a machine‑readable payment request. When a resource is behind a paywall, the server replies with 402 and includes structured headers that tell a client exactly how to pay (amount, currency, settlement layer, and a nonce to prevent replay). The client can then attach a signed payment proof in a subsequent request, and if the proof validates, the server returns the requested resource with a 200 OK.

Because the protocol lives entirely in HTTP headers, it works with any HTTP client, requires no new transport layer, and can be composed with existing middleware (rate limiting, logging, auth). For AI agents built on top of LLMs or reinforcement learning loops, this means they can discover a service, negotiate price, pay, and continue—all in a single request‑response cycle.

Core components of an x402 exchange

Piece Where it lives What it contains
402 Response Server Status: 402 Payment Required
X-Payment-Info: {"schema":"x402/v1","network":"base","asset":"USDC","amount":"0.05","maxAmount":"0.10","payTo":"0xAbc…","expires":"2025-12-31T23:59:59Z","nonce":"a1b2c3d4"}
Payment Proof Client (agent) Signed message (EIP‑712 typedsigned) that includes the nonce, amount, payTo, and chain ID. Sent as X-Payment: <signature> header on the retry.
Successful Response Server Status: 200 OK
X-Payment-Status: settled
Response body (data, model output, etc.)
Failure Responses Server 402 with updated maxAmount if underpaid, or 400 Bad Request if proof invalid/missing.

The flow is deliberately stateless: the server only needs to verify the signature against the nonce it issued. No session storage, no database look‑ups for the payment itself (though you may still keep usage logs for auditing).

Implementing an x402‑protected endpoint (Node.js/Express)

Below is a minimal, production‑ready example that protects a simple “text‑summarizer” microservice. It uses the ethers library for signature verification and assumes the agent holds a USDC balance on Base (Chain ID 8453).


javascript
// server.js
import express from 'express';
import { ethers } from 'ethers';
import cors from 'cors';

const app = express();
app.use(express.json());
app.use(cors());

// Configuration – in practice load from env or secret manager
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
const PAY_TO = '0xYourAgentWalletHere';
const NETWORK = { name: 'base', chainId: 8453 };
const PRICE_USDC = ethers.parseUnits('0.05', 6); // 5 cents USDC (6 decimals)

// Helper: build the EIP‑712 typed data for x402 payment
function buildPaymentData(nonce, amount) {
  return {
    types: {
      EIP712Domain: [
        { name: 'name', type: 'string' },
        { name: 'version', type: 'string' },
        { name: 'chainId', type: 'uint256' },
        { name: 'verifyingContract', type: 'address' },
      ],
      Payment: [
        { name: 'payTo', type: 'address' },
        { name: 'asset', type: 'address' },
        { name: 'value', type: 'uint256' },
        { name: 'nonce', type: 'bytes32' },
        { name: 'expires', type: 'uint256' },
      ],
    },
    domain: {
      name: 'x402 Payment',
      version: '1',
      chainId: NETWORK.chainId,
      verifyingContract: USDC_ADDRESS,
    },
    primaryType: 'Payment',
    message: {
      payTo: PAY_TO,
      asset: USDC_ADDRESS,
      value: amount,
      nonce: ethers.keccak256(ethers.toUtf8Bytes(nonce)), // bytes32
      expires: Math.floor(Date.now() / 1000) + 3600, // 1 h validity
    },
  };
}

app.post('/summarize', async (req, res) => {
  const { text } = req.body;
  if (!text) return res.status(400).json({ error: 'Missing text' });

  // 1️⃣ Check for existing payment proof
  const paymentHeader = req.headers['x-payment'];
  const nonceHeader = req.headers['x-payment-nonce']; // optional, sent by server in 402
  if (paymentHeader && nonceHeader) {
    try {
      // Reconstruct the typed data the server originally sent
      // In a real service you’d store the nonce→amount map temporarily (e.g., Redis)
      const stored = await getStoredNonce(nonceHeader); // implement your own cache
      if (!stored) throw new Error('Nonce unknown or expired');

      const domain = {
        name: 'x402 Payment',
        version: '1',
        chainId: NETWORK.chainId,
        verifyingContract: USDC_ADDRESS,
      };
      const typedData = {
        types: {
          EIP712Domain: [{ name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }],
          Payment: [{ name: 'payTo', type: 'address' }, { name: 'asset', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'bytes32' }, { name: 'expires', type: 'uint256' }],
        },
        domain,
        primaryType: 'Payment',
        message: {
          payTo: PAY_TO,
          asset: USDC_ADDRESS,
          value: stored.amount,
          nonce: ethers.keccak256(ethers.toUtf8Bytes(nonceHeader)),
          expires: stored.expires,
        },
      };
      const recovered = ethers.verifyTypedData(typedData.domain, typedData.types, typedData.message, paymentHeader);
      if (recovered.toLowerCase() !== PAY_TO.toLowerCase()) throw new Error('Signature mismatch');

      // Payment verified – proceed
      const summary = await summarizeWithLLM(text); // your LLM call
      return res.json({ summary });
    } catch (e) {
      // Invalid proof – fall through to request payment
      console.warn('Payment proof invalid:', e.message);
    }
  }

  // 2️⃣ No valid proof – ask for payment
  const nonce = ethers.randomBytes(32).toString('hex');
  await storeNonce(nonce, { amount: PRICE_USDC, expires: Math.floor(Date.now() / 1000) + 3600 }); // TTL 1h

  const paymentInfo = {
    schema: 'x402/v1',
    network: NETWORK.name,
    asset: USDC_ADDRESS,
    amount: ethers.formatUnits(PRICE_USDC, 6), // human‑readable
    maxAmount: ethers.formatUnits(PRICE_USDC * 2, 6), // allow a small over‑pay buffer
    payTo: PAY_TO,
    expires: Math.floor(Date.now() / 1000) + 3600,
    nonce,
  };

  return res
    .status(402)
    .set('X-Payment-Info', JSON.stringify(paymentInfo))
    .json({ error: 'Payment required', paymentInfo });
});

// Dummy storage – replace with Redis, Postgres, etc.
const store = new Map();
async function storeNonce(nonce, data) { store.set(nonce, { ...data, ts: Date.now() }); }
async function getStoredNonce(nonce) {
  const entry = store.get(nonce);
  if (!entry) return null;
  if (Date.now() - entry.ts > 3600 * 1000) { store.delete(nonce); return null; }
  return entry;
}

// Fake LLM summarizer – plug in your own provider
async function summarizeWithLLM(text) {
  // Example: call OpenAI, Anthropic, or a local model
  return text.split('.').slice(0, 2).join('. ') + '.';
}

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log
Enter fullscreen mode Exit fullscreen mode

Top comments (0)