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
| Aspect | Self-Hosted (Recommended) | Hosted |
|---|---|---|
| Server config | facilitatorUrl` + `facilitatorSecret | `projectKey` only |
| Client config | None (auto-detected) | None (auto-detected) |
| Gas fees | You pay | We pay |
| Per-tx fee | Free | 1 MNT = 100 tx |
| Control | Full | Standard |