/** * Route pricing configuration for the payment proxy. * * Routes are priced per payment method (stripe / tempo / base). * The proxy picks the right underlying protocol automatically. * * Paths support a single trailing wildcard (*) matching one path segment, * e.g. "/v1/customers/*" matches "/v1/customers/cus_123". * * Routes not listed here are forwarded to the origin without any payment gate. */ // --------------------------------------------------------------------------- // Per-method price types // --------------------------------------------------------------------------- export type StripePrice = { /** Price in major currency units, e.g. "0.05" = $0.05 */ amount: string /** ISO 4217 currency code (default "usd") */ currency: string } export type TempoPrice = { /** * Amount in PATH stablecoin units (dollar-pegged). * "0.04" ≈ $0.04 in PATH tokens. */ amount: string } export type BasePrice = { /** * Price in USD, e.g. "0.05" = $0.05 USDC. * Converted to USDC base units (6 decimals) internally. */ amount: string } export type RoutePricing = { /** Stripe card / Link */ stripe?: StripePrice /** Tempo crypto (PATH stablecoin) */ tempo?: TempoPrice /** Base USDC */ base?: BasePrice } // --------------------------------------------------------------------------- // Request schema validation // --------------------------------------------------------------------------- /** * Minimal structural interface for a Zod-compatible validator. * * Any object with a `safeParse` method that returns a Zod-style result * satisfies this type — Zod, Valibot, ArkType, and similar libraries all work * out of the box. No schema library is added as a dependency; bring your own. * * @example * import { z } from 'zod' * const bodyValidator: SchemaValidator = z.object({ prompt: z.string().min(1) }) */ export type SchemaValidator = { safeParse(data: unknown): | { success: true } | { success: false; error: { issues: { path: (string | number)[]; message: string }[] } } } /** * Validators for a route's incoming request. * * Both fields are optional — supply only the ones that are relevant. * Validated after route matching but before any payment challenge is issued. * A 400 is returned immediately when validation fails so callers are never * charged for malformed input. */ export type RouteSchema = { /** * Validator for the parsed JSON request body. * Only evaluated for methods that carry a body (POST, PUT, PATCH). * Note: the full body object is passed, including any `_meta` fields. */ body?: SchemaValidator /** * Validator for URL query parameters. * Evaluated for all HTTP methods. The value passed to `safeParse` is a * plain `Record` — use `z.coerce` in your schema if you * need numeric or boolean coercion. */ query?: SchemaValidator } // --------------------------------------------------------------------------- // Route config // --------------------------------------------------------------------------- export type RoutePrice = { /** * Flat price applied to Stripe when `pricing.stripe` is not set. * Ignored if `pricing` is provided for the relevant method. */ amount?: string /** ISO 4217 currency for the flat `amount` (default "usd") */ currency?: string } export type RoutePriceConfig = RoutePrice & { /** HTTP method or '*' to match any method */ method: string /** * URL path pattern. Supports a single wildcard "*" matching any * sequence of non-slash characters in one path segment. */ path: string /** Human-readable description surfaced in payment challenges */ description?: string /** * Per-method pricing. Overrides `amount`/`currency` for that method. * Entries for unconfigured methods are silently ignored. */ pricing?: RoutePricing /** * When true, the request body must include a `_meta.billing_address` object * with at least `line1`, `city`, `state`, `postal_code`, and `country`. */ requireBillingAddress?: boolean /** When true, the request body must include a non-empty `_meta.email` string. */ requireEmail?: boolean /** When true, the request body must include a non-empty `_meta.phone` string. */ requirePhone?: boolean /** * Schema for the route's request parameters. * Validated after route matching but before any payment challenge is issued. * Invalid requests are rejected with 400 so callers are never charged for * malformed input. */ schema?: RouteSchema /** * When true, this route is explicitly free — no payment gate is applied. * Requests are forwarded directly to the origin. * * Use this (rather than omitting the route) when `blockUnlistedRoutes` is * enabled on the proxy, so you can distinguish "intentionally free" from * "forgotten route". */ free?: boolean } // --------------------------------------------------------------------------- // Factory-level payment method configs // --------------------------------------------------------------------------- export type StripeConfig = { /** Stripe secret key (sk_test_… or sk_live_…) */ secretKey: string /** * Stripe Business Network profile ID. * Defaults to STRIPE_NETWORK_ID env var then "internal". */ networkId?: string /** Accepted payment method types (default: ['card', 'link']) */ paymentMethodTypes?: string[] } export type TempoConfig = { /** EVM wallet address that receives Tempo (PATH) payments */ recipient: `0x${string}` /** Use the Tempo testnet (default false) */ testnet?: boolean /** * PATH stablecoin token address on Tempo. * Defaults to the canonical PATH_USD address. */ currency?: string } export type BaseConfig = { /** EVM wallet address that receives USDC on Base */ payTo: `0x${string}` /** * Target network. Accepts common aliases or EIP-155 chain IDs: * 'base' → Base Mainnet (eip155:8453) * 'base-sepolia' → Base Sepolia (eip155:84532) ← default * 'eip155:8453' → Base Mainnet (explicit) */ network?: string /** * Payment facilitator URL for on-chain proof verification. * Defaults to 'https://x402.org/facilitator'. */ facilitatorUrl?: string } // --------------------------------------------------------------------------- // Price resolution helpers // --------------------------------------------------------------------------- export function resolveStripePrice(route: RoutePriceConfig): StripePrice | null { if (route.pricing?.stripe) return route.pricing.stripe if (route.amount) return { amount: route.amount, currency: route.currency ?? 'usd' } return null } export function resolveTempoPrice(route: RoutePriceConfig): TempoPrice | null { return route.pricing?.tempo ?? null } export function resolveBasePrice(route: RoutePriceConfig): BasePrice | null { return route.pricing?.base ?? null } // --------------------------------------------------------------------------- // Route matching // --------------------------------------------------------------------------- export function matchRoute( method: string, pathname: string, routes: RoutePriceConfig[], ): RoutePriceConfig | null { for (const route of routes) { if (route.method !== '*' && route.method.toUpperCase() !== method.toUpperCase()) continue if (pathMatches(route.path, pathname)) return route } return null } function pathMatches(pattern: string, pathname: string): boolean { if (!pattern.includes('*')) return pattern === pathname const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]+') return new RegExp(`^${escaped}$`).test(pathname) }