executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
agent-payment-x402将 x402 支付执行添加到 AI 代理中,具备每任务预算、支出控制和非托管钱包。通过 agentwallet-sdk 支持 Base,通过 OKX Payments / OKX 代理支付协议支持 X Layer,并通过上游 x402 包与基于结算器的结算支持 Solana 及多网络 EVM。
Independent GitHub snapshot · Automated classification · Original documentation governs
让 AI 代理能够进行策略门控的支付并内置支出控制。使用 x402 HTTP 支付协议和 MCP 工具,使代理能够为外部服务、API 或其他代理付费,无托管风险。
适用于:代理需要支付 API 调用、购买服务、与其他代理结算、强制执行每任务支出限额,或管理非托管钱包。与 cost-aware-llm-pipeline 和 security-review 技能自然搭配。
根据代理是购买对付费 API 的访问权,还是向他人收费,选择集成路径:
| 需求 | 推荐路径 |
|---|---|
| 代理为 Base 或其他 agentwallet 支持链上的 402 门控 API 付费 | 使用 agentwallet-sdk 作为 MCP 支付服务器,并配置严格的支出策略 |
| 代理为 X Layer 上的 402 门控 API 付费 | 使用 okx/onchainos-skills 中的 OKX 代理支付协议;okx-x402-payment 是已弃用的旧别名 |
| 代理为 Solana 或其他 x402 v2 网络上的 402 门控 API 付费 | 用上游的 @x402/fetch 或 @x402/axios 包包装代理的 HTTP 客户端并注册 EVM/SVM 方案;由资源服务器的结算器验证和结算 |
| API 在 Solana 或多个网络上向代理收费(TypeScript、Python 或 Go) | 使用来自 x402-foundation/x402 的上游 x402 中间件 —— TypeScript 用 @x402/express、@x402/hono、@x402/next 或 @x402/fastify,Python 用 x402,Go 用 github.com/x402-foundation/x402/go/v2 |
| TypeScript API 向代理收费 | 使用面向 Express、Hono、Fastify 或 Next.js 的 OKX Payments TypeScript 卖家 SDK 文档 |
| Go API 向代理收费 | 使用面向 Gin、Echo 或 net/http 的 OKX Payments Go 卖家 SDK 文档 |
| Rust API 向代理收费 | 使用面向 Axum 的 OKX Payments Rust 卖家 SDK 文档 |
| Java API 向代理收费 | 使用面向 Spring Boot 2/3、Java EE 或 Jakarta 的 OKX Payments Java 卖家 SDK 文档 |
| Python API 向代理收费 | 实现前先检查当前 OKX Payments 仓库;可能尚无 Python 卖家指南 |
agentwallet-sdk:在生产使用前,通过包文档确认当前网络覆盖范围。Base Sepolia 是最安全的开发默认值;Base 主网是原始技能所述的生产路径。eip155:196)和 USDT0 结算。由于支付包和结算器行为可能快速变化,生成生产代码前请获取当前 SDK 文档。x402.org 结算器,它仅限测试网(Base Sepolia eip155:84532、Solana 开发网 solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1,以及 Stellar、Aptos、Hedera、XRPL 测试网),不适用于主网路由。主网(Solana 为 solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp)请选择自结算、自行运行结算器,或从上游结算器列表中挑选托管方案——参见选项 C 中的结算器对比。生产前请在结算器的 /supported 端点确认实时覆盖范围,而不要在此硬编码。x402 将 HTTP 402(需要付款)扩展为机器可协商的流程。当服务器返回 402 时,代理的支付工具会协商价格、检查预算、签署交易,并仅在编排器设定的策略与确认边界内重试。
每次支付工具调用都会强制执行 SpendingPolicy:
代理通过 ERC-4337 智能账户持有自己的密钥。编排器在委托前设置策略;代理只能在限定范围内支出。无资金池,无托管风险。
支付层暴露标准 MCP 工具,可无缝接入任何 Claude Code 或代理框架设置。
安全提示:务必锁定包版本。此工具管理私钥——未锁定的
npx安装会引入供应链风险。
{
"mcpServers": {
"agentpay": {
"command": "npx",
"args": ["agentwallet-sdk@6.0.0"]
}
}
}
| 工具 | 用途 |
|---|---|
get_balance | 检查代理钱包余额 |
send_payment | 向地址或 ENS 发送付款 |
check_spending | 查询剩余预算 |
list_transactions | 所有付款的审计追踪 |
注意:支出策略由编排器在委托给代理之前设置——而非代理本身。这可防止代理自行提高支出限额。通过编排层或任务前钩子中的
set_policy配置策略,切勿将其作为代理可调用工具。
将此路径用于 X Layer x402、多方支付(MPP)、会话支付、收费和 A2A 收费流程。
对于买方代理流程:
okx/onchainos-skills 仓库。skills/okx-agent-payments-protocol/SKILL.md 作为调度器。skills/okx-x402-payment/SKILL.md 视为已弃用的兼容别名,而非规范技能。对于卖方 API 流程,生成代码前先获取最新的语言专用指南:
| 运行时 | 当前指南 |
|---|---|
| TypeScript | https://raw.githubusercontent.com/okx/payments/main/typescript/SELLER.md |
| Go | https://raw.githubusercontent.com/okx/payments/main/go/x402/SELLER.md |
| Rust | https://raw.githubusercontent.com/okx/payments/main/rust/x402/SELLER.md |
| Java | https://raw.githubusercontent.com/okx/payments/main/java/SELLER.md |
不要在未检查当前 OKX 仓库的情况下复制旧文档中的示例。当前 OKX 指南使用 okx-agent-payments-protocol 作为调度器,且 Java 卖家文档现已可用。
当代理在 Solana、Base 或上游协议实现支持的其他网络上付费(或你的 API 收费)时,使用此路径。位于 x402-foundation/x402 的规范 x402 monorepo 正在积极维护,并直接发布客户端和中间件包。与选项 A 和 B 不同,这不是独立的 MCP 服务器——你包装代理自己的 HTTP 客户端,由资源服务器选择的结算器验证并结算。
对于买方代理流程:
从维护中的 examples/typescript/clients 示例(fetch、axios、MCP)开始,而不是复制旧文档中的片段。
在签署或提交第一笔付费请求之前要求明确的用户确认,与选项 B 对 OKX 流程的要求完全一致。不要将支付执行隐藏在通用工具调用之后。
锁定包版本(例如 @x402/fetch@2.22.0);上游所有包以相同步调发布版本。
用注册到客户端的 PaymentPolicy 强制预算,使检查在每次调用时都针对服务器的真实挑战运行。与你自己传入的数字比较的预算不能证明任何事——金额、资产和网络都来自服务器,因此必须在签名产生之前全部验证。
在收款方和预算策略之外,单独配置精确的资源来源白名单。下方所有支付 fetch 均拒绝重定向。会话确认应明确列出允许的来源、网络、资产、收款方和以可读单位表示的支出上限;并发调用共享一次确认结果。客户端和策略只能由编排器持有。
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { ExactSvmScheme } from "@x402/svm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
import { createKeyPairSignerFromBytes } from "@solana/kit";
import { base58 } from "@scure/base";
// Signer keys belong to the ORCHESTRATOR's env — never hardcoded, never agent-writable.
const evmKey = process.env.EVM_PRIVATE_KEY as `0x${string}`;
const svmKey = process.env.SVM_PRIVATE_KEY;
if (!evmKey || !svmKey) {
throw new Error("Signer keys are not set — refusing to start payment client");
}
// One client, both network families: the buyer pays whichever chain the 402 offers.
const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(privateKeyToAccount(evmKey)));
client.register("solana:*", new ExactSvmScheme(await createKeyPairSignerFromBytes(base58.decode(svmKey))));
// A PaymentPolicy filters the SERVER's payment requirements before any
// signature is created. Returning an empty array means "nothing here is
// acceptable" and the client refuses to pay rather than falling back.
const ALLOWED_NETWORKS = new Set([
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", // Solana mainnet
"eip155:8453", // Base
]);
const MAX_AMOUNT = 10_000n; // atomic units, 6-decimal USDC: 0.01 USDC per call
const SESSION_CAP = 50_000n; // 0.05 USDC across the whole session
// EVM addresses are case-insensitive, so compare them lowercased. Solana
// addresses are base58 and ARE case-sensitive — never lowercase those, or a
// different account could slip through.
const normalizeAddress = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a);
const ALLOWED_ASSETS = new Set(
[
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC, Solana mainnet
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC, Base
].map(normalizeAddress),
);
// Who you are willing to pay. Without this, any 402 an agent happens to hit
// can name its own recipient.
const ALLOWED_PAY_TO = new Set(
["7pr7NCaQRz5PEhPy7BAeB3Z72TVkiShhjRyVCN5DA6yC"].map(normalizeAddress),
);
client.registerPolicy((_x402Version, requirements) =>
requirements.filter(r => {
if (!ALLOWED_NETWORKS.has(r.network)) return false; // wrong chain
if (!ALLOWED_ASSETS.has(normalizeAddress(r.asset))) return false; // wrong token
if (!ALLOWED_PAY_TO.has(normalizeAddress(r.payTo))) return false; // wrong recipient
try {
const amount = BigInt(r.amount);
return amount >= 0n && amount <= MAX_AMOUNT; // over budget / negative
} catch {
return false; // unparseable amount
}
}),
);
// The policy sees one challenge at a time, so it cannot enforce a session
// total or ask a human anything. Keep the payment-enabled client private and
// route every paid call through the payOnce boundary below.
// Supply this from your harness — a real prompt, never a stub that returns true.
declare function confirmWithUser(prompt: string): Promise<boolean>;
// Resource authorization is independent of recipient and budget policy.
// The ORCHESTRATOR supplies exact approved HTTPS origins before delegation;
// never populate this set from a server challenge or agent-controlled input.
const ALLOWED_ORIGINS = new Set(["https://api.example.com"]);
function requireAllowedOrigin(url: string): void {
const parsed = new URL(url);
if (!ALLOWED_ORIGINS.has(parsed.origin) || parsed.username || parsed.password) {
throw new Error("Resource origin is not authorized");
}
}
// Both approved assets above are 6-decimal USDC. Update this trusted formatting
// policy along with the asset allowlist if you support other assets/decimals.
const USDC_SCALE = 1_000_000n;
const humanUSDC = (amount: bigint) =>
`${amount / USDC_SCALE}.${(amount % USDC_SCALE).toString().padStart(6, "0").replace(/0+$/, "") || "0"} USDC`;
let sessionSpent = 0n;
let sessionApproval: Promise<boolean> | undefined;
async function payOnce(url: string, init?: RequestInit): Promise<Response> {
requireAllowedOrigin(url); // Before reservation, prompt, fetch, or signature.
// Reserve the worst case the policy allows. Settlement responses do not
// carry an amount, so counting MAX_AMOUNT per call is a deliberate
// over-estimate — it can stop early, never late. Reserve before any await,
// so concurrent calls cannot all pass the check.
if (sessionSpent + MAX_AMOUNT > SESSION_CAP) {
throw new Error("Session budget exhausted — blocked");
}
sessionSpent += MAX_AMOUNT;
// Release the reservation only when no signed payment left this process:
// a declined or failed prompt, a challenge the policy rejected, or a free
// response. Once a signed request is sent it may settle, so keep it counted.
let signedRequestSent = false;
const trackingFetch: typeof fetch = async (input, reqInit) => {
// Override caller options on BOTH the challenge and signed retry. Native
// fetch refuses redirects, so an unauthorized host cannot return a 402
// or receive a payment header via an automatic redirect.
const req = new Request(input, { ...reqInit, redirect: "error" });
requireAllowedOrigin(req.url);
if (req.headers.has("PAYMENT-SIGNATURE") || req.headers.has("X-PAYMENT")) {
signedRequestSent = true;
}
const response = await fetch(req);
// Also fail closed for adapters that expose a redirect response instead.
if (response.redirected || (response.status >= 300 && response.status < 400)) {
throw new Error("Paid request redirects are blocked");
}
return response;
};
try {
// Install one promise before awaiting the prompt. Concurrent calls share
// the same decision, including a decline or failure; never auto-reprompt.
sessionApproval ??= Promise.resolve().then(() => confirmWithUser(
`Allow paid requests to origins: ${[...ALLOWED_ORIGINS].join(", ")}? ` +
`Networks: ${[...ALLOWED_NETWORKS].join(", ")}. ` +
`Assets: ${[...ALLOWED_ASSETS].join(", ")} (6-decimal USDC). ` +
`Recipients (payTo): ${[...ALLOWED_PAY_TO].join(", ")}. ` +
`Per-call cap: ${humanUSDC(MAX_AMOUNT)}; session cap: ${humanUSDC(SESSION_CAP)}.`,
));
if (!await sessionApproval) {
throw new Error("User declined: no payment attempted");
}
return await wrapFetchWithPayment(trackingFetch, client)(url, init);
} finally {
if (!signedRequestSent) sessionSpent -= MAX_AMOUNT;
}
}
const res = await payOnce("https://api.example.com/data", { method: "GET" });
注册该策略后,超预算金额、非预期代币或未注册的链都会故障关闭——所有候选项都被过滤掉,createPaymentPayload 会抛出异常而不是签名。开发网 USDC 是 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU;仅在开发时把它加入 ALLOWED_ASSETS。对策略做对抗性测试——分别喂入要求 5000000 原子单位的挑战、报价另一种铸币地址的挑战,以及你从未注册的链上的挑战,并断言它们都不会产生签名。在 exact-SVM 方案中,结算器是交易费用支付方,因此买方钱包只需持有 USDC——无需 SOL 支付 gas。
结算器选择。 结算器代表资源服务器执行验证和结算,因此这是资源服务器的决定,而非买方的决定。按委托信任由少到多排列:
| 选项 | 适用场景 |
|---|---|
| 进程内自结算 | 你不希望结算路径中有第三方,且能自行持有密钥和 RPC 访问 |
| 自行运行结算器 | 你想要同样的控制力,但在多个服务间共享 |
x402.org 结算器 | 开发与测试网——它是包的默认值,无需配置,上游明确说明它不适用于主网路由 |
| 托管的生产结算器 | 你希望获得主网覆盖而不必自己运维基础设施 |
选择托管方案时,请从上游文档的结算器列表中挑选,而不是照搬这里的名字——该列表有人维护、并不详尽,且覆盖范围会变化。撰写时它包含 Coinbase 的 CDP(对每笔交易执行 KYT/OFAC 筛查)、PayAI、Corbits、Dexter、Solvador 等,其中若干同时覆盖 EVM 与 Solana 主网。无论选择哪一个,上线前都要在其 /supported 端点确认实时覆盖范围,并在新增网络时重新确认。
披露:本节由参与 PayAI(所列结算器之一)的人贡献。它只是若干选项之一,上面的自托管与上游默认路径是有意排在前面的。
卖方侧(API 向代理收费)。 使用上游中间件;一条路由可以同时提供 Base 和 Solana(可运行版本参见 examples/typescript/servers):
| 运行时 | 包 |
|---|---|
| Express / Hono / Next.js / Fastify | @x402/express@2.22.0、@x402/hono@2.22.0、@x402/next@2.22.0、@x402/fastify@2.22.0 |
| Python(FastAPI、Flask) | PyPI 上的 x402 |
Go(Gin、Echo、net/http) | github.com/x402-foundation/x402/go/v2 |
Solana 卖方:payTo 地址需要先有其规范代币账户。 exact-SVM 客户端用 findAssociatedTokenPda 为 payTo 推导关联代币账户(ATA)并转账到那里,但不会创建它。如果那个确切账户不存在,结算会在模拟阶段失败,402 返回 transaction_simulation_failed,看起来像客户端 bug,实际是收款方账户缺失。要检查推导出的地址本身——扫描该所有者的代币账户并不等价,因为同一铸币地址下的非规范辅助账户会让检查通过,而客户端真正要用的 ATA 仍然不存在:
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
const [ata] = await findAssociatedTokenPda({
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mainnet
owner: payToAddress,
tokenProgram: TOKEN_PROGRAM_ADDRESS, // TOKEN_2022_PROGRAM_ADDRESS for Token-2022 mints
});
然后确认该确切地址存在——value: null 表示不存在,向它结算将会失败:
curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"getAccountInfo",
"params":["<DERIVED_ATA>",{"encoding":"base64"}]}' \
| jq '.result.value != null'
创建方式(由创建者支付少量租金,而非付款方):
@solana-program/token 的 getCreateAssociatedTokenIdempotentInstruction 加入你的开通流程——幂等版本可安全重复执行,也是唯一确定性的方式。spl-token create-account <MINT> --owner <PAYTO_ADDRESS>。payTo 转账该代币也可以,但仅当发送方包含创建指令时——钱包和 spl-token transfer --fund-recipient 会包含;对缺失 ATA 的裸 transferChecked 会像结算一样失败。这个问题在向新开通的钱包或托管钱包付款时最容易出现,这类钱包往往还没有该资产的 ATA。
发现。 实现 x402 集市扩展的结算器会公开 /discovery/resources 端点——可在 https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources 查询 CDP 目录,在 https://facilitator.payai.network/discovery/resources 查询 PayAI 目录。对于 Solana 可付费服务,还有 Solana 基金会的精选目录 pay.sh。
在构建调用 agentpay MCP 服务器的编排器时,在分派付费工具调用前强制执行预算。
前提条件:在添加 MCP 配置前安装包——
npx不带-y会在非交互环境中提示确认,导致服务器挂起:npm install -g agentwallet-sdk@6.0.0
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function main() {
// 1. Validate credentials before constructing the transport.
// A missing key must fail immediately — never let the subprocess start without auth.
const walletKey = process.env.WALLET_PRIVATE_KEY;
if (!walletKey) {
throw new Error("WALLET_PRIVATE_KEY is not set — refusing to start payment server");
}
// Connect to the agentpay MCP server via stdio transport.
// Whitelist only the env vars the server needs — never forward all of process.env
// to a third-party subprocess that manages private keys.
const transport = new StdioClientTransport({
command: "npx",
args: ["agentwallet-sdk@6.0.0"],
env: {
PATH: process.env.PATH ?? "",
NODE_ENV: process.env.NODE_ENV ?? "production",
WALLET_PRIVATE_KEY: walletKey,
},
});
const agentpay = new Client({ name: "orchestrator", version: "1.0.0" });
await agentpay.connect(transport);
// 2. Set spending policy before delegating to the agent.
// Always verify success — a silent failure means no controls are active.
const policyResult = await agentpay.callTool({
name: "set_policy",
arguments: {
per_task_budget: 0.50,
per_session_budget: 5.00,
allowlisted_recipients: ["api.example.com"],
},
});
if (policyResult.isError) {
throw new Error(
`Failed to set spending policy — do not delegate: ${JSON.stringify(policyResult.content)}`
);
}
// 3. Use preToolCheck before any paid action
await preToolCheck(agentpay, 0.01);
}
// Pre-tool hook: fail-closed budget enforcement with four distinct error paths.
async function preToolCheck(agentpay: Client, apiCost: number): Promise<void> {
// Path 1: Reject invalid input (NaN/Infinity bypass the < comparison)
if (!Number.isFinite(apiCost) || apiCost < 0) {
throw new Error(`Invalid apiCost: ${apiCost} — action blocked`);
}
// Path 2: Transport/connectivity failure
let result;
try {
result = await agentpay.callTool({ name: "check_spending" });
} catch (err) {
throw new Error(`Payment service unreachable — action blocked: ${err}`);
}
// Path 3: Tool returned an error (e.g., auth failure, wallet not initialised)
if (result.isError) {
throw new Error(
`check_spending failed — action blocked: ${JSON.stringify(result.content)}`
);
}
// Path 4: Parse and validate the response shape
let remaining: number;
try {
const parsed = JSON.parse(
(result.content as Array<{ text: string }>)[0].text
);
if (!Number.isFinite(parsed?.remaining)) {
throw new TypeError("missing or non-finite 'remaining' field");
}
remaining = parsed.remaining;
} catch (err) {
throw new Error(
`check_spending returned unexpected format — action blocked: ${err}`
);
}
// Path 5: Budget exceeded
if (remaining < apiCost) {
throw new Error(
`Budget exceeded: need ${apiCost} but only ${remaining} remaining`
);
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
agentwallet-sdk@6.0.0)。部署到生产环境前验证包完整性。list_transactions 记录支出内容和原因。solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1)和免费的 x402.org 结算器开发,再转到主网的生产结算器。asset 与预期的 USDC 铸币地址核对(主网 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v,开发网 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU)——一个会支付任意资产的包装客户端是预算上的漏洞。agentwallet-sdkokx/payments — 面向 X Layer x402 的 TypeScript、Go、Rust 和 Java 卖家集成okx/onchainos-skillsx402-foundation/x402 — TypeScript、Python 和 Go 实现,以及维护中的客户端和服务器示例@x402 包:npmjs.com/org/x402 — @x402/fetch、@x402/axios、@x402/express、@x402/hono、@x402/next、@x402/fastify、@x402/evm、@x402/svm/discovery/resources;Solana 可付费服务见 pay.sh