From b9fcf97bb9e72a2bd5424fafca2681def341e103 Mon Sep 17 00:00:00 2001 From: "Notorious D.E.V" Date: Sat, 25 Jul 2026 20:28:46 +0800 Subject: [PATCH 1/6] feat(skills): add Solana and multi-network x402 to agent-payment-x402 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a third integration path to the agent-payment-x402 skill — Solana plus multi-network EVM via the upstream x402 packages — alongside the existing Base (agentwallet-sdk) and X Layer (OKX) options. The skill had no Solana coverage and no pointer to the canonical implementation. Option C centers on the actively maintained x402-foundation/x402 monorepo and stays facilitator-neutral: - Buyer: wrap the agent's HTTP client with @x402/fetch or @x402/axios and register both eip155:* and solana:* schemes, mirroring the upstream client examples — one client pays whichever chain the 402 offers - Seller: upstream middleware (@x402/express|hono|next|fastify pinned at 2.19.0, Python x402, github.com/x402-foundation/x402/go/v2); one route can advertise Base and Solana simultaneously - Facilitators: x402.org for testnets (the packages' default, testnet only), production via the upstream facilitator list — PayAI suggested (multi-network incl. Solana mainnet, no API keys), CDP noted (KYT/OFAC) - Discovery: bazaar /discovery/resources on CDP and PayAI, plus pay.sh for Solana-payable services - Safety rails: explicit user confirmation before the first paid request (mirroring Option B), a defined fail-closed budget gate invoked before every paid call, USDC mint validation with concrete mainnet/devnet addresses, and no unpinned npx commands Also resyncs the zh-CN translation, which predated the X Layer / Options structure, and mirrors the additions into ja-JP. Package names, versions, facilitator scope, USDC mints, and discovery URLs verified against x402-foundation/x402 main. --- docs/ja-JP/skills/agent-payment-x402/SKILL.md | 76 +++++++- docs/zh-CN/skills/agent-payment-x402/SKILL.md | 178 ++++++++++++++---- skills/agent-payment-x402/SKILL.md | 76 +++++++- 3 files changed, 294 insertions(+), 36 deletions(-) diff --git a/docs/ja-JP/skills/agent-payment-x402/SKILL.md b/docs/ja-JP/skills/agent-payment-x402/SKILL.md index 20a09d917..30712807f 100644 --- a/docs/ja-JP/skills/agent-payment-x402/SKILL.md +++ b/docs/ja-JP/skills/agent-payment-x402/SKILL.md @@ -1,6 +1,6 @@ --- name: agent-payment-x402 -description: タスクごとのバジェット、支出コントロール、ノンカストディアルウォレットを備えた x402 決済実行を AI エージェントに追加します。agentwallet-sdk を通じて Base をサポートし、OKX Payments / OKX エージェント決済プロトコルを通じて X Layer をサポートします。 +description: タスクごとのバジェット、支出コントロール、ノンカストディアルウォレットを備えた x402 決済実行を AI エージェントに追加します。agentwallet-sdk を通じて Base を、OKX Payments / OKX エージェント決済プロトコルを通じて X Layer を、アップストリームの x402 パッケージとファシリテーターベースの決済を通じて Solana とマルチネットワーク EVM をサポートします。 origin: community --- @@ -20,6 +20,8 @@ origin: community |------|------------------| | エージェントが Base または他の agentwallet 対応チェーンの 402 ゲート API に支払う | 厳格な支出ポリシーで `agentwallet-sdk` を MCP 決済サーバーとして使用 | | エージェントが X Layer の 402 ゲート API に支払う | `okx/onchainos-skills` の OKX エージェント決済プロトコルを使用;`okx-x402-payment` は廃止されたレガシーエイリアス | +| エージェントが Solana または他の x402 v2 ネットワークの 402 ゲート API に支払う | エージェントの HTTP クライアントをアップストリームの `@x402/fetch` または `@x402/axios` パッケージでラップし、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 ドキュメントを使用 | @@ -30,6 +32,7 @@ origin: community - `agentwallet-sdk`: 本番使用前に現在のネットワークカバレッジをパッケージドキュメントで確認。Base Sepolia が最も安全な開発デフォルト;Base メインネットがオリジナルスキルで説明されている本番パス。 - OKX Payments / X Layer: 現在のセラードキュメントは X Layer(`eip155:196`)と USDT0 決済を対象。決済パッケージとファシリテーターの動作が迅速に変わる可能性があるため、本番コードを生成する前に現在の SDK ドキュメントを取得すること。 +- アップストリーム x402 パッケージ: 設計上マルチネットワーク — 1 つのルートで Base と Solana を同時に提示し、買い手に選ばせることができる。パッケージのデフォルトは `x402.org` ファシリテーターで、テストネット専用(Base Sepolia `eip155:84532`、Solana デブネット `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`、加えて Stellar、Aptos、Hedera、XRPL の各テストネット)であり、メインネットルート向けではない。本番はアップストリームドキュメントのファシリテーターリストから選ぶ — PayAI ファシリテーターは Solana メインネット(`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`)ほか複数ネットワークで API キー不要の妥当なデフォルト;CDP ファシリテーターは全トランザクションに KYT/OFAC スクリーニングを行う Coinbase ホスト型。本番前にここにハードコードするのではなく、各ファシリテーターの `/supported` エンドポイントでライブカバレッジを確認すること。 ## 仕組み @@ -98,6 +101,69 @@ X Layer x402、マルチパーティ決済(MPP)、セッション決済、 現在の OKX リポジトリを確認せずに古いドキュメントの例をコピーしないこと。現在の OKX ガイダンスはディスパッチャーとして `okx-agent-payments-protocol` を使用しており、Java セラードキュメントが利用可能になっています。 +### オプション C: アップストリーム x402 パッケージ(Solana + Base/EVM) + +エージェントが Solana、Base、またはアップストリームのプロトコル実装がサポートする他のネットワークで支払う(または API が課金する)場合にこのパスを使用します。[`x402-foundation/x402`](https://github.com/x402-foundation/x402) の正規 x402 モノレポは活発にメンテナンスされており、クライアントとミドルウェアのパッケージを直接公開しています。オプション A・B と異なり、これは別個の MCP サーバーではありません — エージェント自身の HTTP クライアントをラップし、リソースサーバーが選んだファシリテーターが検証・決済します。 + +バイヤー側エージェントフローの場合: + +1. 古いドキュメントのスニペットをコピーするのではなく、メンテナンスされている [`examples/typescript/clients`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients)(fetch、axios、MCP)の例から始める。 +2. オプション B が OKX フローに要求するのとまったく同様に、最初の有料リクエストの署名または送信の前に明示的なユーザー確認を求める。汎用ツール呼び出しの背後に決済実行を隠さない。 +3. パッケージバージョンを固定する(例:`@x402/fetch@2.19.0`);アップストリームの全パッケージはロックステップでバージョニングされる。 +4. すべての有料呼び出しを、呼び出しの直前にバジェットポリシーでフェイルクローズドにゲートする — クライアント構築時に一度だけではない。 + +```typescript +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"; + +// 署名者のキーはオーケストレーターの env に属する — ハードコードせず、エージェントが書き込めないようにする。 +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"); +} + +// 1 つのクライアントで両方のネットワークファミリーに対応:買い手は 402 が提示するチェーンで支払う。 +const client = new x402Client(); +client.register("eip155:*", new ExactEvmScheme(privateKeyToAccount(evmKey))); +client.register("solana:*", new ExactSvmScheme(await createKeyPairSignerFromBytes(base58.decode(svmKey)))); +const fetchWithPayment = wrapFetchWithPayment(fetch, client); + +// 最小限のフェイルクローズドゲート。バジェット追跡を備えた完全な MCP バックの +// バージョンは、下の Examples セクションの preToolCheck を再利用する。 +const ALLOWED_HOSTS = new Set(["api.example.com"]); +let sessionSpend = 0; +function assertPaymentAllowed(url: string, maxCost: number, sessionCap = 5.0): void { + if (!ALLOWED_HOSTS.has(new URL(url).host)) throw new Error("Host not allowlisted — blocked"); + if (!Number.isFinite(maxCost) || maxCost < 0) throw new Error("Invalid cost — blocked"); + if (sessionSpend + maxCost > sessionCap) throw new Error("Session budget exceeded — blocked"); + sessionSpend += maxCost; +} + +// すべての有料呼び出しの直前にゲートする — 構築時に一度だけチェックされたラップ +// クライアントは、以降のすべての呼び出しを無計量のままにしてしまう。 +assertPaymentAllowed("https://api.example.com/data", 0.01); +const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); +``` + +ラップされたクライアントは、`network` が登録済みスキームに一致するチャレンジのみに支払います。署名前にチャレンジの `asset` も検証すること:Solana では USDC はメインネットが `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`、デブネットが `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`。不一致はフェイルクローズドとして扱う:署名せず、リトライしない。exact-SVM スキームではファシリテーターがトランザクション手数料支払者となるため、買い手のウォレットは USDC のみを保有すればよい — ガス用の SOL は不要。 + +**ファシリテーターの選択。** パッケージのデフォルトは [`x402.org` ファシリテーター](https://x402.org/facilitator) — テストネット専用(Base Sepolia、Solana デブネットほか)、セットアップ不要で開発に最適。メインネットはアップストリームドキュメントの[ファシリテーターリスト](https://docs.x402.org/dev-tools/facilitators)から選ぶ:[PayAI ファシリテーター](https://facilitator.payai.network) は妥当な本番デフォルト(Solana メインネットを含むマルチネットワーク、API キー不要)で、CDP ファシリテーターは全トランザクションに KYT/OFAC スクリーニングを行う Coinbase ホスト型。自前のファシリテーター運用やセルフファシリテーションも可能。 + +**セラー側(API がエージェントに課金する)。** アップストリームのミドルウェアを使用する;1 つのルートで Base と Solana を同時に提示できる(実行可能なバージョンは [`examples/typescript/servers`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers) を参照): + +| ランタイム | パッケージ | +|---------|---------| +| Express / Hono / Next.js / Fastify | `@x402/express@2.19.0`、`@x402/hono@2.19.0`、`@x402/next@2.19.0`、`@x402/fastify@2.19.0` | +| Python(FastAPI、Flask) | PyPI の `x402` | +| Go(Gin、Echo、`net/http`) | `github.com/x402-foundation/x402/go/v2` | + +**発見。** x402 バザール拡張を実装するファシリテーターは `/discovery/resources` エンドポイントを公開する — CDP カタログは `https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources`、PayAI カタログは `https://facilitator.payai.network/discovery/resources` で照会できる。Solana で支払い可能なサービスには、Solana Foundation のキュレーションカタログである [pay.sh](https://pay.sh) もある。 + ## 例 ### MCP クライアントでのバジェット強制 @@ -212,7 +278,8 @@ main().catch((err) => { - **監査証跡**: タスク後のフックで `list_transactions` を使用して何が使われたかをログに記録する。 - **フェイルクローズド**: 決済ツールに到達できない場合、有料アクションをブロックする — 課金されないアクセスにフォールバックしない。 - **security-review と組み合わせる**: 決済ツールは高い権限を持つ。シェルアクセスと同じ精査を適用する。 -- **まずテストネットでテストする**: 開発には Base Sepolia を使用;本番には Base メインネットに切り替える。 +- **まずテストネットでテストする**: 開発には Base Sepolia を使用;本番には Base メインネットに切り替える。Solana では、メインネットの本番ファシリテーターに移る前に、Solana デブネット(`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`)と無料の x402.org ファシリテーターで開発する。 +- **Solana では SOL ではなく USDC を資金として入れる**: exact-SVM スキームではファシリテーターがトランザクション手数料支払者となるため、SOL を持たないウォレットでも支払える。署名前に各チャレンジの `asset` を期待する USDC ミント(メインネット `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`、デブネット `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`)と照合する — 任意のアセットに支払うラップクライアントはバジェットの穴になる。 ## 本番リファレンス @@ -222,3 +289,8 @@ main().catch((err) => { - **OKX Payments SDK**: [`okx/payments`](https://github.com/okx/payments) — X Layer x402 向け TypeScript、Go、Rust、Java セラー統合 - **OKX エージェント決済プロトコルスキル**: [`okx/onchainos-skills`](https://github.com/okx/onchainos-skills/tree/main/skills/okx-agent-payments-protocol) - **OKX Payments 概要**: [web3.okx.com/onchainos/dev-docs/payments/overview](https://web3.okx.com/onchainos/dev-docs/payments/overview) +- **アップストリーム x402 モノレポ**: [`x402-foundation/x402`](https://github.com/x402-foundation/x402) — TypeScript、Python、Go の実装と、メンテナンスされているクライアント・サーバー例 +- **x402 ドキュメント**: [docs.x402.org](https://docs.x402.org);本番ファシリテーターリストは [docs.x402.org/dev-tools/facilitators](https://docs.x402.org/dev-tools/facilitators) +- **`@x402` パッケージ**: [npmjs.com/org/x402](https://www.npmjs.com/org/x402) — `@x402/fetch`、`@x402/axios`、`@x402/express`、`@x402/hono`、`@x402/next`、`@x402/fastify`、`@x402/evm`、`@x402/svm` +- **ファシリテーター**: [x402.org ファシリテーター](https://x402.org/facilitator)(テストネットデフォルト)、[PayAI](https://facilitator.payai.network)(マルチネットワーク本番、API キー不要)、[CDP](https://docs.cdp.coinbase.com/x402/docs/quickstart-sellers)(Coinbase ホスト型、KYT/OFAC) +- **発見**: CDP と PayAI のバザールは `/discovery/resources`;Solana で支払い可能なサービスは [pay.sh](https://pay.sh) diff --git a/docs/zh-CN/skills/agent-payment-x402/SKILL.md b/docs/zh-CN/skills/agent-payment-x402/SKILL.md index f282c6d39..e749e0227 100644 --- a/docs/zh-CN/skills/agent-payment-x402/SKILL.md +++ b/docs/zh-CN/skills/agent-payment-x402/SKILL.md @@ -1,41 +1,61 @@ --- name: agent-payment-x402 -description: 将 x402 支付执行添加到 AI 代理中——通过 MCP 工具实现每任务预算、支出控制和非托管钱包。当代理需要为 API、服务或其他代理付费时使用。 +description: 将 x402 支付执行添加到 AI 代理中,具备每任务预算、支出控制和非托管钱包。通过 agentwallet-sdk 支持 Base,通过 OKX Payments / OKX 代理支付协议支持 X Layer,并通过上游 x402 包与基于结算器的结算支持 Solana 及多网络 EVM。 origin: community --- # 代理支付执行 (x402) -让AI代理能够自主支付并内置消费控制。使用x402 HTTP支付协议和MCP工具,使代理能够为外部服务、API或其他代理支付,无需托管风险。 +让 AI 代理能够进行策略门控的支付并内置支出控制。使用 x402 HTTP 支付协议和 MCP 工具,使代理能够为外部服务、API 或其他代理付费,无托管风险。 ## 使用场景 -适用于:代理需要支付API调用、购买服务、与其他代理结算、强制执行每项任务消费限额,或管理非托管钱包。与成本感知LLM流水线和安全审查技能自然搭配。 +适用于:代理需要支付 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 主网是原始技能所述的生产路径。 +- OKX Payments / X Layer:当前卖家文档面向 X Layer(`eip155:196`)和 USDT0 结算。由于支付包和结算器行为可能快速变化,生成生产代码前请获取当前 SDK 文档。 +- 上游 x402 包:设计上即多网络 —— 一条路由可以同时提供 Base 和 Solana,由买方选择。包默认使用 `x402.org` 结算器,它仅限测试网(Base Sepolia `eip155:84532`、Solana 开发网 `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`,以及 Stellar、Aptos、Hedera、XRPL 测试网),不适用于主网路由。生产环境请从上游文档的结算器列表中选择 —— PayAI 结算器是 Solana 主网(`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`)及其他网络的合理默认选择,无需 API 密钥;CDP 结算器由 Coinbase 托管,对每笔交易执行 KYT/OFAC 筛查。生产前请在各结算器的 `/supported` 端点确认实时覆盖范围,而不要在此硬编码。 ## 工作原理 -### x402协议 +### x402 协议 +x402 将 HTTP 402(需要付款)扩展为机器可协商的流程。当服务器返回 `402` 时,代理的支付工具会协商价格、检查预算、签署交易,并仅在编排器设定的策略与确认边界内重试。 -x402将HTTP 402(需要付款)扩展为机器可协商的流程。当服务器返回`402`时,代理的支付工具会自动协商价格、检查预算、签署交易并重试——无需人工干预。 - -### 消费控制 - -每次支付工具调用都会强制执行`SpendingPolicy`: - -* **每任务预算** — 单次代理操作的最大支出 -* **每会话预算** — 整个会话的累计限额 -* **白名单接收方** — 限制代理可支付的地址/服务 -* **速率限制** — 每分钟/小时的最大交易数 +### 支出控制 +每次支付工具调用都会强制执行 `SpendingPolicy`: +- **每任务预算** — 单次代理操作的最大支出 +- **每会话预算** — 整个会话的累计限额 +- **白名单接收方** — 限制代理可支付的地址/服务 +- **速率限制** — 每分钟/小时的最大交易数 ### 非托管钱包 +代理通过 ERC-4337 智能账户持有自己的密钥。编排器在委托前设置策略;代理只能在限定范围内支出。无资金池,无托管风险。 -代理通过ERC-4337智能账户持有自己的密钥。编排器在委托前设置策略;代理只能在限定范围内支出。无资金池,无托管风险。 +## MCP 集成 -## MCP集成 +支付层暴露标准 MCP 工具,可无缝接入任何 Claude Code 或代理框架设置。 -支付层暴露标准MCP工具,可无缝接入任何Claude Code或代理框架设置。 +> **安全提示**:务必锁定包版本。此工具管理私钥——未锁定的 `npx` 安装会引入供应链风险。 -> **安全提示**:务必锁定包版本。此工具管理私钥——未锁定的`npx`安装会引入供应链风险。 +### 选项 A:agentwallet-sdk(Base / 多链) ```json { @@ -53,19 +73,104 @@ x402将HTTP 402(需要付款)扩展为机器可协商的流程。当服务 | 工具 | 用途 | |------|---------| | `get_balance` | 检查代理钱包余额 | -| `send_payment` | 向地址或ENS发送付款 | +| `send_payment` | 向地址或 ENS 发送付款 | | `check_spending` | 查询剩余预算 | | `list_transactions` | 所有付款的审计追踪 | -> **注意**:消费策略由**编排器**在委托给代理之前设置——而非代理本身。这防止代理自行提高消费限额。通过编排层或任务前钩子中的`set_policy`配置策略,切勿将其作为代理可调用工具。 +> **注意**:支出策略由**编排器**在委托给代理之前设置——而非代理本身。这可防止代理自行提高支出限额。通过编排层或任务前钩子中的 `set_policy` 配置策略,切勿将其作为代理可调用工具。 + +### 选项 B:OKX 代理支付协议(X Layer) + +将此路径用于 X Layer x402、多方支付(MPP)、会话支付、收费和 A2A 收费流程。 + +对于买方代理流程: + +1. 安装或引用当前的 `okx/onchainos-skills` 仓库。 +2. 使用 `skills/okx-agent-payments-protocol/SKILL.md` 作为调度器。 +3. 将 `skills/okx-x402-payment/SKILL.md` 视为已弃用的兼容别名,而非规范技能。 +4. 在钱包状态检查或支付操作前要求明确的用户确认。不要将支付执行隐藏在通用工具调用之后。 + +对于卖方 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 卖家文档现已可用。 + +### 选项 C:上游 x402 包(Solana + Base/EVM) + +当代理在 Solana、Base 或上游协议实现支持的其他网络上付费(或你的 API 收费)时,使用此路径。位于 [`x402-foundation/x402`](https://github.com/x402-foundation/x402) 的规范 x402 monorepo 正在积极维护,并直接发布客户端和中间件包。与选项 A 和 B 不同,这不是独立的 MCP 服务器——你包装代理自己的 HTTP 客户端,由资源服务器选择的结算器验证并结算。 + +对于买方代理流程: + +1. 从维护中的 [`examples/typescript/clients`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients) 示例(fetch、axios、MCP)开始,而不是复制旧文档中的片段。 +2. 在签署或提交第一笔付费请求之前要求明确的用户确认,与选项 B 对 OKX 流程的要求完全一致。不要将支付执行隐藏在通用工具调用之后。 +3. 锁定包版本(例如 `@x402/fetch@2.19.0`);上游所有包以相同步调发布版本。 +4. 在每次付费调用之前立即用预算策略进行故障关闭门控——而不是在客户端构建时只做一次。 + +```typescript +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)))); +const fetchWithPayment = wrapFetchWithPayment(fetch, client); + +// Minimal fail-closed gate. For the full MCP-backed version with budget +// tracking, reuse preToolCheck from the Examples section below. +const ALLOWED_HOSTS = new Set(["api.example.com"]); +let sessionSpend = 0; +function assertPaymentAllowed(url: string, maxCost: number, sessionCap = 5.0): void { + if (!ALLOWED_HOSTS.has(new URL(url).host)) throw new Error("Host not allowlisted — blocked"); + if (!Number.isFinite(maxCost) || maxCost < 0) throw new Error("Invalid cost — blocked"); + if (sessionSpend + maxCost > sessionCap) throw new Error("Session budget exceeded — blocked"); + sessionSpend += maxCost; +} + +// Gate immediately before EVERY paid call — a wrapped client checked only once +// at construction leaves every later call unmetered. +assertPaymentAllowed("https://api.example.com/data", 0.01); +const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); +``` + +包装后的客户端只会支付 `network` 与已注册方案匹配的挑战。签署前还应验证挑战的 `asset`:在 Solana 上,USDC 主网为 `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`,开发网为 `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`。任何不匹配都按故障关闭处理:不签署,不重试。在 exact-SVM 方案中,结算器是交易费用支付方,因此买方钱包只需持有 USDC——无需 SOL 支付 gas。 + +**结算器选择。** 包默认使用 [`x402.org` 结算器](https://x402.org/facilitator)——仅限测试网(Base Sepolia、Solana 开发网等),零配置,适合开发。主网请从上游文档的[结算器列表](https://docs.x402.org/dev-tools/facilitators)中选择:[PayAI 结算器](https://facilitator.payai.network) 是合理的生产默认选择(包括 Solana 主网在内的多网络,无需 API 密钥),CDP 结算器由 Coinbase 托管并对每笔交易执行 KYT/OFAC 筛查。也可以自行运行结算器或自结算。 + +**卖方侧(API 向代理收费)。** 使用上游中间件;一条路由可以同时提供 Base 和 Solana(可运行版本参见 [`examples/typescript/servers`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers)): + +| 运行时 | 包 | +|---------|---------| +| Express / Hono / Next.js / Fastify | `@x402/express@2.19.0`、`@x402/hono@2.19.0`、`@x402/next@2.19.0`、`@x402/fastify@2.19.0` | +| Python(FastAPI、Flask) | PyPI 上的 `x402` | +| Go(Gin、Echo、`net/http`) | `github.com/x402-foundation/x402/go/v2` | + +**发现。** 实现 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](https://pay.sh)。 ## 示例 -### MCP客户端中的预算执行 +### MCP 客户端中的预算执行 -在构建调用agentpay MCP服务器的编排器时,在分派付费工具调用前强制执行预算。 +在构建调用 agentpay MCP 服务器的编排器时,在分派付费工具调用前强制执行预算。 -> **前提条件**:在添加MCP配置前安装包——`npx`不带`-y`会在非交互环境中提示确认,导致服务器挂起:`npm install -g agentwallet-sdk@6.0.0` +> **前提条件**:在添加 MCP 配置前安装包——`npx` 不带 `-y` 会在非交互环境中提示确认,导致服务器挂起:`npm install -g agentwallet-sdk@6.0.0` ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js"; @@ -168,15 +273,24 @@ main().catch((err) => { ## 最佳实践 -* **委托前设置预算**:生成子代理时,通过编排层附加SpendingPolicy。切勿让代理拥有无限支出权限。 -* **锁定依赖项**:始终在MCP配置中指定确切版本(例如`agentwallet-sdk@6.0.0`)。部署到生产环境前验证包完整性。 -* **审计追踪**:在任务后钩子中使用`list_transactions`记录支出内容和原因。 -* **故障关闭**:如果支付工具不可达,阻止付费操作——不要回退到无计量访问。 -* **配合安全审查**:支付工具是高权限操作。应用与shell访问相同的审查标准。 -* **先在测试网测试**:开发时使用Base Sepolia;生产环境切换到Base主网。 +- **委托前设置预算**:生成子代理时,通过编排层附加 SpendingPolicy。切勿让代理拥有无限支出权限。 +- **锁定依赖项**:始终在 MCP 配置中指定确切版本(例如 `agentwallet-sdk@6.0.0`)。部署到生产环境前验证包完整性。 +- **审计追踪**:在任务后钩子中使用 `list_transactions` 记录支出内容和原因。 +- **故障关闭**:如果支付工具不可达,阻止付费操作——不要回退到无计量访问。 +- **配合 security-review**:支付工具是高权限操作。应用与 shell 访问相同的审查标准。 +- **先在测试网测试**:开发时使用 Base Sepolia;生产环境切换到 Base 主网。在 Solana 上,先针对 Solana 开发网(`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`)和免费的 x402.org 结算器开发,再转到主网的生产结算器。 +- **在 Solana 上注入 USDC 而非 SOL**:exact-SVM 方案让结算器成为交易费用支付方,因此没有 SOL 的钱包也能付费。签署前将每个挑战的 `asset` 与预期的 USDC 铸币地址核对(主网 `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`,开发网 `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`)——一个会支付任意资产的包装客户端是预算上的漏洞。 ## 生产参考 -* **npm**:[`agentwallet-sdk`](https://www.npmjs.com/package/agentwallet-sdk) -* **合并到NVIDIA NeMo Agent Toolkit**:[PR #17](https://github.com/NVIDIA/NeMo-Agent-Toolkit-Examples/pull/17) — NVIDIA代理示例的x402支付工具 -* **协议规范**:[x402.org](https://x402.org) +- **npm**:[`agentwallet-sdk`](https://www.npmjs.com/package/agentwallet-sdk) +- **合并到 NVIDIA NeMo Agent Toolkit**:[PR #17](https://github.com/NVIDIA/NeMo-Agent-Toolkit-Examples/pull/17) — 面向 NVIDIA 代理示例的 x402 支付工具 +- **协议规范**:[x402.org](https://x402.org) +- **OKX Payments SDK**:[`okx/payments`](https://github.com/okx/payments) — 面向 X Layer x402 的 TypeScript、Go、Rust 和 Java 卖家集成 +- **OKX 代理支付协议技能**:[`okx/onchainos-skills`](https://github.com/okx/onchainos-skills/tree/main/skills/okx-agent-payments-protocol) +- **OKX Payments 概览**:[web3.okx.com/onchainos/dev-docs/payments/overview](https://web3.okx.com/onchainos/dev-docs/payments/overview) +- **上游 x402 monorepo**:[`x402-foundation/x402`](https://github.com/x402-foundation/x402) — TypeScript、Python 和 Go 实现,以及维护中的客户端和服务器示例 +- **x402 文档**:[docs.x402.org](https://docs.x402.org);生产结算器列表位于 [docs.x402.org/dev-tools/facilitators](https://docs.x402.org/dev-tools/facilitators) +- **`@x402` 包**:[npmjs.com/org/x402](https://www.npmjs.com/org/x402) — `@x402/fetch`、`@x402/axios`、`@x402/express`、`@x402/hono`、`@x402/next`、`@x402/fastify`、`@x402/evm`、`@x402/svm` +- **结算器**:[x402.org 结算器](https://x402.org/facilitator)(测试网默认)、[PayAI](https://facilitator.payai.network)(多网络生产,无需 API 密钥)、[CDP](https://docs.cdp.coinbase.com/x402/docs/quickstart-sellers)(Coinbase 托管,KYT/OFAC) +- **发现**:CDP 与 PayAI 集市位于 `/discovery/resources`;Solana 可付费服务见 [pay.sh](https://pay.sh) diff --git a/skills/agent-payment-x402/SKILL.md b/skills/agent-payment-x402/SKILL.md index adc340c98..7fae8f368 100644 --- a/skills/agent-payment-x402/SKILL.md +++ b/skills/agent-payment-x402/SKILL.md @@ -1,6 +1,6 @@ --- name: agent-payment-x402 -description: Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. +description: Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk, X Layer through OKX Payments / OKX Agent Payments Protocol, and Solana plus multi-network EVM through the upstream x402 packages with facilitator-based settlement. metadata: origin: community --- @@ -21,6 +21,8 @@ Choose the integration path based on whether your agent is buying access to a pa |------|------------------| | Agent pays a 402-gated API on Base or another agentwallet-supported chain | Use `agentwallet-sdk` as an MCP payment server with strict spending policy | | Agent pays a 402-gated API on X Layer | Use OKX Agent Payments Protocol from `okx/onchainos-skills`; `okx-x402-payment` is a deprecated legacy alias | +| Agent pays a 402-gated API on Solana or another x402 v2 network | Wrap the agent's HTTP client with the upstream `@x402/fetch` or `@x402/axios` package and register the EVM/SVM schemes; the resource server's facilitator verifies and settles | +| API charges agents on Solana or multiple networks (TypeScript, Python, or Go) | Use the upstream x402 middleware from `x402-foundation/x402` — `@x402/express`, `@x402/hono`, `@x402/next`, or `@x402/fastify` for TypeScript, `x402` for Python, `github.com/x402-foundation/x402/go/v2` for Go | | TypeScript API charges agents | Use OKX Payments TypeScript seller SDK docs for Express, Hono, Fastify, or Next.js | | Go API charges agents | Use OKX Payments Go seller SDK docs for Gin, Echo, or `net/http` | | Rust API charges agents | Use OKX Payments Rust seller SDK docs for Axum | @@ -31,6 +33,7 @@ Choose the integration path based on whether your agent is buying access to a pa - `agentwallet-sdk`: use the package docs to confirm current network coverage before production. Base Sepolia is the safest development default; Base mainnet is the production path called out by the original skill. - OKX Payments / X Layer: current seller docs target X Layer (`eip155:196`) and USDT0 settlement. Fetch current SDK docs before generating production code because payment packages and facilitator behavior can change quickly. +- Upstream x402 packages: multi-network by design — one route can advertise Base and Solana simultaneously and let the buyer pick. The packages default to the `x402.org` facilitator, which is testnet-only (Base Sepolia `eip155:84532`, Solana devnet `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`, plus Stellar, Aptos, Hedera, and XRPL testnets) and is not intended for mainnet routes. For production, choose from the facilitator list in the upstream docs — the PayAI facilitator is a reasonable default for Solana mainnet (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) and other networks with no API keys required; the CDP facilitator is Coinbase-hosted with KYT/OFAC screening. Confirm live coverage at each facilitator's `/supported` endpoint before production rather than hardcoding it here. ## How It Works @@ -99,6 +102,69 @@ For seller-side API flows, fetch the latest language-specific guide before gener Do not copy examples from older docs without checking the current OKX repository. Current OKX guidance uses `okx-agent-payments-protocol` as the dispatcher, and Java seller docs are now available. +### Option C: Upstream x402 packages (Solana + Base/EVM) + +Use this path when the agent pays — or your API charges — on Solana, Base, or another network the upstream protocol implementation supports. The canonical x402 monorepo at [`x402-foundation/x402`](https://github.com/x402-foundation/x402) is actively maintained and publishes the client and middleware packages directly. Unlike Options A and B this is not a separate MCP server — you wrap the agent's own HTTP client, and a facilitator chosen by the resource server verifies and settles. + +For buyer-side agent flows: + +1. Start from the maintained examples in [`examples/typescript/clients`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients) (fetch, axios, MCP) rather than copying snippets from older docs. +2. Require explicit user confirmation before signing or submitting the first paid request, exactly as Option B requires for OKX flows. Do not hide payment execution behind a generic tool call. +3. Pin package versions (for example `@x402/fetch@2.19.0`); all upstream packages version in lockstep. +4. Gate every paid call fail-closed with your budget policy immediately before the call — not once at client construction. + +```typescript +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)))); +const fetchWithPayment = wrapFetchWithPayment(fetch, client); + +// Minimal fail-closed gate. For the full MCP-backed version with budget +// tracking, reuse preToolCheck from the Examples section below. +const ALLOWED_HOSTS = new Set(["api.example.com"]); +let sessionSpend = 0; +function assertPaymentAllowed(url: string, maxCost: number, sessionCap = 5.0): void { + if (!ALLOWED_HOSTS.has(new URL(url).host)) throw new Error("Host not allowlisted — blocked"); + if (!Number.isFinite(maxCost) || maxCost < 0) throw new Error("Invalid cost — blocked"); + if (sessionSpend + maxCost > sessionCap) throw new Error("Session budget exceeded — blocked"); + sessionSpend += maxCost; +} + +// Gate immediately before EVERY paid call — a wrapped client checked only once +// at construction leaves every later call unmetered. +assertPaymentAllowed("https://api.example.com/data", 0.01); +const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); +``` + +The wrapped client only pays challenges whose `network` matches a scheme you registered. Before signing, also verify the challenge's `asset`: on Solana, USDC is `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` (mainnet) and `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` (devnet). Treat any mismatch as fail-closed: do not sign, do not retry. In the exact-SVM scheme the facilitator is the transaction fee payer, so the buyer wallet holds USDC only — no SOL for gas. + +**Facilitator choice.** The packages default to the [`x402.org` facilitator](https://x402.org/facilitator) — testnet-only (Base Sepolia, Solana devnet, and other testnets), zero setup, right for development. For mainnet, pick from the [facilitator list](https://docs.x402.org/dev-tools/facilitators) in the upstream docs: the [PayAI facilitator](https://facilitator.payai.network) is a reasonable production default (multi-network including Solana mainnet, no API keys required), and the CDP facilitator is Coinbase-hosted with KYT/OFAC screening on every transaction. Anyone can also run their own facilitator or self-facilitate. + +**Seller side (API charges agents).** Use the upstream middleware; one route can advertise Base and Solana simultaneously (see [`examples/typescript/servers`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers) for runnable versions): + +| Runtime | Package | +|---------|---------| +| Express / Hono / Next.js / Fastify | `@x402/express@2.19.0`, `@x402/hono@2.19.0`, `@x402/next@2.19.0`, `@x402/fastify@2.19.0` | +| Python (FastAPI, Flask) | `x402` on PyPI | +| Go (Gin, Echo, `net/http`) | `github.com/x402-foundation/x402/go/v2` | + +**Discovery.** Facilitators that implement the x402 bazaar extension expose a `/discovery/resources` endpoint — query the CDP catalog at `https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources` and the PayAI catalog at `https://facilitator.payai.network/discovery/resources`. For Solana-payable services there is also [pay.sh](https://pay.sh), the Solana Foundation's curated catalog. + ## Examples ### Budget enforcement in an MCP client @@ -213,7 +279,8 @@ main().catch((err) => { - **Audit trails**: Use `list_transactions` in post-task hooks to log what was spent and why. - **Fail closed**: If the payment tool is unreachable, block the paid action — don't fall back to unmetered access. - **Pair with security-review**: Payment tools are high-privilege. Apply the same scrutiny as shell access. -- **Test with testnets first**: Use Base Sepolia for development; switch to Base mainnet for production. +- **Test with testnets first**: Use Base Sepolia for development; switch to Base mainnet for production. On Solana, develop against Solana devnet (`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`) and the free x402.org facilitator before moving to a production facilitator on mainnet. +- **On Solana, fund USDC not SOL**: The exact-SVM scheme makes the facilitator the transaction fee payer, so a SOL-less wallet still pays. Verify each challenge's `asset` against the expected USDC mint (mainnet `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`, devnet `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`) before signing — a wrapped client that pays any asset is a hole in your budget. ## Production Reference @@ -223,3 +290,8 @@ main().catch((err) => { - **OKX Payments SDKs**: [`okx/payments`](https://github.com/okx/payments) — TypeScript, Go, Rust, and Java seller integrations for X Layer x402 - **OKX Agent Payments Protocol skill**: [`okx/onchainos-skills`](https://github.com/okx/onchainos-skills/tree/main/skills/okx-agent-payments-protocol) - **OKX Payments overview**: [web3.okx.com/onchainos/dev-docs/payments/overview](https://web3.okx.com/onchainos/dev-docs/payments/overview) +- **Upstream x402 monorepo**: [`x402-foundation/x402`](https://github.com/x402-foundation/x402) — TypeScript, Python, and Go implementations plus maintained client and server examples +- **x402 docs**: [docs.x402.org](https://docs.x402.org); production facilitator list at [docs.x402.org/dev-tools/facilitators](https://docs.x402.org/dev-tools/facilitators) +- **`@x402` packages**: [npmjs.com/org/x402](https://www.npmjs.com/org/x402) — `@x402/fetch`, `@x402/axios`, `@x402/express`, `@x402/hono`, `@x402/next`, `@x402/fastify`, `@x402/evm`, `@x402/svm` +- **Facilitators**: [x402.org facilitator](https://x402.org/facilitator) (testnet default), [PayAI](https://facilitator.payai.network) (multi-network production, no API keys), [CDP](https://docs.cdp.coinbase.com/x402/docs/quickstart-sellers) (Coinbase-hosted, KYT/OFAC) +- **Discovery**: CDP and PayAI bazaars at `/discovery/resources`; [pay.sh](https://pay.sh) for Solana-payable services From e70fba447abcf9e04b3ed7481c2a28d78fc55666 Mon Sep 17 00:00:00 2001 From: "Notorious D.E.V" Date: Sat, 25 Jul 2026 21:15:06 +0800 Subject: [PATCH 2/6] docs(skills): note the Solana recipient token-account requirement The exact-SVM client transfers to the associated token account it derives for payTo and does not create it. When that account is missing, settlement fails at simulation and the 402 returns transaction_simulation_failed, which reads like a client bug rather than a missing recipient account. Adds a pre-flight check plus the three ways to create the account, and notes that freshly provisioned and custodial wallets are the common case. Found while running the documented flow end to end on Solana devnet; confirmed against the upstream client scheme, which only derives the destination ATA. Not currently covered in the upstream docs. Mirrored across en, ja-JP, and zh-CN. --- docs/ja-JP/skills/agent-payment-x402/SKILL.md | 17 +++++++++++++++++ docs/zh-CN/skills/agent-payment-x402/SKILL.md | 17 +++++++++++++++++ skills/agent-payment-x402/SKILL.md | 17 +++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/docs/ja-JP/skills/agent-payment-x402/SKILL.md b/docs/ja-JP/skills/agent-payment-x402/SKILL.md index 30712807f..902dbd5bd 100644 --- a/docs/ja-JP/skills/agent-payment-x402/SKILL.md +++ b/docs/ja-JP/skills/agent-payment-x402/SKILL.md @@ -162,6 +162,23 @@ const res = await fetchWithPayment("https://api.example.com/data", { method: "GE | Python(FastAPI、Flask) | PyPI の `x402` | | Go(Gin、Echo、`net/http`) | `github.com/x402-foundation/x402/go/v2` | +**Solana のセラー:`payTo` アドレスには先にトークンアカウントが必要。** exact-SVM クライアントは `payTo` から導出した*関連トークンアカウント*(ATA)に送金するが、それを作成はしない。そのアカウントが存在しないと決済はシミュレーション段階で失敗し、402 が `transaction_simulation_failed` を返す — クライアントのバグのように見えるが、実際は受取側アカウントの欠落である。本番投入前に確認すること: + +```bash +curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getTokenAccountsByOwner", + "params":["",{"mint":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},{"encoding":"jsonParsed"}]}' \ + | jq '.result.value | length' # 0 なら USDC アカウントなし — 支払い受付前に作成する +``` + +次のいずれかで作成できる(作成者が少額のレントを負担し、支払者ではない): + +- そのトークンを `payTo` に一度でも送金する — 送金側の転送でアカウントが作成される。 +- spl-token CLI で `spl-token create-account --owner `。 +- コード上では `@solana-program/token` の `getCreateAssociatedTokenIdempotentInstruction` をプロビジョニングフローに追加する — 冪等版なので再実行しても安全。 + +これは新規プロビジョニングされたウォレットやカストディアルウォレットへの支払いで最も問題になりやすい。そうしたウォレットは対象アセットの ATA をまだ持っていないことが多い。 + **発見。** x402 バザール拡張を実装するファシリテーターは `/discovery/resources` エンドポイントを公開する — CDP カタログは `https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources`、PayAI カタログは `https://facilitator.payai.network/discovery/resources` で照会できる。Solana で支払い可能なサービスには、Solana Foundation のキュレーションカタログである [pay.sh](https://pay.sh) もある。 ## 例 diff --git a/docs/zh-CN/skills/agent-payment-x402/SKILL.md b/docs/zh-CN/skills/agent-payment-x402/SKILL.md index e749e0227..d1183b9ed 100644 --- a/docs/zh-CN/skills/agent-payment-x402/SKILL.md +++ b/docs/zh-CN/skills/agent-payment-x402/SKILL.md @@ -162,6 +162,23 @@ const res = await fetchWithPayment("https://api.example.com/data", { method: "GE | Python(FastAPI、Flask) | PyPI 上的 `x402` | | Go(Gin、Echo、`net/http`) | `github.com/x402-foundation/x402/go/v2` | +**Solana 卖方:`payTo` 地址需要先有代币账户。** exact-SVM 客户端会转账到它为 `payTo` 推导出的*关联代币账户*(ATA),但不会创建它。如果该账户不存在,结算会在模拟阶段失败——402 返回 `transaction_simulation_failed`,看起来像客户端 bug,实际是收款方账户缺失。上线前请检查: + +```bash +curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getTokenAccountsByOwner", + "params":["",{"mint":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},{"encoding":"jsonParsed"}]}' \ + | jq '.result.value | length' # 0 表示没有 USDC 账户 —— 接受付款前先创建 +``` + +以下任一方式都能创建(由创建者支付少量租金,而非付款方): + +- 向 `payTo` 转一次该代币的任意数量——发送方的转账会创建该账户。 +- 用 spl-token CLI:`spl-token create-account --owner `。 +- 在代码中,将 `@solana-program/token` 的 `getCreateAssociatedTokenIdempotentInstruction` 加入你的开通流程——幂等版本可安全重复执行。 + +这个问题在向新开通的钱包或托管钱包付款时最容易出现,这类钱包往往还没有该资产的 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](https://pay.sh)。 ## 示例 diff --git a/skills/agent-payment-x402/SKILL.md b/skills/agent-payment-x402/SKILL.md index 7fae8f368..af634673a 100644 --- a/skills/agent-payment-x402/SKILL.md +++ b/skills/agent-payment-x402/SKILL.md @@ -163,6 +163,23 @@ The wrapped client only pays challenges whose `network` matches a scheme you reg | Python (FastAPI, Flask) | `x402` on PyPI | | Go (Gin, Echo, `net/http`) | `github.com/x402-foundation/x402/go/v2` | +**Solana sellers: the `payTo` address needs a token account first.** The exact-SVM client transfers to the *associated token account* (ATA) it derives for `payTo` and does not create it. If that account does not exist, settlement fails at simulation — the 402 comes back with `transaction_simulation_failed`, which reads like a client bug but is a missing recipient account. Check before going live: + +```bash +curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getTokenAccountsByOwner", + "params":["",{"mint":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},{"encoding":"jsonParsed"}]}' \ + | jq '.result.value | length' # 0 means no USDC account — create it before accepting payments +``` + +Any one of these creates it (whoever creates it pays the small rent, not the payer): + +- Send any amount of that token to `payTo` once — the sender's transfer creates the account. +- `spl-token create-account --owner ` with the spl-token CLI. +- In code, add `getCreateAssociatedTokenIdempotentInstruction` from `@solana-program/token` to your provisioning flow — the idempotent variant is safe to re-run. + +This bites hardest when payouts go to freshly provisioned or custodial wallets, which often have no ATA for the asset yet. + **Discovery.** Facilitators that implement the x402 bazaar extension expose a `/discovery/resources` endpoint — query the CDP catalog at `https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources` and the PayAI catalog at `https://facilitator.payai.network/discovery/resources`. For Solana-payable services there is also [pay.sh](https://pay.sh), the Solana Foundation's curated catalog. ## Examples From b78ea7293505c6aadae591f2e6675a2fc12b478a Mon Sep 17 00:00:00 2001 From: "Notorious D.E.V" Date: Wed, 12 Aug 2026 12:08:01 +0800 Subject: [PATCH 3/6] fix(skills): enforce budget against the server's challenge, refresh to 2.22.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the security review on #2575. Enforce policy against the actual challenge. The previous example compared a budget against a number the caller passed in, so a server quoting any amount could still be paid. Replaces it with a PaymentPolicy registered on the client, which filters the server-provided requirements before a signature exists and validates all three fields — amount, asset, network. Filtering everything out makes createPaymentPayload throw rather than fall back to paying. Testing this adversarially surfaced a bug in the guard itself: EVM addresses are case-insensitive, so a server quoting lowercase USDC — the same token — was dropped. Assets are now normalised before comparison, lowercasing only 0x addresses, since Solana base58 is case-sensitive and must not be folded. Also rejects negative amounts. Verified against 12 cases: over-budget, wrong mint, wrong network, case-flipped Solana mint, non-numeric and negative amounts all refuse; checksummed, lowercase and uppercase EVM forms and the cap boundary all pass. Refresh the supported contract. Pins move 2.19.0 -> 2.22.0 (upstream shipped 2.20, 2.21 and 2.22 since this branch opened). The documented server and client APIs are unchanged across that range — re-ran the seller middleware on 2.22.0 and it still returns a multi-network 402. Keep facilitator guidance vendor-neutral. Self-facilitation and running your own facilitator are now listed first, ahead of the testnet default and hosted options, and the hosted paragraph points at the upstream maintained list rather than naming a default. Adds an explicit disclosure of the author's affiliation with one listed facilitator. Mirrored across en, ja-JP, and zh-CN. --- docs/ja-JP/skills/agent-payment-x402/SKILL.md | 77 +++++++++++++------ docs/zh-CN/skills/agent-payment-x402/SKILL.md | 73 +++++++++++++----- skills/agent-payment-x402/SKILL.md | 73 +++++++++++++----- 3 files changed, 158 insertions(+), 65 deletions(-) diff --git a/docs/ja-JP/skills/agent-payment-x402/SKILL.md b/docs/ja-JP/skills/agent-payment-x402/SKILL.md index 902dbd5bd..ea069053d 100644 --- a/docs/ja-JP/skills/agent-payment-x402/SKILL.md +++ b/docs/ja-JP/skills/agent-payment-x402/SKILL.md @@ -32,7 +32,7 @@ origin: community - `agentwallet-sdk`: 本番使用前に現在のネットワークカバレッジをパッケージドキュメントで確認。Base Sepolia が最も安全な開発デフォルト;Base メインネットがオリジナルスキルで説明されている本番パス。 - OKX Payments / X Layer: 現在のセラードキュメントは X Layer(`eip155:196`)と USDT0 決済を対象。決済パッケージとファシリテーターの動作が迅速に変わる可能性があるため、本番コードを生成する前に現在の SDK ドキュメントを取得すること。 -- アップストリーム x402 パッケージ: 設計上マルチネットワーク — 1 つのルートで Base と Solana を同時に提示し、買い手に選ばせることができる。パッケージのデフォルトは `x402.org` ファシリテーターで、テストネット専用(Base Sepolia `eip155:84532`、Solana デブネット `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`、加えて Stellar、Aptos、Hedera、XRPL の各テストネット)であり、メインネットルート向けではない。本番はアップストリームドキュメントのファシリテーターリストから選ぶ — PayAI ファシリテーターは Solana メインネット(`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`)ほか複数ネットワークで API キー不要の妥当なデフォルト;CDP ファシリテーターは全トランザクションに KYT/OFAC スクリーニングを行う Coinbase ホスト型。本番前にここにハードコードするのではなく、各ファシリテーターの `/supported` エンドポイントでライブカバレッジを確認すること。 +- アップストリーム x402 パッケージ: 設計上マルチネットワーク — 1 つのルートで Base と Solana を同時に提示し、買い手に選ばせることができる。パッケージのデフォルトは `x402.org` ファシリテーターで、テストネット専用(Base Sepolia `eip155:84532`、Solana デブネット `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`、加えて Stellar、Aptos、Hedera、XRPL の各テストネット)であり、メインネットルート向けではない。メインネット(Solana は `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`)では、自己ファシリテートするか、自前のファシリテーターを運用するか、アップストリームのファシリテーターリストからホスト型を選ぶ — オプション C のファシリテーター比較を参照。本番前にここにハードコードするのではなく、ファシリテーターの `/supported` エンドポイントでライブカバレッジを確認すること。 ## 仕組み @@ -109,8 +109,8 @@ X Layer x402、マルチパーティ決済(MPP)、セッション決済、 1. 古いドキュメントのスニペットをコピーするのではなく、メンテナンスされている [`examples/typescript/clients`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients)(fetch、axios、MCP)の例から始める。 2. オプション B が OKX フローに要求するのとまったく同様に、最初の有料リクエストの署名または送信の前に明示的なユーザー確認を求める。汎用ツール呼び出しの背後に決済実行を隠さない。 -3. パッケージバージョンを固定する(例:`@x402/fetch@2.19.0`);アップストリームの全パッケージはロックステップでバージョニングされる。 -4. すべての有料呼び出しを、呼び出しの直前にバジェットポリシーでフェイルクローズドにゲートする — クライアント構築時に一度だけではない。 +3. パッケージバージョンを固定する(例:`@x402/fetch@2.22.0`);アップストリームの全パッケージはロックステップでバージョニングされる。 +4. バジェットはクライアントに登録した `PaymentPolicy` で強制し、毎回サーバーの実際のチャレンジに対して検査が走るようにする。自分で渡した数値と突き合わせるバジェットは何も証明しない — 金額・アセット・ネットワークはすべてサーバー由来なので、署名が生成される前に 3 つとも検証すること。 ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; @@ -120,45 +120,76 @@ import { privateKeyToAccount } from "viem/accounts"; import { createKeyPairSignerFromBytes } from "@solana/kit"; import { base58 } from "@scure/base"; -// 署名者のキーはオーケストレーターの env に属する — ハードコードせず、エージェントが書き込めないようにする。 +// 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"); } -// 1 つのクライアントで両方のネットワークファミリーに対応:買い手は 402 が提示するチェーンで支払う。 +// 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, so $0.01 + +// 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 normalizeAsset = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); +const ALLOWED_ASSETS = new Set( + [ + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC, Solana mainnet + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC, Base + ].map(normalizeAsset), +); + +client.registerPolicy((_x402Version, requirements) => + requirements.filter(r => { + if (!ALLOWED_NETWORKS.has(r.network)) return false; // wrong chain + if (!ALLOWED_ASSETS.has(normalizeAsset(r.asset))) return false; // wrong token + try { + const amount = BigInt(r.amount); + return amount >= 0n && amount <= MAX_AMOUNT; // over budget / negative + } catch { + return false; // unparseable amount + } + }), +); + const fetchWithPayment = wrapFetchWithPayment(fetch, client); -// 最小限のフェイルクローズドゲート。バジェット追跡を備えた完全な MCP バックの -// バージョンは、下の Examples セクションの preToolCheck を再利用する。 -const ALLOWED_HOSTS = new Set(["api.example.com"]); -let sessionSpend = 0; -function assertPaymentAllowed(url: string, maxCost: number, sessionCap = 5.0): void { - if (!ALLOWED_HOSTS.has(new URL(url).host)) throw new Error("Host not allowlisted — blocked"); - if (!Number.isFinite(maxCost) || maxCost < 0) throw new Error("Invalid cost — blocked"); - if (sessionSpend + maxCost > sessionCap) throw new Error("Session budget exceeded — blocked"); - sessionSpend += maxCost; -} - -// すべての有料呼び出しの直前にゲートする — 構築時に一度だけチェックされたラップ -// クライアントは、以降のすべての呼び出しを無計量のままにしてしまう。 -assertPaymentAllowed("https://api.example.com/data", 0.01); const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); ``` -ラップされたクライアントは、`network` が登録済みスキームに一致するチャレンジのみに支払います。署名前にチャレンジの `asset` も検証すること:Solana では USDC はメインネットが `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`、デブネットが `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`。不一致はフェイルクローズドとして扱う:署名せず、リトライしない。exact-SVM スキームではファシリテーターがトランザクション手数料支払者となるため、買い手のウォレットは USDC のみを保有すればよい — ガス用の SOL は不要。 +ポリシーを登録すると、予算超過の金額、想定外のトークン、未登録のチェーンはすべてフェイルクローズドになる — 候補がすべて除外されるため、`createPaymentPayload` は署名せずに例外を投げる。デブネット USDC は `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`;開発時のみ `ALLOWED_ASSETS` に追加すること。ポリシーは敵対的にテストする — `5000000` アトミック単位を要求するチャレンジ、別のミントを提示するチャレンジ、登録していないチェーンのチャレンジを与え、いずれも署名が生成されないことを確認する。exact-SVM スキームではファシリテーターがトランザクション手数料支払者となるため、買い手のウォレットは USDC のみを保有すればよい — ガス用の SOL は不要。 -**ファシリテーターの選択。** パッケージのデフォルトは [`x402.org` ファシリテーター](https://x402.org/facilitator) — テストネット専用(Base Sepolia、Solana デブネットほか)、セットアップ不要で開発に最適。メインネットはアップストリームドキュメントの[ファシリテーターリスト](https://docs.x402.org/dev-tools/facilitators)から選ぶ:[PayAI ファシリテーター](https://facilitator.payai.network) は妥当な本番デフォルト(Solana メインネットを含むマルチネットワーク、API キー不要)で、CDP ファシリテーターは全トランザクションに KYT/OFAC スクリーニングを行う Coinbase ホスト型。自前のファシリテーター運用やセルフファシリテーションも可能。 +**ファシリテーターの選択。** ファシリテーターはリソースサーバーに代わって検証と決済を行うため、これは買い手ではなくリソースサーバーの判断である。委ねる信頼が小さい順に: + +| 選択肢 | 適する場面 | +|--------|--------------| +| [インプロセスで自己ファシリテート](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/self-facilitation) | 決済経路に第三者を入れたくなく、鍵と RPC アクセスを自前で保持できる | +| 自前のファシリテーターを運用 | 同じ制御を複数サービスで共有したい | +| [`x402.org` ファシリテーター](https://x402.org/facilitator) | 開発・テストネット向け — パッケージのデフォルトでセットアップ不要。アップストリームはメインネットルート向けではないと明記している | +| ホスト型の本番ファシリテーター | インフラを運用せずにメインネット対応が欲しい | + +ホスト型を選ぶ場合は、ここに書かれた名前ではなくアップストリームドキュメントの[ファシリテーターリスト](https://docs.x402.org/dev-tools/facilitators)から選ぶこと — メンテナンスされており、網羅的ではなく、カバレッジも変わる。執筆時点では Coinbase の CDP(全トランザクションに KYT/OFAC スクリーニング)、PayAI、Corbits、Dexter、Solvador などが掲載され、複数が EVM に加えて Solana メインネットをカバーしている。どれを選ぶ場合も、本番前に `/supported` エンドポイントでライブカバレッジを確認し、ネットワークを追加したときは再確認すること。 + +> **開示**: このセクションは、掲載されているファシリテーターの 1 つである PayAI に関わる者が寄稿した。複数ある選択肢の 1 つとして記載しており、上記の自己ホストおよびアップストリームデフォルトのパスを意図的に先に挙げている。 **セラー側(API がエージェントに課金する)。** アップストリームのミドルウェアを使用する;1 つのルートで Base と Solana を同時に提示できる(実行可能なバージョンは [`examples/typescript/servers`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers) を参照): | ランタイム | パッケージ | |---------|---------| -| Express / Hono / Next.js / Fastify | `@x402/express@2.19.0`、`@x402/hono@2.19.0`、`@x402/next@2.19.0`、`@x402/fastify@2.19.0` | +| 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` | @@ -309,5 +340,5 @@ main().catch((err) => { - **アップストリーム x402 モノレポ**: [`x402-foundation/x402`](https://github.com/x402-foundation/x402) — TypeScript、Python、Go の実装と、メンテナンスされているクライアント・サーバー例 - **x402 ドキュメント**: [docs.x402.org](https://docs.x402.org);本番ファシリテーターリストは [docs.x402.org/dev-tools/facilitators](https://docs.x402.org/dev-tools/facilitators) - **`@x402` パッケージ**: [npmjs.com/org/x402](https://www.npmjs.com/org/x402) — `@x402/fetch`、`@x402/axios`、`@x402/express`、`@x402/hono`、`@x402/next`、`@x402/fastify`、`@x402/evm`、`@x402/svm` -- **ファシリテーター**: [x402.org ファシリテーター](https://x402.org/facilitator)(テストネットデフォルト)、[PayAI](https://facilitator.payai.network)(マルチネットワーク本番、API キー不要)、[CDP](https://docs.cdp.coinbase.com/x402/docs/quickstart-sellers)(Coinbase ホスト型、KYT/OFAC) +- **ファシリテーター**: [自己ファシリテーションの例](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/self-facilitation)(第三者なし)、[x402.org ファシリテーター](https://x402.org/facilitator)(テストネットデフォルト)、メンテナンスされている[本番リスト](https://docs.x402.org/dev-tools/facilitators) - **発見**: CDP と PayAI のバザールは `/discovery/resources`;Solana で支払い可能なサービスは [pay.sh](https://pay.sh) diff --git a/docs/zh-CN/skills/agent-payment-x402/SKILL.md b/docs/zh-CN/skills/agent-payment-x402/SKILL.md index d1183b9ed..dca78ad01 100644 --- a/docs/zh-CN/skills/agent-payment-x402/SKILL.md +++ b/docs/zh-CN/skills/agent-payment-x402/SKILL.md @@ -32,7 +32,7 @@ origin: community - `agentwallet-sdk`:在生产使用前,通过包文档确认当前网络覆盖范围。Base Sepolia 是最安全的开发默认值;Base 主网是原始技能所述的生产路径。 - OKX Payments / X Layer:当前卖家文档面向 X Layer(`eip155:196`)和 USDT0 结算。由于支付包和结算器行为可能快速变化,生成生产代码前请获取当前 SDK 文档。 -- 上游 x402 包:设计上即多网络 —— 一条路由可以同时提供 Base 和 Solana,由买方选择。包默认使用 `x402.org` 结算器,它仅限测试网(Base Sepolia `eip155:84532`、Solana 开发网 `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`,以及 Stellar、Aptos、Hedera、XRPL 测试网),不适用于主网路由。生产环境请从上游文档的结算器列表中选择 —— PayAI 结算器是 Solana 主网(`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`)及其他网络的合理默认选择,无需 API 密钥;CDP 结算器由 Coinbase 托管,对每笔交易执行 KYT/OFAC 筛查。生产前请在各结算器的 `/supported` 端点确认实时覆盖范围,而不要在此硬编码。 +- 上游 x402 包:设计上即多网络 —— 一条路由可以同时提供 Base 和 Solana,由买方选择。包默认使用 `x402.org` 结算器,它仅限测试网(Base Sepolia `eip155:84532`、Solana 开发网 `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`,以及 Stellar、Aptos、Hedera、XRPL 测试网),不适用于主网路由。主网(Solana 为 `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`)请选择自结算、自行运行结算器,或从上游结算器列表中挑选托管方案——参见选项 C 中的结算器对比。生产前请在结算器的 `/supported` 端点确认实时覆盖范围,而不要在此硬编码。 ## 工作原理 @@ -109,8 +109,8 @@ x402 将 HTTP 402(需要付款)扩展为机器可协商的流程。当服务 1. 从维护中的 [`examples/typescript/clients`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients) 示例(fetch、axios、MCP)开始,而不是复制旧文档中的片段。 2. 在签署或提交第一笔付费请求之前要求明确的用户确认,与选项 B 对 OKX 流程的要求完全一致。不要将支付执行隐藏在通用工具调用之后。 -3. 锁定包版本(例如 `@x402/fetch@2.19.0`);上游所有包以相同步调发布版本。 -4. 在每次付费调用之前立即用预算策略进行故障关闭门控——而不是在客户端构建时只做一次。 +3. 锁定包版本(例如 `@x402/fetch@2.22.0`);上游所有包以相同步调发布版本。 +4. 用注册到客户端的 `PaymentPolicy` 强制预算,使检查在每次调用时都针对服务器的真实挑战运行。与你自己传入的数字比较的预算不能证明任何事——金额、资产和网络都来自服务器,因此必须在签名产生之前全部验证。 ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; @@ -131,34 +131,65 @@ if (!evmKey || !svmKey) { 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, so $0.01 + +// 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 normalizeAsset = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); +const ALLOWED_ASSETS = new Set( + [ + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC, Solana mainnet + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC, Base + ].map(normalizeAsset), +); + +client.registerPolicy((_x402Version, requirements) => + requirements.filter(r => { + if (!ALLOWED_NETWORKS.has(r.network)) return false; // wrong chain + if (!ALLOWED_ASSETS.has(normalizeAsset(r.asset))) return false; // wrong token + try { + const amount = BigInt(r.amount); + return amount >= 0n && amount <= MAX_AMOUNT; // over budget / negative + } catch { + return false; // unparseable amount + } + }), +); + const fetchWithPayment = wrapFetchWithPayment(fetch, client); -// Minimal fail-closed gate. For the full MCP-backed version with budget -// tracking, reuse preToolCheck from the Examples section below. -const ALLOWED_HOSTS = new Set(["api.example.com"]); -let sessionSpend = 0; -function assertPaymentAllowed(url: string, maxCost: number, sessionCap = 5.0): void { - if (!ALLOWED_HOSTS.has(new URL(url).host)) throw new Error("Host not allowlisted — blocked"); - if (!Number.isFinite(maxCost) || maxCost < 0) throw new Error("Invalid cost — blocked"); - if (sessionSpend + maxCost > sessionCap) throw new Error("Session budget exceeded — blocked"); - sessionSpend += maxCost; -} - -// Gate immediately before EVERY paid call — a wrapped client checked only once -// at construction leaves every later call unmetered. -assertPaymentAllowed("https://api.example.com/data", 0.01); const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); ``` -包装后的客户端只会支付 `network` 与已注册方案匹配的挑战。签署前还应验证挑战的 `asset`:在 Solana 上,USDC 主网为 `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`,开发网为 `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`。任何不匹配都按故障关闭处理:不签署,不重试。在 exact-SVM 方案中,结算器是交易费用支付方,因此买方钱包只需持有 USDC——无需 SOL 支付 gas。 +注册该策略后,超预算金额、非预期代币或未注册的链都会故障关闭——所有候选项都被过滤掉,`createPaymentPayload` 会抛出异常而不是签名。开发网 USDC 是 `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`;仅在开发时把它加入 `ALLOWED_ASSETS`。对策略做对抗性测试——分别喂入要求 `5000000` 原子单位的挑战、报价另一种铸币地址的挑战,以及你从未注册的链上的挑战,并断言它们都不会产生签名。在 exact-SVM 方案中,结算器是交易费用支付方,因此买方钱包只需持有 USDC——无需 SOL 支付 gas。 -**结算器选择。** 包默认使用 [`x402.org` 结算器](https://x402.org/facilitator)——仅限测试网(Base Sepolia、Solana 开发网等),零配置,适合开发。主网请从上游文档的[结算器列表](https://docs.x402.org/dev-tools/facilitators)中选择:[PayAI 结算器](https://facilitator.payai.network) 是合理的生产默认选择(包括 Solana 主网在内的多网络,无需 API 密钥),CDP 结算器由 Coinbase 托管并对每笔交易执行 KYT/OFAC 筛查。也可以自行运行结算器或自结算。 +**结算器选择。** 结算器代表资源服务器执行验证和结算,因此这是资源服务器的决定,而非买方的决定。按委托信任由少到多排列: + +| 选项 | 适用场景 | +|--------|--------------| +| [进程内自结算](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/self-facilitation) | 你不希望结算路径中有第三方,且能自行持有密钥和 RPC 访问 | +| 自行运行结算器 | 你想要同样的控制力,但在多个服务间共享 | +| [`x402.org` 结算器](https://x402.org/facilitator) | 开发与测试网——它是包的默认值,无需配置,上游明确说明它不适用于主网路由 | +| 托管的生产结算器 | 你希望获得主网覆盖而不必自己运维基础设施 | + +选择托管方案时,请从上游文档的[结算器列表](https://docs.x402.org/dev-tools/facilitators)中挑选,而不是照搬这里的名字——该列表有人维护、并不详尽,且覆盖范围会变化。撰写时它包含 Coinbase 的 CDP(对每笔交易执行 KYT/OFAC 筛查)、PayAI、Corbits、Dexter、Solvador 等,其中若干同时覆盖 EVM 与 Solana 主网。无论选择哪一个,上线前都要在其 `/supported` 端点确认实时覆盖范围,并在新增网络时重新确认。 + +> **披露**:本节由参与 PayAI(所列结算器之一)的人贡献。它只是若干选项之一,上面的自托管与上游默认路径是有意排在前面的。 **卖方侧(API 向代理收费)。** 使用上游中间件;一条路由可以同时提供 Base 和 Solana(可运行版本参见 [`examples/typescript/servers`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers)): | 运行时 | 包 | |---------|---------| -| Express / Hono / Next.js / Fastify | `@x402/express@2.19.0`、`@x402/hono@2.19.0`、`@x402/next@2.19.0`、`@x402/fastify@2.19.0` | +| 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` | @@ -309,5 +340,5 @@ main().catch((err) => { - **上游 x402 monorepo**:[`x402-foundation/x402`](https://github.com/x402-foundation/x402) — TypeScript、Python 和 Go 实现,以及维护中的客户端和服务器示例 - **x402 文档**:[docs.x402.org](https://docs.x402.org);生产结算器列表位于 [docs.x402.org/dev-tools/facilitators](https://docs.x402.org/dev-tools/facilitators) - **`@x402` 包**:[npmjs.com/org/x402](https://www.npmjs.com/org/x402) — `@x402/fetch`、`@x402/axios`、`@x402/express`、`@x402/hono`、`@x402/next`、`@x402/fastify`、`@x402/evm`、`@x402/svm` -- **结算器**:[x402.org 结算器](https://x402.org/facilitator)(测试网默认)、[PayAI](https://facilitator.payai.network)(多网络生产,无需 API 密钥)、[CDP](https://docs.cdp.coinbase.com/x402/docs/quickstart-sellers)(Coinbase 托管,KYT/OFAC) +- **结算器**:[自结算示例](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/self-facilitation)(无第三方)、[x402.org 结算器](https://x402.org/facilitator)(测试网默认)、以及有人维护的[生产列表](https://docs.x402.org/dev-tools/facilitators) - **发现**:CDP 与 PayAI 集市位于 `/discovery/resources`;Solana 可付费服务见 [pay.sh](https://pay.sh) diff --git a/skills/agent-payment-x402/SKILL.md b/skills/agent-payment-x402/SKILL.md index af634673a..5ab39951f 100644 --- a/skills/agent-payment-x402/SKILL.md +++ b/skills/agent-payment-x402/SKILL.md @@ -33,7 +33,7 @@ Choose the integration path based on whether your agent is buying access to a pa - `agentwallet-sdk`: use the package docs to confirm current network coverage before production. Base Sepolia is the safest development default; Base mainnet is the production path called out by the original skill. - OKX Payments / X Layer: current seller docs target X Layer (`eip155:196`) and USDT0 settlement. Fetch current SDK docs before generating production code because payment packages and facilitator behavior can change quickly. -- Upstream x402 packages: multi-network by design — one route can advertise Base and Solana simultaneously and let the buyer pick. The packages default to the `x402.org` facilitator, which is testnet-only (Base Sepolia `eip155:84532`, Solana devnet `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`, plus Stellar, Aptos, Hedera, and XRPL testnets) and is not intended for mainnet routes. For production, choose from the facilitator list in the upstream docs — the PayAI facilitator is a reasonable default for Solana mainnet (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) and other networks with no API keys required; the CDP facilitator is Coinbase-hosted with KYT/OFAC screening. Confirm live coverage at each facilitator's `/supported` endpoint before production rather than hardcoding it here. +- Upstream x402 packages: multi-network by design — one route can advertise Base and Solana simultaneously and let the buyer pick. The packages default to the `x402.org` facilitator, which is testnet-only (Base Sepolia `eip155:84532`, Solana devnet `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`, plus Stellar, Aptos, Hedera, and XRPL testnets) and is not intended for mainnet routes. For mainnet (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` for Solana), either self-facilitate, run your own facilitator, or pick a hosted one from the upstream facilitator list — see the facilitator comparison under Option C. Confirm live coverage at the facilitator's `/supported` endpoint before production rather than hardcoding it here. ## How It Works @@ -110,8 +110,8 @@ For buyer-side agent flows: 1. Start from the maintained examples in [`examples/typescript/clients`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients) (fetch, axios, MCP) rather than copying snippets from older docs. 2. Require explicit user confirmation before signing or submitting the first paid request, exactly as Option B requires for OKX flows. Do not hide payment execution behind a generic tool call. -3. Pin package versions (for example `@x402/fetch@2.19.0`); all upstream packages version in lockstep. -4. Gate every paid call fail-closed with your budget policy immediately before the call — not once at client construction. +3. Pin package versions (for example `@x402/fetch@2.22.0`); all upstream packages version in lockstep. +4. Enforce your budget with a `PaymentPolicy` registered on the client, so the check runs against the server's actual challenge on every call. A budget compared against a number you passed in yourself proves nothing — the amount, asset, and network all come from the server, so all three must be validated before a signature exists. ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; @@ -132,34 +132,65 @@ if (!evmKey || !svmKey) { 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, so $0.01 + +// 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 normalizeAsset = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); +const ALLOWED_ASSETS = new Set( + [ + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC, Solana mainnet + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC, Base + ].map(normalizeAsset), +); + +client.registerPolicy((_x402Version, requirements) => + requirements.filter(r => { + if (!ALLOWED_NETWORKS.has(r.network)) return false; // wrong chain + if (!ALLOWED_ASSETS.has(normalizeAsset(r.asset))) return false; // wrong token + try { + const amount = BigInt(r.amount); + return amount >= 0n && amount <= MAX_AMOUNT; // over budget / negative + } catch { + return false; // unparseable amount + } + }), +); + const fetchWithPayment = wrapFetchWithPayment(fetch, client); -// Minimal fail-closed gate. For the full MCP-backed version with budget -// tracking, reuse preToolCheck from the Examples section below. -const ALLOWED_HOSTS = new Set(["api.example.com"]); -let sessionSpend = 0; -function assertPaymentAllowed(url: string, maxCost: number, sessionCap = 5.0): void { - if (!ALLOWED_HOSTS.has(new URL(url).host)) throw new Error("Host not allowlisted — blocked"); - if (!Number.isFinite(maxCost) || maxCost < 0) throw new Error("Invalid cost — blocked"); - if (sessionSpend + maxCost > sessionCap) throw new Error("Session budget exceeded — blocked"); - sessionSpend += maxCost; -} - -// Gate immediately before EVERY paid call — a wrapped client checked only once -// at construction leaves every later call unmetered. -assertPaymentAllowed("https://api.example.com/data", 0.01); const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); ``` -The wrapped client only pays challenges whose `network` matches a scheme you registered. Before signing, also verify the challenge's `asset`: on Solana, USDC is `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` (mainnet) and `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` (devnet). Treat any mismatch as fail-closed: do not sign, do not retry. In the exact-SVM scheme the facilitator is the transaction fee payer, so the buyer wallet holds USDC only — no SOL for gas. +With the policy registered, an over-budget amount, an unexpected token, or an unregistered chain all fail closed — `createPaymentPayload` throws instead of signing, because every candidate was filtered out. Devnet USDC is `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`; add it to `ALLOWED_ASSETS` only for development. Test the policy adversarially — feed it a challenge demanding `5000000` atomic units, one quoting a different mint, and one on a chain you never registered, and assert that none of them produce a signature. In the exact-SVM scheme the facilitator is the transaction fee payer, so the buyer wallet holds USDC only — no SOL for gas. -**Facilitator choice.** The packages default to the [`x402.org` facilitator](https://x402.org/facilitator) — testnet-only (Base Sepolia, Solana devnet, and other testnets), zero setup, right for development. For mainnet, pick from the [facilitator list](https://docs.x402.org/dev-tools/facilitators) in the upstream docs: the [PayAI facilitator](https://facilitator.payai.network) is a reasonable production default (multi-network including Solana mainnet, no API keys required), and the CDP facilitator is Coinbase-hosted with KYT/OFAC screening on every transaction. Anyone can also run their own facilitator or self-facilitate. +**Facilitator choice.** A facilitator verifies and settles on the resource server's behalf, so this is the resource server's decision, not the buyer's. In rough order of least to most trust delegated: + +| Option | When it fits | +|--------|--------------| +| [Self-facilitate in-process](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/self-facilitation) | You want no third party in the settlement path and can hold keys and RPC access yourself | +| Run your own facilitator | You want that same control but shared across several services | +| [`x402.org` facilitator](https://x402.org/facilitator) | Development and testnets — it is the package default, requires no setup, and upstream documents it as not intended for mainnet routes | +| A hosted production facilitator | You want mainnet coverage without operating infrastructure | + +For the hosted option, choose from the [facilitator list](https://docs.x402.org/dev-tools/facilitators) in the upstream docs rather than a name copied from here — it is maintained, it is not exhaustive, and coverage changes. As of writing it includes Coinbase's CDP (KYT/OFAC screening on every transaction), PayAI, Corbits, Dexter, Solvador, and others; several cover Solana mainnet alongside EVM. Whichever you pick, confirm live coverage at its `/supported` endpoint before production and re-check when you add a network. + +> **Disclosure**: this section was contributed by someone who works on PayAI, one of the listed facilitators. It is included as one option among several, and the self-hosted and upstream-default paths above are deliberately listed first. **Seller side (API charges agents).** Use the upstream middleware; one route can advertise Base and Solana simultaneously (see [`examples/typescript/servers`](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers) for runnable versions): | Runtime | Package | |---------|---------| -| Express / Hono / Next.js / Fastify | `@x402/express@2.19.0`, `@x402/hono@2.19.0`, `@x402/next@2.19.0`, `@x402/fastify@2.19.0` | +| 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) | `x402` on PyPI | | Go (Gin, Echo, `net/http`) | `github.com/x402-foundation/x402/go/v2` | @@ -310,5 +341,5 @@ main().catch((err) => { - **Upstream x402 monorepo**: [`x402-foundation/x402`](https://github.com/x402-foundation/x402) — TypeScript, Python, and Go implementations plus maintained client and server examples - **x402 docs**: [docs.x402.org](https://docs.x402.org); production facilitator list at [docs.x402.org/dev-tools/facilitators](https://docs.x402.org/dev-tools/facilitators) - **`@x402` packages**: [npmjs.com/org/x402](https://www.npmjs.com/org/x402) — `@x402/fetch`, `@x402/axios`, `@x402/express`, `@x402/hono`, `@x402/next`, `@x402/fastify`, `@x402/evm`, `@x402/svm` -- **Facilitators**: [x402.org facilitator](https://x402.org/facilitator) (testnet default), [PayAI](https://facilitator.payai.network) (multi-network production, no API keys), [CDP](https://docs.cdp.coinbase.com/x402/docs/quickstart-sellers) (Coinbase-hosted, KYT/OFAC) +- **Facilitators**: [self-facilitation example](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/self-facilitation) (no third party), [x402.org facilitator](https://x402.org/facilitator) (testnet default), and the maintained [production list](https://docs.x402.org/dev-tools/facilitators) - **Discovery**: CDP and PayAI bazaars at `/discovery/resources`; [pay.sh](https://pay.sh) for Solana-payable services From f1c3511268a0c62510c9fc039e775c54e8065183 Mon Sep 17 00:00:00 2001 From: "Notorious D.E.V" Date: Wed, 12 Aug 2026 17:35:29 +0800 Subject: [PATCH 4/6] fix(skills): add a session boundary to the buyer example, tighten the ATA check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from the bot review. The reusable payment client had no authorization boundary. The policy validates one challenge at a time, so it cannot see a session total and cannot ask a human anything — a long-lived wrapped client could pay repeatedly, to any recipient, past any intended budget. This also contradicted the skill's own Spending Controls section, which promises per-session budgets and allowlisted recipients. Adds an allowlist of recipients to the policy, and routes every paid call through a boundary that asks for confirmation once and holds a session ledger. Settlement responses carry no amount, so the ledger reserves the per-call cap — a deliberate over-estimate that can stop early, never late. The payment-enabled client stays private behind it. The Solana recipient check was too loose. Scanning an owner's token accounts proves only that some account exists for the mint; the exact-SVM client targets the canonical ATA specifically, so an auxiliary account would pass while settlement still fails. Now derives the ATA with findAssociatedTokenPda and checks that exact address. Also corrects the provisioning advice: a bare transfer does not create the account, only one that includes the create instruction does. Mirrored across en, ja-JP, and zh-CN. --- docs/ja-JP/skills/agent-payment-x402/SKILL.md | 75 +++++++++++++++---- docs/zh-CN/skills/agent-payment-x402/SKILL.md | 75 +++++++++++++++---- skills/agent-payment-x402/SKILL.md | 75 +++++++++++++++---- 3 files changed, 180 insertions(+), 45 deletions(-) diff --git a/docs/ja-JP/skills/agent-payment-x402/SKILL.md b/docs/ja-JP/skills/agent-payment-x402/SKILL.md index ea069053d..dccb57688 100644 --- a/docs/ja-JP/skills/agent-payment-x402/SKILL.md +++ b/docs/ja-JP/skills/agent-payment-x402/SKILL.md @@ -139,35 +139,68 @@ const ALLOWED_NETWORKS = new Set([ "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", // Solana mainnet "eip155:8453", // Base ]); -const MAX_AMOUNT = 10_000n; // atomic units — 6-decimal USDC, so $0.01 +const MAX_AMOUNT = 10_000n; // atomic units — 6-decimal USDC, so $0.01 per call +const SESSION_CAP = 50_000n; // $0.05 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 normalizeAsset = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); +const normalizeAddress = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); const ALLOWED_ASSETS = new Set( [ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC, Solana mainnet "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC, Base - ].map(normalizeAsset), + ].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(normalizeAsset(r.asset))) return false; // wrong token + 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 + return amount >= 0n && amount <= MAX_AMOUNT; // over budget / negative } catch { - return false; // unparseable amount + 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 this boundary. const fetchWithPayment = wrapFetchWithPayment(fetch, client); -const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); +// Supply this from your harness — a real prompt, never a stub that returns true. +declare function confirmWithUser(prompt: string): Promise; + +let sessionSpent = 0n; +let sessionApproved = false; + +async function payOnce(url: string, init?: RequestInit): Promise { + // 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. + if (sessionSpent + MAX_AMOUNT > SESSION_CAP) { + throw new Error("Session budget exhausted — blocked"); + } + if (!sessionApproved) { + sessionApproved = await confirmWithUser( + `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, + ); + if (!sessionApproved) throw new Error("User declined — no payment attempted"); + } + sessionSpent += MAX_AMOUNT; + return fetchWithPayment(url, init); +} + +const res = await payOnce("https://api.example.com/data", { method: "GET" }); ``` ポリシーを登録すると、予算超過の金額、想定外のトークン、未登録のチェーンはすべてフェイルクローズドになる — 候補がすべて除外されるため、`createPaymentPayload` は署名せずに例外を投げる。デブネット USDC は `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`;開発時のみ `ALLOWED_ASSETS` に追加すること。ポリシーは敵対的にテストする — `5000000` アトミック単位を要求するチャレンジ、別のミントを提示するチャレンジ、登録していないチェーンのチャレンジを与え、いずれも署名が生成されないことを確認する。exact-SVM スキームではファシリテーターがトランザクション手数料支払者となるため、買い手のウォレットは USDC のみを保有すればよい — ガス用の SOL は不要。 @@ -193,20 +226,32 @@ const res = await fetchWithPayment("https://api.example.com/data", { method: "GE | Python(FastAPI、Flask) | PyPI の `x402` | | Go(Gin、Echo、`net/http`) | `github.com/x402-foundation/x402/go/v2` | -**Solana のセラー:`payTo` アドレスには先にトークンアカウントが必要。** exact-SVM クライアントは `payTo` から導出した*関連トークンアカウント*(ATA)に送金するが、それを作成はしない。そのアカウントが存在しないと決済はシミュレーション段階で失敗し、402 が `transaction_simulation_failed` を返す — クライアントのバグのように見えるが、実際は受取側アカウントの欠落である。本番投入前に確認すること: +**Solana のセラー:`payTo` アドレスには先に正規のトークンアカウントが必要。** exact-SVM クライアントは `findAssociatedTokenPda` で `payTo` の*関連トークンアカウント*(ATA)を導出してそこに送金するが、それを作成はしない。その正確なアカウントが存在しないと決済はシミュレーション段階で失敗し、402 が `transaction_simulation_failed` を返す — クライアントのバグのように見えるが、実際は受取側アカウントの欠落である。導出したアドレス自体を確認すること — オーナーのトークンアカウントを走査するのは等価ではない。同じミントの非正規な補助アカウントがあると検査は通るのに、クライアントが対象とする ATA は依然として存在しないからだ: + +```typescript +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` なら存在せず、そこへの決済は失敗する: ```bash curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","id":1,"method":"getTokenAccountsByOwner", - "params":["",{"mint":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},{"encoding":"jsonParsed"}]}' \ - | jq '.result.value | length' # 0 なら USDC アカウントなし — 支払い受付前に作成する + -d '{"jsonrpc":"2.0","id":1,"method":"getAccountInfo", + "params":["",{"encoding":"base64"}]}' \ + | jq '.result.value != null' ``` -次のいずれかで作成できる(作成者が少額のレントを負担し、支払者ではない): +作成方法(作成者が少額のレントを負担し、支払者ではない): -- そのトークンを `payTo` に一度でも送金する — 送金側の転送でアカウントが作成される。 +- コード上では `@solana-program/token` の `getCreateAssociatedTokenIdempotentInstruction` をオンボーディングフローに追加する — 冪等版なので再実行しても安全で、決定的に動作する唯一の方法。 - spl-token CLI で `spl-token create-account --owner `。 -- コード上では `@solana-program/token` の `getCreateAssociatedTokenIdempotentInstruction` をプロビジョニングフローに追加する — 冪等版なので再実行しても安全。 +- そのトークンを `payTo` に送金する方法も使えるが、送金側が作成インストラクションを含む場合に限る — ウォレットや `spl-token transfer --fund-recipient` は含める。ATA が無い状態への素の `transferChecked` は、決済と同じように失敗する。 これは新規プロビジョニングされたウォレットやカストディアルウォレットへの支払いで最も問題になりやすい。そうしたウォレットは対象アセットの ATA をまだ持っていないことが多い。 diff --git a/docs/zh-CN/skills/agent-payment-x402/SKILL.md b/docs/zh-CN/skills/agent-payment-x402/SKILL.md index dca78ad01..2be21ce7b 100644 --- a/docs/zh-CN/skills/agent-payment-x402/SKILL.md +++ b/docs/zh-CN/skills/agent-payment-x402/SKILL.md @@ -139,35 +139,68 @@ const ALLOWED_NETWORKS = new Set([ "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", // Solana mainnet "eip155:8453", // Base ]); -const MAX_AMOUNT = 10_000n; // atomic units — 6-decimal USDC, so $0.01 +const MAX_AMOUNT = 10_000n; // atomic units — 6-decimal USDC, so $0.01 per call +const SESSION_CAP = 50_000n; // $0.05 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 normalizeAsset = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); +const normalizeAddress = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); const ALLOWED_ASSETS = new Set( [ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC, Solana mainnet "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC, Base - ].map(normalizeAsset), + ].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(normalizeAsset(r.asset))) return false; // wrong token + 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 + return amount >= 0n && amount <= MAX_AMOUNT; // over budget / negative } catch { - return false; // unparseable amount + 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 this boundary. const fetchWithPayment = wrapFetchWithPayment(fetch, client); -const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); +// Supply this from your harness — a real prompt, never a stub that returns true. +declare function confirmWithUser(prompt: string): Promise; + +let sessionSpent = 0n; +let sessionApproved = false; + +async function payOnce(url: string, init?: RequestInit): Promise { + // 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. + if (sessionSpent + MAX_AMOUNT > SESSION_CAP) { + throw new Error("Session budget exhausted — blocked"); + } + if (!sessionApproved) { + sessionApproved = await confirmWithUser( + `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, + ); + if (!sessionApproved) throw new Error("User declined — no payment attempted"); + } + sessionSpent += MAX_AMOUNT; + return fetchWithPayment(url, init); +} + +const res = await payOnce("https://api.example.com/data", { method: "GET" }); ``` 注册该策略后,超预算金额、非预期代币或未注册的链都会故障关闭——所有候选项都被过滤掉,`createPaymentPayload` 会抛出异常而不是签名。开发网 USDC 是 `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`;仅在开发时把它加入 `ALLOWED_ASSETS`。对策略做对抗性测试——分别喂入要求 `5000000` 原子单位的挑战、报价另一种铸币地址的挑战,以及你从未注册的链上的挑战,并断言它们都不会产生签名。在 exact-SVM 方案中,结算器是交易费用支付方,因此买方钱包只需持有 USDC——无需 SOL 支付 gas。 @@ -193,20 +226,32 @@ const res = await fetchWithPayment("https://api.example.com/data", { method: "GE | Python(FastAPI、Flask) | PyPI 上的 `x402` | | Go(Gin、Echo、`net/http`) | `github.com/x402-foundation/x402/go/v2` | -**Solana 卖方:`payTo` 地址需要先有代币账户。** exact-SVM 客户端会转账到它为 `payTo` 推导出的*关联代币账户*(ATA),但不会创建它。如果该账户不存在,结算会在模拟阶段失败——402 返回 `transaction_simulation_failed`,看起来像客户端 bug,实际是收款方账户缺失。上线前请检查: +**Solana 卖方:`payTo` 地址需要先有其规范代币账户。** exact-SVM 客户端用 `findAssociatedTokenPda` 为 `payTo` 推导*关联代币账户*(ATA)并转账到那里,但不会创建它。如果那个确切账户不存在,结算会在模拟阶段失败,402 返回 `transaction_simulation_failed`,看起来像客户端 bug,实际是收款方账户缺失。要检查推导出的地址本身——扫描该所有者的代币账户并不等价,因为同一铸币地址下的非规范辅助账户会让检查通过,而客户端真正要用的 ATA 仍然不存在: + +```typescript +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` 表示不存在,向它结算将会失败: ```bash curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","id":1,"method":"getTokenAccountsByOwner", - "params":["",{"mint":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},{"encoding":"jsonParsed"}]}' \ - | jq '.result.value | length' # 0 表示没有 USDC 账户 —— 接受付款前先创建 + -d '{"jsonrpc":"2.0","id":1,"method":"getAccountInfo", + "params":["",{"encoding":"base64"}]}' \ + | jq '.result.value != null' ``` -以下任一方式都能创建(由创建者支付少量租金,而非付款方): +创建方式(由创建者支付少量租金,而非付款方): -- 向 `payTo` 转一次该代币的任意数量——发送方的转账会创建该账户。 +- 在代码中,将 `@solana-program/token` 的 `getCreateAssociatedTokenIdempotentInstruction` 加入你的开通流程——幂等版本可安全重复执行,也是唯一确定性的方式。 - 用 spl-token CLI:`spl-token create-account --owner `。 -- 在代码中,将 `@solana-program/token` 的 `getCreateAssociatedTokenIdempotentInstruction` 加入你的开通流程——幂等版本可安全重复执行。 +- 向 `payTo` 转账该代币也可以,但仅当发送方包含创建指令时——钱包和 `spl-token transfer --fund-recipient` 会包含;对缺失 ATA 的裸 `transferChecked` 会像结算一样失败。 这个问题在向新开通的钱包或托管钱包付款时最容易出现,这类钱包往往还没有该资产的 ATA。 diff --git a/skills/agent-payment-x402/SKILL.md b/skills/agent-payment-x402/SKILL.md index a3fc91859..fbf3fa7e0 100644 --- a/skills/agent-payment-x402/SKILL.md +++ b/skills/agent-payment-x402/SKILL.md @@ -140,35 +140,68 @@ const ALLOWED_NETWORKS = new Set([ "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", // Solana mainnet "eip155:8453", // Base ]); -const MAX_AMOUNT = 10_000n; // atomic units — 6-decimal USDC, so $0.01 +const MAX_AMOUNT = 10_000n; // atomic units — 6-decimal USDC, so $0.01 per call +const SESSION_CAP = 50_000n; // $0.05 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 normalizeAsset = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); +const normalizeAddress = (a: string) => (a.startsWith("0x") ? a.toLowerCase() : a); const ALLOWED_ASSETS = new Set( [ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC, Solana mainnet "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC, Base - ].map(normalizeAsset), + ].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(normalizeAsset(r.asset))) return false; // wrong token + 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 + return amount >= 0n && amount <= MAX_AMOUNT; // over budget / negative } catch { - return false; // unparseable amount + 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 this boundary. const fetchWithPayment = wrapFetchWithPayment(fetch, client); -const res = await fetchWithPayment("https://api.example.com/data", { method: "GET" }); +// Supply this from your harness — a real prompt, never a stub that returns true. +declare function confirmWithUser(prompt: string): Promise; + +let sessionSpent = 0n; +let sessionApproved = false; + +async function payOnce(url: string, init?: RequestInit): Promise { + // 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. + if (sessionSpent + MAX_AMOUNT > SESSION_CAP) { + throw new Error("Session budget exhausted — blocked"); + } + if (!sessionApproved) { + sessionApproved = await confirmWithUser( + `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, + ); + if (!sessionApproved) throw new Error("User declined — no payment attempted"); + } + sessionSpent += MAX_AMOUNT; + return fetchWithPayment(url, init); +} + +const res = await payOnce("https://api.example.com/data", { method: "GET" }); ``` With the policy registered, an over-budget amount, an unexpected token, or an unregistered chain all fail closed — `createPaymentPayload` throws instead of signing, because every candidate was filtered out. Devnet USDC is `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`; add it to `ALLOWED_ASSETS` only for development. Test the policy adversarially — feed it a challenge demanding `5000000` atomic units, one quoting a different mint, and one on a chain you never registered, and assert that none of them produce a signature. In the exact-SVM scheme the facilitator is the transaction fee payer, so the buyer wallet holds USDC only — no SOL for gas. @@ -194,20 +227,32 @@ For the hosted option, choose from the [facilitator list](https://docs.x402.org/ | Python (FastAPI, Flask) | `x402` on PyPI | | Go (Gin, Echo, `net/http`) | `github.com/x402-foundation/x402/go/v2` | -**Solana sellers: the `payTo` address needs a token account first.** The exact-SVM client transfers to the *associated token account* (ATA) it derives for `payTo` and does not create it. If that account does not exist, settlement fails at simulation — the 402 comes back with `transaction_simulation_failed`, which reads like a client bug but is a missing recipient account. Check before going live: +**Solana sellers: the `payTo` address needs its canonical token account first.** The exact-SVM client derives the *associated token account* (ATA) for `payTo` with `findAssociatedTokenPda` and transfers there; it never creates it. If that exact account is missing, settlement fails at simulation and the 402 comes back with `transaction_simulation_failed`, which reads like a client bug rather than a missing recipient account. Check the derived address itself — scanning the owner's token accounts is not equivalent, because a non-canonical auxiliary account for the same mint would pass while the ATA the client targets is still absent: + +```typescript +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 +}); +``` + +Then confirm that exact address exists — `value: null` means it does not, and settlement to it will fail: ```bash curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","id":1,"method":"getTokenAccountsByOwner", - "params":["",{"mint":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},{"encoding":"jsonParsed"}]}' \ - | jq '.result.value | length' # 0 means no USDC account — create it before accepting payments + -d '{"jsonrpc":"2.0","id":1,"method":"getAccountInfo", + "params":["",{"encoding":"base64"}]}' \ + | jq '.result.value != null' ``` -Any one of these creates it (whoever creates it pays the small rent, not the payer): +To provision it (whoever creates it pays the small rent, not the payer): -- Send any amount of that token to `payTo` once — the sender's transfer creates the account. +- In code, add `getCreateAssociatedTokenIdempotentInstruction` from `@solana-program/token` to your onboarding flow — the idempotent variant is safe to re-run and is the only option that is deterministic. - `spl-token create-account --owner ` with the spl-token CLI. -- In code, add `getCreateAssociatedTokenIdempotentInstruction` from `@solana-program/token` to your provisioning flow — the idempotent variant is safe to re-run. +- A transfer of that token to `payTo` also works, but only when the sender includes the create instruction — wallets and `spl-token transfer --fund-recipient` do; a bare `transferChecked` to a missing ATA fails the same way settlement does. This bites hardest when payouts go to freshly provisioned or custodial wallets, which often have no ATA for the asset yet. From ec879062f937a077af0278d138e96f93e12b4359 Mon Sep 17 00:00:00 2001 From: "Notorious D.E.V" Date: Sat, 26 Sep 2026 17:59:54 +0800 Subject: [PATCH 5/6] fix(agent-payment-x402): reserve session budget before awaiting confirmation Two overlapping first payOnce calls could both pass the session-cap check while sessionSpent was still 0, then both increment after the confirm await, exceeding SESSION_CAP. Reserve MAX_AMOUNT before any await and release it only when the user declines. Mirrored in ja-JP and zh-CN. --- docs/ja-JP/skills/agent-payment-x402/SKILL.md | 10 +++++++--- docs/zh-CN/skills/agent-payment-x402/SKILL.md | 10 +++++++--- skills/agent-payment-x402/SKILL.md | 10 +++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/ja-JP/skills/agent-payment-x402/SKILL.md b/docs/ja-JP/skills/agent-payment-x402/SKILL.md index dccb57688..bf0e13f03 100644 --- a/docs/ja-JP/skills/agent-payment-x402/SKILL.md +++ b/docs/ja-JP/skills/agent-payment-x402/SKILL.md @@ -186,17 +186,21 @@ let sessionApproved = false; async function payOnce(url: string, init?: RequestInit): Promise { // 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. + // 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; if (!sessionApproved) { sessionApproved = await confirmWithUser( `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, ); - if (!sessionApproved) throw new Error("User declined — no payment attempted"); + if (!sessionApproved) { + sessionSpent -= MAX_AMOUNT; // nothing was signed + throw new Error("User declined — no payment attempted"); + } } - sessionSpent += MAX_AMOUNT; return fetchWithPayment(url, init); } diff --git a/docs/zh-CN/skills/agent-payment-x402/SKILL.md b/docs/zh-CN/skills/agent-payment-x402/SKILL.md index 2be21ce7b..e2095d78c 100644 --- a/docs/zh-CN/skills/agent-payment-x402/SKILL.md +++ b/docs/zh-CN/skills/agent-payment-x402/SKILL.md @@ -186,17 +186,21 @@ let sessionApproved = false; async function payOnce(url: string, init?: RequestInit): Promise { // 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. + // 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; if (!sessionApproved) { sessionApproved = await confirmWithUser( `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, ); - if (!sessionApproved) throw new Error("User declined — no payment attempted"); + if (!sessionApproved) { + sessionSpent -= MAX_AMOUNT; // nothing was signed + throw new Error("User declined — no payment attempted"); + } } - sessionSpent += MAX_AMOUNT; return fetchWithPayment(url, init); } diff --git a/skills/agent-payment-x402/SKILL.md b/skills/agent-payment-x402/SKILL.md index fbf3fa7e0..f32efb687 100644 --- a/skills/agent-payment-x402/SKILL.md +++ b/skills/agent-payment-x402/SKILL.md @@ -187,17 +187,21 @@ let sessionApproved = false; async function payOnce(url: string, init?: RequestInit): Promise { // 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. + // 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; if (!sessionApproved) { sessionApproved = await confirmWithUser( `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, ); - if (!sessionApproved) throw new Error("User declined — no payment attempted"); + if (!sessionApproved) { + sessionSpent -= MAX_AMOUNT; // nothing was signed + throw new Error("User declined — no payment attempted"); + } } - sessionSpent += MAX_AMOUNT; return fetchWithPayment(url, init); } From d24c97d13c7fac27b54892fc6d36ab6448f0ee5c Mon Sep 17 00:00:00 2001 From: "Notorious D.E.V" Date: Thu, 1 Oct 2026 11:03:09 +0800 Subject: [PATCH 6/6] fix(agent-payment-x402): release the session reservation when nothing was signed payOnce reserved MAX_AMOUNT before awaiting confirmation but only released it when confirmWithUser returned false. A prompt that threw (timeout, closed window) or a challenge the payment policy rejected before signing kept the reservation, so five such failures exhausted SESSION_CAP with zero payments. Track per call whether a request carrying PAYMENT-SIGNATURE / X-PAYMENT was actually sent, and release the reservation in a finally block unless it was. A sent signed request may settle, so it stays counted even if the response fails. The reservation still happens before any await, so concurrent calls cannot overrun the cap. Mirrored in ja-JP and zh-CN. --- docs/ja-JP/skills/agent-payment-x402/SKILL.md | 33 +++++++++++++------ docs/zh-CN/skills/agent-payment-x402/SKILL.md | 33 +++++++++++++------ skills/agent-payment-x402/SKILL.md | 33 +++++++++++++------ 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/docs/ja-JP/skills/agent-payment-x402/SKILL.md b/docs/ja-JP/skills/agent-payment-x402/SKILL.md index bf0e13f03..94af8757e 100644 --- a/docs/ja-JP/skills/agent-payment-x402/SKILL.md +++ b/docs/ja-JP/skills/agent-payment-x402/SKILL.md @@ -174,8 +174,7 @@ client.registerPolicy((_x402Version, requirements) => // 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 this boundary. -const fetchWithPayment = wrapFetchWithPayment(fetch, client); +// 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; @@ -192,16 +191,30 @@ async function payOnce(url: string, init?: RequestInit): Promise { throw new Error("Session budget exhausted — blocked"); } sessionSpent += MAX_AMOUNT; - if (!sessionApproved) { - sessionApproved = await confirmWithUser( - `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, - ); - if (!sessionApproved) { - sessionSpent -= MAX_AMOUNT; // nothing was signed - throw new Error("User declined — no payment attempted"); + // 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 = (input, reqInit) => { + const req = new Request(input, reqInit); + if (req.headers.has("PAYMENT-SIGNATURE") || req.headers.has("X-PAYMENT")) { + signedRequestSent = true; } + return fetch(req); + }; + try { + if (!sessionApproved) { + sessionApproved = await confirmWithUser( + `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, + ); + if (!sessionApproved) { + throw new Error("User declined — no payment attempted"); + } + } + return await wrapFetchWithPayment(trackingFetch, client)(url, init); + } finally { + if (!signedRequestSent) sessionSpent -= MAX_AMOUNT; } - return fetchWithPayment(url, init); } const res = await payOnce("https://api.example.com/data", { method: "GET" }); diff --git a/docs/zh-CN/skills/agent-payment-x402/SKILL.md b/docs/zh-CN/skills/agent-payment-x402/SKILL.md index e2095d78c..0fe5ca400 100644 --- a/docs/zh-CN/skills/agent-payment-x402/SKILL.md +++ b/docs/zh-CN/skills/agent-payment-x402/SKILL.md @@ -174,8 +174,7 @@ client.registerPolicy((_x402Version, requirements) => // 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 this boundary. -const fetchWithPayment = wrapFetchWithPayment(fetch, client); +// 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; @@ -192,16 +191,30 @@ async function payOnce(url: string, init?: RequestInit): Promise { throw new Error("Session budget exhausted — blocked"); } sessionSpent += MAX_AMOUNT; - if (!sessionApproved) { - sessionApproved = await confirmWithUser( - `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, - ); - if (!sessionApproved) { - sessionSpent -= MAX_AMOUNT; // nothing was signed - throw new Error("User declined — no payment attempted"); + // 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 = (input, reqInit) => { + const req = new Request(input, reqInit); + if (req.headers.has("PAYMENT-SIGNATURE") || req.headers.has("X-PAYMENT")) { + signedRequestSent = true; } + return fetch(req); + }; + try { + if (!sessionApproved) { + sessionApproved = await confirmWithUser( + `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, + ); + if (!sessionApproved) { + throw new Error("User declined — no payment attempted"); + } + } + return await wrapFetchWithPayment(trackingFetch, client)(url, init); + } finally { + if (!signedRequestSent) sessionSpent -= MAX_AMOUNT; } - return fetchWithPayment(url, init); } const res = await payOnce("https://api.example.com/data", { method: "GET" }); diff --git a/skills/agent-payment-x402/SKILL.md b/skills/agent-payment-x402/SKILL.md index f32efb687..636415848 100644 --- a/skills/agent-payment-x402/SKILL.md +++ b/skills/agent-payment-x402/SKILL.md @@ -175,8 +175,7 @@ client.registerPolicy((_x402Version, requirements) => // 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 this boundary. -const fetchWithPayment = wrapFetchWithPayment(fetch, client); +// 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; @@ -193,16 +192,30 @@ async function payOnce(url: string, init?: RequestInit): Promise { throw new Error("Session budget exhausted — blocked"); } sessionSpent += MAX_AMOUNT; - if (!sessionApproved) { - sessionApproved = await confirmWithUser( - `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, - ); - if (!sessionApproved) { - sessionSpent -= MAX_AMOUNT; // nothing was signed - throw new Error("User declined — no payment attempted"); + // 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 = (input, reqInit) => { + const req = new Request(input, reqInit); + if (req.headers.has("PAYMENT-SIGNATURE") || req.headers.has("X-PAYMENT")) { + signedRequestSent = true; } + return fetch(req); + }; + try { + if (!sessionApproved) { + sessionApproved = await confirmWithUser( + `Allow paid requests up to ${SESSION_CAP} atomic units this session?`, + ); + if (!sessionApproved) { + throw new Error("User declined — no payment attempted"); + } + } + return await wrapFetchWithPayment(trackingFetch, client)(url, init); + } finally { + if (!signedRequestSent) sessionSpent -= MAX_AMOUNT; } - return fetchWithPayment(url, init); } const res = await payOnce("https://api.example.com/data", { method: "GET" });