diff --git a/.env-example b/.env-example index 26159ab..f57fe26 100644 --- a/.env-example +++ b/.env-example @@ -31,13 +31,18 @@ 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 +# For Dexter (no API keys): https://dexter.cash/facilitator 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 2c652f5..8050017 100644 --- a/bin/server.js +++ b/bin/server.js @@ -16,7 +16,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' // Load environment variables @@ -109,14 +109,17 @@ class Server { const routes = buildX402Routes(this.config.apiPrefix) let facilitatorOptions = x402Settings.facilitatorUrl - if (facilitatorOptions) { - facilitatorOptions = { - url: x402Settings.facilitatorUrl, - createAuthHeaders: createAuthHeader - } + // Support for multiple facilitators (CDP and Dexter) + const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp' + const facilitatorConfig = getFacilitatorConfig(primaryFacilitator) + + // Use configured facilitator URL + facilitatorOptions = { + url: facilitatorConfig.url, + createAuthHeaders: facilitatorConfig.requiresAuth ? createAuthHeader : null } - wlogger.info(`x402 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceUSDC} satoshis per request (unless basic auth provided)`) + wlogger.info(`x402 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceUSDC} USDC per request (unless basic auth provided) [facilitator: ${facilitatorConfig.name}]`) // Create conditional x402 middleware that bypasses if basic auth is valid const conditionalX402Middleware = (req, res, next) => { diff --git a/examples/02-x402-dexter-client.js b/examples/02-x402-dexter-client.js new file mode 100644 index 0000000..8776102 --- /dev/null +++ b/examples/02-x402-dexter-client.js @@ -0,0 +1,74 @@ +import axios from 'axios' +import { withPaymentInterceptor } from 'x402-axios' +import { privateKeyToAccount } from 'viem/accounts' +import { base } from 'viem/chains' +// import { baseSepolia } from 'viem/chains' // Testnet + +import { createWalletClient, http } from 'viem' + +// const baseURL = 'http://localhost:5942' // Local +const baseURL = 'https://x402.fullstack.cash' // Production + +const endpointPath = '/v6/full-node/blockchain/getBlockchainInfo' +const pKey = process.env.PRIVATE_KEY || '' + +// PRIVATE_KEY=0x... node examples/02-x402-dexter-client.js +if (!pKey) throw new Error('PRIVATE_KEY env required!') + +console.log('Using Dexter facilitator - no API keys required!') + +// Create a wallet client +const account = privateKeyToAccount(pKey) +const client = createWalletClient({ + account, + transport: http(), + chain: base +}) + +// Configure axios to use Dexter facilitator +// Dexter uses the same x402 protocol but with no API key requirement +const api = withPaymentInterceptor( + axios.create({ + baseURL, + headers: { + 'Content-Type': 'application/json', + 'X-Facilitator': 'dexter' // Optional hint for server + } + }), + client, + { + // Dexter facilitator URL (for client-side settlement) + facilitatorUrl: 'https://dexter.cash/facilitator' + } +) + +const request = async () => { + console.log('\n\nStep 1: Making first call, expecting a 402 error returned.') + try { + const response = await axios.get(baseURL + endpointPath) + console.log(response.data) + console.log('Step 1 failed. Expected a 402 error.') + } catch (err) { + console.log(`Status code: ${err?.response?.status}`) + console.log(`Error data: ${JSON.stringify(err.response.data, null, 2)}`) + console.log('\n\n') + } + + console.log('\n\nStep 2: Making second call with Dexter payment.') + console.log('Dexter facilitator: https://dexter.cash/facilitator') + console.log('Features: No API keys, gas sponsored, 600K+ free tx/month') + + try { + // Call the same endpoint path with a payment via Dexter + const paidRes = await api.get(endpointPath) + console.log('Data returned after Dexter payment: ', paidRes.data) + } catch (err) { + console.log('Step 2 failed. Expected a 200 success status code.') + console.log(`Status code: ${err?.response?.status}`) + console.log(`Error data: ${JSON.stringify(err.response.data, null, 2)}`) + + process.exit(1) + } +} + +request() diff --git a/src/config/x402.js b/src/config/x402.js index cb8215b..7f78b3c 100644 --- a/src/config/x402.js +++ b/src/config/x402.js @@ -4,6 +4,25 @@ import { generateJwt } from '@coinbase/cdp-sdk/auth' const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources' const DEFAULT_TIMEOUT_SECONDS = 120 +/** + * 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://dexter.cash/facilitator', + requiresAuth: false, + authType: 'none' + } +} + /** * Builds a route configuration map for x402-bch middleware. * @@ -48,11 +67,14 @@ export function buildX402Routes (apiPrefix = '/v6') { export function getX402Settings () { return { enabled: Boolean(config.x402?.enabled), - facilitatorUrl: config.x402?.facilitatorUrl, // https://docs.cdp.coinbase.com/x402/quickstart-for-sellers + facilitatorUrl: config.x402?.facilitatorUrl, facilitatorKeyId: config.x402?.facilitatorKeyId, facilitatorSecretKey: config.x402?.facilitatorSecretKey, serverAddress: config.x402?.serverAddress, - priceUSDC: config.x402?.priceUSDC + priceUSDC: config.x402?.priceUSDC, + // Support for multiple facilitators + facilitators: FACILITATORS, + primaryFacilitator: config.x402?.primaryFacilitator || 'cdp' } } @@ -64,25 +86,27 @@ export function getBasicAuthSettings () { } /** - * - * Auth headers for interact with mainnet coin base facilitator endpoints. - * + * Create auth headers for CDP facilitator + * Dexter doesn't require auth */ -// https://docs.cdp.coinbase.com/get-started/authentication/jwt-authentication#javascript-2 -// https://docs.cdp.coinbase.com/api-reference/v2/rest-api/x402-facilitator/verify-a-payment export async function createAuthHeader () { + // Only CDP requires JWT auth + if (!config.x402?.facilitatorKeyId || !config.x402?.facilitatorSecretKey) { + return null + } + // /verify endpoint jwt const verifyToken = await generateJwt({ - apiKeyId: config.x402?.facilitatorKeyId, - apiKeySecret: config.x402?.facilitatorSecretKey, + apiKeyId: config.x402.facilitatorKeyId, + apiKeySecret: config.x402.facilitatorSecretKey, requestMethod: 'POST', requestHost: 'api.cdp.coinbase.com', requestPath: '/platform/v2/x402/verify' }) // /settle endpoint jwt const settleToken = await generateJwt({ - apiKeyId: config.x402?.facilitatorKeyId, - apiKeySecret: config.x402?.facilitatorSecretKey, + apiKeyId: config.x402.facilitatorKeyId, + apiKeySecret: config.x402.facilitatorSecretKey, requestMethod: 'POST', requestHost: 'api.cdp.coinbase.com', requestPath: '/platform/v2/x402/settle' @@ -97,3 +121,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 config = FACILITATORS[name] + return config ? config.requiresAuth : false +}