diff --git a/.env-example b/.env-example index ce5c0ca..d2588c8 100644 --- a/.env-example +++ b/.env-example @@ -34,13 +34,19 @@ export X402_PRICE_USDC=0.1 # Use 'eip155:84532' for Base Sepolia (testnet) export x402_NETWORK=eip155:8453 -# Facilitator URL +# Primary Facilitator +# Options: 'cdp' (Coinbase) or 'dexter' (Dexter.cash - no API keys required) +export PRIMARY_FACILITATOR=cdp + +# Facilitator URLs # For Coinbase CDP (recommended): https://api.cdp.coinbase.com/platform/v2/x402 -# For custom/local facilitator: http://localhost:4022 -export x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402 +# For Dexter (no API keys):https://x402.dexter.cash +# URL are not used when PRIMARY_FACILITATOR is set +# export x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402 # CDP API Credentials (required when using CDP facilitator) # Get these from https://cdp.coinbase.com - API Keys section +# Not required for Dexter facilitator export FACILITATOR_KEY_ID=your_key_id_here export FACILITATOR_SECRET_KEY=your_secret_key_here diff --git a/bin/server.js b/bin/server.js index 63b05da..d5f0d2a 100644 --- a/bin/server.js +++ b/bin/server.js @@ -18,7 +18,7 @@ import { dirname, join } from 'path' import config from '../src/config/index.js' import Controllers from '../src/controllers/index.js' import wlogger from '../src/adapters/wlogger.js' -import { buildX402Routes, getX402Settings, getBasicAuthSettings, createAuthHeader } from '../src/config/x402.js' +import { buildX402Routes, getX402Settings, getBasicAuthSettings, createAuthHeader, getFacilitatorConfig } from '../src/config/x402.js' import { basicAuthMiddleware } from '../src/middleware/basic-auth.js' import DiscoveryRouter from '../src/controllers/discovery/router.js' @@ -125,14 +125,15 @@ class Server { if (x402Settings.enabled && basicAuthSettings.enabled) { // X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally const routes = buildX402Routes(this.config.apiPrefix) - const facilitatorOptions = x402Settings.facilitatorUrl - ? { - url: x402Settings.facilitatorUrl, - createAuthHeaders: createAuthHeader - } - : undefined - wlogger.info(`x402 v2 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceUSDC} USDC per request (unless basic auth provided)`) + const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp' + const facilitatorConfig = getFacilitatorConfig(primaryFacilitator) + const facilitatorOptions = { + url: facilitatorConfig.url, + createAuthHeaders: facilitatorConfig.requiresAuth ? createAuthHeader : null + } + + wlogger.info(`x402 v2 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceUSDC} USDC per request (unless basic auth provided) [facilitator: ${facilitatorConfig.name}]`) const facilitatorClient = new HTTPFacilitatorClient(facilitatorOptions) // x402 v2 exports use lowercase class names (x402ResourceServer). @@ -141,6 +142,7 @@ class Server { registerExactEvmScheme(resourceServer, {}) const x402Mw = x402PaymentMiddleware(routes, resourceServer) + // Create conditional x402 middleware that bypasses if basic auth is valid const conditionalX402Middleware = (req, res, next) => { if (req.locals?.basicAuthValid === true) { return next() @@ -149,16 +151,19 @@ class Server { } app.use(conditionalX402Middleware) + + // Display facilitator running url + console.log(`Facilitator running on: ${facilitatorOptions.url}`) } else if (x402Settings.enabled && !basicAuthSettings.enabled) { const routes = buildX402Routes(this.config.apiPrefix) - const facilitatorOptions = x402Settings.facilitatorUrl - ? { - url: x402Settings.facilitatorUrl, - createAuthHeaders: createAuthHeader - } - : undefined + const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp' + const facilitatorConfig = getFacilitatorConfig(primaryFacilitator) + const facilitatorOptions = { + url: facilitatorConfig.url, + createAuthHeaders: facilitatorConfig.requiresAuth ? createAuthHeader : null + } - wlogger.info(`x402 v2 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceUSDC} USDC per request`) + wlogger.info(`x402 v2 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceUSDC} USDC per request [facilitator: ${facilitatorConfig.name}]`) const facilitatorClient = new HTTPFacilitatorClient(facilitatorOptions) // eslint-disable-next-line new-cap diff --git a/src/config/env/common.js b/src/config/env/common.js index 9c2cce9..f4e3a54 100644 --- a/src/config/env/common.js +++ b/src/config/env/common.js @@ -57,6 +57,7 @@ const x402Defaults = { serverAddress: (process.env.SERVER_BASE_ADDRESS || '').trim(), facilitatorKeyId: process.env.FACILITATOR_KEY_ID || '', facilitatorSecretKey: process.env.FACILITATOR_SECRET_KEY || '', + primaryFacilitator: process.env.PRIMARY_FACILITATOR || 'cdp', network: x402Network, priceUSDC, // Optional: USDC token contract (0x…). If unset, Base / Base Sepolia use built-in USDC addresses. diff --git a/src/config/x402.js b/src/config/x402.js index 71a8009..925cded 100644 --- a/src/config/x402.js +++ b/src/config/x402.js @@ -5,9 +5,23 @@ const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources' const DEFAULT_TIMEOUT_SECONDS = 120 /** - * Resolve USDC contract + EIP-712 domain hints for the configured network. - * @param {string} network CAIP-2 + * Facilitator configurations + * Supports multiple facilitators for redundancy and market coverage */ +const FACILITATORS = { + cdp: { + name: 'Coinbase CDP', + url: 'https://api.cdp.coinbase.com/platform/v2/x402', + requiresAuth: true, + authType: 'jwt' + }, + dexter: { + name: 'Dexter', + url: 'https://x402.dexter.cash', + requiresAuth: false, + authType: 'none' + } +} /** x402 `exact` on EVM expects a checksummable 0x address for `payTo`. */ function assertEvmPayTo (payTo) { @@ -22,6 +36,7 @@ function assertEvmPayTo (payTo) { * Builds x402 v2 RoutesConfig for @x402/express (CAIP-2 network + accepts[]). * Payment asset is explicitly USDC (ERC-20) via AssetAmount, not generic "money". * @see https://docs.x402.org/guides/migration-v1-to-v2 + * @param {string} apiPrefix Express API prefix (e.g., "/v6") */ export function buildX402Routes (apiPrefix = '/v6') { const normalizedPrefix = apiPrefix.endsWith('/') @@ -65,7 +80,9 @@ export function getX402Settings () { serverAddress: config.x402?.serverAddress, priceUSDC: config.x402?.priceUSDC, usdcAssetAddress: config.x402?.usdcAssetAddress, - network: config.x402?.network + network: config.x402?.network, + facilitators: FACILITATORS, + primaryFacilitator: config.x402?.primaryFacilitator || 'cdp' } } @@ -194,3 +211,22 @@ export async function createAuthHeader () { } } } + +/** + * Get facilitator configuration by name + * @param {string} name - Facilitator name ('cdp' or 'dexter') + * @returns {Object} Facilitator config + */ +export function getFacilitatorConfig (name = 'cdp') { + return FACILITATORS[name] || FACILITATORS.cdp +} + +/** + * Check if facilitator requires authentication + * @param {string} name - Facilitator name + * @returns {boolean} + */ +export function facilitatorRequiresAuth (name = 'cdp') { + const cfg = FACILITATORS[name] + return cfg ? cfg.requiresAuth : false +}