diff --git a/.env-example b/.env-example index d2588c8..0ef652e 100644 --- a/.env-example +++ b/.env-example @@ -34,16 +34,24 @@ export X402_PRICE_USDC=0.1 # Use 'eip155:84532' for Base Sepolia (testnet) export x402_NETWORK=eip155:8453 -# Primary Facilitator -# Options: 'cdp' (Coinbase) or 'dexter' (Dexter.cash - no API keys required) -export PRIMARY_FACILITATOR=cdp +# Primary Facilitator (used when ACTIVE_FACILITATORS is unset — single-facilitator mode) +# Options: 'cdp' (Coinbase), 'dexter' (Dexter.cash), or 'payai' (PayAI) +export PRIMARY_FACILITATOR=dexter -# Facilitator URLs -# For Coinbase CDP (recommended): 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 +# Multi-facilitator (x402 v2): comma-separated *additional* facilitators; PRIMARY_FACILITATOR is always first (precedence). +# x402_FACILITATOR_URL applies only in single-facilitator mode. +# export ACTIVE_FACILITATORS=dexter,payai + +# Bazaar discovery extension on 402 payment requirements (default on). Set false to omit. +# export X402_BAZAAR_ENABLED=true + +# Facilitator URL override (optional). If set, this URL is used regardless of PRIMARY_FACILITATOR. +# Otherwise the URL is chosen from PRIMARY_FACILITATOR (built-in defaults per provider). # export x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402 +# PayAI only: optional custom facilitator host (default https://facilitator.payai.network) +# export PAYAI_FACILITATOR_URL=https://facilitator.payai.network + # CDP API Credentials (required when using CDP facilitator) # Get these from https://cdp.coinbase.com - API Keys section # Not required for Dexter facilitator diff --git a/README.md b/README.md index b207cb8..6f9c574 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ The API surface remains focused on BCH infrastructure (BCH full node, Fulcrum, a - Pricing is configured in `X402_PRICE_USDC` (USDC amount), not BCH satoshis. - x402 network is configured using CAIP-2 format (for example `eip155:8453` for Base mainnet and `eip155:84532` for Base Sepolia). - Facilitator auth headers are generated using Coinbase CDP JWT auth (`FACILITATOR_KEY_ID` and `FACILITATOR_SECRET_KEY`) when using CDP endpoints. +- Optional facilitators: **Dexter** (`PRIMARY_FACILITATOR=dexter`) and **PayAI** (`PRIMARY_FACILITATOR=payai`, default URL `https://facilitator.payai.network`, no API keys). +- **Bazaar** discovery metadata on payment requirements via `@x402/extensions/bazaar`, and optional **multi-facilitator** mode (`ACTIVE_FACILITATORS=cdp,dexter,payai`) so clients can verify/settle through CDP, Dexter, or PayAI (`@x402/core` picks a matching facilitator). ## Architecture @@ -85,7 +87,11 @@ All configuration is loaded from environment variables (typically from `.env`). - `SERVER_BASE_ADDRESS` (EVM address receiving x402 settlements) - `X402_PRICE_USDC` (USDC charged per request) - `x402_NETWORK` (CAIP-2 chain ID; e.g. `eip155:8453` or `eip155:84532`) -- `x402_FACILITATOR_URL` (default: `https://api.cdp.coinbase.com/platform/v2/x402`) +- `PRIMARY_FACILITATOR` (default: `cdp`) — one of `cdp`, `dexter`, or `payai`. Used for **single-facilitator** mode when `ACTIVE_FACILITATORS` is unset; selects the default facilitator base URL unless `x402_FACILITATOR_URL` is set. +- `ACTIVE_FACILITATORS` — optional comma-separated list (e.g. `dexter,payai`). When set with multi-facilitator mode, the server registers **multiple** `HTTPFacilitatorClient` instances. **`PRIMARY_FACILITATOR` is always first** in that list (x402 gives earlier facilitators precedence); remaining names follow in the order listed, deduped. Each facilitator uses its default base URL (`x402_FACILITATOR_URL` applies only in **single-facilitator** mode). +- `X402_BAZAAR_ENABLED` (default: `true`) — attach Bazaar **discovery** extension to protected route payment requirements (for facilitator catalogs). Set `false` to disable. +- `x402_FACILITATOR_URL` — optional override for the facilitator HTTP base URL (must expose `/verify`, `/settle`, and `/supported` like the x402 reference facilitator). When unset, the URL is derived from `PRIMARY_FACILITATOR`. **Ignored per-facilitator when `ACTIVE_FACILITATORS` lists more than one entry** (each entry uses its provider URL). +- `PAYAI_FACILITATOR_URL` — optional; when `PRIMARY_FACILITATOR=payai` and `x402_FACILITATOR_URL` is unset, defaults to `https://facilitator.payai.network`. - `FACILITATOR_KEY_ID` (required for CDP facilitator auth) - `FACILITATOR_SECRET_KEY` (required for CDP facilitator auth) @@ -123,17 +129,30 @@ Authorization: Bearer my-secret-token ### 3) x402 Payments on Base (USDC) +**Coinbase CDP facilitator** + ```bash X402_ENABLED=true USE_BASIC_AUTH=false SERVER_BASE_ADDRESS=0xYourBaseAddress X402_PRICE_USDC=0.1 x402_NETWORK=eip155:8453 -x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402 +PRIMARY_FACILITATOR=cdp FACILITATOR_KEY_ID=your_key_id FACILITATOR_SECRET_KEY=your_secret ``` +**PayAI facilitator** (no CDP keys; default URL `https://facilitator.payai.network`) + +```bash +X402_ENABLED=true +USE_BASIC_AUTH=false +SERVER_BASE_ADDRESS=0xYourBaseAddress +X402_PRICE_USDC=0.1 +x402_NETWORK=eip155:8453 +PRIMARY_FACILITATOR=payai +``` + Protected endpoints return `402 Payment Required` when no valid payment is attached. x402-capable clients can pay and retry automatically. ### 4) x402 + Bearer Bypass diff --git a/bin/server.js b/bin/server.js index d5f0d2a..8e06691 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, getFacilitatorConfig } from '../src/config/x402.js' +import { buildX402Routes, getX402Settings, getBasicAuthSettings, createAuthHeader, getFacilitatorConnectionOptions } from '../src/config/x402.js' import { basicAuthMiddleware } from '../src/middleware/basic-auth.js' import DiscoveryRouter from '../src/controllers/discovery/router.js' @@ -126,19 +126,17 @@ class Server { // X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally const routes = buildX402Routes(this.config.apiPrefix) - const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp' - const facilitatorConfig = getFacilitatorConfig(primaryFacilitator) - const facilitatorOptions = { - url: facilitatorConfig.url, - createAuthHeaders: facilitatorConfig.requiresAuth ? createAuthHeader : null - } + const connectionOpts = getFacilitatorConnectionOptions() + const facilitatorClients = connectionOpts.map(o => new HTTPFacilitatorClient({ + url: o.url, + createAuthHeaders: o.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}]`) + wlogger.info(`x402 v2 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceUSDC} USDC per request (unless basic auth provided) [facilitators: ${connectionOpts.map(o => o.name).join(', ')}]`) - const facilitatorClient = new HTTPFacilitatorClient(facilitatorOptions) // x402 v2 exports use lowercase class names (x402ResourceServer). // eslint-disable-next-line new-cap - const resourceServer = new x402ResourceServer(facilitatorClient) + const resourceServer = new x402ResourceServer(facilitatorClients) registerExactEvmScheme(resourceServer, {}) const x402Mw = x402PaymentMiddleware(routes, resourceServer) @@ -152,22 +150,19 @@ class Server { app.use(conditionalX402Middleware) - // Display facilitator running url - console.log(`Facilitator running on: ${facilitatorOptions.url}`) + console.log(`Facilitator(s): ${connectionOpts.map(o => `${o.name} → ${o.url}`).join(' | ')}`) } else if (x402Settings.enabled && !basicAuthSettings.enabled) { const routes = buildX402Routes(this.config.apiPrefix) - const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp' - const facilitatorConfig = getFacilitatorConfig(primaryFacilitator) - const facilitatorOptions = { - url: facilitatorConfig.url, - createAuthHeaders: facilitatorConfig.requiresAuth ? createAuthHeader : null - } + const connectionOpts = getFacilitatorConnectionOptions() + const facilitatorClients = connectionOpts.map(o => new HTTPFacilitatorClient({ + url: o.url, + createAuthHeaders: o.requiresAuth ? createAuthHeader : null + })) - wlogger.info(`x402 v2 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceUSDC} USDC per request [facilitator: ${facilitatorConfig.name}]`) + wlogger.info(`x402 v2 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceUSDC} USDC per request [facilitators: ${connectionOpts.map(o => o.name).join(', ')}]`) - const facilitatorClient = new HTTPFacilitatorClient(facilitatorOptions) // eslint-disable-next-line new-cap - const resourceServer = new x402ResourceServer(facilitatorClient) + const resourceServer = new x402ResourceServer(facilitatorClients) registerExactEvmScheme(resourceServer, {}) app.use(x402PaymentMiddleware(routes, resourceServer)) } else if (basicAuthSettings.enabled && !x402Settings.enabled) { diff --git a/package-lock.json b/package-lock.json index f41adb7..b5d589a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@x402/core": "2.6.0", "@x402/evm": "2.6.0", "@x402/express": "2.6.0", + "@x402/extensions": "2.6.0", "axios": "1.7.7", "cors": "2.8.5", "dotenv": "16.3.1", diff --git a/package.json b/package.json index 82ff747..d579286 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@x402/core": "2.6.0", "@x402/evm": "2.6.0", "@x402/express": "2.6.0", + "@x402/extensions": "2.6.0", "axios": "1.7.7", "cors": "2.8.5", "dotenv": "16.3.1", diff --git a/production/docker/.env-example b/production/docker/.env-example index 8dc176c..0ebdfba 100644 --- a/production/docker/.env-example +++ b/production/docker/.env-example @@ -30,9 +30,23 @@ export X402_ENABLED=true export SERVER_BASE_ADDRESS=0xd32585CE60815654C50CAf350e18de8096061e63 export X402_PRICE_USDC=0.1 export x402_NETWORK=base +export ACTIVE_FACILITATORS=dexter,payai +export X402_BAZAAR_ENABLED=true +export PRIMARY_FACILITATOR=cdp -# Coinbase CDP Facilitator (for Bazaar registration) -export x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402 +# Facilitator: cdp (default) | dexter | payai. Default base URL follows PRIMARY_FACILITATOR unless x402_FACILITATOR_URL is set. +# export PRIMARY_FACILITATOR=payai +# Multi-facilitator (CDP + Dexter + PayAI): unset x402_FACILITATOR_URL or use single-facilitator mode only. +# export ACTIVE_FACILITATORS=cdp,dexter,payai +# Bazaar discovery on 402 responses (default on) +# export X402_BAZAAR_ENABLED=true +# Optional PayAI host override (default https://facilitator.payai.network) +# export PAYAI_FACILITATOR_URL=https://facilitator.payai.network + +# Optional: force a specific facilitator base URL (overrides PRIMARY_FACILITATOR URL resolution) +# export x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402 + +# CDP JWT credentials (required only when using Coinbase CDP) export FACILITATOR_KEY_ID=your_key_id_here export FACILITATOR_SECRET_KEY=your_secret_key_here diff --git a/src/config/env/common.js b/src/config/env/common.js index f4e3a54..b015aaf 100644 --- a/src/config/env/common.js +++ b/src/config/env/common.js @@ -48,16 +48,36 @@ const x402NetworkRaw = 'eip155:8453' const x402Network = toV2Caip2Network(x402NetworkRaw) +const primaryFacilitatorRaw = (process.env.PRIMARY_FACILITATOR || 'cdp').trim().toLowerCase() +const x402PrimaryKeys = new Set(['cdp', 'dexter', 'payai']) +const primaryFacilitator = x402PrimaryKeys.has(primaryFacilitatorRaw) + ? primaryFacilitatorRaw + : 'cdp' + +function resolveX402FacilitatorUrl () { + const explicit = (process.env.x402_FACILITATOR_URL || '').trim() + if (explicit) return explicit.replace(/\/$/, '') + + if (primaryFacilitator === 'dexter') return 'https://x402.dexter.cash' + if (primaryFacilitator === 'payai') { + return (process.env.PAYAI_FACILITATOR_URL || 'https://facilitator.payai.network') + .trim() + .replace(/\/$/, '') + } + return 'https://api.cdp.coinbase.com/platform/v2/x402' +} + const x402Defaults = { enabled: normalizeBoolean(process.env.X402_ENABLED, true), - // CDP Facilitator: https://api.cdp.coinbase.com/platform/v2/x402 - // Custom/Local Facilitator: http://localhost:4022 - facilitatorUrl: process.env.x402_FACILITATOR_URL || 'https://api.cdp.coinbase.com/platform/v2/x402', + // Bazaar discovery extension on payment requirements (for facilitator catalogs). @x402/express loads @x402/extensions/bazaar when enabled. + bazaarEnabled: normalizeBoolean(process.env.X402_BAZAAR_ENABLED, true), + // Resolved from PRIMARY_FACILITATOR unless x402_FACILITATOR_URL is set (see resolveX402FacilitatorUrl). + facilitatorUrl: resolveX402FacilitatorUrl(), // EVM 0x… address for USDC settlement; required for x402 exact scheme (no legacy BCH default). 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', + primaryFacilitator, 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 925cded..612a0a3 100644 --- a/src/config/x402.js +++ b/src/config/x402.js @@ -1,5 +1,6 @@ import config from './index.js' import { generateJwt } from '@coinbase/cdp-sdk/auth' +import { declareDiscoveryExtension } from '@x402/extensions/bazaar' const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources' const DEFAULT_TIMEOUT_SECONDS = 120 @@ -20,6 +21,12 @@ const FACILITATORS = { url: 'https://x402.dexter.cash', requiresAuth: false, authType: 'none' + }, + payai: { + name: 'PayAI', + url: 'https://facilitator.payai.network', + requiresAuth: false, + authType: 'none' } } @@ -54,26 +61,114 @@ export function buildX402Routes (apiPrefix = '/v6') { if (!payTo) throw new Error('SERVER_BASE_ADDRESS is required for x402 v2 payTo.') assertEvmPayTo(payTo) - return { - [routeKey]: { - accepts: [ - { - scheme: 'exact', - payTo, - price: config.x402.priceUSDC, - network, - maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS - } - ], - description: `${DEFAULT_DESCRIPTION} (${config.x402.priceUSDC} USDC)`, - mimeType: 'application/json' - } + const entry = { + accepts: [ + { + scheme: 'exact', + payTo, + price: config.x402.priceUSDC, + network, + maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS + } + ], + description: `${DEFAULT_DESCRIPTION} (${config.x402.priceUSDC} USDC)`, + mimeType: 'application/json' } + + if (config.x402.bazaarEnabled) { + entry.extensions = declareDiscoveryExtension({ + output: { + example: { + service: 'psf-bch-api', + apiPrefix: prefixWithSlash, + description: 'Bitcoin Cash full node, Fulcrum, and SLP indexer REST proxy (x402 USDC on Base)' + } + } + }) + } + + return { + [routeKey]: entry + } +} + +/** + * Default facilitator order when ACTIVE_FACILITATORS lists CDP, Dexter, and PayAI (Bazaar-friendly multi-facilitator). + */ +export const DEFAULT_MULTI_FACILITATORS = ['cdp', 'dexter', 'payai'] + +/** + * Per-facilitator HTTP base URL (no trailing slash). Used when multiple facilitators are active. + * @param {string} key + */ +export function getFacilitatorHttpUrl (key) { + const k = String(key || 'cdp').trim().toLowerCase() + if (k === 'dexter') return 'https://x402.dexter.cash' + if (k === 'payai') { + return (process.env.PAYAI_FACILITATOR_URL || 'https://facilitator.payai.network') + .trim() + .replace(/\/$/, '') + } + return 'https://api.cdp.coinbase.com/platform/v2/x402' +} + +/** + * Resolved primary facilitator key (env PRIMARY_FACILITATOR, then config, then cdp). + * @returns {string} + */ +export function resolvePrimaryFacilitatorKey () { + const primaryRaw = ( + process.env.PRIMARY_FACILITATOR || + config.x402?.primaryFacilitator || + 'cdp' + ).trim().toLowerCase() + return FACILITATORS[primaryRaw] ? primaryRaw : 'cdp' +} + +/** + * Active facilitator keys: PRIMARY_FACILITATOR is always first (x402 core gives earlier + * facilitator clients precedence). Remaining keys follow ACTIVE_FACILITATORS order, deduped. + * @returns {string[]} + */ +export function getActiveFacilitatorKeys () { + const primary = resolvePrimaryFacilitatorKey() + const raw = (process.env.ACTIVE_FACILITATORS || '').trim() + if (!raw) { + return [primary] + } + const fromEnv = raw.split(',').map(s => s.trim().toLowerCase()).filter(k => FACILITATORS[k]) + if (fromEnv.length === 0) { + return [primary] + } + const rest = fromEnv.filter(k => k !== primary) + return [primary, ...rest] +} + +/** + * Options for constructing one {@link import('@x402/core/server').HTTPFacilitatorClient} per facilitator. + * When only one facilitator is active, `url` is `config.x402.facilitatorUrl` so `x402_FACILITATOR_URL` still applies. + * @returns {{ key: string, name: string, url: string, requiresAuth: boolean }[]} + */ +export function getFacilitatorConnectionOptions () { + const keys = getActiveFacilitatorKeys() + return keys.map(key => { + const cfg = getFacilitatorConfig(key) + const url = + keys.length === 1 ? config.x402.facilitatorUrl : getFacilitatorHttpUrl(key) + return { + key, + name: cfg.name, + url, + requiresAuth: Boolean(cfg.requiresAuth) + } + }) } export function getX402Settings () { return { enabled: Boolean(config.x402?.enabled), + bazaarEnabled: Boolean(config.x402?.bazaarEnabled), + activeFacilitatorKeys: getActiveFacilitatorKeys(), facilitatorUrl: config.x402?.facilitatorUrl, facilitatorKeyId: config.x402?.facilitatorKeyId, facilitatorSecretKey: config.x402?.facilitatorSecretKey, @@ -214,11 +309,12 @@ export async function createAuthHeader () { /** * Get facilitator configuration by name - * @param {string} name - Facilitator name ('cdp' or 'dexter') + * @param {string} name - Facilitator name (`cdp`, `dexter`, or `payai`) * @returns {Object} Facilitator config */ export function getFacilitatorConfig (name = 'cdp') { - return FACILITATORS[name] || FACILITATORS.cdp + const key = String(name || 'cdp').trim().toLowerCase() + return FACILITATORS[key] || FACILITATORS.cdp } /** @@ -227,6 +323,7 @@ export function getFacilitatorConfig (name = 'cdp') { * @returns {boolean} */ export function facilitatorRequiresAuth (name = 'cdp') { - const cfg = FACILITATORS[name] + const key = String(name || 'cdp').trim().toLowerCase() + const cfg = FACILITATORS[key] return cfg ? cfg.requiresAuth : false } diff --git a/test/unit/config/x402-facilitators-unit.js b/test/unit/config/x402-facilitators-unit.js new file mode 100644 index 0000000..b126f87 --- /dev/null +++ b/test/unit/config/x402-facilitators-unit.js @@ -0,0 +1,30 @@ +/* + Unit tests for x402 facilitator registry (PayAI, Dexter, CDP). +*/ + +import { assert } from 'chai' + +import { + getFacilitatorConfig, + facilitatorRequiresAuth +} from '../../../src/config/x402.js' + +describe('#x402 facilitators', () => { + it('should register PayAI without auth', () => { + assert.equal(facilitatorRequiresAuth('payai'), false) + const cfg = getFacilitatorConfig('payai') + assert.equal(cfg.name, 'PayAI') + assert.equal(cfg.url, 'https://facilitator.payai.network') + assert.equal(cfg.requiresAuth, false) + }) + + it('should normalize facilitator key case', () => { + const cfg = getFacilitatorConfig('PAYAI') + assert.equal(cfg.name, 'PayAI') + }) + + it('should require auth for CDP only', () => { + assert.equal(facilitatorRequiresAuth('cdp'), true) + assert.equal(facilitatorRequiresAuth('dexter'), false) + }) +}) diff --git a/test/unit/config/x402-multifac-bazaar-unit.js b/test/unit/config/x402-multifac-bazaar-unit.js new file mode 100644 index 0000000..153855f --- /dev/null +++ b/test/unit/config/x402-multifac-bazaar-unit.js @@ -0,0 +1,51 @@ +/* + Multi-facilitator and Bazaar route config (env-dependent helpers). +*/ + +import { assert } from 'chai' + +import { + getActiveFacilitatorKeys, + getFacilitatorHttpUrl +} from '../../../src/config/x402.js' + +describe('#x402 multi-facilitator + bazaar helpers', () => { + const saved = {} + + beforeEach(() => { + saved.ACTIVE_FACILITATORS = process.env.ACTIVE_FACILITATORS + saved.PRIMARY_FACILITATOR = process.env.PRIMARY_FACILITATOR + delete process.env.ACTIVE_FACILITATORS + process.env.PRIMARY_FACILITATOR = 'cdp' + }) + + afterEach(() => { + if (saved.ACTIVE_FACILITATORS === undefined) delete process.env.ACTIVE_FACILITATORS + else process.env.ACTIVE_FACILITATORS = saved.ACTIVE_FACILITATORS + if (saved.PRIMARY_FACILITATOR === undefined) delete process.env.PRIMARY_FACILITATOR + else process.env.PRIMARY_FACILITATOR = saved.PRIMARY_FACILITATOR + }) + + it('getActiveFacilitatorKeys puts PRIMARY_FACILITATOR first, then ACTIVE_FACILITATORS', () => { + process.env.PRIMARY_FACILITATOR = 'cdp' + process.env.ACTIVE_FACILITATORS = 'payai,cdp,dexter' + assert.deepEqual(getActiveFacilitatorKeys(), ['cdp', 'payai', 'dexter']) + }) + + it('getActiveFacilitatorKeys prepends primary when not listed in ACTIVE_FACILITATORS', () => { + process.env.PRIMARY_FACILITATOR = 'cdp' + process.env.ACTIVE_FACILITATORS = 'dexter,payai' + assert.deepEqual(getActiveFacilitatorKeys(), ['cdp', 'dexter', 'payai']) + }) + + it('getActiveFacilitatorKeys falls back to PRIMARY_FACILITATOR', () => { + process.env.PRIMARY_FACILITATOR = 'dexter' + assert.deepEqual(getActiveFacilitatorKeys(), ['dexter']) + }) + + it('getFacilitatorHttpUrl returns known bases', () => { + assert.include(getFacilitatorHttpUrl('cdp'), 'cdp.coinbase.com') + assert.equal(getFacilitatorHttpUrl('dexter'), 'https://x402.dexter.cash') + assert.equal(getFacilitatorHttpUrl('payai'), 'https://facilitator.payai.network') + }) +})