mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
Merge pull request #13 from Permissionless-Software-Foundation/ct-unstable
Adding more AI-friendly endpoints for discovery
This commit is contained in:
+10
-1
@@ -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')
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ RUN git clone https://github.com/Permissionless-Software-Foundation/psf-bch-api-
|
|||||||
# and `stage` has the most up-to-date changes.
|
# and `stage` has the most up-to-date changes.
|
||||||
WORKDIR /home/safeuser/psf-bch-api-base
|
WORKDIR /home/safeuser/psf-bch-api-base
|
||||||
|
|
||||||
RUN git checkout round-robin-facilitator
|
#RUN git checkout round-robin-facilitator
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user