cloudflare / cloudflare/agents
agents/x402 hardcodes the EVM scheme; docs claim Stellar/Sui/Aptos support that doesn't exist in the code
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
## Problem
[developers.cloudflare.com/agents/x402](https://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`](https://github.com/cloudflare/agents/blob/main/packages/agents/src/mcp/x402.ts)), only the EVM exact scheme is ever registered:
```ts
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:
```ts
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:
```ts
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}\` | string` and `account: 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/`](https://github.com/cloudflare/agents/tree/main/examples/x402) (raw `@x402/core`/`@x402/hono`, not EVM-locked) for anyone needing Stellar today
Contributor guide
Assessment
This issue has not been assessed yet.