Desktop Only

This application is optimized for desktop browsers. Please open it on a device with a screen width of at least 1024px.

Server SDK

The server SDK provides middleware to protect your API endpoints with payments.

Supported Platforms

PlatformPackageRuntime
Express.js@puga-labs/x402-mantle-sdk/server/expressNode.js
Next.js (App Router)@puga-labs/x402-mantle-sdk/server/nextjsNode.js / Edge
Hono@puga-labs/x402-mantle-sdk/server/webAny
Cloudflare Workers@puga-labs/x402-mantle-sdk/server/webEdge
Deno@puga-labs/x402-mantle-sdk/server/webDeno
Bun@puga-labs/x402-mantle-sdk/server/webBun

The server/web package uses the standard Web Fetch API and works with any runtime that supports it.

How It Works

  1. Middleware checks for X-PAYMENT header
  2. If missing: returns 402 with payment requirements
  3. If present: validates with facilitator
  4. If valid: calls your handler
  5. 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

OptionTypeRequiredDescription
priceUsdnumberYesPrice in USD (e.g., 0.01 for 1 cent)
payTostringYesYour wallet address to receive payments
projectKeystringFor hostedProject key from dashboard (for billing + analytics)
facilitatorUrlstringFor self-hostedFacilitator URL (defaults to hosted service)
facilitatorSecretstringFor self-hostedShared secret that ensures only your backend can use your facilitator (prevents unauthorized gas spending)
networkNetworkIdNoNetwork ID (default: mantle-mainnet)
onPaymentSettledfunctionNoCallback when payment verified
telemetryTelemetryConfigNoAnalytics 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