From d8de5050468775d32d78f302cc5d362689b45434 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 22:39:45 +0000 Subject: [PATCH] Pick x402 payment options on a supported network only A server may offer the same price on several chains at once. Apify's x402 endpoints list Base and two Solana entries, and the accept selector took the first entry with the requested scheme regardless of its network, so a Solana entry listed first made "x402 sign" and the --x402 session auto-pay fail with "Unknown network" even though a Base entry was present. The selector now skips entries on networks mcpc cannot sign for, in the PAYMENT-REQUIRED header path and in the flat _meta.x402 tool-meta fallback alike. When no entry is on a supported network, the error lists what the server offered and the supported networks. Refs #465 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015xAB5XbFwVwx3u72PLZFzQ --- CHANGELOG.md | 1 + src/lib/x402/fetch-middleware.ts | 7 ++- src/lib/x402/signer.ts | 26 ++++++++++-- test/unit/lib/x402/fetch-middleware.test.ts | 12 ++++++ test/unit/lib/x402/signer.test.ts | 47 +++++++++++++++++++++ 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd3225a..cb593fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Argument completions for prompts and resource templates, tasks on MCP 2026-07-28 ### Fixed +- x402 payments no longer fail with "Unknown network" when a server lists a payment option on a chain mcpc cannot sign for (e.g. Solana) before the Base one: `x402 sign` and `--x402` sessions now pick the first option on a supported network. When there is none, the error lists what the server offered and which networks are supported. - `tools-call`, `prompts-get` and `completion-complete` no longer send arguments other than what you typed: a JSON array passed inline or on stdin is now rejected (MCP arguments must be an object), a `__proto__:=...` argument is no longer dropped, and a number too large to represent (e.g. `n:=1e400`) is kept as a string instead of being sent as `null`. (#448) - Bridge log rotation no longer races the file it rotates: under load the rename could run before the log file was opened or flushed, so the rotation was silently skipped and the first lines of the new file were lost. - Windows sessions no longer pile up duplicate bridge processes when `tasklist` is slow or unavailable. A liveness check that could not run used to report every bridge dead, so each command spawned a replacement without stopping the healthy original; the check now falls back to a signal probe, the old bridge is always asked to shut down, and a bridge that finds another one registered for its session exits on its own. (#427) diff --git a/src/lib/x402/fetch-middleware.ts b/src/lib/x402/fetch-middleware.ts index 052219fc..fe907327 100644 --- a/src/lib/x402/fetch-middleware.ts +++ b/src/lib/x402/fetch-middleware.ts @@ -30,6 +30,7 @@ import { signPayment, parsePaymentRequired, selectAcceptEntry, + isSupportedNetwork, DEFAULT_PAYMENT_EXPIRY_SECONDS, type SignerWallet, type PaymentRequiredAccept, @@ -520,7 +521,8 @@ function extractToolCallName(body: RequestInit['body'] | undefined): string | un * Pick an accept entry from a tool's `_meta.x402` block honoring `schemePreference`. * * Prefers the spec-shaped `accepts[]` array when present; falls back to the flat fields - * only when the preference matches the flat scheme (or the preference is `auto`). + * only when the preference matches the flat scheme (or the preference is `auto`) and the + * flat network is one mcpc can sign for. * Returning undefined defers signing to the 402 fallback path, which re-runs the * selector against the authoritative PAYMENT-REQUIRED header. */ @@ -540,6 +542,9 @@ function selectAcceptFromToolMeta( if (preference !== 'auto' && preference !== x402.scheme) { return undefined; } + if (!isSupportedNetwork(x402.network)) { + return undefined; + } return { scheme: x402.scheme, diff --git a/src/lib/x402/signer.ts b/src/lib/x402/signer.ts index 3254a4f3..76f22ab4 100644 --- a/src/lib/x402/signer.ts +++ b/src/lib/x402/signer.ts @@ -127,6 +127,15 @@ const NETWORKS: Record = { }, }; +/** + * Whether mcpc can sign payments on the given network (CAIP-2 id, e.g. `eip155:8453`). + * Servers may offer the same price on several chains at once (Base, Solana, ...); only + * the networks in `NETWORKS` are supported. + */ +export function isSupportedNetwork(network: string): boolean { + return Object.hasOwn(NETWORKS, network); +} + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -221,14 +230,19 @@ function randomPermit2Nonce(): string { // --------------------------------------------------------------------------- function isValidExactAccept(a: PaymentRequiredAccept): boolean { - return a.scheme === 'exact' && Boolean(a.payTo && a.amount && a.network && a.asset); + return ( + a.scheme === 'exact' && + Boolean(a.payTo && a.amount && a.network && a.asset) && + isSupportedNetwork(a.network) + ); } function isValidUptoAccept(a: PaymentRequiredAccept): boolean { return ( a.scheme === 'upto' && Boolean(a.payTo && a.amount && a.network && a.asset) && - Boolean(a.extra?.facilitatorAddress) + Boolean(a.extra?.facilitatorAddress) && + isSupportedNetwork(a.network) ); } @@ -238,6 +252,9 @@ function isValidUptoAccept(a: PaymentRequiredAccept): boolean { * - `auto` → prefer valid `upto`, fallback to valid `exact` * - `upto` → require valid `upto`; undefined if none * - `exact` → require valid `exact`; undefined if none + * + * Entries on networks mcpc cannot sign for (see `isSupportedNetwork`) are skipped, so a + * server that lists a Solana option before the Base one still gets a Base payment. */ export function selectAcceptEntry( accepts: PaymentRequiredAccept[], @@ -291,9 +308,10 @@ export function parsePaymentRequired( const accept = selectAcceptEntry(header.accepts, schemePreference); if (!accept) { const requested = schemePreference === 'auto' ? 'exact or upto' : schemePreference; - const available = header.accepts.map((a) => a.scheme).join(', '); + const offered = header.accepts.map((a) => `${a.scheme} on ${a.network}`).join(', '); throw new ClientError( - `No valid "${requested}" scheme found in PAYMENT-REQUIRED accepts. Available: ${available}` + `No valid "${requested}" scheme on a supported network found in PAYMENT-REQUIRED accepts. ` + + `Offered: ${offered}. Supported networks: ${Object.keys(NETWORKS).join(', ')}` ); } diff --git a/test/unit/lib/x402/fetch-middleware.test.ts b/test/unit/lib/x402/fetch-middleware.test.ts index ed282161..ffdbc139 100644 --- a/test/unit/lib/x402/fetch-middleware.test.ts +++ b/test/unit/lib/x402/fetch-middleware.test.ts @@ -338,6 +338,18 @@ describe('extractAcceptFromPaymentRequired', () => { const result = extractAcceptFromPaymentRequired(uptoOnly, 'exact'); expect(result).toBeUndefined(); }); + + it('skips entries on networks mcpc cannot sign for', () => { + const solanaFirst = { + x402Version: 2, + accepts: [ + { ...EXACT_ACCEPT, network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' }, + EXACT_ACCEPT, + ], + }; + const result = extractAcceptFromPaymentRequired(solanaFirst, 'exact'); + expect(result?.accept.network).toBe(EXACT_ACCEPT.network); + }); }); // --------------------------------------------------------------------------- diff --git a/test/unit/lib/x402/signer.test.ts b/test/unit/lib/x402/signer.test.ts index d15f9070..8b6661df 100644 --- a/test/unit/lib/x402/signer.test.ts +++ b/test/unit/lib/x402/signer.test.ts @@ -94,6 +94,16 @@ const VALID_UPTO_ACCEPT: PaymentRequiredAccept = { }, }; +// Apify's x402 endpoints offer the same price on Base and on Solana; mcpc signs EVM only. +const SOLANA_EXACT_ACCEPT: PaymentRequiredAccept = { + scheme: 'exact', + network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + amount: '1000000', + asset: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + payTo: 'ECUPkq6DP8VUSEueL4qNznyNDJLwu6PQhnMQ5cuZsTLH', + maxTimeoutSeconds: 60, +}; + const MOCK_WALLET: SignerWallet = { privateKey: '0x1234567890abcdef', address: MOCK_ADDRESS, @@ -166,6 +176,28 @@ describe('selectAcceptEntry', () => { const result = selectAcceptEntry([VALID_EXACT_ACCEPT, secondExact], 'auto'); expect(result?.amount).toBe('1000000'); }); + + it('exact: skips entries on unsupported networks', () => { + const result = selectAcceptEntry([SOLANA_EXACT_ACCEPT, VALID_EXACT_ACCEPT], 'exact'); + expect(result?.network).toBe('eip155:84532'); + }); + + it('upto: skips entries on unsupported networks', () => { + const solanaUpto = { ...VALID_UPTO_ACCEPT, network: SOLANA_EXACT_ACCEPT.network }; + const result = selectAcceptEntry([solanaUpto, VALID_UPTO_ACCEPT], 'upto'); + expect(result?.network).toBe('eip155:84532'); + }); + + it('auto: prefers a supported exact over an upto on an unsupported network', () => { + const solanaUpto = { ...VALID_UPTO_ACCEPT, network: SOLANA_EXACT_ACCEPT.network }; + const result = selectAcceptEntry([solanaUpto, VALID_EXACT_ACCEPT], 'auto'); + expect(result?.scheme).toBe('exact'); + }); + + it('returns undefined when every entry is on an unsupported network', () => { + const result = selectAcceptEntry([SOLANA_EXACT_ACCEPT], 'auto'); + expect(result).toBeUndefined(); + }); }); // --------------------------------------------------------------------------- @@ -227,6 +259,21 @@ describe('parsePaymentRequired', () => { expect(() => parsePaymentRequired(b64, 'upto')).toThrow(ClientError); expect(() => parsePaymentRequired(b64, 'upto')).toThrow('upto'); }); + + it('picks the Base entry when a Solana entry is listed first', () => { + const b64 = buildPaymentRequired([SOLANA_EXACT_ACCEPT, VALID_EXACT_ACCEPT]); + const { accept } = parsePaymentRequired(b64, 'auto'); + expect(accept.network).toBe('eip155:84532'); + }); + + it('throws naming the offered and the supported networks when none is supported', () => { + const b64 = buildPaymentRequired([SOLANA_EXACT_ACCEPT]); + expect(() => parsePaymentRequired(b64, 'exact')).toThrow(ClientError); + expect(() => parsePaymentRequired(b64, 'exact')).toThrow( + 'exact on solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' + ); + expect(() => parsePaymentRequired(b64, 'exact')).toThrow('Supported networks: eip155:8453'); + }); }); // ---------------------------------------------------------------------------