From 52524fc2c5bdc95dc28270b9c7b8171708d63ff8 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 8 Feb 2026 15:57:34 +0100 Subject: [PATCH 1/3] Clean up the facilitator. --- .../test/x402_facilitator.test.ts | 37 +++++++++++++++++++ x402_facilitator/test/x402_settle.test.js | 11 ++++++ .../test/x402_splitter_supported.test.js | 18 +++++++-- x402_facilitator/test/x402_supported.test.js | 19 ++++++++++ x402_facilitator/x402_facilitator.ts | 3 ++ x402_facilitator/x402_fee.ts | 5 ++- x402_facilitator/x402_settle.ts | 32 +++++++++++++++- x402_facilitator/x402_splitter_facilitator.js | 1 + x402_facilitator/x402_splitter_settle.js | 12 ++++++ x402_facilitator/x402_splitter_supported.js | 18 ++++++++- x402_facilitator/x402_supported.ts | 27 +++++++++++++- 11 files changed, 174 insertions(+), 9 deletions(-) diff --git a/x402_facilitator/test/x402_facilitator.test.ts b/x402_facilitator/test/x402_facilitator.test.ts index b030d76d..358691d9 100644 --- a/x402_facilitator/test/x402_facilitator.test.ts +++ b/x402_facilitator/test/x402_facilitator.test.ts @@ -223,6 +223,43 @@ describe("x402_facilitator handlers", () => { expect(body.transaction).toBe("0xabc123"); }); + it("should include extensions in settle response when present", async () => { + settlePayment.mockResolvedValue({ + success: true, + payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + transaction: "0xabc123", + network: "eip155:10", + fee: { collected: true, txHash: "0xfee123" }, + extensions: { + facilitatorFees: { + info: { + version: "1", + facilitatorFeePaid: "10000", + asset: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + model: "flat", + }, + }, + }, + }); + + const event = { + httpMethod: "POST", + body: JSON.stringify({ + paymentPayload: { accepted: { network: "eip155:10" } }, + paymentRequirements: { amount: "1000000" }, + }), + }; + const result = await handleSettle(event, {}); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.success).toBe(true); + expect(body.extensions).toBeDefined(); + expect(body.extensions.facilitatorFees.info.version).toBe("1"); + expect(body.extensions.facilitatorFees.info.facilitatorFeePaid).toBe("10000"); + expect(body.extensions.facilitatorFees.info.model).toBe("flat"); + }); + it("should return failed settlement with error reason", async () => { settlePayment.mockResolvedValue({ success: false, diff --git a/x402_facilitator/test/x402_settle.test.js b/x402_facilitator/test/x402_settle.test.js index 3b747687..becc908a 100644 --- a/x402_facilitator/test/x402_settle.test.js +++ b/x402_facilitator/test/x402_settle.test.js @@ -536,6 +536,13 @@ describe("x402_settle with mocked facilitator", () => { expect(result.fee).toBeDefined(); expect(result.fee.collected).toBe(true); expect(result.fee.txHash).toBe("0xfeetxhash123"); + // Verify facilitatorFees extension (per x402 Fee Disclosure proposal #1016) + expect(result.extensions).toBeDefined(); + expect(result.extensions.facilitatorFees).toBeDefined(); + expect(result.extensions.facilitatorFees.info.version).toBe("1"); + expect(result.extensions.facilitatorFees.info.facilitatorFeePaid).toBe("10000"); + expect(result.extensions.facilitatorFees.info.asset).toBeDefined(); + expect(result.extensions.facilitatorFees.info.model).toBe("flat"); expect(feeModule.collectFee).toHaveBeenCalledWith( "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", "eip155:11155420", @@ -572,6 +579,10 @@ describe("x402_settle with mocked facilitator", () => { expect(result.fee).toBeDefined(); expect(result.fee.collected).toBe(false); expect(result.fee.error).toBe("insufficient_fee_allowance"); + // Verify facilitatorFees extension shows 0 when fee collection failed + expect(result.extensions).toBeDefined(); + expect(result.extensions.facilitatorFees.info.facilitatorFeePaid).toBe("0"); + expect(result.extensions.facilitatorFees.info.model).toBe("flat"); }); it("does not collect fee when feeRequired is not set", async () => { diff --git a/x402_facilitator/test/x402_splitter_supported.test.js b/x402_facilitator/test/x402_splitter_supported.test.js index 2c3f8a8e..d25df9f2 100644 --- a/x402_facilitator/test/x402_splitter_supported.test.js +++ b/x402_facilitator/test/x402_splitter_supported.test.js @@ -120,10 +120,19 @@ describe("x402 Splitter /supported endpoint", () => { }); }); - test("extensions array is empty (no whitelist extension)", () => { + test("extensions includes facilitatorFees for fee-aware routing (#1016)", () => { const capabilities = getSplitterCapabilities(); - expect(capabilities.extensions).toEqual([]); + expect(capabilities.extensions.length).toBeGreaterThan(0); + const feesExtension = capabilities.extensions.find((e) => e.name === "facilitatorFees"); + expect(feesExtension).toBeDefined(); + expect(feesExtension.version).toBe("1"); + expect(feesExtension.model).toBe("flat"); + expect(feesExtension.asset).toBe("USDC"); + expect(feesExtension.flatFee).toBe("10000"); + expect(feesExtension.decimals).toBe(6); + expect(feesExtension.collection).toBe("on_chain_split"); + expect(feesExtension.networks).toContain("eip155:11155420"); }); test("signers is empty (payments signed by payers, not facilitator)", () => { @@ -134,10 +143,11 @@ describe("x402 Splitter /supported endpoint", () => { expect(capabilities.signers).toEqual({}); }); - test("returns empty extensions (no recipient whitelist)", () => { + test("returns facilitatorFees extension (no recipient whitelist)", () => { const capabilities = getSplitterCapabilities(); - expect(capabilities.extensions).toEqual([]); + // Has facilitatorFees but no whitelist + expect(capabilities.extensions.length).toBeGreaterThan(0); // Explicitly verify no whitelist extension exists const whitelistExtension = capabilities.extensions.find( diff --git a/x402_facilitator/test/x402_supported.test.js b/x402_facilitator/test/x402_supported.test.js index 26a8fc56..b8aa15d0 100644 --- a/x402_facilitator/test/x402_supported.test.js +++ b/x402_facilitator/test/x402_supported.test.js @@ -138,6 +138,25 @@ describe("x402 /supported endpoint", () => { expect(feeExtension.setup.spender).not.toBeNull(); }); + test("includes facilitatorFees extension for fee-aware routing (#1016)", () => { + process.env.FACILITATOR_WALLET_PRIVATE_KEY = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + + const capabilities = getSupportedCapabilities(); + + const feesExtension = capabilities.extensions.find((e) => e.name === "facilitatorFees"); + expect(feesExtension).toBeDefined(); + expect(feesExtension.version).toBe("1"); + expect(feesExtension.model).toBe("flat"); + expect(feesExtension.asset).toBe("USDC"); + expect(feesExtension.flatFee).toBe("10000"); + expect(feesExtension.decimals).toBe(6); + expect(feesExtension.networks).toContain("eip155:10"); + expect(feesExtension.networks).toContain("eip155:8453"); + expect(feesExtension.networks).toContain("eip155:11155420"); + expect(feesExtension.networks).toContain("eip155:84532"); + }); + test("omits facilitator_fee extension when private key is missing", () => { delete process.env.FACILITATOR_WALLET_PRIVATE_KEY; const capabilities = getSupportedCapabilities(); diff --git a/x402_facilitator/x402_facilitator.ts b/x402_facilitator/x402_facilitator.ts index d268a232..9029aa71 100644 --- a/x402_facilitator/x402_facilitator.ts +++ b/x402_facilitator/x402_facilitator.ts @@ -202,6 +202,9 @@ async function handlePaymentRequest( if (result.fee) { responseBody.fee = result.fee; } + if (result.extensions) { + responseBody.extensions = result.extensions; + } return { statusCode: 200, headers: CORS_HEADERS, diff --git a/x402_facilitator/x402_fee.ts b/x402_facilitator/x402_fee.ts index ba9a6082..47e42fa3 100644 --- a/x402_facilitator/x402_fee.ts +++ b/x402_facilitator/x402_fee.ts @@ -84,7 +84,10 @@ export function getFeeAmount(): bigint { } return parsed; } catch { - logger.warn({ envFee }, "Invalid FACILITATOR_FEE_AMOUNT (not a valid integer), using default"); + logger.warn( + { envFee }, + "Invalid FACILITATOR_FEE_AMOUNT (not a valid integer), using default", + ); return DEFAULT_FEE_AMOUNT; } } diff --git a/x402_facilitator/x402_settle.ts b/x402_facilitator/x402_settle.ts index 948fa7e0..d40e120f 100644 --- a/x402_facilitator/x402_settle.ts +++ b/x402_facilitator/x402_settle.ts @@ -6,12 +6,21 @@ import { getFacilitator } from "./facilitator_instance"; import { verifyPayment } from "./x402_verify"; -import { collectFee } from "./x402_fee"; +import { collectFee, getFeeAmount } from "./x402_fee"; +import { getChainConfig } from "./chain_utils"; import type { Address } from "viem"; import pino from "pino"; const logger = pino({ level: process.env.LOG_LEVEL || "info" }); +/** Facilitator fee receipt per x402 Fee Disclosure proposal (coinbase/x402#1016) */ +export interface FacilitatorFeePaid { + version: string; + facilitatorFeePaid: string; + asset: string; + model: string; +} + export interface SettleResult { success: boolean; payer?: string; @@ -24,6 +33,12 @@ export interface SettleResult { txHash?: string; error?: string; }; + /** x402 v2 extensions (facilitatorFees receipt per #1016 proposal) */ + extensions?: { + facilitatorFees?: { + info: FacilitatorFeePaid; + }; + }; } /** @@ -95,6 +110,20 @@ export async function settlePayment( ); } + // Build facilitatorFees receipt (per x402 Fee Disclosure proposal #1016) + const feeAmountStr = getFeeAmount().toString(); + const chainConfig = getChainConfig(network); + const facilitatorFeesExtension: SettleResult["extensions"] = { + facilitatorFees: { + info: { + version: "1", + facilitatorFeePaid: feeResult.success ? feeAmountStr : "0", + asset: chainConfig.USDC_ADDRESS, + model: "flat", + }, + }, + }; + return { success: true, payer: verifyResult.payer, @@ -105,6 +134,7 @@ export async function settlePayment( txHash: feeResult.txHash, error: feeResult.error, }, + extensions: facilitatorFeesExtension, }; } diff --git a/x402_facilitator/x402_splitter_facilitator.js b/x402_facilitator/x402_splitter_facilitator.js index eb9e723d..1a89737e 100644 --- a/x402_facilitator/x402_splitter_facilitator.js +++ b/x402_facilitator/x402_splitter_facilitator.js @@ -207,6 +207,7 @@ export async function handleSettle(event) { payer: result.payer, transaction: result.transaction, network: result.network, + ...(result.extensions ? { extensions: result.extensions } : {}), }), }; } else { diff --git a/x402_facilitator/x402_splitter_settle.js b/x402_facilitator/x402_splitter_settle.js index 37c72a51..7dc4f89b 100644 --- a/x402_facilitator/x402_splitter_settle.js +++ b/x402_facilitator/x402_splitter_settle.js @@ -39,6 +39,7 @@ import { verifySplitterPayment } from "./x402_splitter_verify.js"; // Alias for backward compatibility const SPLITTER_ABI = EIP3009SplitterV1ABI; const getSplitterAddress = getEIP3009SplitterAddress; +const FIXED_FEE = process.env.FIXED_FEE || "10000"; // 0.01 USDC const logger = pino({ level: process.env.LOG_LEVEL || "info" }); @@ -222,6 +223,17 @@ export async function settleSplitterPayment(paymentPayload, paymentRequirements) payer: buyer, transaction: hash, network, + // x402 Fee Disclosure receipt (coinbase/x402#1016) + extensions: { + facilitatorFees: { + info: { + version: "1", + facilitatorFeePaid: FIXED_FEE, + asset: usdcAddress, + model: "flat", + }, + }, + }, }; } else { logger.warn({ hash, status: receipt.status }, "Transaction failed on-chain"); diff --git a/x402_facilitator/x402_splitter_supported.js b/x402_facilitator/x402_splitter_supported.js index edf366e3..4c811b64 100644 --- a/x402_facilitator/x402_splitter_supported.js +++ b/x402_facilitator/x402_splitter_supported.js @@ -47,8 +47,22 @@ export function getSplitterCapabilities() { return { // x402 v2 spec: /supported endpoint returns "kinds" array kinds, - // x402 v2 spec: extensions array (empty for now - no whitelist) - extensions: [], + // x402 v2 spec: extensions array + extensions: + kinds.length > 0 + ? [ + { + name: "facilitatorFees", + version: "1", + model: "flat", + asset: "USDC", + flatFee: FIXED_FEE, + decimals: 6, + collection: "on_chain_split", + networks: kinds.map((k) => k.network), + }, + ] + : [], // x402 v2 spec: signers map (empty - payments are signed by payers, not facilitator) signers: {}, }; diff --git a/x402_facilitator/x402_supported.ts b/x402_facilitator/x402_supported.ts index 8bca6e77..3d41bf05 100644 --- a/x402_facilitator/x402_supported.ts +++ b/x402_facilitator/x402_supported.ts @@ -6,6 +6,18 @@ import { formatUnits } from "viem"; import { createReadOnlyFacilitator } from "./facilitator_instance"; import { getFeeAmount, getFacilitatorAddress } from "./x402_fee"; +import { getSupportedNetworks } from "./chain_utils"; + +/** Facilitator fee model disclosure per x402 Fee Disclosure proposal (coinbase/x402#1016) */ +interface FacilitatorFeesExtension { + name: string; + version: string; + model: string; + asset: string; + flatFee: string; + decimals: number; + networks: string[]; +} interface FeeExtension { name: string; @@ -33,7 +45,7 @@ interface SupportedCapabilities { network: string; extra?: Record; }>; - extensions: Array>; + extensions: Array>; signers: Record; } @@ -76,6 +88,19 @@ export function getSupportedCapabilities(): SupportedCapabilities { }, }; supported.extensions.push(feeExtension); + + // Add facilitatorFees extension per x402 Fee Disclosure proposal (#1016) + // Static fee model disclosure for fee-aware multi-facilitator routing + const facilitatorFeesExtension: FacilitatorFeesExtension = { + name: "facilitatorFees", + version: "1", + model: "flat", + asset: "USDC", + flatFee: feeAmount.toString(), + decimals: 6, + networks: getSupportedNetworks(), + }; + supported.extensions.push(facilitatorFeesExtension); } return supported; From e736bde0b32f196e8d0d99334aefafcde5464bf8 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 8 Feb 2026 16:05:15 +0100 Subject: [PATCH 2/3] Update facilitator_instance.test.ts --- .../test/facilitator_instance.test.ts | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/x402_facilitator/test/facilitator_instance.test.ts b/x402_facilitator/test/facilitator_instance.test.ts index f348a456..2625f774 100644 --- a/x402_facilitator/test/facilitator_instance.test.ts +++ b/x402_facilitator/test/facilitator_instance.test.ts @@ -72,7 +72,7 @@ vi.mock("../x402_fee.js", () => ({ getFacilitatorAddress: vi.fn(), })); -import { createFacilitator, resetFacilitator } from "../facilitator_instance.js"; +import { createFacilitator, resetFacilitator, getFacilitator } from "../facilitator_instance.js"; import { checkMerchantAllowance, getFeeAmount, getFacilitatorAddress } from "../x402_fee.js"; // ═══════════════════════════════════════════════════════════════ @@ -253,3 +253,97 @@ describe("facilitator_instance onAfterVerify hook (fee model)", () => { expect(args.result.invalidReason).toBe("invalid_payload"); }); }); + +// ═══════════════════════════════════════════════════════════════ +// Private key validation & error paths +// ═══════════════════════════════════════════════════════════════ + +describe("createFacilitator — private key validation", () => { + const originalEnv = { ...process.env }; + + afterEach(() => { + process.env = { ...originalEnv }; + resetFacilitator(); + }); + + it("throws when private key is missing and requirePrivateKey=true", () => { + delete process.env.FACILITATOR_WALLET_PRIVATE_KEY; + + expect(() => createFacilitator(true)).toThrow( + "FACILITATOR_WALLET_PRIVATE_KEY not configured", + ); + }); + + it("returns read-only facilitator when private key is missing and requirePrivateKey=false", () => { + delete process.env.FACILITATOR_WALLET_PRIVATE_KEY; + + const facilitator = createFacilitator(false); + expect(facilitator).toBeDefined(); + // Read-only facilitator was created (no throw) + }); + + it("throws when private key has invalid length and requirePrivateKey=true", () => { + process.env.FACILITATOR_WALLET_PRIVATE_KEY = "0xdeadbeef"; // too short + + expect(() => createFacilitator(true)).toThrow( + "Invalid FACILITATOR_WALLET_PRIVATE_KEY: Expected 64 hex characters", + ); + }); + + it("returns read-only facilitator when private key has invalid length and requirePrivateKey=false", () => { + process.env.FACILITATOR_WALLET_PRIVATE_KEY = "0xdeadbeef"; + + const facilitator = createFacilitator(false); + expect(facilitator).toBeDefined(); + // Falls back to read-only (no throw) + }); + + it("normalizes private key without 0x prefix", () => { + // Valid 64-char hex without 0x prefix + process.env.FACILITATOR_WALLET_PRIVATE_KEY = + "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + + const facilitator = createFacilitator(true); + expect(facilitator).toBeDefined(); + }); + + it("trims whitespace from private key", () => { + process.env.FACILITATOR_WALLET_PRIVATE_KEY = + " 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 "; + + const facilitator = createFacilitator(true); + expect(facilitator).toBeDefined(); + }); +}); + +// ═══════════════════════════════════════════════════════════════ +// Singleton pattern +// ═══════════════════════════════════════════════════════════════ + +describe("getFacilitator — singleton", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + process.env.FACILITATOR_WALLET_PRIVATE_KEY = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + resetFacilitator(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + resetFacilitator(); + }); + + it("returns the same instance on subsequent calls", () => { + const first = getFacilitator(); + const second = getFacilitator(); + expect(first).toBe(second); + }); + + it("creates new instance after resetFacilitator", () => { + const first = getFacilitator(); + resetFacilitator(); + const second = getFacilitator(); + expect(first).not.toBe(second); + }); +}); From 1bed0b54422a881de17314570a8bfa1b1060c9f7 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 8 Feb 2026 16:27:19 +0100 Subject: [PATCH 3/3] Fix it --- x402_facilitator/test/x402_settle.test.js | 9 +++++++-- x402_facilitator/test/x402_splitter_supported.test.js | 8 ++++++++ x402_facilitator/x402_settle.ts | 2 +- x402_facilitator/x402_splitter_settle.js | 2 +- x402_facilitator/x402_supported.ts | 4 ++-- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/x402_facilitator/test/x402_settle.test.js b/x402_facilitator/test/x402_settle.test.js index becc908a..0d07f092 100644 --- a/x402_facilitator/test/x402_settle.test.js +++ b/x402_facilitator/test/x402_settle.test.js @@ -350,9 +350,11 @@ describe("x402_settle with mocked facilitator", () => { const originalEnv = { ...process.env }; beforeEach(() => { + vi.clearAllMocks(); process.env.FACILITATOR_WALLET_PRIVATE_KEY = "0x1234567890123456789012345678901234567890123456789012345678901234"; - vi.clearAllMocks(); + // Pin fee amount for deterministic assertions on facilitatorFeePaid + vi.spyOn(feeModule, "getFeeAmount").mockReturnValue(10000n); }); afterEach(() => { @@ -541,7 +543,10 @@ describe("x402_settle with mocked facilitator", () => { expect(result.extensions.facilitatorFees).toBeDefined(); expect(result.extensions.facilitatorFees.info.version).toBe("1"); expect(result.extensions.facilitatorFees.info.facilitatorFeePaid).toBe("10000"); - expect(result.extensions.facilitatorFees.info.asset).toBeDefined(); + // Asset uses CAIP-19 format: {network}/erc20:{address} + expect(result.extensions.facilitatorFees.info.asset).toBe( + "eip155:11155420/erc20:0x5fd84259d66Cd46123540766Be93DFE6D43130D7", + ); expect(result.extensions.facilitatorFees.info.model).toBe("flat"); expect(feeModule.collectFee).toHaveBeenCalledWith( "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", diff --git a/x402_facilitator/test/x402_splitter_supported.test.js b/x402_facilitator/test/x402_splitter_supported.test.js index d25df9f2..6d0a6f3d 100644 --- a/x402_facilitator/test/x402_splitter_supported.test.js +++ b/x402_facilitator/test/x402_splitter_supported.test.js @@ -15,6 +15,7 @@ import { getSplitterCapabilities } from "../x402_splitter_supported.js"; describe("x402 Splitter /supported endpoint", () => { const originalEnv = process.env.FACILITATOR_WALLET_PRIVATE_KEY; + const originalFixedFee = process.env.FIXED_FEE; afterEach(() => { // Restore original env @@ -23,6 +24,11 @@ describe("x402 Splitter /supported endpoint", () => { } else { delete process.env.FACILITATOR_WALLET_PRIVATE_KEY; } + if (originalFixedFee) { + process.env.FIXED_FEE = originalFixedFee; + } else { + delete process.env.FIXED_FEE; + } }); test("returns supported capabilities", () => { @@ -121,6 +127,8 @@ describe("x402 Splitter /supported endpoint", () => { }); test("extensions includes facilitatorFees for fee-aware routing (#1016)", () => { + // FIXED_FEE is read at module-load time as a top-level const (default "10000"). + // This assertion relies on that default; afterEach restores FIXED_FEE to prevent leaks. const capabilities = getSplitterCapabilities(); expect(capabilities.extensions.length).toBeGreaterThan(0); diff --git a/x402_facilitator/x402_settle.ts b/x402_facilitator/x402_settle.ts index d40e120f..591c2c4d 100644 --- a/x402_facilitator/x402_settle.ts +++ b/x402_facilitator/x402_settle.ts @@ -118,7 +118,7 @@ export async function settlePayment( info: { version: "1", facilitatorFeePaid: feeResult.success ? feeAmountStr : "0", - asset: chainConfig.USDC_ADDRESS, + asset: `${network}/erc20:${chainConfig.USDC_ADDRESS}`, model: "flat", }, }, diff --git a/x402_facilitator/x402_splitter_settle.js b/x402_facilitator/x402_splitter_settle.js index 7dc4f89b..28ea7dcf 100644 --- a/x402_facilitator/x402_splitter_settle.js +++ b/x402_facilitator/x402_splitter_settle.js @@ -229,7 +229,7 @@ export async function settleSplitterPayment(paymentPayload, paymentRequirements) info: { version: "1", facilitatorFeePaid: FIXED_FEE, - asset: usdcAddress, + asset: `${network}/erc20:${usdcAddress}`, model: "flat", }, }, diff --git a/x402_facilitator/x402_supported.ts b/x402_facilitator/x402_supported.ts index 3d41bf05..267e2454 100644 --- a/x402_facilitator/x402_supported.ts +++ b/x402_facilitator/x402_supported.ts @@ -6,7 +6,6 @@ import { formatUnits } from "viem"; import { createReadOnlyFacilitator } from "./facilitator_instance"; import { getFeeAmount, getFacilitatorAddress } from "./x402_fee"; -import { getSupportedNetworks } from "./chain_utils"; /** Facilitator fee model disclosure per x402 Fee Disclosure proposal (coinbase/x402#1016) */ interface FacilitatorFeesExtension { @@ -91,6 +90,7 @@ export function getSupportedCapabilities(): SupportedCapabilities { // Add facilitatorFees extension per x402 Fee Disclosure proposal (#1016) // Static fee model disclosure for fee-aware multi-facilitator routing + // Derive networks from supported.kinds to stay consistent with the response const facilitatorFeesExtension: FacilitatorFeesExtension = { name: "facilitatorFees", version: "1", @@ -98,7 +98,7 @@ export function getSupportedCapabilities(): SupportedCapabilities { asset: "USDC", flatFee: feeAmount.toString(), decimals: 6, - networks: getSupportedNetworks(), + networks: [...new Set(supported.kinds.map((k) => k.network))], }; supported.extensions.push(facilitatorFeesExtension); }