Desktop Only

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

Examples

Complete working examples for different setups.

React + Express (Self-Hosted Facilitator) — Recommended

Full control over your infrastructure with no per-transaction fees.

Server (server/src/index.ts)

import express from 'express';
import cors from 'cors';
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';

const app = express();

app.use(cors({ origin: 'http://localhost:5173' }));
app.use(express.json());

// Self-hosted mode (recommended) - requires facilitatorUrl + facilitatorSecret
const pay = mantlePaywall({
  priceUsd: 0.01,
  payTo: process.env.PAY_TO!,
  facilitatorUrl: process.env.FACILITATOR_URL!,  // Your facilitator
  facilitatorSecret: process.env.FACILITATOR_SECRET!,  // Required for security
  onPaymentSettled: (entry) => {
    console.log(`Payment received: ${entry.valueAtomic} from ${entry.from}`);
    console.log(`Transaction: ${entry.txHash}`);
  }
});

// Protected endpoint
app.post('/api/generate', pay, async (req, res) => {
  const { prompt } = req.body;

  // Simulate AI generation
  await new Promise(resolve => setTimeout(resolve, 1000));

  res.json({
    result: `Generated content for: ${prompt}`,
    timestamp: Date.now()
  });
});

// Health check
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Client (client/src/App.tsx)

import { useState } from 'react';
import { useMantleX402, useEthersWallet } from '@puga-labs/x402-mantle-sdk/react';

const API_URL = 'http://localhost:3001';

function App() {
  const [prompt, setPrompt] = useState('');
  const [result, setResult] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const wallet = useEthersWallet();

  // facilitatorUrl auto-detected from backend 402 response
  const { postWithPayment } = useMantleX402({
    resourceUrl: API_URL
  });

  const handleConnect = async () => {
    try {
      await wallet.connect();
    } catch (err: any) {
      setError(err.message);
    }
  };

  const handleGenerate = async () => {
    if (!prompt.trim()) return;

    setLoading(true);
    setError(null);
    setResult(null);

    try {
      const { response, txHash } = await postWithPayment<{ result: string }>(
        '/api/generate',
        { prompt }
      );

      setResult(response.result);
      console.log('Transaction:', txHash);
    } catch (err: any) {
      if (err.code === 4001) {
        setError('Transaction cancelled');
      } else {
        setError(err.message);
      }
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ padding: 20, maxWidth: 600, margin: '0 auto' }}>
      <h1>x402 Demo</h1>

      {/* Wallet Connection */}
      {!wallet.isConnected ? (
        <button onClick={handleConnect}>Connect Wallet</button>
      ) : (
        <p>Connected: {wallet.address?.slice(0, 6)}...{wallet.address?.slice(-4)}</p>
      )}

      {/* Input */}
      <div style={{ marginTop: 20 }}>
        <input
          type="text"
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Enter your prompt..."
          style={{ width: '100%', padding: 10 }}
        />
      </div>

      {/* Generate Button */}
      <button
        onClick={handleGenerate}
        disabled={!wallet.isConnected || loading || !prompt.trim()}
        style={{ marginTop: 10 }}
      >
        {loading ? 'Processing...' : 'Generate ($0.01)'}
      </button>

      {/* Error */}
      {error && (
        <div style={{ color: 'red', marginTop: 10 }}>{error}</div>
      )}

      {/* Result */}
      {result && (
        <div style={{ marginTop: 20, padding: 15, background: '#f0f0f0' }}>
          <strong>Result:</strong>
          <p>{result}</p>
        </div>
      )}
    </div>
  );
}

export default App;

Environment Variables

Server (.env)

PAY_TO=0xYourWalletAddress
FACILITATOR_URL=https://your-facilitator.com
FACILITATOR_SECRET=fac_xxx...  # Copy from facilitator .env

React + Express (Hosted Facilitator)

Server (server/src/index.ts)

import express from 'express';
import cors from 'cors';
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';

const app = express();
app.use(cors({ origin: 'http://localhost:5173' }));
app.use(express.json());

// Hosted mode - no facilitatorUrl needed!
const pay = mantlePaywall({
  priceUsd: 0.01,
  payTo: process.env.PAY_TO!,
  projectKey: process.env.PROJECT_KEY,  // Get from dashboard
  onPaymentSettled: (entry) => {
    console.log(`Payment: ${entry.txHash}`);
  }
});

app.post('/api/generate', pay, async (req, res) => {
  const { prompt } = req.body;
  res.json({ result: `Generated: ${prompt}` });
});

app.listen(3001, () => console.log('Server on :3001'));

Client

Same as self-hosted example - client code doesn't change! The facilitator URL is auto-detected from the backend's 402 response.

Environment Variables

Server (.env)

PAY_TO=0xYourWalletAddress
PROJECT_KEY=pk_xxx  # From dashboard

Next.js Full Stack (Self-Hosted)

API Route (app/api/generate/route.ts)

import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/nextjs';
import { NextRequest, NextResponse } from 'next/server';

// Self-hosted mode (recommended)
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 paid resource logic here
  const result = `Generated: ${prompt}`;

  return NextResponse.json({ result });
});

Client Component (components/PaymentButton.tsx)

'use client';

import { useState } from 'react';
import { useMantleX402, useEthersWallet } from '@puga-labs/x402-mantle-sdk/react';

export function PaymentButton() {
  const [result, setResult] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const wallet = useEthersWallet();

  // No configuration needed - auto-detected from backend!
  const { postWithPayment } = useMantleX402();

  const handleGenerate = async () => {
    setLoading(true);
    try {
      const { response } = await postWithPayment<{ result: string }>(
        '/api/generate',
        { prompt: 'Hello world' }
      );
      setResult(response.result);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  if (!wallet.isConnected) {
    return <button onClick={wallet.connect}>Connect Wallet</button>;
  }

  return (
    <div>
      <button onClick={handleGenerate} disabled={loading}>
        {loading ? 'Processing...' : 'Generate ($0.01)'}
      </button>
      {result && <p>{result}</p>}
    </div>
  );
}

Page (app/page.tsx)

import { PaymentButton } from '@/components/PaymentButton';

export default function Home() {
  return (
    <main>
      <h1>My x402 App</h1>
      <PaymentButton />
    </main>
  );
}

Environment Variables (.env.local)

PAY_TO=0xYourWalletAddress
FACILITATOR_URL=https://your-facilitator.com
FACILITATOR_SECRET=fac_xxx...  # Copy from facilitator .env

Next.js Full Stack (Hosted)

API Route (app/api/generate/route.ts)

import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/nextjs';
import { NextRequest, NextResponse } from 'next/server';

// Hosted mode - no facilitatorUrl needed!
const pay = mantlePaywall({
  priceUsd: 0.01,
  payTo: process.env.PAY_TO!,
  projectKey: process.env.PROJECT_KEY  // Get from dashboard
});

export const POST = pay(async (req: NextRequest) => {
  const { prompt } = await req.json();
  return NextResponse.json({ result: `Generated: ${prompt}` });
});

Client

Same as self-hosted example - client code doesn't change!

Environment Variables (.env.local)

PAY_TO=0xYourWalletAddress
PROJECT_KEY=pk_xxx  # From dashboard

Vanilla JS + Node.js (Self-Hosted)

Server (server.js)

import express from 'express';
import cors from 'cors';
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('public'));

// Self-hosted mode (recommended)
const pay = mantlePaywall({
  priceUsd: 0.01,
  payTo: process.env.PAY_TO,
  facilitatorUrl: process.env.FACILITATOR_URL,
  facilitatorSecret: process.env.FACILITATOR_SECRET
});

app.post('/api/generate', pay, (req, res) => {
  res.json({ result: 'Success!' });
});

app.listen(3000, () => console.log('Server on :3000'));

Client (public/index.html)

<!DOCTYPE html>
<html>
<head>
  <title>x402 Vanilla Example</title>
  <script type="module">
    import { createMantleClient } from 'https://esm.sh/@puga-labs/x402-mantle-sdk/client';

    let userAddress = null;

    // Connect wallet
    document.getElementById('connect').onclick = async () => {
      const accounts = await window.ethereum.request({
        method: 'eth_requestAccounts'
      });
      userAddress = accounts[0];
      document.getElementById('address').textContent = userAddress;
    };

    // Make payment
    document.getElementById('pay').onclick = async () => {
      // facilitatorUrl auto-detected from backend 402 response
      const client = createMantleClient({
        resourceUrl: window.location.origin,
        getProvider: () => window.ethereum,
        getAccount: () => userAddress
      });

      try {
        const { response, txHash } = await client.postWithPayment('/api/generate', {
          prompt: 'Hello'
        });
        document.getElementById('result').textContent = JSON.stringify(response);
        console.log('TxHash:', txHash);
      } catch (error) {
        alert(error.message);
      }
    };
  </script>
</head>
<body>
  <h1>x402 Vanilla Example</h1>
  <button id="connect">Connect Wallet</button>
  <p>Address: <span id="address">Not connected</span></p>
  <button id="pay">Pay $0.01</button>
  <pre id="result"></pre>
</body>
</html>

Environment Variables (.env)

PAY_TO=0xYourWalletAddress
FACILITATOR_URL=https://your-facilitator.com
FACILITATOR_SECRET=fac_xxx...

Vanilla JS + Node.js (Hosted)

Server (server.js)

import express from 'express';
import cors from 'cors';
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('public'));

// Hosted mode
const pay = mantlePaywall({
  priceUsd: 0.01,
  payTo: process.env.PAY_TO,
  projectKey: process.env.PROJECT_KEY  // Get from dashboard
});

app.post('/api/generate', pay, (req, res) => {
  res.json({ result: 'Success!' });
});

app.listen(3000, () => console.log('Server on :3000'));

Client

Same as self-hosted example - client code doesn't change!

Environment Variables (.env)

PAY_TO=0xYourWalletAddress
PROJECT_KEY=pk_xxx  # From dashboard

Key Differences: Self-Hosted vs Hosted

AspectSelf-Hosted (Recommended)Hosted
Server configfacilitatorUrl` + `facilitatorSecret`projectKey` only
Client configNone (auto-detected)None (auto-detected)
Gas feesYou payWe pay
Per-tx feeFree1 MNT = 100 tx
ControlFullStandard

Next Steps

  • FAQ — Common questions and troubleshooting
  • AI Prompt — Integration prompt for AI assistants