Resolving merge conflicts

This commit is contained in:
Daniel Gonzalez
2026-04-08 13:46:57 -04:00
8 changed files with 319 additions and 4847 deletions
+2 -1
View File
@@ -41,7 +41,8 @@ export PRIMARY_FACILITATOR=cdp
# Facilitator URLs
# For Coinbase CDP (recommended): https://api.cdp.coinbase.com/platform/v2/x402
# For Dexter (no API keys):https://x402.dexter.cash
export x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402
# 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
+43 -32
View File
@@ -7,7 +7,9 @@
import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import { paymentMiddleware as x402PaymentMiddleware } from 'x402-express'
import { paymentMiddleware as x402PaymentMiddleware } from '@x402/express'
import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server'
import { registerExactEvmScheme } from '@x402/evm/exact/server'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
@@ -74,7 +76,22 @@ class Server {
origin: true, // Allow all origins (more reliable than '*')
credentials: false, // Set to true if you need to support credentials
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'PAYMENT-SIGNATURE',
'PAYMENT-REQUIRED',
'PAYMENT-RESPONSE',
'X-PAYMENT',
'X-PAYMENT-RESPONSE'
],
exposedHeaders: [
'PAYMENT-REQUIRED',
'PAYMENT-RESPONSE',
'PAYMENT-SIGNATURE',
'X-PAYMENT-RESPONSE'
]
}))
// URL normalization middleware - collapse multiple slashes
@@ -108,57 +125,51 @@ class Server {
if (x402Settings.enabled && basicAuthSettings.enabled) {
// X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally
const routes = buildX402Routes(this.config.apiPrefix)
let facilitatorOptions = x402Settings.facilitatorUrl
// Support for multiple facilitators (CDP and Dexter)
const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp'
const facilitatorConfig = getFacilitatorConfig(primaryFacilitator)
// Use configured facilitator URL
facilitatorOptions = {
const facilitatorOptions = {
url: facilitatorConfig.url,
createAuthHeaders: facilitatorConfig.requiresAuth ? createAuthHeader : null
}
wlogger.info(`x402 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) [facilitator: ${facilitatorConfig.name}]`)
const facilitatorClient = new HTTPFacilitatorClient(facilitatorOptions)
// x402 v2 exports use lowercase class names (x402ResourceServer).
// eslint-disable-next-line new-cap
const resourceServer = new x402ResourceServer(facilitatorClient)
registerExactEvmScheme(resourceServer, {})
const x402Mw = x402PaymentMiddleware(routes, resourceServer)
// Create conditional x402 middleware that bypasses if basic auth is valid
const conditionalX402Middleware = (req, res, next) => {
// req.headers['accept'] = 'application/json';
// If basic auth is valid, bypass x402
if (req.locals?.basicAuthValid === true) {
return next()
}
// Otherwise, apply x402 middleware
return x402PaymentMiddleware(
x402Settings.serverAddress,
routes,
facilitatorOptions
)(req, res, next)
return x402Mw(req, res, next)
}
app.use(conditionalX402Middleware)
// Display facilitator running url
console.log(`Facilitator running on: ${facilitatorOptions.url}`)
} else if (x402Settings.enabled && !basicAuthSettings.enabled) {
// X402_ENABLED=true AND USE_BASIC_AUTH=false: Apply x402 unconditionally (no basic auth bypass)
const routes = buildX402Routes(this.config.apiPrefix)
let facilitatorOptions = x402Settings.facilitatorUrl
if (facilitatorOptions) {
facilitatorOptions = {
url: x402Settings.facilitatorUrl,
createAuthHeaders: createAuthHeader
}
const primaryFacilitator = x402Settings.primaryFacilitator || 'cdp'
const facilitatorConfig = getFacilitatorConfig(primaryFacilitator)
const facilitatorOptions = {
url: facilitatorConfig.url,
createAuthHeaders: facilitatorConfig.requiresAuth ? createAuthHeader : null
}
wlogger.info(`x402 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceUSDC} satoshis per request`)
wlogger.info(`x402 v2 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceUSDC} USDC per request [facilitator: ${facilitatorConfig.name}]`)
// Apply x402 middleware unconditionally - no basic auth bypass
app.use(x402PaymentMiddleware(
x402Settings.serverAddress,
routes,
facilitatorOptions
))
const facilitatorClient = new HTTPFacilitatorClient(facilitatorOptions)
// eslint-disable-next-line new-cap
const resourceServer = new x402ResourceServer(facilitatorClient)
registerExactEvmScheme(resourceServer, {})
app.use(x402PaymentMiddleware(routes, resourceServer))
} else if (basicAuthSettings.enabled && !x402Settings.enabled) {
// USE_BASIC_AUTH=true AND X402_ENABLED=false: Require basic auth, reject unauthenticated requests
wlogger.info('Basic auth enforcement enabled (x402 disabled)')
+48 -21
View File
@@ -1,38 +1,58 @@
import axios from 'axios'
import { withPaymentInterceptor } from 'x402-axios'
/**
* x402 v2 axios example — USDC on Base (CAIP-2). Run from repo root:
* `node examples/01-x402-axios-client.js`
* The x402 stack pulls in `siwe`, which expects `ethers` as a peer — it is listed in package.json.
*
* Uses `toClientEvmSigner(account, publicClient)` so the signer has `readContract` (required by
* @x402/evm ClientEvmSigner). Optional: `BASE_RPC_URL` for Base / Base Sepolia HTTP RPC.
*/
import axios, { AxiosHeaders } from 'axios'
import { createPublicClient, http } from 'viem'
import { base, baseSepolia } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { base } from 'viem/chains'
// import { baseSepolia } from 'viem/chains' // Testnet
import { x402Client, wrapAxiosWithPayment } from '@x402/axios'
import { registerExactEvmScheme } from '@x402/evm/exact/client'
import { toClientEvmSigner } from '@x402/evm'
import { createWalletClient, http } from 'viem'
// const baseURL = 'http://localhost:5942' // Local
const baseURL = 'https://x402.fullstack.cash' // Production
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 || ''
const pKey = process.env.PRIVATE_KEY || process.env.EVM_PRIVATE_KEY || ''
const x402Network = 'eip155:8453' // sepolia eip155:84532
if (!pKey) throw new Error('PRIVATE_KEY or EVM_PRIVATE_KEY env required!')
// PRIVATE_KEY=0x... node examples/08-x402-axios-client.js
if (!pKey) throw new Error('PRIVATE_KEY env required!')
console.log('pKey', pKey)
// Create a wallet client
const account = privateKeyToAccount(pKey)
const client = createWalletClient({
account,
transport: http(),
chain: base
const chain = x402Network === 'eip155:84532' ? baseSepolia : base
const defaultRpc =
x402Network === 'eip155:84532'
? 'https://sepolia.base.org'
: 'https://mainnet.base.org'
const publicClient = createPublicClient({
chain,
transport: http(process.env.BASE_RPC_URL || defaultRpc)
})
const signer = toClientEvmSigner(account, publicClient)
// eslint-disable-next-line new-cap
const client = new x402Client()
registerExactEvmScheme(client, {
signer,
networks: [x402Network]
})
const api = withPaymentInterceptor(
const api = wrapAxiosWithPayment(
axios.create({
baseURL,
headers: {
headers: new AxiosHeaders({
'Content-Type': 'application/json'
}
})
}),
client
)
const request = async () => {
console.log('\n\nStep 1: Making first call, expecting a 402 error returned.')
try {
@@ -42,6 +62,10 @@ const request = async () => {
} catch (err) {
console.log(`Status code: ${err?.response?.status}`)
console.log(`Error data: ${JSON.stringify(err.response.data, null, 2)}`)
console.log(
`Error StatusText: ${JSON.stringify(err.response.statusText, null, 2)}`
)
console.log('\n\n')
}
@@ -54,6 +78,9 @@ const request = async () => {
} catch (err) {
console.log('Step 2 failed. Expected a 200 success status code.')
console.log(`Status code: ${err?.response?.status}`)
console.log(
`Error StatusText: ${JSON.stringify(err.response.statusText, null, 2)}`
)
console.log(`Error data: ${JSON.stringify(err.response.data, null, 2)}`)
process.exit(1)
-74
View File
@@ -1,74 +0,0 @@
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()
+73 -4671
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -19,20 +19,20 @@
"dependencies": {
"@coinbase/cdp-sdk": "1.45.0",
"@psf/bch-js": "7.1.14",
"@x402/axios": "2.6.0",
"@x402/core": "2.6.0",
"@x402/evm": "2.6.0",
"@x402/express": "2.6.0",
"axios": "1.7.7",
"cors": "2.8.5",
"dotenv": "16.3.1",
"ethers": "6.13.5",
"express": "5.1.0",
"minimal-slp-wallet": "7.1.5",
"psffpp": "1.2.1",
"slp-token-media": "1.2.10",
"winston": "3.11.0",
"winston-daily-rotate-file": "4.7.1",
"x402-axios": "1.1.0",
"x402-express": "1.1.0",
"x402-fetch": "1.1.0"
"winston-daily-rotate-file": "4.7.1"
},
"devDependencies": {
"apidoc": "1.2.0",
+23 -8
View File
@@ -26,27 +26,42 @@ const normalizeBoolean = (value, defaultValue) => {
return defaultValue
}
// By default, the price per API call is 200 satoshis.
// But the user can override this value by setting the X402_PRICE_SAT environment variable.
// Default price per API call in USDC (x402 on Base). Override with X402_PRICE_USDC.
const parsedPrice = Number(process.env.X402_PRICE_USDC)
const priceUSDC = Number.isFinite(parsedPrice) && parsedPrice > 0 ? parsedPrice : 200
// Network configuration: CDP requires CAIP-2 format (eip155:chainId)
// base-sepolia = eip155:84532, base-mainnet = eip155:8453
const x402Network = process.env.x402_NETWORK || 'eip155:8453'
// x402 v2 uses CAIP-2 network ids (see https://docs.x402.org/guides/migration-v1-to-v2).
// Accept legacy short names from env for convenience. Also accept X402_NETWORK or x402_NETWORK.
function toV2Caip2Network (raw) {
const s = String(raw ?? '').trim()
if (!s) return 'eip155:8453'
const lower = s.toLowerCase()
if (lower === 'base' || lower === 'eip155:8453') return 'eip155:8453'
if (lower === 'base-sepolia' || lower === 'eip155:84532') return 'eip155:84532'
if (lower.startsWith('eip155:')) return s
return s
}
const x402NetworkRaw =
process.env.x402_NETWORK ||
process.env.X402_NETWORK ||
'eip155:8453'
const x402Network = toV2Caip2Network(x402NetworkRaw)
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',
serverAddress: process.env.SERVER_BASE_ADDRESS || 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
// 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',
// CDP requires CAIP-2 network format: eip155:8453 (base) or eip155:84532 (base-sepolia)
network: x402Network,
priceUSDC
priceUSDC,
// Optional: USDC token contract (0x…). If unset, Base / Base Sepolia use built-in USDC addresses.
usdcAssetAddress: (process.env.X402_USDC_ASSET || '').trim()
}
const basicAuthDefaults = {
+124 -34
View File
@@ -23,11 +23,20 @@ const FACILITATORS = {
}
}
/** x402 `exact` on EVM expects a checksummable 0x address for `payTo`. */
function assertEvmPayTo (payTo) {
if (typeof payTo !== 'string' || !/^0x[a-fA-F0-9]{40}$/.test(payTo)) {
throw new Error(
'SERVER_BASE_ADDRESS must be a 0x-prefixed 40-hex EVM address (Base USDC settlement).'
)
}
}
/**
* Builds a route configuration map for x402-bch middleware.
*
* 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")
* @returns {Object} Routes configuration compatible with x402-bch-express
*/
export function buildX402Routes (apiPrefix = '/v6') {
const normalizedPrefix = apiPrefix.endsWith('/')
@@ -37,29 +46,27 @@ export function buildX402Routes (apiPrefix = '/v6') {
? normalizedPrefix
: `/${normalizedPrefix}`
const routeKey = `${prefixWithSlash}/*`
const routeKey = `* ${prefixWithSlash}/*`
const network = config.x402.network
if (!network) throw new Error('x402 network is required (set x402_NETWORK / X402_NETWORK).')
// Get network from config (now supports CAIP-2 format: eip155:8453)
const NETWORK = config.x402.network
if (!NETWORK) throw new Error('x402_NETWORK env required!')
const payTo = config.x402.serverAddress
if (!payTo) throw new Error('SERVER_BASE_ADDRESS is required for x402 v2 payTo.')
assertEvmPayTo(payTo)
return {
network: NETWORK,
[routeKey]: {
accepts: [
{
scheme: 'exact',
payTo,
price: config.x402.priceUSDC,
network: NETWORK,
config: {
network,
maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS
}
],
description: `${DEFAULT_DESCRIPTION} (${config.x402.priceUSDC} USDC)`,
maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
mimeType: 'application/json',
outputSchema: {
input: {
type: 'http',
method: 'GET',
discoverable: true
}
}
}
mimeType: 'application/json'
}
}
}
@@ -72,12 +79,82 @@ export function getX402Settings () {
facilitatorSecretKey: config.x402?.facilitatorSecretKey,
serverAddress: config.x402?.serverAddress,
priceUSDC: config.x402?.priceUSDC,
// Support for multiple facilitators
usdcAssetAddress: config.x402?.usdcAssetAddress,
network: config.x402?.network,
facilitators: FACILITATORS,
primaryFacilitator: config.x402?.primaryFacilitator || 'cdp'
}
}
/**
* JSON for `GET /.well-known/x402` — matches `buildX402Routes` (Base USDC, exact scheme).
* @param {string} [apiPrefix]
*/
export function getX402WellKnownManifest (apiPrefix = '/v6') {
const normalizedPrefix = apiPrefix.endsWith('/')
? apiPrefix.slice(0, -1)
: apiPrefix
const prefixWithSlash = normalizedPrefix.startsWith('/')
? normalizedPrefix
: `/${normalizedPrefix}`
const network = config.x402.network
if (!network) throw new Error('x402 network is required (set x402_NETWORK / X402_NETWORK).')
const payTo = config.x402.serverAddress
if (!payTo) throw new Error('SERVER_BASE_ADDRESS is required for x402 v2 payTo.')
assertEvmPayTo(payTo)
return {
x402Version: 2,
network,
facilitator: {
url: config.x402.facilitatorUrl
},
resources: [
{
resource: `${prefixWithSlash}/*`,
type: 'http',
x402Version: 2,
accepts: [
{
scheme: 'exact',
network,
price: config.x402.priceUSDC,
payTo,
maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
description: `${DEFAULT_DESCRIPTION} (${config.x402.priceUSDC} USDC)`,
mimeType: 'application/json',
extra: { }
}
]
}
]
}
}
/**
* Snapshot for `build-documents` agent manifest — Base USDC / exact scheme.
* @returns {object|null} null when x402 is off or payTo/network invalid.
*/
export function getX402AgentAuthPricing () {
const x402 = config.x402
if (!x402?.enabled || !x402.serverAddress || !x402.network) return null
try {
assertEvmPayTo(x402.serverAddress)
} catch {
return null
}
return {
scheme: 'exact',
x402Version: 2,
network: x402.network,
payTo: x402.serverAddress,
priceUSDC: x402.priceUSDC,
facilitatorUrl: x402.facilitatorUrl
}
}
export function getBasicAuthSettings () {
return {
enabled: Boolean(config.basicAuth?.enabled),
@@ -86,31 +163,41 @@ export function getBasicAuthSettings () {
}
/**
* Create auth headers for CDP facilitator
* Dexter doesn't require auth
* CDP JWT auth for facilitator HTTPFacilitatorClient (verify / settle / supported).
* @see https://docs.cdp.coinbase.com/get-started/authentication/jwt-authentication#javascript-2
*/
export async function createAuthHeader () {
// Only CDP requires JWT auth
if (!config.x402?.facilitatorKeyId || !config.x402?.facilitatorSecretKey) {
return null
const id = config.x402?.facilitatorKeyId
const secret = config.x402?.facilitatorSecretKey
if (!id || !secret) {
return {
verify: {},
settle: {},
supported: {}
}
}
// /verify endpoint jwt
const verifyToken = await generateJwt({
apiKeyId: config.x402.facilitatorKeyId,
apiKeySecret: config.x402.facilitatorSecretKey,
apiKeyId: id,
apiKeySecret: secret,
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: id,
apiKeySecret: secret,
requestMethod: 'POST',
requestHost: 'api.cdp.coinbase.com',
requestPath: '/platform/v2/x402/settle'
})
const supportedToken = await generateJwt({
apiKeyId: id,
apiKeySecret: secret,
requestMethod: 'GET',
requestHost: 'api.cdp.coinbase.com',
requestPath: '/platform/v2/x402/supported'
})
return {
verify: {
@@ -118,6 +205,9 @@ export async function createAuthHeader () {
},
settle: {
Authorization: `Bearer ${settleToken}`
},
supported: {
Authorization: `Bearer ${supportedToken}`
}
}
}
@@ -137,6 +227,6 @@ export function getFacilitatorConfig (name = 'cdp') {
* @returns {boolean}
*/
export function facilitatorRequiresAuth (name = 'cdp') {
const config = FACILITATORS[name]
return config ? config.requiresAuth : false
const cfg = FACILITATORS[name]
return cfg ? cfg.requiresAuth : false
}