@@ -98,7 +98,10 @@ export interface PredicateGateConfig {
9898 */
9999export function predicateGate ( config : PredicateGateConfig ) : GateMiddleware {
100100 const chain = config . chain ?? base
101- const rpcUrl = config . rpcUrl ?? "https://mainnet.base.org"
101+ // Leave rpcUrl undefined when unset so reads follow the configured chain's
102+ // default RPC (viem), instead of silently reading Base for a non-Base chain.
103+ const rpcUrl = config . rpcUrl
104+ const identityChainId = x402IdentityChainId ( chain . id )
102105
103106 const publicClient = createPublicClient ( {
104107 chain,
@@ -148,12 +151,15 @@ export function predicateGate(config: PredicateGateConfig): GateMiddleware {
148151 const paymentHeader = request . headers . get ( "X-Payment" )
149152
150153 if ( ! paymentHeader ) {
151- return buildPredicateChallengeResponse ( config . operatorAddress , chain . id )
154+ return buildPredicateChallengeResponse (
155+ config . operatorAddress ,
156+ identityChainId ,
157+ )
152158 }
153159
154160 const result = await verifyXPaymentAuth ( paymentHeader , {
155161 operatorAddress : config . operatorAddress ,
156- expectedChainId : chain . id ,
162+ expectedChainId : identityChainId ,
157163 requireZeroValue : true ,
158164 } )
159165 if ( result . error ) {
@@ -316,9 +322,10 @@ interface VerifyXPaymentOptions {
316322 */
317323 operatorAddress : `0x${string } `
318324 /**
319- * Chain id the gate is configured for. The resolved chainId of the
320- * client-declared `network` must match, so an authorization signed for a
321- * different chain (e.g. base-sepolia) cannot authenticate to this gate.
325+ * ChainId of the x402 payment network the identity proof must be signed for
326+ * (Base / Base-Sepolia), independent of the registry chain. The resolved
327+ * chainId of the client-declared `network` must match, so an authorization
328+ * signed for a different chain (e.g. base-sepolia) cannot authenticate here.
322329 */
323330 expectedChainId : number
324331 /**
@@ -436,10 +443,10 @@ async function verifyXPaymentAuth(
436443 const chainId = resolved . chainId
437444 const tokenAddress = resolved . usdc as `0x${string } `
438445
439- // Pin the authorization to the gate's configured chain . The EIP-712 domain
440- // used for signature recovery comes from the client-declared network, so
441- // without this an authorization signed for a different chain (e.g.
442- // base-sepolia) would authenticate to a mainnet gate.
446+ // Pin the authorization to the gate's x402 payment network . The EIP-712
447+ // domain used for signature recovery comes from the client-declared network,
448+ // so without this an authorization signed for a different chain (e.g.
449+ // base-sepolia) would authenticate to a mainnet-payment gate.
443450 if ( chainId !== options . expectedChainId ) {
444451 return {
445452 error : `Predicate gate: X-Payment network mismatch (expected chainId ${ options . expectedChainId } , got ${ chainId } )` ,
@@ -535,6 +542,25 @@ const CHAIN_ID_TO_NETWORK: Record<number, string> = {
535542 84532 : "base-sepolia" ,
536543}
537544
545+ const BASE_CHAIN_ID = 8453
546+
547+ /**
548+ * The chainId the x402 identity/payment authorization must be signed for.
549+ *
550+ * The X-Payment proof is an EIP-3009 USDC authorization that only exists on
551+ * supported x402 networks (Base / Base-Sepolia); it is independent of the
552+ * chain the `ToolRegistry` is read from. A registry chain that is itself a
553+ * supported x402 network verifies identity against that network; any other
554+ * registry chain (e.g. mainnet, Monad) verifies against Base — matching the
555+ * `network=base` the 402 challenge advertises. Without this decoupling, a
556+ * non-Base registry chain would pin identity to a chainId no USDC
557+ * authorization is ever signed for, so every caller fails closed and the only
558+ * working configuration is the Base default (which reads the wrong registry).
559+ */
560+ function x402IdentityChainId ( registryChainId : number ) : number {
561+ return CHAIN_ID_TO_NETWORK [ registryChainId ] ? registryChainId : BASE_CHAIN_ID
562+ }
563+
538564function buildPredicateChallengeResponse (
539565 operatorAddress : `0x${string } `,
540566 chainId : number ,
@@ -677,8 +703,13 @@ export function paidPredicateGate(
677703 }
678704
679705 const chain = config . chain ?? base
680- const rpcUrl = config . rpcUrl ?? "https://mainnet.base.org"
706+ // Leave rpcUrl undefined when unset so reads follow the configured chain's
707+ // default RPC (viem), instead of silently reading Base for a non-Base chain.
708+ const rpcUrl = config . rpcUrl
681709 const network = config . network ?? "base"
710+ // Identity/payment rides on the x402 payment network (advertised in the 402
711+ // and used to sign the X-Payment authorization), not the registry chain.
712+ const identityChainId = resolveNetwork ( network ) ?. chainId ?? BASE_CHAIN_ID
682713 const facilitatorUrl =
683714 config . facilitatorUrl ??
684715 ( config . facilitator === "cdp"
@@ -791,7 +822,7 @@ export function paidPredicateGate(
791822 // --- Identity verification (X-Payment signature) ---
792823 const authResult = await verifyXPaymentAuth ( paymentHeader , {
793824 operatorAddress : config . operatorAddress ,
794- expectedChainId : chain . id ,
825+ expectedChainId : identityChainId ,
795826 requireZeroValue : false ,
796827 } )
797828 if ( authResult . error ) {
0 commit comments