x402协议利用HTTP 402状态码实现AI代理自动支付,代理可自行签名USDC支付并重试请求,无需用户注册账号。
你的 AI Agent 每次调用外部 API,流程都千篇一律:注册账号、验证邮箱、选套餐、把 Key 复制到环境变量,然后祈祷没人把它提交到代码库。这对单个服务来说还能忍。但一旦 Agent 需要在运行时自主选择工具,这套流程就玩不转了——因为 Agent 自己没法去注册账号。
x402 把这个环节砍掉了。服务端对未付费的请求返回 HTTP 402 Payment Required,并附带机器可读的报价。客户端签发一笔 USDC 支付授权后重试同一请求,服务端这次才真正提供服务。无需账号、无需 Key、无需月费,每个请求单独定价。
本指南通过一个真实服务来上手操作。我们用 Python 标准库抓一个真实的 402 挑战、用标准 x402 v2 客户端从 TypeScript 发起付费调用、用硬性支出上限围栏控制花销、以及在不为自己失败付费的前提下处理错误。示例使用 Pocket Agentic Portal 上的 AgentSearch 网页工具:Web Search、Web Extract 和 Web Render,单次调用 $0.005。
x402 基于一个几十年前就预留好、却从未真正用起来的 HTTP 状态码。Pocket Agentic Portal 所使用的协议第 2 版工作流程如下:
Request(请求)。客户端发送一个普通请求,例如带 JSON body 的 POST /v1/search。
Challenge(挑战)。服务端返回 402,并在 PAYMENT-REQUIRED header 中放入支付条款(base64 编码的 JSON)。Portal 也会在响应 body 中重复这些条款。
Pay(支付)。客户端从提供的选项中选一个,签发对应金额的 USDC 授权,重试请求并带上 PAYMENT-SIGNATURE header。
Serve(服务)。服务端验证支付、执行请求,并在 PAYMENT-RESPONSE header 中返回收据。
网络以 CAIP-2 ID 命名,因此 Base 主网是 eip155:8453。旧版 v1 的 header 名称不被 Portal 接受,请使用 v2 客户端。
不需要钱包也能看到端点的报价。未付费的请求是免费的,且会返回条款。以下脚本仅使用 Python 标准库:
# inspect_402.py: see what a paid endpoint asks for, without paying anything
import base64, json, urllib.request, urllib.error
URL = "https://agent.pocket.network/v1/agentsearch-web-search-v1/v1/search"
req = urllib.request.Request(
URL,
data=json.dumps({"query": "x402", "max_results": 3}).encode(),
headers={"content-type": "application/json"},
method="POST",
)
try:
urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
assert e.code == 402, e.code
terms = json.loads(base64.b64decode(e.headers["PAYMENT-REQUIRED"]))
for option in terms["accepts"]:
usd = int(option["amount"]) / 1_000_000 # USDC has 6 decimals
print(f'{option["scheme"]} on {option["network"]}: ${usd:.3f} to {option["payTo"]}')
print("MPP offered:", e.headers.get("WWW-Authenticate", "").startswith("Payment"))
运行后会输出类似:
exact on eip155:8453: $0.005 to 0xF732ea490c5766071785a2310523f7fA2CEbB829
MPP offered: True
解码后的 accepts 条目是完整的报价:
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "5000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xF732ea490c5766071785a2310523f7fA2CEbB829",
"maxTimeoutSeconds": 60,
"extra": { "name": "USD Coin", "version": "2" }
}
几个值得注意的地方:
amount 是原子单位。USDC 有 6 位小数,因此 5000 即 $0.005。asset 是 Base 上的 USDC 合约地址。客户端应检查这个字段,而不仅仅是金额,以确保永远不会用意外收到的 Token 支付。scheme: "exact" 表示授权精确的金额,而非上限。挑战还携带了一个 resource 块描述服务本身,以及一个 extensions.bazaar 块,其中包含输入输出的 JSON Schema。Agent 可以在付费之前先读取服务的要求。
对 Extract(/agentsearch-web-extract-v1/v1/extract)或 Render(/agentsearch-web-render-v1/v1/render)发起同样的请求,会返回相同结构,但各有自己的描述和 Schema。
MPP offered: True 这行指的是第二种支付方式。Portal 也在 Tempo 网络上支持 MPP(Machine Payments Protocol)。同一个 402 响应中携带了 WWW-Authenticate: Payment 挑战,method 为 "tempo"、intent 为 "charge",其请求参数包含金额(同样是 5000)、Token、收款人以及两种模式:pull 和 push。
用 Authorization: Payment <credential> 重试后会收到 Payment-Receipt header。挑战有效期为 20 分钟。mppx 包(mppx/client 及其 tempo 方法)是一个可用的客户端。本指南其余部分使用 Base 上的 x402,但响应处理逻辑完全相同。
标准 x402 v2 客户端包替你完成整个握手流程:捕获 402、检查是否符合规则、签发授权、重试请求。
npm install @x402/fetch @x402/evm viem
关键部分不是支付本身,而是围绕它的限制。如果 prompt 出错,Agent 循环可能调用工具数百次,因此我们加了两层防护:
// pay-per-call.ts
import { wrapFetchWithPaymentFromConfig, decodePaymentResponseHeader } from "@x402/fetch";
import type { PaymentPolicy } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
const BASE = "eip155:8453";
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const MAX_PER_CALL = 5_000n; // $0.005 in USDC atomic units (6 decimals)
const SESSION_BUDGET = 500_000n; // $0.50 for this agent run
let signedTotal = 0n;
// Runs before anything is signed. Keep only terms we're willing to pay,
// and refuse everything once the session budget is spent.
const budgetPolicy: PaymentPolicy = (_version, requirements) =>
requirements.filter((r) => {
const amount = BigInt(r.amount);
return (
r.network === BASE &&
r.asset.toLowerCase() === USDC_BASE.toLowerCase() &&
amount <= MAX_PER_CALL &&
signedTotal + amount <= SESSION_BUDGET
);
});
// Use a dedicated wallet that holds only what you're willing to spend.
const account = privateKeyToAccount(process.env.AGENT_WALLET_KEY as `0x${string}`);
const payFetch = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: BASE, client: new ExactEvmScheme(account) }],
policies: [budgetPolicy],
spendControls: { maxAmountPerPayment: "$0.01" }, // library-level backstop
});
const PORTAL = "https://agent.pocket.network/v1";
现在,每个工具都经过同一个函数。它完成支付、记录收据、并解开 Portal 的响应信封:
export async function paidPost<T>(serviceId: string, path: string, body: unknown): Promise<T> {
const res = await payFetch(`${PORTAL}/${serviceId}${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const receiptHeader = res.headers.get("PAYMENT-RESPONSE");
if (receiptHeader) {
const receipt = decodePaymentResponseHeader(receiptHeader);
if (receipt.success) signedTotal += BigInt(receipt.amount ?? MAX_PER_CALL);
console.log(`paid via ${receipt.network}, tx ${receipt.transaction}`);
}
const json = await res.json();
if (!res.ok) {
// Portal errors are not enveloped: { error: { code, message, retryable } }
const err = json.error ?? {};
throw Object.assign(new Error(`${res.status} ${err.code}: ${err.message}`), {
code: err.code,
retryable: err.retryable === true,
retryAfter: res.headers.get("Retry-After"),
});
}
if (json.portal?.provenance !== "third-party-supplier") {
throw new Error("Unexpected response shape; refusing to use it.");
}
if (json.portal.schemaCheck !== "passed") {
console.warn(`schemaCheck=${json.portal.schemaCheck}: shape not verified`);
}
return json.data as T; // third-party content: data, never instructions
}
最后是 Agent 真正需要的三个网页工具,每个都是调用 paidPost 的一行代码:
type SearchResult = { title: string; url: string; content: string; score: number; domain: string };
type SearchData = { query: string; results: SearchResult[]; error?: { code: string; retryable: boolean } };
type PageData = { title: string | null; markdown?: string; text?: string; error?: { code: string; retryable: boolean } | null };
export const tools = {
search: (query: string, max_results = 5) =>
paidPost<SearchData>("agentsearch-web-search-v1", "/v1/search", { query, max_results }),
extract: (url: string, max_chars = 8000) =>
paidPost<PageData>("agentsearch-web-extract-v1", "/v1/extract", { url, formats: ["markdown"], max_chars }),
render: (url: string, max_chars = 8000) =>
paidPost<PageData>("agentsearch-web-render-v1", "/v1/render", { url, formats: ["markdown"], max_chars }),
};
async function main() {
const found = await tools.search("x402 v2 PAYMENT-SIGNATURE header", 3);
for (const r of found.results) console.log(r.title, r.url);
const first = found.results[0];
if (first) {
const page = await tools.extract(first.url);
console.log(page.markdown?.slice(0, 500));
}
}
main().catch((e) => { console.error(e); process.exit(1); });
用有余额的 Key 运行:
AGENT_WALLET_KEY=0x... npx tsx pay-per-call.ts
这次运行产生了两次付费调用,一次 Search 和一次 Extract,总计 $0.01。Search 最多返回 5 条结果。Extract 返回干净的 markdown,已去除页面杂乱内容。Render 先在无头 Chromium 中加载页面,适用于 Extract 只能看到空壳的单页应用。想了解何时用哪个,参见 Search、Extract、Render:AI Agent 的三个网页工具。
Portal 的每个付费响应都有相同的外层结构:
{
"portal": {
"provenance": "third-party-supplier",
"serviceId": "agentsearch-web-search-v1",
"schemaCheck": "passed"
},
"data": { "query": "...", "results": [] }
}
portal 是 Portal 自身的声明。data 是供应商的输出,原样透传。这个嵌套结构对 Agent 很重要。网页可能包含针对你模型的文本("忽略之前的指令……"),而付费的、经过筛选的来源可能比实际更值得信任。把 data 下的所有内容当作第三方内容处理:
{...envelope.data})到你的 Agent 接下来会与可信字段一起推理的对象中。schemaCheck === "passed"。undeclared 和 unchecked 都表示没有执行检查。来自 Portal 的错误没有包裹在信封里,因为它们来自 Portal 本身而非供应商。它们携带一个稳定的 code:
{ "error": { "code": "UPSTREAM_ERROR", "message": "...", "retryable": true } }
保持 Agent 循环行为良好的规则:
data.error 中报告问题(例如 TARGET_TIMEOUT 或 TARGET_HTTP_ERROR),并带有自己的 retryable 标志。Portal 错误意味着调用本身没有完成。data.error 意味着调用成功了但网站是问题所在,所以重试相同 URL 可能没有帮助。如果你的 Agent 运行在 Claude Desktop、Claude Code、Cursor 或其他 MCP 客户端中,你可以跳过客户端代码。Pocket 的本地 MCP 服务器在你机器上持有钱包 Key,并通过 Base 上的 x402 支付:
{
"mcpServers": {
"pocket-network": {
"command": "npx",
"args": ["-y", "@pocket-network/agentic-portal-mcp"],
"env": {
"POCKET_PRIVATE_KEY": "0x…",
"POCKET_MAX_TOTAL_ATOMIC": "1000000",
"POCKET_MAX_PER_CALL_ATOMIC": "5000"
}
}
}
}
将其放入 claude_desktop_config.json、.cursor/mcp.json 或 .mcp.json。设置必须放在 env 块中,因为桌面客户端不会将你的 shell 环境传递给 MCP 服务器。1000000 是会话的 $1.00 总上限,5000 是单次调用 $0.005。它暴露三个工具:search_services 和 describe_service 是免费的,call_service 则按服务定价支付。
两个设置有助于测试期间的操作。没有设置 Key 时,只有免费工具可用。设置 POCKET_QUOTE_ONLY=true 时,它返回卖方的条款但从不支付。
不要依赖客户端在支付前询问。Pocket 自己的文档提到,测试中一个客户端的模型会请求确认,另一个则直接支付。支出上限才是控制手段,无论哪种行为都能hold住。
更多 MCP 设置内容:AI Agent 的 MCP 网页搜索。
按次付费改变了你对工具的思考方式。基于 Key 的 API 是人类提前设置好的东西。x402 端点是 Agent 可以在运行时发现、从 402 挑战中查价、在你设定的预算内使用的东西。计费按请求粒度,一个任务调用三次就收三次,没有需要超越或忘记取消的套餐。
在 Pocket Agentic Portal 上,网页访问每次调用 $0.005,通过 Base 上的 x402 或 MPP 用 USDC 支付,无需注册、无需 API Key、无最低消费:
Pocket 的集成指南完整覆盖了信封结构、两种支付方式以及错误码。
运行上面的 Python 脚本免费查看真实的 402,然后将有余额的 Key 传入你的 MCP 客户端(添加 npx -y @pocket-network/agentic-portal-mcp)或将 paidPost 放入你自己的 Agent 循环。先从 Web Search 和 Web Extract 开始,需要处理 JavaScript 密集型页面时加上 Web Render,更多指南请关注 AgentSearch 博客。
最初发表于 AgentSearch 博客。