/** * Internal protocol handler for Base/USDC payments. * * This module is an implementation detail — consumers configure a `base` * payment method and the proxy handles the underlying protocol automatically. * * Currently uses x402 (https://x402.org) for on-chain payment verification. * Base may support additional protocols in the future; this module can grow * to handle them without any change to the consumer-facing API. */ import type { BaseConfig, BasePrice } from './config.js' // --------------------------------------------------------------------------- // Network helpers // --------------------------------------------------------------------------- /** Friendly aliases → EIP-155 chain ID */ const NETWORK_ALIASES: Record = { 'base': 'eip155:8453', 'base-mainnet': 'eip155:8453', 'base-sepolia': 'eip155:84532', } const DEFAULT_NETWORK = 'eip155:84532' // Base Sepolia /** USDC token contract addresses by EIP-155 network */ const USDC_TOKEN: Record = { 'eip155:8453': '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base Mainnet 'eip155:84532': '0x036CbD53842c5426634e7929541eC2318f3dCF7e', // Base Sepolia } const DEFAULT_FACILITATOR = 'https://x402.org/facilitator' const MAX_TIMEOUT_SECONDS = 300 /** Normalise a user-supplied network string to its EIP-155 chain ID. */ export function normalizeNetwork(network?: string): string { if (!network) return DEFAULT_NETWORK return NETWORK_ALIASES[network] ?? network } /** Convert a USD dollar amount string to USDC base units (6 decimals). */ export function usdToUsdcUnits(amountUsd: string): string { return String(Math.round(parseFloat(amountUsd) * 1_000_000)) } // --------------------------------------------------------------------------- // Challenge construction // --------------------------------------------------------------------------- export type BaseChallengeParams = { payTo: string network: string // normalised EIP-155 ID usdcUnits: string // maxAmount in USDC base units resource: string description?: string } /** * Build the value for the X-PAYMENT-REQUIRED header (base64-encoded JSON * following the x402 v1 spec). */ export function buildBaseChallengeHeader(params: BaseChallengeParams): string { const token = USDC_TOKEN[params.network] const payload = { version: '1', accepts: [ { scheme: 'exact', network: params.network, maxAmountRequired: params.usdcUnits, resource: params.resource, description: params.description ?? '', mimeType: 'application/json', payTo: params.payTo, maxTimeoutSeconds: MAX_TIMEOUT_SECONDS, token, outputSchema: null, extra: null, }, ], } return Buffer.from(JSON.stringify(payload)).toString('base64') } // --------------------------------------------------------------------------- // Verification // --------------------------------------------------------------------------- type PaymentProof = { scheme: string network: string payload: { signature: string authorization: { from: string to: string value: string validAfter: string validBefore: string nonce: string version: string } } } type FacilitatorResponse = { isValid: boolean invalidReason: string | null } export async function verifyBasePayment( paymentHeader: string, params: BaseChallengeParams, facilitatorUrl = DEFAULT_FACILITATOR, ): Promise<{ success: boolean; payer?: string; error?: string }> { let proof: PaymentProof try { proof = JSON.parse(Buffer.from(paymentHeader, 'base64').toString()) as PaymentProof } catch { return { success: false, error: 'Could not decode payment header' } } const network = proof.network ?? params.network const token = USDC_TOKEN[network] let res: Response try { res = await fetch(`${facilitatorUrl}/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ x402Version: 1, scheme: proof.scheme, network, payload: proof.payload, resource: params.resource, amount: params.usdcUnits, payTo: params.payTo, token, }), }) } catch (err) { return { success: false, error: `Facilitator unreachable: ${String(err)}` } } if (!res.ok) { return { success: false, error: `Facilitator returned ${res.status}` } } const result = (await res.json()) as FacilitatorResponse if (!result.isValid) { return { success: false, error: result.invalidReason ?? 'Payment invalid' } } return { success: true, payer: proof.payload.authorization.from } } // --------------------------------------------------------------------------- // Receipt // --------------------------------------------------------------------------- export function buildBaseReceiptHeader(payer: string, network: string): string { return Buffer.from( JSON.stringify({ success: true, scheme: 'exact', network, payer }), ).toString('base64') } // --------------------------------------------------------------------------- // Route config resolution // --------------------------------------------------------------------------- export type ResolvedBaseRoute = { network: string payTo: string usdcUnits: string facilitatorUrl: string } export function resolveBaseRouteConfig( routePrice: BasePrice, globalConfig: BaseConfig, ): ResolvedBaseRoute { return { network: normalizeNetwork(globalConfig.network), payTo: globalConfig.payTo, usdcUnits: usdToUsdcUnits(routePrice.amount), facilitatorUrl: globalConfig.facilitatorUrl ?? DEFAULT_FACILITATOR, } }