diff --git a/examples/typescript/servers/cloudfront-lambda-edge/README.md b/examples/typescript/servers/cloudfront-lambda-edge/README.md new file mode 100644 index 000000000..90cdd01f3 --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/README.md @@ -0,0 +1,137 @@ +# x402 CloudFront + Lambda@Edge + +Add x402 payments to any web server without modifying your backend. Put CloudFront in front of your origin, and Lambda@Edge handles all payment logic at the edge. + +> **Reference Implementation**: This is a reference approach demonstrating the pattern. Actual deployment and code bundling will depend on your project's infrastructure and build tooling (CDK, Terraform, SAM, Serverless Framework, etc.). + +## Why This Approach? + +- **Zero backend changes**: Your origin server stays untouched +- **Any origin, anywhere**: Works with any HTTP server — AWS, GCP, Azure, on-prem, or third-party APIs +- **Drop-in monetization**: Add payments to existing APIs in minutes +- **Edge performance**: Payment verification at CloudFront's global edge locations + +## How It Works + +```mermaid +sequenceDiagram + participant Client + participant CloudFront + participant Lambda@Edge + participant Facilitator as x402 Facilitator + participant Origin as Your Origin + + Client->>CloudFront: Request /api/data + CloudFront->>Lambda@Edge: origin-request event + + alt No payment header + Lambda@Edge-->>Client: 402 Payment Required + else Has PAYMENT-SIGNATURE header + Lambda@Edge->>Facilitator: Verify payment + Facilitator-->>Lambda@Edge: Valid + Lambda@Edge->>Facilitator: Settle payment + Facilitator-->>Lambda@Edge: Settled + Lambda@Edge->>Origin: Forward request + Origin-->>Client: Response + end +``` + +Lambda@Edge intercepts requests, checks for payment, verifies with the facilitator, and only forwards paid requests to your origin. Your backend never sees unpaid requests. + +## Quick Start + +### 1. Copy the Lambda Source + +Copy the `lambda/src/` files into your project and adapt the build process to your tooling. + +### 2. Configure Routes + +Edit `config.ts` with your payment settings: + +```typescript +export const CONFIG: X402Config = { + facilitatorUrl: 'https://x402.org/facilitator', + network: 'eip155:84532', // Base Sepolia testnet + payTo: '0xYourAddress', + routes: { + '/api/*': { price: '$0.001', description: 'API access' }, + '/premium/**': { price: '$0.01', description: 'Premium content' }, + }, +}; +``` + +### 3. Deploy + +Bundle and deploy the Lambda function using your preferred tooling (CDK, SAM, Terraform, etc.), then attach it to your CloudFront distribution's origin-request event. + + +## Networks + +| Network | ID | Use | +| ------------ | -------------- | ---------- | +| Base Sepolia | `eip155:84532` | Testing | +| Base Mainnet | `eip155:8453` | Production | + +## File Structure + +``` +cloudfront-lambda-edge/ +├── lambda/src/ +│ ├── index.ts # Handler +│ ├── config.ts # Routes & settings +│ ├── payment.ts # Facilitator client +│ └── responses.ts # 402 formatting +└── cdk/ # Optional: Infrastructure as code +``` + +## Advanced Patterns + +### WAF Integration for Bot Protection + +AWS WAF associated with CloudFront to label bots or suspicious traffic. Lambda@Edge can then check these labels and require payment only for labeled requests: + +```typescript +// In your Lambda@Edge handler +const isBot = request.headers['x-amzn-waf-bot']?.[0]?.value; + +if (isBot) { + // Require payment for bot traffic + return requirePayment(request); +} +// Allow non-bot traffic through without payment +``` + +This lets you monetize bot/scraper traffic while keeping human users free. + +### Caching Optimization + +CloudFront caching can reduce facilitator and Lambda@Edge calls for repeated requests: + +- **Unpaid requests**: Cache 402 responses so repeated requests without payment don't hit Lambda@Edge +- **Token-based payments**: Cache responses by payment token to serve repeated requests with the same token from edge cache + +Configure cache behaviors to include `PAYMENT-SIGNATURE` header in the cache key, allowing paid responses to be cached per-token. + +### Cookie-Based Sessions + +The current implementation reads payment info from the `PAYMENT-SIGNATURE` header. For session-based flows (e.g., browser apps), you can switch to cookies: + +```typescript +// Read from cookie instead of header +const paymentCookie = request.headers.cookie?.[0]?.value + ?.split(';') + .find(c => c.trim().startsWith('x402-payment=')); + +const paymentSignature = paymentCookie + ? decodeURIComponent(paymentCookie.split('=')[1]) + : null; +``` + +This enables payment persistence across page navigations without requiring the client to attach headers to every request. + +## Notes + +- Lambda@Edge must deploy to `us-east-1` +- No env vars in Lambda@Edge — config is bundled +- Max 30s timeout for origin-request +- Add `PAYMENT-SIGNATURE` to CloudFront cache key headers diff --git a/examples/typescript/servers/cloudfront-lambda-edge/lambda/package.json b/examples/typescript/servers/cloudfront-lambda-edge/lambda/package.json new file mode 100644 index 000000000..698128a94 --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/lambda/package.json @@ -0,0 +1,23 @@ +{ + "name": "@x402-examples/cloudfront-lambda-edge-handler", + "version": "1.0.0", + "private": true, + "description": "Lambda@Edge handler for x402 payment verification", + "main": "dist/index.js", + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --external:@aws-sdk/*", + "build:prod": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --external:@aws-sdk/* --minify", + "clean": "rm -rf dist" + }, + "dependencies": { + "@x402/core": "^2.2.0" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.159", + "esbuild": "^0.27.2", + "typescript": "^5.9.3" + } +} diff --git a/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/config.ts b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/config.ts new file mode 100644 index 000000000..bb8f6ed29 --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/config.ts @@ -0,0 +1,123 @@ +/** + * x402 Lambda@Edge Configuration + * + * Configure your payment routes and settings here. + * Lambda@Edge doesn't support environment variables, so all config is bundled. + */ + +import type { Network } from '@x402/core'; + +export interface RouteConfig { + /** Price in USD (e.g., '$0.001' or '0.001') */ + price: string; + /** Human-readable description */ + description: string; + /** Optional: Override default payTo address for this route */ + payTo?: `0x${string}`; +} + +export interface X402Config { + /** x402 facilitator URL */ + facilitatorUrl: string; + /** Network in CAIP-2 format */ + network: Network; + /** Default payment recipient address */ + payTo: `0x${string}`; + /** Route-specific payment requirements */ + routes: Record; +} + +/** + * Your x402 configuration + * + * Networks: + * - Testnet: 'eip155:84532' (Base Sepolia) + * - Mainnet: 'eip155:8453' (Base) + */ +export const CONFIG: X402Config = { + facilitatorUrl: 'https://x402.org/facilitator', + network: 'eip155:84532', + payTo: '0xYourPaymentAddressHere', + routes: { + // Example routes - customize for your use case + '/api/*': { + price: '$0.001', + description: 'API access', + }, + '/api/premium/**': { + price: '$0.01', + description: 'Premium API access', + }, + '/content/**': { + price: '$0.005', + description: 'Premium content', + }, + }, +}; + +// USDC asset info per network +const USDC_ASSETS: Record = { + 'eip155:8453': { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6 }, + 'eip155:84532': { address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', decimals: 6 }, +}; + +export function getAssetInfo(network: Network) { + const asset = USDC_ASSETS[network]; + if (!asset) throw new Error(`Unsupported network: ${network}`); + return asset; +} + +/** + * Match a path against route patterns + * - '*' matches single path segment + * - '**' matches multiple segments + */ +export function matchRoute(path: string): RouteConfig | undefined { + // Exact match first + if (CONFIG.routes[path]) return CONFIG.routes[path]; + + // Pattern matching (more specific patterns first) + const patterns = Object.keys(CONFIG.routes).sort((a, b) => { + const aDouble = a.includes('**') ? 1 : 0; + const bDouble = b.includes('**') ? 1 : 0; + if (aDouble !== bDouble) return aDouble - bDouble; + return b.length - a.length; + }); + + for (const pattern of patterns) { + if (matchPattern(pattern, path)) { + return CONFIG.routes[pattern]; + } + } + return undefined; +} + +function matchPattern(pattern: string, path: string): boolean { + if (!pattern.includes('*')) { + return path === pattern || path.startsWith(pattern + '/'); + } + + const patternParts = pattern.split('/').filter(Boolean); + const pathParts = path.split('/').filter(Boolean); + + let pi = 0, pa = 0; + + while (pi < patternParts.length && pa < pathParts.length) { + const pp = patternParts[pi]; + + if (pp === '**') { + if (pi === patternParts.length - 1) return true; + const next = patternParts[pi + 1]; + while (pa < pathParts.length && pathParts[pa] !== next) pa++; + pi++; + } else if (pp === '*') { + pi++; pa++; + } else { + if (pp !== pathParts[pa]) return false; + pi++; pa++; + } + } + + if (pi === patternParts.length) return pa === pathParts.length; + return pi === patternParts.length - 1 && patternParts[pi] === '**'; +} diff --git a/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/index.ts b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/index.ts new file mode 100644 index 000000000..2773d2738 --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/index.ts @@ -0,0 +1,57 @@ +import type { CloudFrontRequestEvent, CloudFrontRequestResult } from 'aws-lambda'; +import { matchRoute } from './config'; +import { createPaymentRequirements, verifyPayment, settlePayment } from './payment'; +import { createPaymentRequiredResponse, createPaymentInvalidResponse, LambdaEdgeResponse } from './responses'; + +/** + * Lambda@Edge Origin Request handler for x402 payment verification + */ +export const handler = async ( + event: CloudFrontRequestEvent +): Promise => { + const request = event.Records[0].cf.request; + const path = request.uri; + + console.log('x402 check:', path); + + // Check if route requires payment + const route = matchRoute(path); + if (!route) { + return request; // No payment required, pass through + } + + // Build resource URL - prefer Host header (custom domain), fallback to CloudFront domain + const host = request.headers['host']?.[0]?.value + || event.Records[0].cf.config.distributionDomainName; + const protocol = request.headers['cloudfront-forwarded-proto']?.[0]?.value || 'https'; + const resourceUrl = `${protocol}://${host}${path}`; + const requirements = createPaymentRequirements(resourceUrl, route); + + // Check for payment header + const paymentHeader = request.headers['payment-signature']?.[0]?.value; + + if (!paymentHeader) { + console.log('No payment, returning 402'); + return createPaymentRequiredResponse(requirements, 'Payment required', resourceUrl); + } + + // Verify payment + console.log('Verifying payment...'); + const verification = await verifyPayment(paymentHeader, requirements); + + if (!verification.isValid) { + console.log('Payment invalid:', verification.error); + return createPaymentInvalidResponse(requirements, verification.error || 'Invalid payment', resourceUrl, verification.payer); + } + + // Settle payment + console.log('Settling payment...'); + const settlement = await settlePayment(paymentHeader, requirements); + + if (settlement.responseHeader) { + request.headers['payment-response'] = [{ key: 'PAYMENT-RESPONSE', value: settlement.responseHeader }]; + } + + console.log('Payment verified, forwarding to origin'); + return request; +}; diff --git a/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/payment.ts b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/payment.ts new file mode 100644 index 000000000..ac552a241 --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/payment.ts @@ -0,0 +1,103 @@ +import type { PaymentRequirements, PaymentPayload, Network } from '@x402/core'; +import { CONFIG, getAssetInfo, RouteConfig } from './config'; + +/** + * Simple HTTP client for x402 facilitator + */ +class FacilitatorClient { + constructor(private url: string) {} + + async verify(payload: PaymentPayload, requirements: PaymentRequirements) { + const res = await fetch(`${this.url}/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paymentPayload: payload, paymentRequirements: requirements }), + }); + return res.json() as Promise<{ isValid: boolean; invalidReason?: string; payer?: string }>; + } + + async settle(payload: PaymentPayload, requirements: PaymentRequirements) { + const res = await fetch(`${this.url}/settle`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paymentPayload: payload, paymentRequirements: requirements }), + }); + return res.json(); + } +} + +const facilitator = new FacilitatorClient(CONFIG.facilitatorUrl); + +/** + * Convert price string to atomic units + */ +function toAtomicAmount(price: string, decimals: number): string { + const value = price.startsWith('$') ? price.slice(1) : price; + const [int, dec = ''] = value.split('.'); + const padded = dec.padEnd(decimals, '0').slice(0, decimals); + return (int + padded).replace(/^0+/, '') || '0'; +} + +/** + * Create payment requirements for a route + */ +export function createPaymentRequirements( + resource: string, + route: RouteConfig +): PaymentRequirements { + const asset = getAssetInfo(CONFIG.network); + const payTo = route.payTo || CONFIG.payTo; + + return { + scheme: 'exact', + network: CONFIG.network, + amount: toAtomicAmount(route.price, asset.decimals), + payTo, + maxTimeoutSeconds: 60, + asset: asset.address, + extra: { name: 'USDC', version: '2' }, + }; +} + +/** + * Decode payment from base64 header + */ +function decodePayment(header: string): PaymentPayload { + return JSON.parse(Buffer.from(header, 'base64').toString('utf-8')); +} + +/** + * Verify payment with facilitator + */ +export async function verifyPayment( + paymentHeader: string, + requirements: PaymentRequirements +): Promise<{ isValid: boolean; error?: string; payer?: string }> { + try { + const payload = decodePayment(paymentHeader); + const result = await facilitator.verify(payload, requirements); + + if (!result.isValid) { + return { isValid: false, error: result.invalidReason, payer: result.payer }; + } + return { isValid: true, payer: result.payer }; + } catch (e) { + return { isValid: false, error: e instanceof Error ? e.message : 'Verification failed' }; + } +} + +/** + * Settle payment with facilitator + */ +export async function settlePayment( + paymentHeader: string, + requirements: PaymentRequirements +): Promise<{ responseHeader?: string; error?: string }> { + try { + const payload = decodePayment(paymentHeader); + const result = await facilitator.settle(payload, requirements); + return { responseHeader: Buffer.from(JSON.stringify(result)).toString('base64') }; + } catch (e) { + return { error: e instanceof Error ? e.message : 'Settlement failed' }; + } +} diff --git a/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/responses.ts b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/responses.ts new file mode 100644 index 000000000..346d8fb6b --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/lambda/src/responses.ts @@ -0,0 +1,63 @@ +import type { PaymentRequirements } from '@x402/core'; + +const X402_VERSION = 2; + +export interface LambdaEdgeResponse { + status: string; + statusDescription?: string; + body?: string; + headers?: Record>; +} + +/** + * Create 402 Payment Required response + */ +export function createPaymentRequiredResponse( + paymentRequirements: PaymentRequirements, + error: string, + resourceUrl: string +): LambdaEdgeResponse { + const body = JSON.stringify({ + x402Version: X402_VERSION, + error, + resource: { url: resourceUrl, mimeType: 'application/json' }, + accepts: [paymentRequirements], + }); + + return { + status: '402', + statusDescription: 'Payment Required', + headers: { + 'content-type': [{ key: 'Content-Type', value: 'application/json; charset=utf-8' }], + 'payment-required': [{ key: 'PAYMENT-REQUIRED', value: Buffer.from(body).toString('base64') }], + }, + body, + }; +} + +/** + * Create 402 response for invalid payment + */ +export function createPaymentInvalidResponse( + paymentRequirements: PaymentRequirements, + error: string, + resourceUrl: string, + payer?: string +): LambdaEdgeResponse { + const body = JSON.stringify({ + x402Version: X402_VERSION, + error, + resource: { url: resourceUrl, mimeType: 'application/json' }, + accepts: [paymentRequirements], + payer, + }); + + return { + status: '402', + statusDescription: 'Payment Required', + headers: { + 'content-type': [{ key: 'Content-Type', value: 'application/json; charset=utf-8' }], + }, + body, + }; +} diff --git a/examples/typescript/servers/cloudfront-lambda-edge/lambda/tsconfig.json b/examples/typescript/servers/cloudfront-lambda-edge/lambda/tsconfig.json new file mode 100644 index 000000000..195177849 --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/lambda/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/typescript/servers/cloudfront-lambda-edge/package.json b/examples/typescript/servers/cloudfront-lambda-edge/package.json new file mode 100644 index 000000000..559b55cbf --- /dev/null +++ b/examples/typescript/servers/cloudfront-lambda-edge/package.json @@ -0,0 +1,8 @@ +{ + "name": "@x402-examples/cloudfront-lambda-edge", + "version": "1.0.0", + "private": true, + "description": "x402 payment using Amazon CloudFront and Lambda@Edge", + "scripts": { + } +}