SDK Plugins

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.

Free Tier built-inCustom hooksDashboard managementNon-blocking

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.

Terminal
npm install @relai-fi/x402
Imports
import 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

server.js
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

OptionTypeDefaultDescription
serviceKeystringOptional. Your sk_live_... key. Omit to run in local in-memory mode.
perBuyerLimitnumberRequired. Free calls per buyer per reset period
resetPeriod'none' | 'daily' | 'monthly''none'When per-buyer counters reset (none = permanent limit, no reset)
globalCapnumberOptional cap across all buyers combined
pathsstring[]['*']Paths to apply free tier to (* = all)
baseUrlstringhttps://api.relai.fiOverride RelAI API base URL
cacheTtlMsnumber5000Cache TTL for check results (ms)

Local vs Cloud Mode

FeatureCloud (with serviceKey)Local (no serviceKey)
Usage storageRelAI backend (persistent)In-memory (per process)
Dashboard statsYesNo
Survives restartYesNo (resets on restart)
Data exportVia dashboard APIplugin.getUsageData()
SetupNeeds service keyZero 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:

PrioritySourceFormatExample
1JWT Bearer token (sub claim)user:<sub>user:abc123
2X-Wallet-Address headerwallet:<addr>wallet:0x1234...
3IP address fallbackip:<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

1
Request arrives
No payment header
2
Plugin checks
Calls RelAI /check endpoint
3
Free? Skip 402
Sets req.x402Free = true
4
Exhausted?
Returns 402 as normal

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:

HeaderDescription
X-Free-Calls-RemainingFree calls left for this buyer in current period
X-Free-Calls-TotalTotal free calls per period (= perBuyerLimit)
X-Free-Calls-Global-RemainingGlobal 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:

PropertyTypeDescription
req.x402Freebooleantrue if free tier bypass
req.x402Paidbooleanfalse on free bypass, true after payment
req.x402PluginstringPlugin name that triggered skip ('free-tier')
req.pluginMetaobject{ freeTier: true, buyerId, remaining, total }
Usage in handler
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

server.js
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

  1. Plugin discovers bridge capabilities — on startup, bridge() calls/bridge/info and learns which source chains, wallets, and fees are available.
  2. 402 response includes bridge info — when a buyer hits your endpoint without payment, the plugin enriches the 402 response with extensions.bridge containing supported source chains and the settle endpoint.
  3. Client detects mismatch — the buyer's wallet is on Solana but the API accepts SKALE. The client SDK sees extensions.bridge and finds Solana as a supported source.
  4. 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.
  5. Server trusts bridge proof — the SDK retries the request with an X-PAYMENT header containing bridged: 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.

OptionTypeDefaultDescription
serviceKeystringRecommended. Your sk_live_... service key for tracking bridge usage in dashboard
baseUrlstringautoOverride RelAI backend URL
settleEndpointstring/bridge/settleCustom settle endpoint path
feeBpsnumber100Bridge fee in basis points (100 = 1%)

Supported Chains

ChainDirectionToken
SolanaSource & TargetUSDC
SKALE BaseSource & TargetUSDC
BaseSource & TargetUSDC

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.

Combined setup
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

HookWhenReturn
nameUnique string identifier
onInit()Once, before first requestPromise<void>
beforePaymentCheck(req, ctx)Before 402 responsePromise<PluginResult>
afterSettled(req, result, ctx)After payment settlementPromise<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

rate-limit-plugin.ts
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:

.env
RELAI_SERVICE_KEY=sk_live_your_key_here
FREE_TIER_LIMIT=5
FREE_TIER_RESET=daily
VariableDescription
RELAI_SERVICE_KEYYour service key from the Dashboard
FREE_TIER_LIMITFree calls per buyer per reset period
FREE_TIER_RESETnone, 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:

app/api/data/route.ts
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.