Conversation
There was a problem hiding this comment.
Pull request overview
Adds x402 fee disclosure metadata/receipts (“facilitatorFees”, per coinbase/x402#1016) to capability discovery and settlement responses in the x402_facilitator service, enabling fee-aware routing across facilitators.
Changes:
- Advertise a new
facilitatorFeesextension from/supportedfor both the standard facilitator and the splitter facilitator. - Return an
extensions.facilitatorFeesreceipt from settlement responses (standard settle + splitter settle) and plumb it through HTTP handlers. - Expand Vitest coverage for
/supported, settle responses, and facilitator instance behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| x402_facilitator/x402_supported.ts | Adds facilitatorFees extension to /supported response. |
| x402_facilitator/x402_splitter_supported.js | Adds facilitatorFees extension to splitter /supported response. |
| x402_facilitator/x402_splitter_settle.js | Adds facilitatorFees receipt to successful splitter settlement responses. |
| x402_facilitator/x402_splitter_facilitator.js | Includes extensions in splitter /settle HTTP response when present. |
| x402_facilitator/x402_settle.ts | Adds typed extensions.facilitatorFees receipt to standard settle results. |
| x402_facilitator/x402_fee.ts | Minor logging formatting tweak in fee parsing. |
| x402_facilitator/x402_facilitator.ts | Includes extensions in standard facilitator /settle HTTP response when present. |
| x402_facilitator/test/x402_supported.test.js | Tests presence/shape of facilitatorFees in /supported. |
| x402_facilitator/test/x402_splitter_supported.test.js | Updates splitter /supported tests to expect facilitatorFees. |
| x402_facilitator/test/x402_settle.test.js | Asserts facilitatorFees receipt behavior in settle results. |
| x402_facilitator/test/x402_facilitator.test.ts | Ensures handler includes extensions in HTTP response. |
| x402_facilitator/test/facilitator_instance.test.ts | Adds private key validation + singleton tests; updates imports. |
| const facilitatorFeesExtension: FacilitatorFeesExtension = { | ||
| name: "facilitatorFees", | ||
| version: "1", | ||
| model: "flat", | ||
| asset: "USDC", | ||
| flatFee: feeAmount.toString(), | ||
| decimals: 6, | ||
| networks: getSupportedNetworks(), | ||
| }; |
There was a problem hiding this comment.
facilitatorFees.networks is populated from getSupportedNetworks(), which is a separate source of truth from the actual supported.kinds you just built. To avoid drift (e.g., if the facilitator ever filters networks), derive this from supported.kinds.map(k => k.network) so the extension always matches the response’s supported networks.
| 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); |
There was a problem hiding this comment.
This test asserts a hard-coded fee amount ("10000") but doesn’t set FACILITATOR_FEE_AMOUNT. If the test runner environment overrides that env var, this will fail. Set process.env.FACILITATOR_FEE_AMOUNT = "10000" (and restore it in afterEach) or mock getFeeAmount() for determinism.
| // 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"); |
There was a problem hiding this comment.
These new assertions depend on the default FACILITATOR_FEE_AMOUNT being 10000 via getFeeAmount(). If FACILITATOR_FEE_AMOUNT is set differently in the environment running tests, this becomes flaky. Prefer explicitly setting process.env.FACILITATOR_FEE_AMOUNT in the test setup (or mocking getFeeAmount) before asserting facilitatorFeePaid.
| info: { | ||
| version: "1", | ||
| facilitatorFeePaid: FIXED_FEE, | ||
| asset: usdcAddress, |
There was a problem hiding this comment.
The facilitatorFees receipt here reports asset: usdcAddress (an address), while /supported for the same facilitator advertises asset: "USDC". Pick one asset identifier format and use it consistently across /supported and settlement receipts so clients can parse this reliably (CAIP-19 is already used elsewhere in kinds.extra.asset).
| asset: usdcAddress, | |
| asset: "USDC", |
| // Alias for backward compatibility | ||
| const SPLITTER_ABI = EIP3009SplitterV1ABI; | ||
| const getSplitterAddress = getEIP3009SplitterAddress; | ||
| const FIXED_FEE = process.env.FIXED_FEE || "10000"; // 0.01 USDC |
There was a problem hiding this comment.
This introduces/relies on a FIXED_FEE environment variable to populate the fee receipt. The repository’s .env.example only documents FACILITATOR_FEE_AMOUNT, so it’s easy to misconfigure the splitter fee in deployments. Consider documenting FIXED_FEE (or renaming it to something more specific like SPLITTER_FIXED_FEE) and keeping it consistent across supported/verify/settle.
| 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"); |
There was a problem hiding this comment.
This test expects flatFee to be "10000" but doesn’t set process.env.FIXED_FEE. If the env var is set differently in the test environment, this will fail. Set process.env.FIXED_FEE = "10000" within the test (and restore in afterEach) to keep it deterministic.
| return { | ||
| success: true, | ||
| payer: buyer, | ||
| transaction: hash, | ||
| network, | ||
| // x402 Fee Disclosure receipt (coinbase/x402#1016) | ||
| extensions: { | ||
| facilitatorFees: { | ||
| info: { | ||
| version: "1", | ||
| facilitatorFeePaid: FIXED_FEE, | ||
| asset: usdcAddress, | ||
| model: "flat", | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
New behavior: successful splitter settlements now include an extensions.facilitatorFees receipt. There are existing tests for settleSplitterPayment (see test/x402_splitter_settle.test.js), but none appear to assert this new response field. Adding an assertion for the extension (including facilitatorFeePaid, model, and asset) would prevent regressions.
x402_facilitator/x402_settle.ts
Outdated
| info: { | ||
| version: "1", | ||
| facilitatorFeePaid: feeResult.success ? feeAmountStr : "0", | ||
| asset: chainConfig.USDC_ADDRESS, |
There was a problem hiding this comment.
The facilitatorFees receipt uses asset: chainConfig.USDC_ADDRESS (a bare address), while /supported advertises asset: "USDC" for the same extension. This makes the extension harder to consume because clients can’t rely on a stable asset identifier format. Consider using the same asset identifier in both places (e.g., consistently "USDC", or consistently a CAIP-19 string like ${network}/erc20:${USDC_ADDRESS}).
| asset: chainConfig.USDC_ADDRESS, | |
| // Use a stable asset identifier consistent with /supported | |
| asset: "USDC", |
No description provided.