Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions examples/typescript/servers/cloudfront-lambda-edge/README.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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<string, RouteConfig>;
}

/**
* 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<string, { address: string; decimals: number }> = {
'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] === '**';
}
Original file line number Diff line number Diff line change
@@ -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<CloudFrontRequestResult | LambdaEdgeResponse> => {
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;
};
Loading