Server SDK
The server SDK provides middleware to protect your API endpoints with payments.
Supported Platforms
| Platform | Package | Runtime |
|---|---|---|
| Express.js | @puga-labs/x402-mantle-sdk/server/express | Node.js |
| Next.js (App Router) | @puga-labs/x402-mantle-sdk/server/nextjs | Node.js / Edge |
| Hono | @puga-labs/x402-mantle-sdk/server/web | Any |
| Cloudflare Workers | @puga-labs/x402-mantle-sdk/server/web | Edge |
| Deno | @puga-labs/x402-mantle-sdk/server/web | Deno |
| Bun | @puga-labs/x402-mantle-sdk/server/web | Bun |
The server/web package uses the standard Web Fetch API and works with any runtime that supports it.
How It Works
- Middleware checks for
X-PAYMENTheader - If missing: returns 402 with payment requirements
- If present: validates with facilitator
- If valid: calls your handler
- If invalid: returns error
Self-Hosted vs Hosted Configuration
Self-hosted facilitator is recommended for full control and no per-transaction fees. Run your own with npx create-mantle-facilitator.
Self-Hosted Mode (Recommended)
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWalletAddress',
facilitatorUrl: 'https://your-facilitator.com',
facilitatorSecret: process.env.FACILITATOR_SECRET!,
projectKey: 'pk_xxx' // Optional: for analytics
});
When using self-hosted facilitator, facilitatorSecret is required for security. Copy it from your facilitator's .env file.
Hosted Mode
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWalletAddress',
projectKey: 'pk_xxx' // Get from https://x402mantlesdk.xyz/dashboard
});
// facilitatorUrl defaults to hosted service automatically
Express.js
import express from 'express';
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';
const app = express();
app.use(express.json());
// Self-hosted mode (recommended)
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWalletAddress',
facilitatorUrl: process.env.FACILITATOR_URL!,
facilitatorSecret: process.env.FACILITATOR_SECRET!
});
// Protected route
app.post('/api/generate', pay, async (req, res) => {
const { prompt } = req.body;
// Your business logic here - only runs after payment
res.json({ result: `Generated: ${prompt}` });
});
// Multiple prices for different endpoints
const paySmall = mantlePaywall({ priceUsd: 0.01, payTo: '0x...', facilitatorUrl: '...', facilitatorSecret: '...' });
const payLarge = mantlePaywall({ priceUsd: 0.10, payTo: '0x...', facilitatorUrl: '...', facilitatorSecret: '...' });
app.post('/api/small', paySmall, handler);
app.post('/api/large', payLarge, handler);
// Unprotected routes work normally
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
app.listen(3000);
With Payment Callback
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWalletAddress',
facilitatorUrl: process.env.FACILITATOR_URL!,
facilitatorSecret: process.env.FACILITATOR_SECRET!,
onPaymentSettled: (entry) => {
console.log('Payment received!');
console.log('Amount:', entry.valueAtomic);
console.log('From:', entry.from);
console.log('TxHash:', entry.txHash);
}
});
Next.js (App Router)
// app/api/generate/route.ts
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/nextjs';
import { NextRequest, NextResponse } from 'next/server';
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: process.env.PAY_TO!,
facilitatorUrl: process.env.FACILITATOR_URL!,
facilitatorSecret: process.env.FACILITATOR_SECRET!
});
export const POST = pay(async (req: NextRequest) => {
const { prompt } = await req.json();
// Your business logic
const result = await generateContent(prompt);
return NextResponse.json({ result });
});
// You can also export other methods
export const GET = async () => {
return NextResponse.json({ message: 'Use POST to generate' });
};
With Payment Callback
// app/api/generate/route.ts
import { mantlePaywall, PaymentLogEntry } from '@puga-labs/x402-mantle-sdk/server/nextjs';
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: process.env.PAY_TO!,
facilitatorUrl: process.env.FACILITATOR_URL!,
facilitatorSecret: process.env.FACILITATOR_SECRET!,
onPaymentSettled: async (entry: PaymentLogEntry) => {
// Log to database
await db.payments.create({
from: entry.from,
amount: entry.valueAtomic,
txHash: entry.txHash,
route: entry.route,
timestamp: entry.timestamp
});
}
});
export const POST = pay(async (req: NextRequest) => {
// Handler code
});
Hono
import { Hono } from 'hono';
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/web';
const app = new Hono();
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWallet',
facilitatorUrl: process.env.FACILITATOR_URL!,
facilitatorSecret: process.env.FACILITATOR_SECRET!
});
app.post('/api/generate', pay(async (c) => {
const { prompt } = await c.req.json();
return c.json({ result: `Generated: ${prompt}` });
}));
export default app;
Cloudflare Workers
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/web';
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWallet',
facilitatorUrl: 'https://your-facilitator.com',
facilitatorSecret: process.env.FACILITATOR_SECRET!
});
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/api/generate' && request.method === 'POST') {
return pay(async (req) => {
const body = await req.json();
return new Response(JSON.stringify({ result: 'success' }), {
headers: { 'Content-Type': 'application/json' }
});
})(request);
}
return new Response('Not found', { status: 404 });
}
};
Deno
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/web';
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWallet',
facilitatorUrl: 'https://your-facilitator.com',
facilitatorSecret: Deno.env.get('FACILITATOR_SECRET')!
});
Deno.serve(pay(async (req) => {
const body = await req.json();
return new Response(JSON.stringify({ result: 'success' }), {
headers: { 'Content-Type': 'application/json' }
});
}));
Bun
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/web';
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWallet',
facilitatorUrl: 'https://your-facilitator.com',
facilitatorSecret: process.env.FACILITATOR_SECRET!
});
Bun.serve({
port: 3000,
fetch: pay(async (req) => {
const body = await req.json();
return new Response(JSON.stringify({ result: 'success' }), {
headers: { 'Content-Type': 'application/json' }
});
})
});
Configuration
MinimalPaywallOptions
| Option | Type | Required | Description |
|---|---|---|---|
priceUsd | number | Yes | Price in USD (e.g., 0.01 for 1 cent) |
payTo | string | Yes | Your wallet address to receive payments |
projectKey | string | For hosted | Project key from dashboard (for billing + analytics) |
facilitatorUrl | string | For self-hosted | Facilitator URL (defaults to hosted service) |
facilitatorSecret | string | For self-hosted | Shared secret that ensures only your backend can use your facilitator (prevents unauthorized gas spending) |
network | NetworkId | No | Network ID (default: mantle-mainnet) |
onPaymentSettled | function | No | Callback when payment verified |
telemetry | TelemetryConfig | No | Analytics configuration (auto-uses projectKey if not set) |
TelemetryConfig
interface TelemetryConfig {
projectKey?: string; // Auto-derived from mantlePaywall projectKey
debug?: boolean; // Enable debug logging (default: false)
}
PaymentLogEntry (onPaymentSettled callback)
interface PaymentLogEntry {
id: string; // Unique payment ID (nonce)
from: string; // Payer's wallet address
to: string; // Your wallet address
valueAtomic: string; // Amount in atomic units (string)
network: NetworkId; // "mantle-mainnet"
asset: string; // USDC contract address
route?: string; // API route (e.g., "POST /api/generate")
txHash?: string; // Blockchain transaction hash
timestamp: number; // Unix timestamp in milliseconds
}
402 Response Format
When payment is required, the server returns:
{
"error": "Payment Required",
"paymentRequirements": {
"scheme": "exact",
"network": "mantle-mainnet",
"asset": "0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9",
"maxAmountRequired": "10000",
"payTo": "0xYourWallet",
"price": "$0.01",
"currency": "USD"
}
}
Next Steps
- Facilitator - Hosted and self-hosted options
- Examples - Full working examples