99} from '@nestjs/common' ;
1010import { ConfigService } from '@nestjs/config' ;
1111import axios , { AxiosError } from 'axios' ;
12- import { createPublicClient , http } from 'viem' ;
12+ import { createPublicClient , getAddress , http } from 'viem' ;
1313import {
1414 USDC_TOKEN ,
1515 getChainById ,
@@ -45,6 +45,37 @@ export const Facilitator = {
4545} as const ;
4646export type Facilitator = ( typeof Facilitator ) [ keyof typeof Facilitator ] ;
4747
48+ // CAIP-2 networks the Coinbase CDP facilitator actually settles, per
49+ // https://docs.cdp.coinbase.com/x402/network-support (verified 2026-06-13):
50+ // Base, Base Sepolia, Polygon, Arbitrum One, World, World Sepolia, Solana
51+ // (+ devnet). NOTE: Arbitrum *Sepolia* (eip155:421614) is NOT supported by
52+ // CDP, and PayAI lists "arbitrum"/"arbitrum-sepolia" in /supported but rejects
53+ // them on verify with invalid_exact_evm_network_mismatch — so Arbitrum One is
54+ // the only Arbitrum chain that settles anywhere, and it must go through CDP.
55+ const CDP_SUPPORTED_NETWORKS : ReadonlySet < string > = new Set ( [
56+ 'eip155:8453' , // Base
57+ 'eip155:84532' , // Base Sepolia
58+ 'eip155:137' , // Polygon
59+ 'eip155:42161' , // Arbitrum One
60+ 'eip155:480' , // World
61+ 'eip155:4801' , // World Sepolia
62+ ] ) ;
63+
64+ // Chains routed to CDP (x402 v2). Everything else falls back to PayAI (v1).
65+ // Only Arbitrum One settles via CDP today; Base stays on PayAI to keep the
66+ // existing, working v1 path untouched.
67+ const CDP_ROUTED_CHAIN_IDS : ReadonlySet < number > = new Set ( [ 42161 ] ) ;
68+
69+ /**
70+ * Chain-aware facilitator selection. Arbitrum One (42161) is only settleable
71+ * through Coinbase CDP (x402 v2); all other supported chains keep using PayAI.
72+ */
73+ export function facilitatorForChain ( chainId : number ) : Facilitator {
74+ return CDP_ROUTED_CHAIN_IDS . has ( chainId )
75+ ? Facilitator . CDP
76+ : Facilitator . PayAI ;
77+ }
78+
4879// --- x402 v2 wire types -----------------------------------------------------
4980// Mirrors @x 402/core/types/payments.ts. Inlined so we don't pull the package
5081// just for type names. CDP requires v2 wire format for bazaar indexing today.
@@ -115,6 +146,18 @@ export class X402Service {
115146
116147 // ---------- public surface ----------
117148
149+ /**
150+ * Resolve which facilitator a multisig's deposits route through, based on the
151+ * account's chainId. Arbitrum One (42161) → CDP (x402 v2, the only facilitator
152+ * that settles it); every other supported chain → PayAI (x402 v1). Lets the
153+ * default /deposit routes stay chain-aware without the controller knowing
154+ * about chains. Throws (via assertAccount) for unknown/unsupported chains.
155+ */
156+ async resolveFacilitator ( multisigAddress : string ) : Promise < Facilitator > {
157+ const account = await this . assertAccount ( multisigAddress ) ;
158+ return facilitatorForChain ( account . chainId ) ;
159+ }
160+
118161 async buildDiscoveryResponse (
119162 multisigAddress : string ,
120163 resourceUrl : string ,
@@ -123,6 +166,7 @@ export class X402Service {
123166 if ( facilitator === Facilitator . CDP ) {
124167 this . assertCdpEnabled ( ) ;
125168 const account = await this . assertAccount ( multisigAddress ) ;
169+ this . assertCdpNetworkSupported ( account . chainId ) ;
126170 return this . buildV2PaymentRequired (
127171 account . chainId ,
128172 account . address ,
@@ -165,6 +209,9 @@ export class X402Service {
165209 ) ;
166210 }
167211 const account = await this . assertAccount ( multisigAddress ) ;
212+ if ( facilitator === Facilitator . CDP ) {
213+ this . assertCdpNetworkSupported ( account . chainId ) ;
214+ }
168215 const payload = decodeXPaymentHeader ( paymentHeader ) ;
169216
170217 await this . validatePayloadAgainstMultisig (
@@ -222,7 +269,14 @@ export class X402Service {
222269 resource : this . buildV2Resource ( resourceUrl , account . address ) ,
223270 accepted : v2Requirements ,
224271 payload : {
225- authorization : payload . payload . authorization ,
272+ // Checksum the EIP-3009 authorization addresses for CDP's strict
273+ // v2 EIP-55 validation. Safe for the signature: EIP-712 encodes
274+ // addresses to the 20-byte value regardless of display casing.
275+ authorization : {
276+ ...payload . payload . authorization ,
277+ from : getAddress ( payload . payload . authorization . from ) ,
278+ to : getAddress ( payload . payload . authorization . to ) ,
279+ } ,
226280 signature : payload . payload . signature ,
227281 } ,
228282 extensions : { bazaar : this . buildCdpBazaarExtension ( resourceUrl ) } ,
@@ -408,6 +462,20 @@ export class X402Service {
408462 }
409463 }
410464
465+ // Fail fast (and clearly) when a chain is routed to CDP but CDP cannot settle
466+ // it — otherwise the request would die deep inside cdpVerify with an opaque
467+ // facilitator error. Notably guards Arbitrum Sepolia (eip155:421614), which
468+ // CDP does not support and PayAI rejects, so it has no working facilitator.
469+ private assertCdpNetworkSupported ( chainId : number ) : void {
470+ const network = `eip155:${ chainId } ` ;
471+ if ( ! CDP_SUPPORTED_NETWORKS . has ( network ) ) {
472+ throw new BadRequestException (
473+ `Chain ${ chainId } (${ network } ) is not supported by the Coinbase CDP ` +
474+ `x402 facilitator. CDP settles: ${ [ ...CDP_SUPPORTED_NETWORKS ] . join ( ', ' ) } .` ,
475+ ) ;
476+ }
477+ }
478+
411479 private buildPaymentRequirements (
412480 chainId : number ,
413481 payTo : string ,
@@ -495,11 +563,20 @@ export class X402Service {
495563 return {
496564 scheme : 'exact' ,
497565 network : `eip155:${ chainId } ` ,
498- asset : USDC_TOKEN . addresses [ chainId ] ,
566+ // CDP's v2 validator requires EIP-55 checksummed EVM addresses; sending
567+ // lowercase makes the payload fail its oneOf and surface a misleading
568+ // "x402V1PaymentPayload requires 'scheme'" error. Checksum here.
569+ asset : getAddress ( USDC_TOKEN . addresses [ chainId ] ) ,
499570 amount,
500- payTo,
571+ payTo : getAddress ( payTo ) ,
501572 maxTimeoutSeconds : 120 ,
502573 extra : {
574+ // `assetTransferMethod: "eip3009"` is part of the canonical v2 exact-EVM
575+ // scheme payload (specs/schemes/exact/scheme_exact_evm.md). USDC supports
576+ // transferWithAuthorization natively, so we declare eip3009 explicitly.
577+ assetTransferMethod : 'eip3009' ,
578+ // EIP-712 domain — required by the facilitator to verify the signature
579+ // against the asset contract for the "exact" scheme on EVM.
503580 name : domain . name ,
504581 version : domain . version ,
505582 minDeposit : MIN_DEPOSIT . toString ( ) ,
0 commit comments