@puga-labs/x402-mantle-sdk
v0.4.2
Published
x402 payments SDK for Mantle (USDC, gasless, facilitator-based)
Readme
@puga-labs/x402-mantle-sdk
SDK for gasless x402 payments on Mantle blockchain using USDC (EIP-3009). Protect your APIs with crypto payments — users sign, facilitator pays gas.
Installation
npm install @puga-labs/x402-mantle-sdkFeatures
- React hooks (
useMantleX402,useEthersWallet) - Vanilla JS client (
createMantleClient) - Server middleware for Express, Next.js, Hono, Cloudflare Workers, Deno
- Full TypeScript support
- Automatic payment flow handling
- Analytics & telemetry integration
Client-Side Usage
React (Recommended)
import { useMantleX402 } from '@puga-labs/x402-mantle-sdk/react';
function PaymentButton() {
// facilitatorUrl auto-detected from backend 402 response
const { postWithPayment } = useMantleX402();
const handlePay = async () => {
try {
const { response, txHash } = await postWithPayment('/api/generate', {
prompt: 'Hello world'
});
console.log('Result:', response);
console.log('Transaction:', txHash);
} catch (error) {
console.error('Payment failed:', error);
}
};
return <button onClick={handlePay}>Generate ($0.01)</button>;
}Wallet Hook
Separate hook for wallet management:
import { useEthersWallet } from '@puga-labs/x402-mantle-sdk/react';
function WalletConnect() {
const {
address,
isConnected,
chainId,
connect,
disconnect,
error
} = useEthersWallet({ autoConnect: false });
if (isConnected) {
return (
<div>
<p>Connected: {address}</p>
<p>Chain ID: {chainId}</p>
<button onClick={disconnect}>Disconnect</button>
</div>
);
}
return <button onClick={connect}>Connect Wallet</button>;
}Vanilla JavaScript
import { createMantleClient } from '@puga-labs/x402-mantle-sdk/client';
// Create client (facilitatorUrl auto-detected from backend)
const client = createMantleClient({
resourceUrl: 'https://your-api.com',
getProvider: () => window.ethereum,
getAccount: () => userAddress
});
// Make paid request
const { response, txHash } = await client.postWithPayment('/api/generate', {
prompt: 'Hello world'
});Low-Level Payment Client
For full control over the payment flow:
import { createPaymentClient } from '@puga-labs/x402-mantle-sdk/client';
// Low-level client (facilitatorUrl auto-detected from backend)
const paymentClient = createPaymentClient({
resourceUrl: 'https://your-api.com',
provider: window.ethereum,
userAddress: '0x...' // Optional
});
const result = await paymentClient.callWithPayment('/api/endpoint', {
data: 'payload'
});Server-Side Usage
Express.js
import express from 'express';
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';
const app = express();
app.use(express.json());
// Create paywall middleware (hosted mode - facilitatorUrl auto-detected)
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWalletAddress',
projectKey: 'pk_xxx' // Get from https://x402mantlesdk.xyz/dashboard
});
// Protected route
app.post('/api/generate', pay, async (req, res) => {
const { prompt } = req.body;
// Your logic here - only runs after payment
res.json({ result: 'Generated content' });
});
// Unprotected route
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000);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: '0xYourWalletAddress',
projectKey: 'pk_xxx' // Get from dashboard
});
export const POST = pay(async (req: NextRequest) => {
const { prompt } = await req.json();
// Your logic here
return NextResponse.json({ result: 'Generated content' });
});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: '0xYourWalletAddress',
projectKey: 'pk_xxx' // Get from dashboard
});
app.post('/api/generate', pay(async (c) => {
const { prompt } = await c.req.json();
return c.json({ result: 'Generated content' });
}));
export default app;Cloudflare Workers
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/web';
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWalletAddress',
projectKey: 'pk_xxx' // Get from dashboard
});
export default {
async fetch(request: Request): Promise<Response> {
if (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('Method not allowed', { status: 405 });
}
};Deno
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/web';
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWalletAddress',
projectKey: 'pk_xxx' // Get from dashboard
});
Deno.serve(pay(async (req) => {
const body = await req.json();
return new Response(JSON.stringify({ result: 'success' }), {
headers: { 'Content-Type': 'application/json' }
});
}));Configuration
Client Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| facilitatorUrl | string | From 402 response | Facilitator service URL (auto-detected from backend) |
| resourceUrl | string | window.location.origin | Your API base URL |
| projectKey | string | From 402 response | Project key (auto-detected from backend) |
| getProvider | () => EIP1193Provider | - | Function returning wallet provider |
| getAccount | () => string | - | Function returning user address |
Note: Both facilitatorUrl and projectKey are automatically passed from your backend via 402 responses, so clients typically don't need to configure them.
Server Options (mantlePaywall)
| 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 | Your facilitator URL (defaults to hosted if not set) |
| facilitatorSecret | string | For self-hosted | Shared secret (when set, requires facilitatorUrl) |
| onPaymentSettled | function | No | Callback when payment is verified |
| telemetry | object | No | Analytics configuration (auto-uses projectKey if not set) |
Hosted vs Self-Hosted Facilitator
Hosted Facilitator (Recommended)
Use our managed facilitator service - zero URL config needed:
// Minimal setup - facilitatorUrl defaults to hosted service!
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWallet',
projectKey: 'pk_xxx' // Get from dashboard (for billing + analytics)
});Get your project key from Dashboard.
Note: projectKey is automatically passed to clients via 402 responses, so they don't need to configure it separately.
Self-Hosted Facilitator
Run your own facilitator for full control and cost savings:
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWallet',
facilitatorUrl: 'https://your-facilitator.com', // Required for self-hosted
facilitatorSecret: process.env.FACILITATOR_SECRET! // Required for self-hosted
});
// Optional: add projectKey for telemetry
const payWithTelemetry = mantlePaywall({
priceUsd: 0.01,
payTo: '0xYourWallet',
facilitatorUrl: 'https://your-facilitator.com',
facilitatorSecret: process.env.FACILITATOR_SECRET!,
projectKey: 'pk_xxx' // Optional: for analytics
});The facilitatorSecret is required for self-hosted facilitators to prevent unauthorized usage. When facilitatorSecret is provided, facilitatorUrl is also required.
Create a facilitator with:
npx create-mantle-facilitatorAfter setup, copy FACILITATOR_SECRET from the generated .env file to your backend's environment variables.
Analytics & Telemetry
Track payments with built-in analytics:
const pay = mantlePaywall({
priceUsd: 0.01,
payTo: '0x...',
projectKey: 'pk_xxx', // Get from dashboard - used for both billing AND analytics
// Optional: Advanced telemetry config
telemetry: {
debug: true // Enable console logging (projectKey auto-derived from above)
},
// Callback on each successful payment
onPaymentSettled: (entry) => {
console.log('Payment received!');
console.log('Amount:', entry.valueAtomic);
console.log('From:', entry.from);
console.log('To:', entry.to);
console.log('TxHash:', entry.txHash);
console.log('Route:', entry.route);
}
});PaymentLogEntry Fields
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique payment identifier (nonce) |
| from | string | Payer's wallet address |
| to | string | Recipient's wallet address |
| valueAtomic | string | Amount in atomic units |
| network | string | Network ID |
| asset | string | Token contract address |
| route | string | API route (e.g., POST /api/generate) |
| txHash | string | Blockchain transaction hash |
| timestamp | number | Unix timestamp (ms) |
TypeScript Types
import type {
// Shared types
PaymentRequirements,
Authorization,
PaymentHeaderObject,
NetworkId,
// Client types
MantleClient,
MantleClientConfig,
PaymentClient,
PaymentClientConfig,
CallWithPaymentResult,
// Server types
PaymentLogEntry,
TelemetryConfig,
MinimalPaywallOptions,
// React types
UseMantleX402Options,
UseEthersWalletOptions,
UseEthersWalletReturn
} from '@puga-labs/x402-mantle-sdk';Import Paths
// Main exports (types + constants)
import { MANTLE_DEFAULTS } from '@puga-labs/x402-mantle-sdk';
// Client
import { createMantleClient, createPaymentClient } from '@puga-labs/x402-mantle-sdk/client';
// React hooks
import { useMantleX402, useEthersWallet } from '@puga-labs/x402-mantle-sdk/react';
// Server - Express
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';
// Server - Next.js
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/nextjs';
// Server - Web Standards (Hono, CF Workers, Deno)
import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/web';Network Configuration
| Network | Chain ID | USDC Address | Decimals |
|---------|----------|--------------|----------|
| Mantle Mainnet | 5000 | 0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9 | 6 |
Supported Tokens
Currently, the SDK supports USDC only for gasless payments.
Why only USDC?
Gasless payments via facilitator require tokens that implement the EIP-3009 standard (transferWithAuthorization). This standard allows users to sign a payment authorization off-chain, while the facilitator submits the transaction and pays gas fees.
On Mantle mainnet, USDC is currently the only token that implements EIP-3009. As other stablecoins and tokens adopt this standard, we will add support for them in the SDK and facilitator.
| Token | EIP-3009 Support | Status | |-------|------------------|--------| | USDC | Yes | Supported | | USDT | No | Not available | | DAI | No | Not available | | Other tokens | — | Pending EIP-3009 adoption |
Price Conversion
priceUsd: 0.01= 1 cent = 10,000 atomic units- Formula:
atomicUnits = priceUsd * 10^6
Error Handling
Client-Side Errors
try {
const { response, txHash } = await postWithPayment('/api/generate', data);
} catch (error) {
if (error.code === 4001) {
// User rejected the transaction
console.log('User cancelled');
} else if (error.code === -32603) {
// Internal error (insufficient funds, etc.)
console.log('Transaction failed:', error.message);
} else {
// Network or facilitator error
console.log('Error:', error.message);
}
}Server-Side Error Responses
// 402 Payment Required
{
"error": "Payment Required",
"paymentRequirements": {
"scheme": "exact",
"network": "mantle-mainnet",
"asset": "0x09Bc4E0D...",
"maxAmountRequired": "10000",
"payTo": "0x...",
"price": "$0.01",
"currency": "USD"
}
}
// 400 Invalid Payment
{
"error": "Invalid payment",
"invalidReason": "Payment amount too low"
}Links
License
MIT
