// A paid agent: one route priced in USDC over x402 (v1 exact scheme), web standard fetch handler.
// GET /health free
// GET /paid 402 without X-PAYMENT; with it: verify, settle, then the work and X-PAYMENT-RESPONSE
import { buildRequirements, encodeHeader, isAddress, isNetwork, readPayment, toAtomicUsdc } from './x402.js';
import type { Facilitator, Network, PaymentRequirements } from './x402.js';
import { httpFacilitator, DEFAULT_FACILITATOR_URL } from './facilitator.js';
export interface AppOptions {
/** Decimal USDC string such as "0.01" */
price: string;
/** Payee address on Base */
payTo: string;
network: Network;
/** A facilitator object, or its URL. Default https://x402.org/facilitator */
facilitator?: Facilitator | string;
description?: string;
/** The paid work. Default: a small JSON receipt of the request. Runs only after settlement. */
work?: (request: Request) => Promise<unknown> | unknown;
/** Public base URL used in the 402 resource field (behind proxies). Default the request origin. */
publicUrl?: string;
}
export interface App {
fetch(request: Request): Promise<Response>;
}
const json = (status: number, body: unknown, headers: Record<string, string> = {}) =>
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } });
export function createApp(options: AppOptions): App {
const { price, payTo, network } = options;
toAtomicUsdc(price); // throws on a bad price at start, not per request
if (!isAddress(payTo)) throw new TypeError('payTo must be 0x and 40 hex characters');
if (!isNetwork(network)) throw new TypeError('network must be base or base-sepolia');
const facilitator: Facilitator =
typeof options.facilitator === 'object' && options.facilitator !== null
? options.facilitator
: httpFacilitator(typeof options.facilitator === 'string' ? options.facilitator : DEFAULT_FACILITATOR_URL);
const description = options.description ?? 'A paid agent call';
const work = options.work ?? ((req: Request) => ({ ok: true, message: 'Paid work done.', path: new URL(req.url).pathname, at: new Date().toISOString() }));
function requirementsFor(request: Request): PaymentRequirements {
const u = new URL(request.url);
const origin = (options.publicUrl ?? u.origin).replace(/\/+$/, '');
return buildRequirements({ price, payTo, network, resource: `${origin}${u.pathname}`, description });
}
const paymentRequired = (req: PaymentRequirements, error: string) => json(402, { x402Version: 1, error, accepts: [req] });
async function paid(request: Request): Promise<Response> {
const req = requirementsFor(request);
const header = request.headers.get('x-payment');
if (!header) return paymentRequired(req, 'X-PAYMENT header is required');
const read = readPayment(header, req);
if ('reason' in read) return paymentRequired(req, read.reason);
let verified;
try {
verified = await facilitator.verify(read.payment, req);
} catch {
return paymentRequired(req, 'the payment could not be verified');
}
if (!verified || verified.isValid !== true) return paymentRequired(req, verified?.invalidReason || 'the payment is not valid');
let settled;
try {
settled = await facilitator.settle(read.payment, req);
} catch {
return paymentRequired(req, 'the payment could not be settled');
}
if (!settled || settled.success !== true) return paymentRequired(req, settled?.errorReason || 'the payment did not settle');
const result = await work(request);
const receipt = { success: true, transaction: settled.transaction, network: settled.network ?? network, payer: settled.payer ?? verified.payer };
return json(200, result, { 'x-payment-response': encodeHeader(receipt), 'access-control-expose-headers': 'X-PAYMENT-RESPONSE' });
}
return {
async fetch(request: Request): Promise<Response> {
const { pathname } = new URL(request.url);
if (pathname === '/health') return request.method === 'GET' ? json(200, { ok: true }) : json(405, { error: 'method not allowed' });
if (pathname === '/paid') return request.method === 'GET' ? paid(request) : json(405, { error: 'method not allowed' });
return json(404, { error: 'not found' });
},
};
}
agentgate-source: code mirror on Cloudflare Pages. All repos