Skip to content

agents/x402 hardcodes the EVM scheme; docs claim Stellar/Sui/Aptos support that doesn't exist in the code #1863

Open

Description

@oceans404

Problem

developers.cloudflare.com/agents/x402 states: "Supported networks include Base, Ethereum, Polygon, Optimism, Arbitrum, Avalanche, Solana, Aptos, Stellar, and Sui." In the actual implementation (packages/agents/src/mcp/x402.ts), only the EVM exact scheme is ever registered:

import { registerExactEvmScheme as registerServerEvmScheme } from "@x402/evm/exact/server"
import { registerExactEvmScheme as registerClientEvmScheme } from "@x402/evm/exact/client"

withX402() calls registerServerEvmScheme(resourceServer) unconditionally — the only scheme registration in the file. withX402Client() calls registerClientEvmScheme(paymentClient, { signer: account }), same story. There's no Stellar, Solana, Aptos, or Sui import anywhere in the repo. @x402/core is scheme-agnostic and would accept a @x402/stellar (or similar) registration — the wrapper just never calls it, and gives no hook to swap it in.

Compounding type-level lock-in

X402Config.recipient is typed `0x${string}` (EVM address literal). X402ClientConfig.account is typed ClientEvmSigner (from @x402/evm). A Stellar G... address or signer won't typecheck as either field — this isn't just a missing runtime registration, the public config types actively reject non-EVM inputs before you'd even get to a registration-order fix.

What does work

X402Config.facilitator?: FacilitatorConfig is re-exported straight from @x402/core/server — custom facilitator URLs and bearer-token auth headers are not Coinbase-CDP-locked. Only the payment scheme is hardcoded, not the facilitator transport.

Repro

Pass a Stellar G... address as X402Config.recipient — TypeScript rejects it against the `0x${string}` literal type. Force past the type (as any) and the resource server still only has the EVM exact scheme registered, so a Stellar-scheme payment payload would never validate.

Proposed fix — additive, backward-compatible, verified end-to-end

Verified with a real Stellar implementation and a full MCP protocol round trip (initialize → tools/call → 402 → client builds a Stellar payment → retries → server verifies + settles → result), confirmed on-chain via Horizon. No existing EVM caller's behavior changes.

  1. Server (withX402) — alongside the existing registerServerEvmScheme(resourceServer) call, unconditionally add:

    resourceServer.register("stellar:*", new ExactStellarScheme()); // from @x402/stellar/exact/server

    buildPaymentRequirements() looks up the scheme matching the requested network, so this additional registration is inert for existing EVM configs.

  2. Client (withX402Client) — account is a single value and client registration is keyed by network, so a single signer can't be dual-registered. The backward-compatible fix is a runtime type guard — @x402/stellar already exports isClientStellarSigner for exactly this:

    if (isClientStellarSigner(account)) {
      paymentClient.register("stellar:*", new ExactStellarScheme(account)); // from @x402/stellar/exact/client
    } else {
      registerClientEvmScheme(paymentClient, { signer: account }); // unchanged existing branch
    }
  3. Types — widen recipient: \0x${string}`→`0x${string}` | stringandaccount: ClientEvmSigner→ClientEvmSigner | ClientStellarSigner`. Widening a literal type to a union never breaks an existing call site.

Heads-up for whoever picks this up: the test mocks need a small, expected extension

Running a patch like this against packages/agents/src/x402-tests/x402.test.ts initially fails 17 of 33 tests with resourceServer.register is not a function. Root cause: @x402/evm's registration goes through a convenience function (registerExactEvmScheme) that the test file mocks as a no-op, so the mock resource server never needed a real .register() method. @x402/stellar has no equivalent convenience function, so the patch has to call .register() directly, which the existing mock object doesn't implement. Fix: add register: vi.fn() to the existing mock fixtures, plus vi.mock() declarations for the new imports, mirroring the existing @x402/evm mock pattern. No existing test's assertions need to change. With that scaffolding, all 33 original tests pass unchanged.

Acceptance criteria

  • withX402() registers both @x402/evm and @x402/stellar exact schemes unconditionally, no regression for EVM configs
  • withX402Client() branches on isClientStellarSigner(account), falling through to the existing EVM branch unchanged when the signer isn't Stellar
  • X402Config.recipient / X402ClientConfig.account widened to EVM-or-Stellar unions, confirmed non-breaking against this repo's own test suite
  • If maintainers prefer EVM-only scope instead: correct the docs' network list and point to examples/x402/ (raw @x402/core/@x402/hono, not EVM-locked) for anyone needing Stellar today

Activity

  1. twzrd-sol commented on Aug 11, 2026

    @twzrd-sol

    Thanks for filing this — the docs/code mismatch is exactly the gap we hit trying to sell Solana mainnet through the Agents MCP surface (withX402 / paidTool / withX402Client).

    What we verified (2026-08-11): packages/agents/src/mcp/x402.ts still registers only @x402/evm (registerExactEvmScheme on server + client); recipient/account are still typed EVM-only. Foundation @x402/svm@2.21.0 already exports the same twin registration shape (registerExactSvmScheme on exact/server + exact/client), so this is registration + type-widen + network map, not inventing a new scheme — same shape as the Stellar approach here.

    Re #596: reading the thread, it reads less like "Solana unwanted" and more like CF having internal design constraints (per threepointone) plus a real Workers → public Solana RPC 403 snag for the recent-blockhash fetch (per adlonymous). That RPC edge is real and worth designing for, not papering over.

    We put together a draft against main to make that concrete rather than just describe it: twzrd-sol#3 (in our fork, not opened here). Single commit, 38/38 tests, tsc --noEmit clean. A few things it does that seemed worth getting right given #596's history: SVM/EVM client-side scheme selection uses @solana/kit's own isTransactionSigner() rather than a hand-rolled method check (a hand-rolled version we tried first missed one of the three TransactionSigner shapes and would've silently misrouted), and it adds an optional RPC override specifically for the Workers-egress-403 case.

    Not trying to race or claim this — happy to open it here as-is, adjust it to whatever shape you'd prefer, or just leave it as reference if something's already in flight. One thing that would help us not duplicate effort: is Solana-on-MCP intended to land as (a) additive to the current module, (b) deferred into the Monetization Gateway rewrite (#1989), or (c) internal-only for now? Also happy to offer our facilitator (intel.twzrd.xyz) as a live Solana-mainnet verification target if useful for testing, the way other teams have offered theirs — not a requirement, just available.

    Thanks for tracking the honesty gap here either way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions