cloudflare / cloudflare/agents

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

Open
#1,863 1 comment 0 reactions 1 assignee Claimed by @mattzcarey View on GitHub
bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.