fix(AI endpoints): Adding AI friendly endpoints

This commit is contained in:
Chris Troutner
2026-04-22 18:01:41 -07:00
parent fd7736213f
commit bfb73c2e14
4 changed files with 96 additions and 60 deletions
+10 -1
View File
@@ -21,6 +21,7 @@ import wlogger from '../src/adapters/wlogger.js'
import { buildX402Routes, getX402Settings, getBasicAuthSettings, createAuthHeader, getFacilitatorConnectionOptions } 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 { basicAuthMiddleware } from '../src/middleware/basic-auth.js'
import DiscoveryRouter from '../src/controllers/discovery/router.js' import DiscoveryRouter from '../src/controllers/discovery/router.js'
import DiscoveryController from '../src/controllers/discovery/controller.js'
import { createFacilitatorClient } from '../src/lib/x402/facilitator-client-factory.js' import { createFacilitatorClient } from '../src/lib/x402/facilitator-client-factory.js'
// Load environment variables // Load environment variables
@@ -259,7 +260,15 @@ class Server {
res.sendFile(docsPath) res.sendFile(docsPath)
}) })
// AI-friendly discovery endpoints // AI-friendly discovery endpoints (aliases for tools that expect these paths; same payloads as DiscoveryRouter)
const discoveryForAi = new DiscoveryController()
app.get('/.well-known/x402.json', (req, res) => {
discoveryForAi.x402Manifest(req, res)
})
app.get('/agent.json', (req, res) => {
discoveryForAi.agentManifest(req, res)
})
app.get('/llms.txt', (req, res) => { app.get('/llms.txt', (req, res) => {
const llmsPath = join(__dirname, '..', 'public', 'llms.txt') const llmsPath = join(__dirname, '..', 'public', 'llms.txt')
res.setHeader('Content-Type', 'text/plain') res.setHeader('Content-Type', 'text/plain')
+15 -38
View File
@@ -3,16 +3,15 @@
*/ */
import config from '../../config/index.js' import config from '../../config/index.js'
import { getX402Settings } from '../../config/x402.js' import wlogger from '../../adapters/wlogger.js'
import { getX402Settings, getX402WellKnownManifest as defaultGetX402WellKnownManifest } from '../../config/x402.js'
import { getDiscoveryDocuments } from '../../discovery/build-documents.js' import { getDiscoveryDocuments } from '../../discovery/build-documents.js'
const BCH_MAINNET_CAIP2 = 'bip122:000000000000000000651ef99cb9fcbe'
const BCH_NATIVE_ASSET = '0x0000000000000000000000000000000000000001'
const X402_TIMEOUT_SECONDS = 60
class DiscoveryController { class DiscoveryController {
constructor (localConfig = {}) { constructor (localConfig = {}) {
this.getX402Settings = localConfig.getX402Settings || getX402Settings this.getX402Settings = localConfig.getX402Settings || getX402Settings
this.getX402WellKnownManifest =
localConfig.getX402WellKnownManifest || defaultGetX402WellKnownManifest
this.getDiscoveryDocuments = localConfig.getDiscoveryDocuments || getDiscoveryDocuments this.getDiscoveryDocuments = localConfig.getDiscoveryDocuments || getDiscoveryDocuments
this.apiPrefix = localConfig.apiPrefix || config.apiPrefix this.apiPrefix = localConfig.apiPrefix || config.apiPrefix
@@ -36,47 +35,25 @@ class DiscoveryController {
} }
/** /**
* @api {get} /.well-known/x402 x402-bch resource discovery * @api {get} /.well-known/x402 x402 v2 resource discovery
* @apiName X402Discovery * @apiName X402Discovery
* @apiGroup Discovery * @apiGroup Discovery
* @apiDescription Returns x402-bch v2 resource and payment requirement metadata. * @apiDescription Returns x402 v2 resource and payment metadata (Base USDC, `exact` scheme) — same payload as `/.well-known/x402.json`.
*/ */
x402Manifest (req, res) { x402Manifest (req, res) {
if (this.respondNotFoundWhenDisabled(res)) return if (this.respondNotFoundWhenDisabled(res)) return
const x402 = this.getX402Settings()
const apiPrefix = this.apiPrefix || '/v6' const apiPrefix = this.apiPrefix || '/v6'
const prefixWithSlash = apiPrefix.startsWith('/') ? apiPrefix : `/${apiPrefix}` try {
const payload = this.getX402WellKnownManifest(apiPrefix)
const payload = { return res.status(200).json(payload)
x402Version: 2, } catch (err) {
network: BCH_MAINNET_CAIP2, wlogger.error('x402 well-known manifest error:', err)
facilitator: { return res.status(500).json({
url: x402.facilitatorUrl error: 'x402 configuration error',
}, message: err instanceof Error ? err.message : String(err)
resources: [ })
{
resource: `${prefixWithSlash}/*`,
type: 'http',
x402Version: 2,
accepts: [
{
scheme: 'utxo',
network: BCH_MAINNET_CAIP2,
amount: String(x402.priceSat),
description: `Access to protected psf-bch-api resources (${x402.priceSat} satoshis)`,
mimeType: 'application/json',
payTo: x402.serverAddress,
maxTimeoutSeconds: X402_TIMEOUT_SECONDS,
asset: BCH_NATIVE_ASSET,
extra: {}
}
]
}
]
} }
return res.status(200).json(payload)
} }
/** /**
+42 -17
View File
@@ -6,9 +6,8 @@ import { readFileSync, existsSync, readdirSync } from 'fs'
import { resolve } from 'path' import { resolve } from 'path'
import config from '../config/index.js' import config from '../config/index.js'
import { getX402Settings } from '../config/x402.js' import { getX402AgentAuthPricing } from '../config/x402.js'
const BCH_MAINNET_CAIP2 = 'bip122:000000000000000000651ef99cb9fcbe'
const X402_SPEC_VERSION = 2 const X402_SPEC_VERSION = 2
let cachedDocs = null let cachedDocs = null
@@ -90,7 +89,7 @@ function normalizePaths (endpoints) {
description: 'Payment required when x402 is enabled' description: 'Payment required when x402 is enabled'
} }
}, },
'x-payment-model': 'x402-bch-v2' 'x-payment-model': 'x402-v2-exact-usdc'
} }
paths[endpoint.path][endpoint.method.toLowerCase()] = op paths[endpoint.path][endpoint.method.toLowerCase()] = op
@@ -134,13 +133,15 @@ function buildLlms (endpoints) {
const lines = [ const lines = [
'# psf-bch-api', '# psf-bch-api',
'', '',
'> REST API proxy to Bitcoin Cash infrastructure with optional x402-bch monetized access.', '> REST API proxy to Bitcoin Cash infrastructure with optional x402 (USDC on Base) monetized access.',
'', '',
'## Discovery', '## Discovery',
'- [OpenAPI document](/openapi.json): OpenAPI 3 projection generated from apiDoc annotations.', '- [OpenAPI document](/openapi.json): OpenAPI 3 projection generated from apiDoc annotations.',
'- [Swagger document](/swagger.json): Swagger 2.0 compatibility projection.', '- [Swagger document](/swagger.json): Swagger 2.0 compatibility projection.',
'- [x402 manifest](/.well-known/x402): x402-bch v2 payment requirement discovery.', '- [x402 manifest](/.well-known/x402): x402 v2 payment requirement discovery (Base USDC).',
'- [x402.json](/.well-known/x402.json): same JSON as the x402 manifest (for tools that expect a `.json` path).',
'- [Agent manifest](/.well-known/agent.json): draft agent capability surface.', '- [Agent manifest](/.well-known/agent.json): draft agent capability surface.',
'- [agent.json](/agent.json): same as the agent manifest (convenience path).',
'', '',
'## API Groups' '## API Groups'
] ]
@@ -157,7 +158,7 @@ function buildLlms (endpoints) {
} }
function buildAgent (endpoints) { function buildAgent (endpoints) {
const x402 = getX402Settings() const pricing = getX402AgentAuthPricing()
const actions = endpoints.map(endpoint => ({ const actions = endpoints.map(endpoint => ({
id: endpoint.operationId, id: endpoint.operationId,
description: endpoint.summary, description: endpoint.summary,
@@ -166,24 +167,48 @@ function buildAgent (endpoints) {
method: endpoint.method method: endpoint.method
})) }))
const auth = pricing
? {
type: 'x402',
x402Version: X402_SPEC_VERSION,
required_for: actions.map(a => a.id),
pricing: {
scheme: pricing.scheme,
network: pricing.network,
payTo: pricing.payTo,
priceUSDC: String(pricing.priceUSDC),
facilitatorUrl: pricing.facilitatorUrl
}
}
: {
type: 'x402',
x402Version: X402_SPEC_VERSION,
required_for: actions.map(a => a.id),
pricing: {
note: 'Configure SERVER_BASE_ADDRESS, x402_NETWORK, and x402 for full pricing when X402 is enabled'
}
}
return { return {
awp_version: '0.1', awp_version: '0.1',
domain: 'localhost', name: 'psf-bch-api',
description:
'REST API for BCH full node, Fulcrum, and SLP data; access via x402 USDC on Base or documented bypasses.',
domain: (process.env.PUBLIC_BASE_URL || '').trim() || null,
intent: 'Programmatic BCH blockchain API access', intent: 'Programmatic BCH blockchain API access',
discovery: {
openapi: '/openapi.json',
x402: '/.well-known/x402',
x402_json: '/.well-known/x402.json',
agent: '/.well-known/agent.json',
agent_json: '/agent.json',
llms: '/llms.txt'
},
capabilities: { capabilities: {
batch_actions: false, batch_actions: false,
streaming: false streaming: false
}, },
auth: { auth,
type: 'x402-bch-v2',
required_for: actions.map(a => a.id),
pricing: {
amountSat: String(x402.priceSat),
payTo: x402.serverAddress,
network: BCH_MAINNET_CAIP2,
x402Version: X402_SPEC_VERSION
}
},
actions actions
} }
} }
@@ -14,8 +14,33 @@ describe('#discovery-controller.js', () => {
getX402Settings: () => ({ getX402Settings: () => ({
enabled, enabled,
facilitatorUrl: 'http://localhost:4345/facilitator', facilitatorUrl: 'http://localhost:4345/facilitator',
serverAddress: 'bitcoincash:qtestaddress', serverAddress: '0x0000000000000000000000000000000000000001',
priceSat: 200 priceUSDC: '0.1',
network: 'eip155:8453'
}),
getX402WellKnownManifest: () => ({
x402Version: 2,
network: 'eip155:8453',
facilitator: { url: 'http://localhost:4345/facilitator' },
resources: [
{
resource: '/v6/*',
type: 'http',
x402Version: 2,
accepts: [
{
scheme: 'exact',
network: 'eip155:8453',
price: '0.1',
payTo: '0x0000000000000000000000000000000000000001',
description: 'test',
mimeType: 'application/json',
maxTimeoutSeconds: 120,
extra: {}
}
]
}
]
}), }),
getDiscoveryDocuments: () => ({ getDiscoveryDocuments: () => ({
openapi: { openapi: '3.0.3', paths: { '/v6/price/bchusd': {} } }, openapi: { openapi: '3.0.3', paths: { '/v6/price/bchusd': {} } },
@@ -59,8 +84,8 @@ describe('#discovery-controller.js', () => {
assert.equal(res.statusValue, 200) assert.equal(res.statusValue, 200)
assert.equal(res.jsonData.x402Version, 2) assert.equal(res.jsonData.x402Version, 2)
assert.isArray(res.jsonData.resources) assert.isArray(res.jsonData.resources)
assert.equal(res.jsonData.resources[0].accepts[0].scheme, 'utxo') assert.equal(res.jsonData.resources[0].accepts[0].scheme, 'exact')
assert.equal(res.jsonData.resources[0].accepts[0].amount, '200') assert.equal(res.jsonData.resources[0].accepts[0].price, '0.1')
}) })
it('should return openapi and swagger docs', async () => { it('should return openapi and swagger docs', async () => {