From 04bb4d97da90302b059e60ac2f9cb06376bf6904 Mon Sep 17 00:00:00 2001 From: franchukkk <223016348+enot3615@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:44:43 -0700 Subject: [PATCH] =?UTF-8?q?feat(x402):=20standard=20x402=20client=20?= =?UTF-8?q?=E2=80=94=20EIP-3009=20X-PAYMENT=20facilitator=20for=20interop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AiFinPay agents can now pay any standard x402 endpoint (x402 Foundation / Coinbase / Dexter / 69k-agent economy), not just AiFinPay-native ones. - facilitators/standard-x402.ts: detect 402 { x402Version, accepts[] }; sign EIP-3009 TransferWithAuthorization (gasless) and emit base64 X-PAYMENT with the 'exact' scheme. EVM only (Base/Polygon/Ethereum/Arbitrum/Optimism/Avalanche/ BSC); Solana exact is a follow-up. - agent.ts: derive the agent's EVM account from the same seed (domain-separated SHA-256), byte-identical to AiFinPayAgent's EVM address. - detect.ts: register StandardX402Facilitator (override name 'x402'). Native 'aifinpay' flavor kept for its 3-way fee-on-top split. Verified end-to-end. tsc clean. Co-Authored-By: Claude Opus 4.8 --- node/src/agent.ts | 23 ++++ node/src/facilitators/detect.ts | 2 + node/src/facilitators/standard-x402.ts | 166 +++++++++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 node/src/facilitators/standard-x402.ts diff --git a/node/src/agent.ts b/node/src/agent.ts index 3cca087..50e3943 100644 --- a/node/src/agent.ts +++ b/node/src/agent.ts @@ -4,6 +4,7 @@ import { sha256 } from "./crypto.js"; import { AiFinPayError, FundingTimeoutError, X402Error } from "./errors.js"; import { detectFacilitator } from "./facilitators/detect.js"; import type { PayOptions } from "./facilitators/base.js"; +import { privateKeyToAccount, type PrivateKeyAccount } from "viem/accounts"; // Canonical domain is aifinpay.io. The legacy aifinpay.company host is // fully retired (DNS removed) — do not use it. @@ -107,6 +108,28 @@ export class Agent { return bs58.encode(this.secretKey); } + private _evm?: PrivateKeyAccount; + /** + * The agent's EVM account (viem), derived from the same 32-byte seed as the + * Solana key via domain-separated SHA-256 ("aifinpay:evm:v1\0" || seed) — + * byte-for-byte identical to AiFinPayAgent's EVM address. Used by the + * standard x402 (EIP-3009) facilitator. Node-only (sync SHA-256). + */ + async evmAccount(): Promise { + if (this._evm) return this._evm; + const { createHash } = await import("node:crypto"); + const h = createHash("sha256"); + h.update("aifinpay:evm:v1\0"); + h.update(this.secretKey.subarray(0, 32)); + this._evm = privateKeyToAccount(`0x${h.digest("hex")}` as `0x${string}`); + return this._evm; + } + + /** The agent's EVM (Polygon / Base / …) address. */ + async evmAddress(): Promise { + return (await this.evmAccount()).address; + } + // ── Discovery ────────────────────────────────────────────────────────── async manifesto(): Promise> { diff --git a/node/src/facilitators/detect.ts b/node/src/facilitators/detect.ts index 8eddbd0..38e55c5 100644 --- a/node/src/facilitators/detect.ts +++ b/node/src/facilitators/detect.ts @@ -2,6 +2,7 @@ import { UnsupportedFacilitatorError } from "../errors.js"; import { AiFinPayFacilitator } from "./aifinpay.js"; import type { Facilitator, FacilitatorClass } from "./base.js"; import { CoinbaseX402Facilitator } from "./coinbase.js"; +import { StandardX402Facilitator } from "./standard-x402.js"; /** * Order matters: most-specific detector first. A response that matches @@ -10,6 +11,7 @@ import { CoinbaseX402Facilitator } from "./coinbase.js"; */ export const REGISTERED: FacilitatorClass[] = [ AiFinPayFacilitator, + StandardX402Facilitator, CoinbaseX402Facilitator, ]; diff --git a/node/src/facilitators/standard-x402.ts b/node/src/facilitators/standard-x402.ts new file mode 100644 index 0000000..bb2d33e --- /dev/null +++ b/node/src/facilitators/standard-x402.ts @@ -0,0 +1,166 @@ +import type { Agent } from "../agent.js"; +import { + PaymentTooExpensiveError, + UnsupportedFacilitatorError, +} from "../errors.js"; +import type { AuthPayload, Facilitator, PayOptions } from "./base.js"; + +/** + * Standard x402 — the `X-PAYMENT` header flow (x402 Foundation / Linux + * Foundation standard, donated by Coinbase 2026-04). This is what makes + * AiFinPay agents interoperable with the wider x402 economy (Coinbase, Dexter, + * 69k+ agents) — distinct from our native `aifinpay` flavor. + * + * Wire format: + * - 402 body: { x402Version, accepts: [ { scheme:"exact", network, + * maxAmountRequired, payTo, asset, maxTimeoutSeconds, extra:{name,version} }, … ] } + * - Client signs an EIP-3009 TransferWithAuthorization (gasless) and retries with + * X-PAYMENT: base64(JSON({ x402Version, scheme, network, payload })) + * + * EVM `exact` only for now; Solana `exact` is a follow-up. + */ + +const CHAIN_IDS: Record = { + base: 8453, + "base-sepolia": 84532, + ethereum: 1, + mainnet: 1, + polygon: 137, + "polygon-amoy": 80002, + arbitrum: 42161, + optimism: 10, + avalanche: 43114, + bsc: 56, +}; + +const TRANSFER_WITH_AUTHORIZATION_TYPES = { + TransferWithAuthorization: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "validAfter", type: "uint256" }, + { name: "validBefore", type: "uint256" }, + { name: "nonce", type: "bytes32" }, + ], +} as const; + +export class StandardX402Facilitator implements Facilitator { + static readonly name = "x402"; + readonly name = "x402"; + + static async detect(resp: Response): Promise { + if (resp.status !== 402) return false; + let body: unknown; + try { + body = await resp.clone().json(); + } catch { + return false; + } + if (typeof body !== "object" || body === null) return false; + const b = body as Record; + return "x402Version" in b && Array.isArray(b.accepts); + } + + async buildAuth( + resp: Response, + agent: Agent, + opts: PayOptions, + ): Promise { + const body = (await resp.clone().json()) as { + x402Version?: number; + accepts?: Array>; + }; + const accepts = body.accepts ?? []; + + // Pick the first `exact` requirement on an EVM chain we know how to pay. + const req = accepts.find( + (a) => + a.scheme === "exact" && + typeof a.network === "string" && + CHAIN_IDS[a.network] !== undefined, + ); + if (!req) { + throw new UnsupportedFacilitatorError( + "standard x402 detected but no payable EVM `exact` requirement " + + `(offered: ${accepts.map((a) => `${a.scheme}/${a.network}`).join(", ") || "none"})`, + ); + } + + const network = req.network as string; + const asset = String(req.asset ?? ""); + const payTo = String(req.payTo ?? ""); + const value = String(req.maxAmountRequired ?? "0"); + if (!asset || !payTo) { + throw new UnsupportedFacilitatorError( + "standard x402 requirement is missing `asset` or `payTo`", + ); + } + + // Best-effort USD cap (assumes 6-decimal USDC/EURC pricing). + if (opts.maxAmountUsd !== undefined) { + const approxUsd = Number(value) / 1e6; + if (Number.isFinite(approxUsd) && approxUsd > opts.maxAmountUsd) { + throw new PaymentTooExpensiveError( + `x402 wants ~$${approxUsd.toFixed(4)}, caller cap is $${opts.maxAmountUsd.toFixed(4)}`, + ); + } + } + + const account = await agent.evmAccount(); + const extra = (req.extra ?? {}) as Record; + const timeout = Number(req.maxTimeoutSeconds ?? 600); + const now = Math.floor(Date.now() / 1000); + const validBefore = String(now + (Number.isFinite(timeout) ? timeout : 600)); + const nonce = randomNonce(); + + const authorization = { + from: account.address, + to: payTo as `0x${string}`, + value, + validAfter: "0", + validBefore, + nonce, + }; + + const signature = await account.signTypedData({ + domain: { + name: String(extra.name ?? "USD Coin"), + version: String(extra.version ?? "2"), + chainId: CHAIN_IDS[network], + verifyingContract: asset as `0x${string}`, + }, + types: TRANSFER_WITH_AUTHORIZATION_TYPES, + primaryType: "TransferWithAuthorization", + message: { + from: authorization.from, + to: authorization.to, + value: BigInt(value), + validAfter: 0n, + validBefore: BigInt(validBefore), + nonce, + }, + }); + + const paymentPayload = { + x402Version: body.x402Version ?? 1, + scheme: "exact", + network, + payload: { signature, authorization }, + }; + + const header = + typeof Buffer !== "undefined" + ? Buffer.from(JSON.stringify(paymentPayload)).toString("base64") + : btoa(JSON.stringify(paymentPayload)); + + return { headers: { "X-PAYMENT": header } }; + } +} + +function randomNonce(): `0x${string}` { + const b = new Uint8Array(32); + (globalThis.crypto ?? crypto).getRandomValues(b); + let s = "0x"; + for (const x of b) s += x.toString(16).padStart(2, "0"); + return s as `0x${string}`; +}