Integrates @x402/extensions/bazaar to enrich 402 responses with machine-readable input/output schemas. AI agents can now auto-discover API capabilities, parameter types, and descriptions from the payment header alone. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
199 lines
6.6 KiB
JavaScript
199 lines
6.6 KiB
JavaScript
// x402.js - x402 micropayment integration for SnapAPI
|
|
// Enables pay-per-request payments from AI agents via the x402 protocol
|
|
|
|
const { x402HTTPResourceServer, x402ResourceServer, HTTPFacilitatorClient } = require('@x402/core/server');
|
|
const { ExactEvmScheme } = require('@x402/evm/exact/server');
|
|
const { declareDiscoveryExtension, bazaarResourceServerExtension } = require('@x402/extensions/bazaar');
|
|
|
|
const WALLET_ADDRESS = '0x7483a9F237cf8043704D6b17DA31c12BfFF860DD';
|
|
|
|
// Pricing per endpoint (in USD, paid in USDC)
|
|
const PRICES = {
|
|
capture: '$0.01', // $0.01 per screenshot
|
|
md2pdf: '$0.005', // $0.005 per markdown-to-PDF
|
|
md2png: '$0.005', // $0.005 per markdown-to-PNG
|
|
};
|
|
|
|
// Accept x402 micropayments on Base (best EVM support for x402)
|
|
// Polygon subscription still available via /api/subscribe
|
|
const NETWORK = 'eip155:8453'; // Base Mainnet
|
|
|
|
// PayAI facilitator: supports Base mainnet, no API key required
|
|
const FACILITATOR_URL = 'https://facilitator.payai.network';
|
|
|
|
// HTTP adapter for raw Node.js http.IncomingMessage
|
|
class NodeHTTPAdapter {
|
|
constructor(req) {
|
|
this.req = req;
|
|
const host = req.headers.host || 'localhost';
|
|
this.parsedUrl = new URL(req.url || '/', `http://${host}`);
|
|
}
|
|
getHeader(name) { return this.req.headers[name.toLowerCase()]; }
|
|
getMethod() { return this.req.method || 'GET'; }
|
|
getPath() { return this.parsedUrl.pathname; }
|
|
getUrl() { return this.parsedUrl.href; }
|
|
getAcceptHeader() { return this.req.headers.accept || ''; }
|
|
getUserAgent() { return this.req.headers['user-agent'] || ''; }
|
|
}
|
|
|
|
let httpResourceServer = null;
|
|
let initialized = false;
|
|
|
|
function createRouteConfig(price, description, mimeType, discoveryMeta) {
|
|
const config = {
|
|
accepts: {
|
|
scheme: 'exact',
|
|
network: NETWORK,
|
|
payTo: WALLET_ADDRESS,
|
|
price,
|
|
maxTimeoutSeconds: 120,
|
|
},
|
|
description,
|
|
mimeType,
|
|
};
|
|
if (discoveryMeta) {
|
|
config.extensions = {
|
|
...declareDiscoveryExtension(discoveryMeta),
|
|
};
|
|
}
|
|
return config;
|
|
}
|
|
|
|
async function initX402() {
|
|
if (initialized) return httpResourceServer;
|
|
|
|
const facilitator = new HTTPFacilitatorClient({ url: FACILITATOR_URL });
|
|
const resourceServer = new x402ResourceServer([facilitator]);
|
|
|
|
resourceServer.register(NETWORK, new ExactEvmScheme());
|
|
resourceServer.registerExtension(bazaarResourceServerExtension);
|
|
|
|
const routes = {
|
|
'GET /api/capture': createRouteConfig(
|
|
PRICES.capture,
|
|
'Capture screenshot or PDF from a URL',
|
|
'image/png',
|
|
{
|
|
input: { url: 'https://example.com', format: 'png' },
|
|
inputSchema: {
|
|
properties: {
|
|
url: { type: 'string', description: 'URL to capture screenshot of' },
|
|
format: { type: 'string', description: 'Output format: png, jpeg, or pdf' },
|
|
width: { type: 'number', description: 'Viewport width in pixels (default 1280)' },
|
|
height: { type: 'number', description: 'Viewport height in pixels (default 800)' },
|
|
fullPage: { type: 'boolean', description: 'Capture full scrollable page' },
|
|
},
|
|
required: ['url'],
|
|
},
|
|
output: { example: 'Binary PNG/JPEG image or PDF document' },
|
|
}
|
|
),
|
|
'POST /api/md2pdf': createRouteConfig(
|
|
PRICES.md2pdf,
|
|
'Convert Markdown to PDF',
|
|
'application/pdf',
|
|
{
|
|
input: { markdown: '# Hello World\n\nSample markdown content' },
|
|
inputSchema: {
|
|
properties: {
|
|
markdown: { type: 'string', description: 'Markdown content to convert to PDF' },
|
|
theme: { type: 'string', description: 'Theme: light or dark (default light)' },
|
|
paperSize: { type: 'string', description: 'Paper size: A4, Letter, Legal, Tabloid' },
|
|
},
|
|
required: ['markdown'],
|
|
},
|
|
output: { example: 'Binary PDF document with styled markdown content' },
|
|
}
|
|
),
|
|
'POST /api/md2png': createRouteConfig(
|
|
PRICES.md2png,
|
|
'Convert Markdown to PNG image',
|
|
'image/png',
|
|
{
|
|
input: { markdown: '# Hello World\n\nSample markdown content' },
|
|
inputSchema: {
|
|
properties: {
|
|
markdown: { type: 'string', description: 'Markdown content to convert to image' },
|
|
theme: { type: 'string', description: 'Theme: light or dark (default light)' },
|
|
width: { type: 'number', description: 'Viewport width in pixels (default 800)' },
|
|
},
|
|
required: ['markdown'],
|
|
},
|
|
output: { example: 'Binary PNG image of rendered markdown' },
|
|
}
|
|
),
|
|
};
|
|
|
|
httpResourceServer = new x402HTTPResourceServer(resourceServer, routes);
|
|
await httpResourceServer.initialize();
|
|
initialized = true;
|
|
console.log('[x402] Micropayment system initialized (PayAI facilitator + Bazaar discovery)');
|
|
console.log(`[x402] Accepting USDC on Base to ${WALLET_ADDRESS}`);
|
|
console.log(`[x402] Prices: capture=${PRICES.capture}, md2pdf=${PRICES.md2pdf}, md2png=${PRICES.md2png}`);
|
|
return httpResourceServer;
|
|
}
|
|
|
|
// Check if a request has an x402 payment header
|
|
function hasX402Payment(req) {
|
|
return !!(req.headers['payment-signature'] || req.headers['x-payment']);
|
|
}
|
|
|
|
// Process an x402 payment request
|
|
// Returns: { allowed: true, settle: Function } or { allowed: false, status, headers, body }
|
|
async function processPayment(req) {
|
|
if (!httpResourceServer) {
|
|
return { allowed: false, status: 503, headers: {}, body: { error: 'x402 not initialized' } };
|
|
}
|
|
|
|
const adapter = new NodeHTTPAdapter(req);
|
|
const context = {
|
|
adapter,
|
|
path: adapter.getPath(),
|
|
method: adapter.getMethod(),
|
|
};
|
|
|
|
const result = await httpResourceServer.processHTTPRequest(context);
|
|
|
|
if (result.type === 'no-payment-required') {
|
|
return { allowed: true, settle: null };
|
|
}
|
|
|
|
if (result.type === 'payment-error') {
|
|
return {
|
|
allowed: false,
|
|
status: result.response.status,
|
|
headers: result.response.headers || {},
|
|
body: result.response.body,
|
|
isHtml: result.response.isHtml,
|
|
};
|
|
}
|
|
|
|
if (result.type === 'payment-verified') {
|
|
const settleFn = async () => {
|
|
try {
|
|
const settleResult = await httpResourceServer.processSettlement(
|
|
result.paymentPayload,
|
|
result.paymentRequirements,
|
|
result.declaredExtensions
|
|
);
|
|
return settleResult;
|
|
} catch (err) {
|
|
console.error('[x402] Settlement error:', err.message);
|
|
return null;
|
|
}
|
|
};
|
|
return { allowed: true, settle: settleFn };
|
|
}
|
|
|
|
return { allowed: false, status: 500, headers: {}, body: { error: 'Unknown payment state' } };
|
|
}
|
|
|
|
module.exports = {
|
|
initX402,
|
|
hasX402Payment,
|
|
processPayment,
|
|
PRICES,
|
|
WALLET_ADDRESS,
|
|
NETWORK,
|
|
};
|