SDK Plugins
Extend Relai.protect() with plugins that run before payment checks. Give buyers free API calls, add rate limits, inject headers, or build custom logic.
Plugins hook into the Relai.protect() middleware at two points:
- beforePaymentCheck — runs before the 402 response. Can skip payment entirely (e.g. free tier).
- afterSettled — runs after successful payment. Use for analytics, logging, webhooks.
If any plugin returns { skip: true } from beforePaymentCheck, the request bypasses payment and goes straight to your handler. The request is marked with req.x402Free = true so you can differentiate free vs paid calls.
Installation
Plugins are included in the @relai-fi/x402 package. No extra dependencies needed.
npm install @relai-fi/x402import Relai from '@relai-fi/x402/server'
import { freeTier } from '@relai-fi/x402/plugins'Free Tier Plugin
The built-in freeTier plugin lets you offer a number of free API calls per buyer before x402 payment kicks in. Works in two modes: cloud (with service key — synced to RelAI dashboard) or local (without key — in-memory tracking, exportable).
Service key optional. With a key, usage syncs to the Dashboard. Without a key, the plugin runs locally in-memory — same logic, exportable data.
Quick Start
import Relai from '@relai-fi/x402/server'
import { freeTier } from '@relai-fi/x402/plugins'
const relai = new Relai({
network: 'base',
plugins: [
freeTier({
serviceKey: process.env.RELAI_SERVICE_KEY!,
perBuyerLimit: 5,
resetPeriod: 'daily',
}),
],
})
app.get('/api/data', relai.protect({
payTo: '0xYourWallet',
price: 0.01,
}), (req, res) => {
if (req.x402Free) {
// Free tier call — no payment was made
console.log('Remaining:', req.pluginMeta?.remaining)
}
res.json({ data: 'content' })
})Configuration
| Option | Type | Default | Description |
|---|---|---|---|
serviceKey | string | — | Optional. Your sk_live_... key. Omit to run in local in-memory mode. |
perBuyerLimit | number | — | Required. Free calls per buyer per reset period |
resetPeriod | 'none' | 'daily' | 'monthly' | 'none' | When per-buyer counters reset (none = permanent limit, no reset) |
globalCap | number | — | Optional cap across all buyers combined |
paths | string[] | ['*'] | Paths to apply free tier to (* = all) |
baseUrl | string | https://api.relai.fi | Override RelAI API base URL |
cacheTtlMs | number | 5000 | Cache TTL for check results (ms) |
Local vs Cloud Mode
| Feature | Cloud (with serviceKey) | Local (no serviceKey) |
|---|---|---|
| Usage storage | RelAI backend (persistent) | In-memory (per process) |
| Dashboard stats | Yes | No |
| Survives restart | Yes | No (resets on restart) |
| Data export | Via dashboard API | plugin.getUsageData() |
| Setup | Needs service key | Zero config |
Local mode example — no key needed, usage tracked in-memory:
const ft = freeTier({ perBuyerLimit: 3, resetPeriod: 'none' }) // every new caller gets 3 lifetime free calls
const relai = new Relai({ network: 'base', plugins: [ft] })
// Export usage data at any time
app.get('/admin/usage', (req, res) => {
res.json(ft.getUsageData()) // returns all tracked usage
})In cloud mode, getUsageData() returns null — use the dashboard instead. You can check which mode is active via ft.mode ('local' or 'cloud').
Buyer Identification
The plugin resolves buyer identity automatically from the request, in priority order:
| Priority | Source | Format | Example |
|---|---|---|---|
| 1 | JWT Bearer token (sub claim) | user:<sub> | user:abc123 |
| 2 | X-Wallet-Address header | wallet:<addr> | wallet:0x1234... |
| 3 | IP address fallback | ip:<addr> | ip:192.168.1.1 |
IP fallback. If no JWT or wallet header is present, the plugin falls back to the client IP. Behind a reverse proxy, make sure X-Forwarded-For is set correctly, or buyers behind the same NAT will share a single free-tier allowance.
How It Works
When a request arrives without a payment header, the plugin calls the RelAI backend (POST /v1/plugins/free-tier/check) to see if the buyer has free calls remaining. If yes, the request skips payment and the call is recorded. If not, the normal 402 flow kicks in.
If the request already has a payment header (client is paying), plugins are skipped entirely — payment is processed as usual.
Response Headers
When a request is served via free tier, the following headers are added to the response:
| Header | Description |
|---|---|
X-Free-Calls-Remaining | Free calls left for this buyer in current period |
X-Free-Calls-Total | Total free calls per period (= perBuyerLimit) |
X-Free-Calls-Global-Remaining | Global cap remaining (only if globalCap is set) |
Request Properties
The plugin attaches metadata to the request object so your handler can detect free vs paid calls:
| Property | Type | Description |
|---|---|---|
req.x402Free | boolean | true if free tier bypass |
req.x402Paid | boolean | false on free bypass, true after payment |
req.x402Plugin | string | Plugin name that triggered skip ('free-tier') |
req.pluginMeta | object | { freeTier: true, buyerId, remaining, total } |
app.get('/api/data', relai.protect({ payTo, price: 0.01 }), (req, res) => {
if (req.x402Free) {
// Free tier access
const remaining = req.pluginMeta?.remaining
return res.json({ data: '...', freeTier: true, remaining })
}
// Paid access
res.json({ data: '...', payment: req.payment })
})Bridge Plugin
The bridge() plugin lets buyers pay from any supported chain, even when your API only accepts a single network. The SDK detects the wallet/network mismatch and automatically routes payment through the RelAI bridge — the buyer signs one transaction on their chain.
Minimal config. The plugin auto-discovers bridge capabilities from the RelAI backend. Pass your serviceKey to track bridge usage per API in the dashboard.
Quick Start
import Relai from '@relai-fi/x402/server'
import { bridge } from '@relai-fi/x402/plugins'
const relai = new Relai({
network: 'skale-bite',
plugins: [
bridge({
serviceKey: process.env.RELAI_SERVICE_KEY,
}),
],
})
app.get('/api/data', relai.protect({
payTo: '0xYourWallet',
price: 0.05,
}), (req, res) => {
// Buyers on Solana, SKALE Base, or Base can pay here
res.json({ data: 'premium content' })
})That's it. Buyers on Solana, SKALE Base, or Base can now pay for your endpoint.
How It Works
- Plugin discovers bridge capabilities — on startup,
bridge()calls/bridge/infoand learns which source chains, wallets, and fees are available. - 402 response includes bridge info — when a buyer hits your endpoint without payment, the plugin enriches the 402 response with
extensions.bridgecontaining supported source chains and the settle endpoint. - Client detects mismatch — the buyer's wallet is on Solana but the API accepts SKALE. The client SDK sees
extensions.bridgeand finds Solana as a supported source. - Payment routes through bridge — the buyer signs a USDC transfer on their chain. The SDK sends it to
/bridge/settle, which settles source and pays out on the target chain. - Server trusts bridge proof — the SDK retries the request with an
X-PAYMENTheader containingbridged: true. The server verifies and lets the request through.
Configuration
Pass your service key to enable per-key bridge analytics in the dashboard. Other options are auto-discovered.
| Option | Type | Default | Description |
|---|---|---|---|
serviceKey | string | — | Recommended. Your sk_live_... service key for tracking bridge usage in dashboard |
baseUrl | string | auto | Override RelAI backend URL |
settleEndpoint | string | /bridge/settle | Custom settle endpoint path |
feeBps | number | 100 | Bridge fee in basis points (100 = 1%) |
Supported Chains
| Chain | Direction | Token |
|---|---|---|
| Solana | Source & Target | USDC |
| SKALE Base | Source & Target | USDC |
| Base | Source & Target | USDC |
New chains are added on the backend — the plugin picks them up automatically via /bridge/info. No SDK update needed.
Combining with Free Tier
Bridge works alongside other plugins. Free tier runs first — if the buyer has free calls left, they bypass payment. If not, the 402 includes bridge info so they can pay from any chain.
import Relai from '@relai-fi/x402/server'
import { freeTier, bridge } from '@relai-fi/x402/plugins'
const relai = new Relai({
network: 'skale-bite',
plugins: [
freeTier({
serviceKey: process.env.RELAI_SERVICE_KEY!,
perBuyerLimit: 5,
resetPeriod: 'daily',
}),
bridge({
serviceKey: process.env.RELAI_SERVICE_KEY!,
}),
],
})Dashboard Management
You can view and manage free tier configuration and usage from the Dashboard → SDK Plugins page. The dashboard allows you to:
- View current configuration per service key
- See per-buyer usage and remaining free calls
- Reset usage counters
- Update configuration (limits, reset period, paths)
Configuration set via the SDK (freeTier({...})) syncs automatically to the dashboard on first request. You can override settings in the dashboard at any time.
Custom Plugins
Build your own plugins by implementing the RelaiPlugin interface. Plugins are evaluated in array order — the first plugin that returns { skip: true } wins.
Plugin Interface
| Hook | When | Return |
|---|---|---|
name | — | Unique string identifier |
onInit() | Once, before first request | Promise<void> |
beforePaymentCheck(req, ctx) | Before 402 response | Promise<PluginResult> |
afterSettled(req, result, ctx) | After payment settlement | Promise<void> |
The PluginContext passed to hooks contains: network, price, path, method.
PluginResult can include: skip (boolean), headers (added to response), meta (attached to req.pluginMeta).
Example: Custom Rate Limit Plugin
import type { RelaiPlugin } from '@relai-fi/x402/plugins'
const rateLimitPlugin: RelaiPlugin = {
name: 'rate-limit',
async beforePaymentCheck(req, ctx) {
const ip = req.ip || req.socket?.remoteAddress || 'unknown'
const count = await getRequestCount(ip, ctx.path)
if (count > 100) {
// Return skip: false (or empty) to require payment
return {}
}
// Under limit — allow free access
return {
skip: true,
headers: { 'X-Rate-Remaining': String(100 - count) },
meta: { rateLimit: true, remaining: 100 - count },
}
},
async afterSettled(req, result, ctx) {
// Log paid requests for analytics
console.log(`Paid $${ctx.price} by ${result.payer} on ${ctx.network}`)
},
async onInit() {
// Called once on first request — setup, sync config, etc.
console.log('Rate limit plugin initialized')
},
}Non-blocking. If a plugin throws an error in beforePaymentCheck, the error is caught and logged — the request falls through to the normal 402 flow. Plugins never break payment processing.
Environment Variables
Recommended environment setup for the Free Tier plugin:
RELAI_SERVICE_KEY=sk_live_your_key_here
FREE_TIER_LIMIT=5
FREE_TIER_RESET=daily| Variable | Description |
|---|---|
RELAI_SERVICE_KEY | Your service key from the Dashboard |
FREE_TIER_LIMIT | Free calls per buyer per reset period |
FREE_TIER_RESET | none, daily, or monthly |
Never hardcode your service key. Always use environment variables. The key grants full access to your plugin configuration and usage data.
Next.js Example
For Next.js App Router (Route Handlers), adapt the Express-like request/response interface:
import { NextRequest, NextResponse } from 'next/server'
import Relai from '@relai-fi/x402/server'
import { freeTier } from '@relai-fi/x402/plugins'
const relai = new Relai({
network: 'base',
plugins: [
freeTier({
serviceKey: process.env.RELAI_SERVICE_KEY!,
perBuyerLimit: 5,
resetPeriod: 'daily',
}),
],
})
export async function GET(req: NextRequest) {
const middleware = relai.protect({
payTo: '0xYourWallet',
price: 0.01,
})
// Adapt NextRequest → Express-like request
const clientIp = req.headers.get('x-forwarded-for')
?.split(',')[0]?.trim() || '127.0.0.1'
const mockReq = {
headers: Object.fromEntries(req.headers.entries()),
path: '/api/data',
ip: clientIp,
socket: { remoteAddress: clientIp },
protocol: 'https',
get: (h: string) => req.headers.get(h),
originalUrl: '/api/data',
}
// ... run middleware and return response
}Make sure to set path, ip, and socket.remoteAddress on the mock request so the plugin can resolve the buyer identity correctly.