x402-paid-agent-starter / tree / typescript/src/app.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// 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