HTTP 402状态码被重新定义,用于在单次请求-响应中附带微支付,适合需要调用大量按次付费服务的AI Agent场景。
当下大多数 AI Agent 工作流依赖 API Key、订阅等级或临时计费来支付第三方服务。这些方案能用,但引入了耦合:Agent 必须管理凭证、处理账单周期,往往还要信任一个中心化的经纪人来居中付款。
HTTP 状态码 402 Payment Required 早在 HTTP/1.0 规范中就已定义,但从未被标准化用于实际用途。x402 草案(参见 W3C Community Group "HTTP Payments")提出了一种轻量、无状态的方案,将微支付附着在单一的 HTTP 请求-响应对上。如果你构建的 Agent 需要调用大量细粒度、按量付费的服务(如分词器、模型推理、数据查询),x402 能让你把支付逻辑内嵌到协议本身,而不是叠加在 OAuth 或自定义计费之上。
下文我们将走过协议流程,展示付款方(Agent)和收款方(服务)的最小可运行代码,并讨论你会遇到的实际权衡。
核心属性是无状态:服务器无需在 Nonce 之外保持会话或发票记录,这防止了重放攻击。客户端只需要一个能在指定链上签署交易的钱包。
以下代码片段假设你有一个以太坊兼容钱包(私钥或助记词)且已安装 ethers 库(npm i ethers)。它展示了 AI Agent 如何调用一个假设的 /summarize 端点——该端点在 Base 链上每次请求收取 0.005 USDC。
// agent.js
import { ethers } from "ethers";
import fetch from "node-fetch";
// ----------------- Configuration -----------------
const RPC_URL = "https://base.mainnet.rpc.dev"; // public Base RPC
const PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"; // agent's wallet
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const SUMMARIZE_ENDPOINT = "https://api.example.com/summarize";
const REQUIRED_AMOUNT = ethers.parseUnits("0.005", 6); // USDC has 6 decimals
// ------------------------------------------------
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const usdc = new ethers.Contract(
USDC_ADDRESS,
["function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)"],
signer
);
// Helper: build the payment header value from a signed tx
function paymentHeader(tx) {
// The x402 draft suggests a compact form: tx=<hex>,sig=<hex>
return `tx=${tx};sig=${signer.signMessage(ethers.getBytes(tx)).slice(2)}`;
}
async function callSummarize(text) {
let attempt = 0;
while (true) {
attempt++;
const resp = await fetch(SUMMARIZE_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
// No payment header on first try
},
body: JSON.stringify({ text }),
});
if (resp.ok) {
const data = await resp.json();
return data.summary; // success path
}
if (resp.status !== 402) {
throw new Error(`Unexpected status ${resp.status}: ${await resp.text()}`);
}
// ----- 402 received -----
const paymentHeaderRaw = resp.headers.get("Payment");
if (!paymentHeaderRaw) {
throw new Error("402 without Payment header");
}
// Parse server's challenge (amount, asset, network, nonce)
const challenge = Object.fromEntries(
paymentHeaderRaw.split(";").map(part => part.trim().split("="))
);
if (challenge.asset !== USDC_ADDRESS.toLowerCase() ||
challenge.network !== "base") {
throw new Error("Server requested unsupported asset/network");
}
const amountRequired = ethers.parseUnits(challenge.amount, 6);
if (!amountRequired.eq(REQUIRED_AMOUNT)) {
throw new Error("Amount mismatch");
}
// Build a simple USDC transfer transaction
const nonce = await provider.getTransactionCount(signer.address);
const tx = {
to: USDC_ADDRESS,
data: usdc.interface.encodeFunctionData("transfer", [
signer.address, // we will proxy via the server? Actually we need to pay the service.
amountRequired
]),
chainId: 8453, // Base
nonce,
gasLimit: 100000,
// gasPrice can be fetched from provider; we use a simple fallback
gasPrice: await provider.getFeeData().then(f => f.gasPrice || ethers.parseUnits("0.1", 9))
};
const signedTx = await signer.signTransaction(tx);
// Retry with payment header
const payment = paymentHeader(signedTx);
// loop continues; server will now see the header and verify
}
}
// Example usage
(async () => {
try {
const summary = await callSummarize("Explain quantum entanglement in two sentences.");
console.log("Summary:", summary);
} catch (e) {
console.error("Agent call failed:", e);
}
})();
首次请求——不带 Payment header,预期收到 402。
解析 402——提取金额、资产(Base 链上的 USDC)和 Nonce(这里我们依赖服务器的金额字段;实际实现还应包含 Nonce 以防止重放)。
构建 USDC 转账——签署一笔将确切金额发送到服务器地址的交易(服务器应在其文档或 .well-known/x402 端点中公开其 USDC 收款地址)。
重试——在 Payment header 中附加已签名的交易;服务器验证通过后返回摘要文本。
注意——在生产级 Agent 中,你可能会缓存服务器的收款地址、动态估算 Gas、处理交易失败(如余额不足、Nonce 间隙)。示例中省略了这些细节以聚焦于协议流程。
服务端只需:
// server.js
import express from "express";
import { ethers } from "ethers";
import crypto from "crypto";
const app = express();
app.use(express.json());
// ----------------- Configuration -----------------
const RPC_URL = "https://base.mainnet.rpc.dev";
const provider = new ethers.JsonRpcProvider(RPC_URL);
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
// The service's own USDC receiving address (must be funded)
const PAYEE_ADDRESS = "0xYourServiceUSDCAddress";
// Price per call in USDC (6 decimals)
const PRICE_PER_CALL = ethers.parseUnits("0.01", 6);
// ------------------------------------------------
// Generate a server‑side nonce to prevent replays
function makeNonce() {
return crypto.randomBytes(16).toString("hex");
}
// Middleware that enforces x402 payment
function requirePayment(req, res, next) {
// If no Payment header → ask client to pay
const paymentHeader = req.headers["payment"];
if (!paymentHeader) {
res.set("Payment", `asset=${USDC_ADDRESS};network=base;amount=${PRICE_PER_CALL};nonce=${makeNonce()}`);
res.status(402).json({ error: "Payment required" });
return;
}
// Parse the header: tx=<hex>,sig=<hex>
const parts = Object.fromEntries(
paymentHeader.split(";").map(p => p.trim().split("="))
);
const txHex = parts.tx;
const sigHex = parts.sig;
// Verify signature
const recovered = ethers.verifyMessage(ethers.getBytes(txHex), "0x" + sigHex);
// In production: also check nonce hasn't been used, balance is sufficient, etc.
// Decode tx to confirm amount and recipient
const tx = ethers.Transaction.from(txHex);
if (!tx) {
res.status(402).json({ error: "Invalid transaction" });
return;
}
// Basic checks (simplified for the example)
if (tx.to?.toLowerCase() !== USDC_ADDRESS.toLowerCase()) {
res.status(402).json({ error: "Wrong token contract" });
return;
}
// In a real implementation: submit tx to chain, wait for confirmation
// For this example: assume success if signature verifies
req.paid = true;
next();
}
// Example endpoint
app.post("/summarize", requirePayment, async (req, res) => {
const { text } = req.body;
// ... actual summarization logic ...
res.json({ summary: "Quantum entanglement is a phenomenon where particles become interconnected..." });
});
app.listen(3000, () => console.log("Server running on port 3000"));