import type { PaymentRequirements, PaymentRequired, VerifyResponse, SettleResponse, Network, } from '@payai/x402/types'; import type { X402ServerConfig, RouteConfig, TokenAsset } from '../types'; import { toCAIP2Network } from '../types'; import { getDefaultRpcUrl, getDefaultTokenAsset } from '../utils'; import { FacilitatorClient } from './facilitator-client'; /** * Internal configuration with defaults resolved */ interface InternalConfig { network: string; // CAIP-2 format treasuryAddress: string; facilitatorUrl: string; rpcUrl: string; defaultToken: TokenAsset; defaultDescription: string; defaultTimeoutSeconds: number; } /** * x402 Payment Handler for server-side payment processing (v2) * Framework agnostic - works with any Node.js HTTP framework */ export class X402PaymentHandler { private facilitatorClient: FacilitatorClient; private config: InternalConfig; constructor(config: X402ServerConfig) { const defaultToken = config.defaultToken || getDefaultTokenAsset(config.network); this.config = { network: toCAIP2Network(config.network), treasuryAddress: config.treasuryAddress, facilitatorUrl: config.facilitatorUrl, rpcUrl: config.rpcUrl || getDefaultRpcUrl(config.network), defaultToken, defaultDescription: config.defaultDescription || 'Payment required', defaultTimeoutSeconds: config.defaultTimeoutSeconds || 300, }; this.facilitatorClient = new FacilitatorClient({ url: config.facilitatorUrl, apiKeyId: config.apiKeyId, apiKeySecret: config.apiKeySecret, }); } /** * Extract payment header from request headers (v2) * Pass in headers object from any framework (Next.js, Express, etc.) * * Note: v2 uses PAYMENT-SIGNATURE header (v1 used X-PAYMENT) */ extractPayment(headers: Record | Headers): string | null { // Handle Headers object (Next.js, Fetch API) if (headers instanceof Headers) { // v2 header (PAYMENT-SIGNATURE) return headers.get('PAYMENT-SIGNATURE') || headers.get('payment-signature'); } // Handle plain object (Express, Fastify, etc.) // v2 header (PAYMENT-SIGNATURE) const paymentSignature = headers['PAYMENT-SIGNATURE'] || headers['payment-signature']; return Array.isArray(paymentSignature) ? paymentSignature[0] || null : paymentSignature || null; } /** * Create payment requirements for a protected resource * * @param routeConfig - Route-specific configuration * @param resourceUrl - URL of the protected resource * @returns Payment requirements object */ async createPaymentRequirements( routeConfig: RouteConfig, resourceUrl: string ): Promise { // Get fee payer from facilitator const feePayer = await this.facilitatorClient.getFeePayer(this.config.network); const paymentRequirements: PaymentRequirements = { scheme: 'exact', network: this.config.network as Network, amount: routeConfig.amount, payTo: this.config.treasuryAddress, maxTimeoutSeconds: routeConfig.maxTimeoutSeconds || this.config.defaultTimeoutSeconds, asset: routeConfig.asset.address, extra: { feePayer, description: routeConfig.description || this.config.defaultDescription, mimeType: routeConfig.mimeType || 'application/json', resource: resourceUrl, }, }; return paymentRequirements; } /** * Create a 402 Payment Required response body (v2) * Use this with your framework's response method * * @param requirements - Payment requirements (from createPaymentRequirements) * @param resourceUrl - URL of the protected resource */ create402Response( requirements: PaymentRequirements, resourceUrl: string ): { status: 402; body: PaymentRequired; } { return { status: 402, body: { x402Version: 2, resource: { url: resourceUrl, description: (requirements.extra?.description as string) || '', mimeType: (requirements.extra?.mimeType as string) || 'application/json', }, accepts: [requirements], error: 'Payment required', }, }; } /** * Verify payment with facilitator * @returns VerifyResponse with isValid and optional invalidReason */ async verifyPayment( paymentHeader: string, paymentRequirements: PaymentRequirements ): Promise { return this.facilitatorClient.verifyPayment(paymentHeader, paymentRequirements); } /** * Settle payment with facilitator * @returns SettleResponse with success status and optional errorReason */ async settlePayment( paymentHeader: string, paymentRequirements: PaymentRequirements ): Promise { return this.facilitatorClient.settlePayment(paymentHeader, paymentRequirements); } /** * Get the network in CAIP-2 format */ getNetwork(): string { return this.config.network; } /** * Get the treasury address */ getTreasuryAddress(): string { return this.config.treasuryAddress; } }