mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
Add multi-facilitator support with Dexter integration
- Add Dexter as secondary facilitator option (no API keys required) - Create FACILITATORS config with CDP and Dexter settings - Update server middleware to support multiple facilitators - Add PRIMARY_FACILITATOR environment variable - Create example client for Dexter payments - Document Dexter features: 600K+ free tx/month, gas sponsored Dexter has ~50% market share and requires no authentication, making it an ideal backup/secondary facilitator option.
This commit is contained in:
+7
-2
@@ -31,13 +31,18 @@ export X402_PRICE_USDC=0.1
|
|||||||
# Use 'eip155:84532' for Base Sepolia (testnet)
|
# Use 'eip155:84532' for Base Sepolia (testnet)
|
||||||
export x402_NETWORK=eip155:8453
|
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 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
|
export x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402
|
||||||
|
|
||||||
# CDP API Credentials (required when using CDP facilitator)
|
# CDP API Credentials (required when using CDP facilitator)
|
||||||
# Get these from https://cdp.coinbase.com - API Keys section
|
# 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_KEY_ID=your_key_id_here
|
||||||
export FACILITATOR_SECRET_KEY=your_secret_key_here
|
export FACILITATOR_SECRET_KEY=your_secret_key_here
|
||||||
|
|
||||||
|
|||||||
+10
-7
@@ -16,7 +16,7 @@ import { dirname, join } from 'path'
|
|||||||
import config from '../src/config/index.js'
|
import config from '../src/config/index.js'
|
||||||
import Controllers from '../src/controllers/index.js'
|
import Controllers from '../src/controllers/index.js'
|
||||||
import wlogger from '../src/adapters/wlogger.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 { basicAuthMiddleware } from '../src/middleware/basic-auth.js'
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
@@ -109,14 +109,17 @@ class Server {
|
|||||||
const routes = buildX402Routes(this.config.apiPrefix)
|
const routes = buildX402Routes(this.config.apiPrefix)
|
||||||
let facilitatorOptions = x402Settings.facilitatorUrl
|
let facilitatorOptions = x402Settings.facilitatorUrl
|
||||||
|
|
||||||
if (facilitatorOptions) {
|
// Support for multiple facilitators (CDP and Dexter)
|
||||||
facilitatorOptions = {
|
const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp'
|
||||||
url: x402Settings.facilitatorUrl,
|
const facilitatorConfig = getFacilitatorConfig(primaryFacilitator)
|
||||||
createAuthHeaders: createAuthHeader
|
|
||||||
}
|
// 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
|
// Create conditional x402 middleware that bypasses if basic auth is valid
|
||||||
const conditionalX402Middleware = (req, res, next) => {
|
const conditionalX402Middleware = (req, res, next) => {
|
||||||
|
|||||||
@@ -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()
|
||||||
+54
-11
@@ -4,6 +4,25 @@ import { generateJwt } from '@coinbase/cdp-sdk/auth'
|
|||||||
const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources'
|
const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources'
|
||||||
const DEFAULT_TIMEOUT_SECONDS = 120
|
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.
|
* Builds a route configuration map for x402-bch middleware.
|
||||||
*
|
*
|
||||||
@@ -48,11 +67,14 @@ export function buildX402Routes (apiPrefix = '/v6') {
|
|||||||
export function getX402Settings () {
|
export function getX402Settings () {
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(config.x402?.enabled),
|
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,
|
facilitatorKeyId: config.x402?.facilitatorKeyId,
|
||||||
facilitatorSecretKey: config.x402?.facilitatorSecretKey,
|
facilitatorSecretKey: config.x402?.facilitatorSecretKey,
|
||||||
serverAddress: config.x402?.serverAddress,
|
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 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Create auth headers for CDP facilitator
|
||||||
* Auth headers for interact with mainnet coin base facilitator endpoints.
|
* 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 () {
|
export async function createAuthHeader () {
|
||||||
|
// Only CDP requires JWT auth
|
||||||
|
if (!config.x402?.facilitatorKeyId || !config.x402?.facilitatorSecretKey) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// /verify endpoint jwt
|
// /verify endpoint jwt
|
||||||
const verifyToken = await generateJwt({
|
const verifyToken = await generateJwt({
|
||||||
apiKeyId: config.x402?.facilitatorKeyId,
|
apiKeyId: config.x402.facilitatorKeyId,
|
||||||
apiKeySecret: config.x402?.facilitatorSecretKey,
|
apiKeySecret: config.x402.facilitatorSecretKey,
|
||||||
requestMethod: 'POST',
|
requestMethod: 'POST',
|
||||||
requestHost: 'api.cdp.coinbase.com',
|
requestHost: 'api.cdp.coinbase.com',
|
||||||
requestPath: '/platform/v2/x402/verify'
|
requestPath: '/platform/v2/x402/verify'
|
||||||
})
|
})
|
||||||
// /settle endpoint jwt
|
// /settle endpoint jwt
|
||||||
const settleToken = await generateJwt({
|
const settleToken = await generateJwt({
|
||||||
apiKeyId: config.x402?.facilitatorKeyId,
|
apiKeyId: config.x402.facilitatorKeyId,
|
||||||
apiKeySecret: config.x402?.facilitatorSecretKey,
|
apiKeySecret: config.x402.facilitatorSecretKey,
|
||||||
requestMethod: 'POST',
|
requestMethod: 'POST',
|
||||||
requestHost: 'api.cdp.coinbase.com',
|
requestHost: 'api.cdp.coinbase.com',
|
||||||
requestPath: '/platform/v2/x402/settle'
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user