README.md

March 28, 2026 · View on GitHub

@relai-fi/x402

Unified x402 payment SDK for Solana and EVM networks, including SKALE Base Sepolia.

npm Node Marketplace

Browse APIs →


What is x402?

x402 is a protocol for HTTP-native micropayments. When a server returns HTTP 402 Payment Required, it includes payment details in the response. The client signs a payment, retries the request, and the server settles the payment and returns the protected content.

This SDK handles the entire flow automatically — call fetch() and payments happen transparently.


Why This SDK?

Multi-chain. Solana, Base, Avalanche, SKALE Base, SKALE Base Sepolia, SKALE BITE, Polygon, and Ethereum with a single API. Connect your wallets and the SDK picks the right chain and signing method automatically.

Zero gas fees. The RelAI facilitator sponsors gas — users only pay for content (USDC).

Auto-detects signing method. EIP-3009 transferWithAuthorization for all supported EVM networks and native SPL transfer for Solana, all handled internally.

Works out of the box. Uses the RelAI facilitator by default.


Examples

Try a live end-to-end flow in the RelAI Playground:


Quick Start

Install

npm install @relai-fi/x402

Client (Browser / Node.js)

import { createX402Client } from '@relai-fi/x402/client';

const client = createX402Client({
  wallets: {
    solana: solanaWallet,  // @solana/wallet-adapter compatible
    evm: evmWallet,        // wagmi/viem compatible
  },
  preferredNetwork: 'base',
  // default: 'prefer_then_any'
  // - prefer_then_any: try preferred network, then any payable wallet/network
  // - strict_preferred: fail if preferred network isn't payable with connected wallets
  networkSelectionMode: 'prefer_then_any',
  integritas: {
    enabled: true,
    flow: 'single', // or 'dual'
  },
  relayWs: {
    enabled: true,
    // optional: explicit WS endpoint
    // wsUrl: 'wss://api.relai.fi/api/ws/relay',
  },
});

// 402 responses are handled automatically
const response = await client.fetch('https://api.example.com/protected');
const data = await response.json();

Preferred network behavior

If the 402 challenge contains multiple accepts networks:

  • networkSelectionMode: 'prefer_then_any' (default) tries preferredNetwork first, then falls back to any payable option.
  • networkSelectionMode: 'strict_preferred' only allows the preferred network and throws if it's not payable with connected wallets.
const client = createX402Client({
  wallets: { evm: evmWallet },
  preferredNetwork: 'solana',
  networkSelectionMode: 'strict_preferred',
});

// Throws if only Solana accept is preferred but no Solana wallet is connected.
await client.fetch('https://api.example.com/protected');

Integritas (client)

createX402Client can set Integritas headers automatically for every request.

const client = createX402Client({
  wallets: { evm: evmWallet },
  integritas: {
    enabled: true,
    flow: 'single',
  },
});

// Sends:
// X-Integritas: true
// X-Integritas-Flow: single
await client.fetch('https://api.relai.fi/relay/<apiId>/v1/chat/completions');

// Per-request override
await client.fetch('https://api.relai.fi/relay/<apiId>/v1/chat/completions', {
  method: 'POST',
  x402: {
    integritas: { enabled: true, flow: 'dual' },
  },
});

AI Agents

Use defaultHeaders to attach agent identity headers to every request — no need to set them manually on each call.

import { createX402Client } from '@relai-fi/x402/client';
import { Keypair } from '@solana/web3.js';

// Agent wallet (e.g. loaded from env or key management)
const keypair = Keypair.fromSecretKey(Buffer.from(process.env.AGENT_PRIVATE_KEY!, 'base64'));

const agentWallet = {
  publicKey: keypair.publicKey,
  signTransaction: async (tx: any) => {
    tx.sign([keypair]);
    return tx;
  },
};

const client = createX402Client({
  wallets: { solana: agentWallet },
  preferredNetwork: 'solana',
  // Attach agent identity headers to every request automatically
  defaultHeaders: {
    'X-Service-Key': process.env.RELAI_SERVICE_KEY!,  // RelAI Service Key
    'X-Agent-ID': process.env.AGENT_ID!,              // Your agent identifier
  },
});

// Agent calls a paid API — payment + identity headers are handled automatically
const response = await client.fetch('https://api.relai.fi/relay/<apiId>/v1/chat/completions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: 'Summarize the latest market news.' }],
  }),
});

const result = await response.json();

Tip: defaultHeaders are merged with per-request headers. Per-request headers always win, so you can override them on individual calls when needed.


Crossmint Smart Wallet (server-side / agent)

Use createCrossmintX402Fetch from @relai-fi/x402/crossmint — no signTransaction needed, Crossmint handles signing and broadcasting.

import { createCrossmintX402Fetch } from '@relai-fi/x402/crossmint';
import { Connection } from '@solana/web3.js';

const fetch402 = createCrossmintX402Fetch({
  apiKey: process.env.CROSSMINT_API_KEY!,   // sk_production_... or sk_staging_...
  wallet: process.env.CROSSMINT_WALLET!,    // Crossmint smart wallet address
  connection: new Connection(process.env.SOLANA_RPC_URL!),
  onPayment: (txHash) => console.log('On-chain tx:', txHash),
});

// RelAI sponsors SOL gas — wallet only needs USDC
const response = await fetch402('https://api.example.com/protected');
const data = await response.json();

For agents that require explicit transaction approval before Crossmint broadcasts, use the delegated mode — an external Ed25519 signer must be registered on the Crossmint smart wallet:

import { createCrossmintDelegatedX402Fetch } from '@relai-fi/x402/crossmint';
import { Connection } from '@solana/web3.js';

const fetch402 = createCrossmintDelegatedX402Fetch({
  apiKey: process.env.CROSSMINT_API_KEY!,
  wallet: process.env.CROSSMINT_WALLET!,
  signerSecretKey: Buffer.from(process.env.SIGNER_SECRET_KEY!, 'base64'), // 64-byte Ed25519
  connection: new Connection(process.env.SOLANA_RPC_URL!),
});

const response = await fetch402('https://api.example.com/protected');

Both modes use Crossmint's API to sign and broadcast — no private key handling in your code.


WebSocket relay transport (optional)

If your protected API is behind a relay URL like https://api.relai.fi/relay/:apiId/... or a whitelabel relay URL like https://<whitelabel>.x402.fi/..., the SDK can use the Relay WebSocket transport automatically.

const client = createX402Client({
  wallets: {
    evm: evmWallet,
  },
  relayWs: {
    enabled: true,
    preflightTimeoutMs: 5000,
    paymentTimeoutMs: 10000,
    fallbackToHttp: true,
  },
});

// Pass your standard relay HTTP URL (apiId-based or whitelabel-based).
// SDK handles WS preflight + paid retry internally.
await client.fetch('https://api.relai.fi/relay/1769629274857/v1/chat/completions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] }),
});

// Whitelabel relay URL is also supported.
await client.fetch('https://tgmetrics.x402.fi/projects?page=1', {
  method: 'GET',
});

For Node.js runtimes without global WebSocket, provide relayWs.webSocketFactory.

If the relay returns multiple accepts options for one request, the SDK automatically falls back to standard HTTP x402 flow for that call.

React Hook

Works with @solana/wallet-adapter-react and wagmi:

import { useRelaiPayment } from '@relai-fi/x402/react';
import { useWallet } from '@solana/wallet-adapter-react';
import { useAccount, useSignTypedData } from 'wagmi';

function PayButton() {
  const solanaWallet = useWallet();
  const { address } = useAccount();
  const { signTypedDataAsync } = useSignTypedData();

  const {
    fetch,
    isLoading,
    status,
    transactionUrl,
    transactionNetworkLabel,
  } = useRelaiPayment({
    wallets: {
      solana: solanaWallet,
      evm: address ? { address, signTypedData: signTypedDataAsync } : undefined,
    },
  });

  return (
    <div>
      <button onClick={() => fetch('/api/protected')} disabled={isLoading}>
        {isLoading ? 'Paying...' : 'Access API'}
      </button>
      {transactionUrl && (
        <a href={transactionUrl} target="_blank">
          View on {transactionNetworkLabel}
        </a>
      )}
    </div>
  );
}

Supported Networks

NetworkIdentifierCAIP-2Signing MethodUSDC Contract
Solanasolanasolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpSPL transfer + fee payerEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Basebaseeip155:8453EIP-3009 transferWithAuthorization0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Avalancheavalancheeip155:43114EIP-3009 transferWithAuthorization0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E
SKALE Baseskale-baseeip155:1187947933EIP-3009 transferWithAuthorization0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20
SKALE Base Sepoliaskale-base-sepoliaeip155:324705682EIP-3009 transferWithAuthorization0x2e08028E3C4c2356572E096d8EF835cD5C6030bD
SKALE BITEskale-biteeip155:103698795EIP-3009 transferWithAuthorization0xc4083B1E81ceb461Ccef3FDa8A9F24F0d764B6D8
Polygonpolygoneip155:137EIP-3009 transferWithAuthorization0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
Ethereumethereumeip155:1EIP-3009 transferWithAuthorization0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

All networks support USDC (6 decimals). On SKALE Base networks, the SDK also supports:

  • SKALE Base (skale-base): USDT (0x2bF5bF154b515EaA82C31a65ec11554fF5aF7fCA), WBTC (0x1aeeCFE5454c83B42D8A316246CAc9739E7f690e), WETH (0x7bD39ABBd0Dd13103542cAe3276C7fA332bCA486)
  • SKALE Base Sepolia (skale-base-sepolia): USDT (0x3ca0a49f511c2c89c4dcbbf1731120d8919050bf), WBTC (0x4512eacd4186b025186e1cf6cc0d89497c530e87), WETH (0xf94056bd7f6965db3757e1b145f200b7346b4fc0)

Gas fees are sponsored by the RelAI facilitator.


Package Exports

// Client — browser & Node.js fetch wrapper with automatic 402 handling
import { createX402Client } from '@relai-fi/x402/client';

// React hook — state management + wallet integration
import { useRelaiPayment } from '@relai-fi/x402/react';

// Server — Express middleware for protecting endpoints
import Relai from '@relai-fi/x402/server';

// Utilities — payload conversion, unit helpers
import {
  convertV1ToV2,
  convertV2ToV1,
  networkV1ToV2,
  networkV2ToV1,
  toAtomicUnits,
  fromAtomicUnits,
} from '@relai-fi/x402/utils';

// Plugins — extend protect() with built-in & custom logic
import { freeTier, bridge, shield, preflight, circuitBreaker, refund } from '@relai-fi/x402/plugins';

// Management API — create/manage APIs, pricing, analytics, agent bootstrap
import {
  createManagementClient,
  bootstrapAgentKeySolana,
  bootstrapAgentKeyEvm,
} from '@relai-fi/x402/management';

// Types & constants
import {
  RELAI_NETWORKS,
  CHAIN_IDS,
  USDC_ADDRESSES,
  NETWORK_CAIP2,
  EXPLORER_TX_URL,
  type RelaiNetwork,
  type SolanaWallet,
  type EvmWallet,
  type WalletSet,
} from '@relai-fi/x402';

API Reference

createX402Client(config)

Creates a fetch wrapper that automatically handles 402 Payment Required responses.

OptionTypeDefaultDescription
wallets{ solana?, evm? }{}Wallet adapters for each chain
relayWsX402RelayWsConfigundefinedOptional WS transport for relay URLs
integritasboolean | X402IntegritasConfigundefinedAutomatically set Integritas headers
facilitatorUrlstringRelAI facilitatorCustom facilitator endpoint
preferredNetworkRelaiNetworkPrefer this network when multiple accepts
networkSelectionMode'prefer_then_any' | 'strict_preferred''prefer_then_any'Selection policy for preferredNetwork when multiple accepts are returned
solanaRpcUrlstringhttps://api.mainnet-beta.solana.comSolana RPC (use Helius/Quicknode for production)
evmRpcUrlsRecord<string, string>Built-in defaultsRPC URLs per network name
maxAmountAtomicstringSafety cap on payment amount
verbosebooleanfalseLog payment flow to console
defaultHeadersRecord<string, string>{}Headers added to every request (e.g. X-Service-Key, X-Agent-ID)

integritas options:

OptionTypeDefaultDescription
enabledbooleantrue when object is providedAdds X-Integritas: true
flow'single' | 'dual'Adds X-Integritas-Flow

relayWs options:

OptionTypeDefaultDescription
enabledbooleanfalseEnable WS transport for relay URLs
wsUrlstringderived from relay hostExplicit WebSocket relay endpoint
preflightTimeoutMsnumber5000Timeout for WS preflight request
paymentTimeoutMsnumber10000Timeout for paid WS retry
fallbackToHttpbooleantrueFall back to standard HTTP flow if WS fails
webSocketFactory(url) => WebSocketLikeruntime WebSocketCustom WS factory for Node.js/server runtimes

Wallet interfaces:

// Solana — compatible with @solana/wallet-adapter-react useWallet()
interface SolanaWallet {
  publicKey: { toString(): string } | null;
  signTransaction: ((tx: unknown) => Promise<unknown>) | null;
}

// EVM — pass address + signTypedData from wagmi
interface EvmWallet {
  address: string;
  signTypedData: (params: {
    domain: Record<string, unknown>;
    types: Record<string, unknown[]>;
    message: Record<string, unknown>;
    primaryType: string;
  }) => Promise<string>;
}

useRelaiPayment(config)

React hook wrapping createX402Client with state management.

Config — same as createX402Client (see above).

Returns:

PropertyTypeDescription
fetch(input, init?) => Promise<Response>Payment-aware fetch
isLoadingbooleanPayment in progress
status'idle' | 'pending' | 'success' | 'error'Current state
errorError | nullError details on failure
transactionIdstring | nullTx hash/signature on success
transactionNetworkRelaiNetwork | nullNetwork used for payment
transactionNetworkLabelstring | nullHuman-readable label (e.g. "Base")
transactionUrlstring | nullBlock explorer link
connectedChains{ solana: boolean, evm: boolean }Which wallets are connected
isConnectedbooleanAny wallet connected
reset() => voidReset state to idle

Server SDK (Express)

import Relai from '@relai-fi/x402/server';

const relai = new Relai({
  network: 'base', // or 'solana', 'avalanche', 'skale-base', 'skale-base-sepolia', ...
});

// Protect any Express route with micropayments
app.get('/api/data', relai.protect({
  payTo: '0xYourWallet',
  price: 0.01,  // \$0.01 USDC
  description: 'Premium data access',
  integritas: {
    enabled: true,
    flow: 'single',
  },
}), (req, res) => {
  // req.payment = { verified, transactionId, payer, network, amount }
  res.json({ data: 'Protected content', payment: req.payment });
});

// Dynamic pricing
app.get('/api/premium', relai.protect({
  payTo: '0xYourWallet',
  price: (req) => req.query.tier === 'pro' ? 0.10 : 0.01,
}), handler);

// Per-endpoint network override
app.get('/api/solana-data', relai.protect({
  payTo: 'SolanaWalletAddress',
  price: 0.005,
  network: 'solana', // overrides the default 'base'
}), handler);

Flow:

  1. Request without payment → 402 with accepts array
  2. Client signs payment (SDK handles this) → retries with X-PAYMENT header
  3. Server calls RelAI facilitator /settle → gas sponsored by RelAI
  4. Settlement success → PAYMENT-RESPONSE header set, req.payment populated, next() called

Integritas on server protect:

  • integritas: true enables Integritas metadata for the endpoint.
  • integritas: { enabled: true, flow: 'single' } sets default flow.
  • Buyer headers (X-Integritas, X-Integritas-Flow) override defaults per request.

req.payment fields:

FieldTypeDescription
verifiedbooleanAlways true after settlement
transactionIdstringOn-chain transaction hash
payerstringPayer wallet address
networkstringNetwork name (e.g., base)
amountnumberPrice in USD

Plugins

Extend Relai.protect() with plugins that hook into the payment lifecycle. Six built-in plugins ship with the SDK:

import { freeTier, bridge, shield, preflight, circuitBreaker, refund } from '@relai-fi/x402/plugins';
PluginPurposeHook
freeTierFree API calls before paymentbeforePaymentCheckskip
bridgeCross-chain payments (Solana ↔ SKALE ↔ Base)enrich402Response
shieldGlobal service health check before paymentbeforePaymentCheckreject
preflightPer-endpoint liveness probe before paymentbeforePaymentCheckreject
circuitBreakerFailure history tracking, auto-open circuitbeforePaymentCheckreject + afterSettled
refundAuto-credit buyers when paid requests failbeforePaymentCheckskip + afterSettled

Free Tier Plugin

Allow buyers to make free API calls before requiring x402 payment. Usage is tracked per buyer (by JWT sub, wallet address, or IP) with optional global caps and periodic resets.

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: 10,        // 10 free calls per buyer
      resetPeriod: 'daily',     // reset daily (or 'monthly', 'never')
      globalCap: 1000,          // optional: max 1000 free calls total
      paths: ['*'],             // optional: apply to all endpoints (default)
    }),
  ],
});

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('Free call from:', req.x402Plugin);
  }
  res.json({ data: 'content' });
});

How it works:

  1. On server start, the plugin syncs its config to the RelAI backend via your service key.
  2. On each request, beforePaymentCheck asks the RelAI API if the buyer has free calls remaining.
  3. If free → next() is called without payment, req.x402Free = true, and usage is recorded.
  4. If exhausted → normal x402 payment flow continues.

Config:

OptionTypeDefaultDescription
serviceKeystringYour sk_live_... key. Omit for local in-memory mode.
perBuyerLimitnumberrequiredFree calls each buyer gets per period
resetPeriod'none' | 'daily' | 'monthly''none'When counters reset
globalCapnumberMax total free calls across all buyers
pathsstring[]['*']Which endpoints the free tier applies to

Request properties set on free-tier bypass:

PropertyTypeDescription
req.x402Freebooleantrue when request was served for free
req.x402Paidbooleanfalse on free tier, true on paid
req.x402PluginstringPlugin name that granted the bypass ('freeTier')
req.pluginMetaobject{ freeTier: true, remaining: number }

Bridge Plugin

Accept cross-chain payments. Buyers on Solana can pay your SKALE Base API — the SDK handles bridging automatically.

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 }),
  ],
});

The plugin auto-discovers bridge capabilities from /bridge/info. No manual chain configuration needed.

OptionTypeDefaultDescription
serviceKeystringRecommended. Tracks bridge usage in dashboard.
settleEndpointstring/bridge/settleCustom settle endpoint
feeBpsnumber100Bridge fee in basis points (100 = 1%)

Shield Plugin

Global service health check — protects buyers from paying for unhealthy endpoints. Before the server returns 402, Shield runs a health check. If unhealthy, returns 503 instead of asking for payment.

import Relai from '@relai-fi/x402/server';
import { shield } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'base',
  plugins: [
    shield({
      healthUrl: 'https://my-api.com/health',
      timeoutMs: 3000,
    }),
    // Or use a custom function:
    // shield({
    //   healthCheck: async () => {
    //     const dbOk = await checkDatabase();
    //     return dbOk;
    //   },
    // }),
  ],
});
OptionTypeDefaultDescription
healthUrlstringURL to probe. 2xx = healthy.
healthCheck() => boolean | Promise<boolean>Custom function. Takes priority over healthUrl.
timeoutMsnumber5000Timeout for health probe (ms)
cacheTtlMsnumber10000Cache health result (ms)
unhealthyStatusnumber503HTTP status when unhealthy
unhealthyMessagestringService temporarily unavailable...Error message

Response headers: X-Shield-Status: healthy|unhealthy, Retry-After (when unhealthy).

Preflight Plugin

Per-endpoint liveness probe — verifies the specific endpoint responds before payment. Sends HEAD with X-Preflight: true header; the middleware responds 200 instantly without triggering payment.

import Relai from '@relai-fi/x402/server';
import { preflight } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'base',
  plugins: [
    preflight({ baseUrl: 'https://my-api.com' }),
  ],
});

// If /api/data doesn't respond, buyers get 503 — never 402
app.get('/api/data', relai.protect({
  payTo: '0xYourWallet',
  price: 0.01,
}), handler);
OptionTypeDefaultDescription
baseUrlstringrequiredBase URL of the API. Request path is appended automatically.
timeoutMsnumber3000Timeout for probe (ms)
cacheTtlMsnumber5000Cache per-path result (ms)
unhealthyStatusnumber503HTTP status when unreachable
unhealthyMessagestringEndpoint not responding...Error message

Response headers: X-Preflight-Status: ok|unreachable, Retry-After (when unreachable).

Shield vs Preflight:

ShieldPreflight
ScopeGlobal service healthPer-endpoint liveness
Probe targetSeparate health URL / functionThe actual protected endpoint
CacheSingle result (10s)Per-path (5s)
Use caseDB/Redis/external API downSpecific endpoint not responding

Circuit Breaker Plugin

Tracks failure history and automatically "opens the circuit" after repeated failures — preventing buyers from paying for broken endpoints. Zero-latency — no extra HTTP requests.

import Relai from '@relai-fi/x402/server';
import { circuitBreaker } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'base',
  plugins: [
    circuitBreaker({
      failureThreshold: 5,  // open after 5 failures
      resetTimeMs: 30000,   // try again after 30s
    }),
  ],
});

States: closed (normal) → open (all rejected 503) → half-open (test requests) → closed.

OptionTypeDefaultDescription
failureThresholdnumber5Consecutive failures before circuit opens
resetTimeMsnumber30000Time (ms) circuit stays open before half-open
halfOpenSuccessesnumber2Successes needed in half-open to close
openStatusnumber503HTTP status when circuit is open
openMessagestringService temporarily unavailable...Error message
failureCodesnumber[][500, 502, 503, 504]HTTP codes treated as failures
scope'global' | 'per-path''per-path'Track globally or per endpoint

Response headers: X-Circuit-State: closed|open|half-open, Retry-After (when open).

Refund Plugin

Automatically compensates buyers when paid requests fail. Records an in-memory credit or calls your custom handler.

import Relai from '@relai-fi/x402/server';
import { refund } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'base',
  plugins: [
    refund({
      triggerCodes: [500, 502, 503],
      mode: 'credit',
      onRefund: (event) => {
        console.log(`Refund: $${event.amount} to ${event.payer}`);
      },
    }),
  ],
});

Modes:

  • credit — records credit per buyer. Next request skips payment automatically. Header: X-Refund-Credit: applied.
  • log — only calls onRefund. Handle refunds externally (DB, Stripe, etc.).
OptionTypeDefaultDescription
triggerCodesnumber[][500, 502, 503, 504]HTTP codes that trigger a refund
mode'credit' | 'log''credit'Auto-credit or callback-only
onRefund(event: RefundEvent) => voidCallback on every refund event
refundOnSettlementFailurebooleantrueAlso refund when settlement itself fails

ERC-8004 Reputation Plugins

Build verifiable on-chain reputation for your API using the ERC-8004 standard. Scores are stored on SKALE Base Sepolia (zero-cost transactions) and readable by any agent before payment.

score() — Inject reputation into the 402 response

Before an agent pays, it can see your API's live on-chain score in the extensions.score field of the 402 response. Fetches directly from the SKALE RPC node — no REST API.

import Relai from '@relai-fi/x402/server';
import { score } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'base',
  plugins: [
    score({ agentId: process.env.ERC8004_AGENT_ID! }),
  ],
});

The 402 response will include:

{
  "extensions": {
    "score": {
      "agentId": "5",
      "feedbackCount": 142,
      "successRate": 98.6,
      "avgResponseMs": 312
    }
  }
}
OptionTypeDefaultDescription
agentIdstring | numberERC-8004 NFT token ID from your dashboard
rpcUrlstringprocess.env.ERC8004_RPC_URLSKALE Base Sepolia RPC URL
identityRegistryAddressstringprocess.env.ERC8004_IDENTITY_REGISTRYIdentityRegistry contract address
reputationRegistryAddressstringprocess.env.ERC8004_REPUTATION_REGISTRYReputationRegistry contract address
cacheTtlMsnumber300000Score cache TTL (default: 5 min)

feedback() — Record your own API metrics on-chain

Submit successRate and responseTime after every settled x402 payment. This is what builds the score that score() later reads.

import Relai from '@relai-fi/x402/server';
import { score, feedback } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'base',
  plugins: [
    score({ agentId: process.env.ERC8004_AGENT_ID! }),
    feedback({ agentId: process.env.ERC8004_AGENT_ID! }),
  ],
});

Requires BACKEND_WALLET_PRIVATE_KEY — the wallet must hold CREDIT tokens on SKALE Base for gas.

OptionTypeDefaultDescription
agentIdstring | numberYour ERC-8004 agent token ID
walletPrivateKeystringprocess.env.BACKEND_WALLET_PRIVATE_KEYEVM private key, needs CREDIT on SKALE
rpcUrlstringprocess.env.ERC8004_RPC_URLSKALE RPC URL
reputationRegistryAddressstringprocess.env.ERC8004_REPUTATION_REGISTRYContract address
submitSuccessRatebooleantrueSubmit 1/0 success signal
submitResponseTimebooleantrueSubmit response time in ms

solanaFeedback() — Native Solana 8004-solana feedback

For Solana APIs registered via 8004-solana (MPL Core NFT). Requires npm install 8004-solana.

import Relai from '@relai-fi/x402/server';
import { solanaFeedback } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'solana',
  plugins: [
    solanaFeedback({
      assetPubkey: process.env.SOLANA_AGENT_ASSET!, // MPL Core NFT address
    }),
  ],
});
OptionTypeDefaultDescription
assetPubkeystringSolana MPL Core NFT address (solanaAgentAsset)
feedbackWalletPrivateKeystringprocess.env.SOLANA_8004_FEEDBACK_KEYbase58 or JSON array
cluster'mainnet-beta' | 'devnet'process.env.SOLANA_8004_CLUSTERSolana cluster
rpcUrlstringprocess.env.SOLANA_8004_RPC_URLCustom RPC (Helius / QuickNode)

submitRelayFeedback() — Third-party feedback utility

If your server acts as a relay or marketplace that calls other APIs, use this standalone function to record feedback about those APIs. Uses a separate relay wallet to avoid self-feedback restrictions.

import { submitRelayFeedback } from '@relai-fi/x402/plugins';

// After calling an external API:
const start = Date.now();
const result = await fetch('https://other-api.com/v1/data');

submitRelayFeedback({
  agentId: '5',                        // target API's ERC-8004 agentId
  success: result.ok,
  responseTimeMs: Date.now() - start,
  endpoint: '/v1/data',
});
OptionTypeDefaultDescription
agentIdstring | numberERC-8004 agentId of the API you called
successbooleanWhether the call succeeded
responseTimeMsnumber0Elapsed time in ms
endpointstring''Endpoint path
feedbackWalletPrivateKeystringprocess.env.FEEDBACK_WALLET_PRIVATE_KEYMust differ from API owner key

Required environment variables (ERC-8004)

ERC8004_IDENTITY_REGISTRY=0x8724C768547d7fFb1722b13a84F21CCF5010641f
ERC8004_REPUTATION_REGISTRY=0xe946A7F08d1CC0Ed0eC1fC131D0135d9c0Dd7d9D
ERC8004_RPC_URL=https://base-sepolia-testnet.skalenodes.com/v1/jubilant-horrible-ancha
ERC8004_AGENT_ID=5                          # your agent NFT token ID

BACKEND_WALLET_PRIVATE_KEY=0x...            # for feedback() — needs CREDIT on SKALE
FEEDBACK_WALLET_PRIVATE_KEY=0x...           # for submitRelayFeedback() — different wallet

# Solana 8004 (only if using solanaFeedback)
SOLANA_AGENT_ASSET=GH93tGR8...             # MPL Core NFT pubkey
SOLANA_8004_FEEDBACK_KEY=...               # base58 or JSON array
SOLANA_8004_CLUSTER=mainnet-beta
SOLANA_8004_RPC_URL=https://...

Combining Plugins

Plugins run in array order. Combine them for layered protection:

const relai = new Relai({
  network: 'base',
  plugins: [
    shield({ healthUrl: 'https://my-api.com/health' }),  // 1. Is the service up?
    preflight({ baseUrl: 'https://my-api.com' }),         // 2. Is this endpoint alive?
    circuitBreaker({ failureThreshold: 5 }),              // 3. Too many recent failures?
    freeTier({ perBuyerLimit: 5, resetPeriod: 'daily' }), // 4. Free calls left?
    refund({ triggerCodes: [500, 502, 503] }),             // 5. Compensate on error
    bridge({ serviceKey: process.env.RELAI_SERVICE_KEY }), // 6. Cross-chain support
    score({ agentId: process.env.ERC8004_AGENT_ID }),      // 7. Show reputation in 402
    feedback({ agentId: process.env.ERC8004_AGENT_ID }),   // 8. Record metrics on-chain
  ],
});

Custom Plugins

import type { RelaiPlugin, PluginContext, PluginResult } from '@relai-fi/x402/plugins';

const myPlugin: RelaiPlugin = {
  name: 'my-plugin',

  async beforePaymentCheck(req, ctx) {
    if (req.headers['x-vip'] === 'true') {
      return { skip: true, headers: { 'X-VIP': 'true' } };
    }
    return {};
  },

  async afterSettled(req, result, ctx) {
    console.log(`Paid $${ctx.price} by ${result.payer} on ${ctx.network}`);
  },

  async onInit() {
    console.log('Plugin initialized');
  },
};

Plugin interface:

interface RelaiPlugin {
  name: string;
  beforePaymentCheck?(req: any, ctx: PluginContext): Promise<PluginResult>;
  afterSettled?(req: any, result: SettleResult, ctx: PluginContext): Promise<void>;
  onInit?(): Promise<void>;
  enrich402Response?(response: any, ctx: PluginContext): any;
}

interface PluginResult {
  skip?: boolean;      // Bypass payment, serve content free
  reject?: boolean;    // Block request entirely (e.g. unhealthy)
  rejectStatus?: number;
  rejectMessage?: string;
  headers?: Record<string, string>;
  meta?: Record<string, unknown>;
}

interface PluginContext {
  network: string;
  price: number;
  path: string;
  method: string;
}

Management API

Programmatically create and manage monetised APIs, update pricing, and read analytics. Designed for agents and CI/CD — no browser needed.

import { createManagementClient } from '@relai-fi/x402/management';

const mgmt = createManagementClient({ serviceKey: process.env.RELAI_SERVICE_KEY! });

// Create a monetised API
const api = await mgmt.createApi({
  name: 'My ML API',
  baseUrl: 'https://inference.example.com',
  merchantWallet: '0xYourWallet',
  network: 'base',
  endpoints: [
    { path: '/v1/predict', method: 'post', usdPrice: 0.05 },
    { path: '/v1/status',  method: 'get',  usdPrice: 0.001 },
  ],
});

// List / get / update / delete
const all   = await mgmt.listApis();
const one   = await mgmt.getApi(api.apiId);
await mgmt.updateApi(api.apiId, { description: 'Updated description' });
await mgmt.deleteApi(api.apiId);

// Pricing
await mgmt.setPricing(api.apiId, [{ path: '/v1/predict', method: 'post', usdPrice: 0.10 }]);
const pricing = await mgmt.getPricing(api.apiId);

// Analytics
const stats    = await mgmt.getStats(api.apiId);
const payments = await mgmt.getPayments(api.apiId, { limit: 20, from: '2025-01-01' });
const logs     = await mgmt.getLogs(api.apiId, { limit: 20 });

Agent self-setup (autonomous key provisioning)

Agents can provision their own service key with zero human involvement — no dashboard, no JWT, no copy-paste. Run once and store the key.

import { bootstrapAgentKeySolana, bootstrapAgentKeyEvm } from '@relai-fi/x402/management';
import { Keypair } from '@solana/web3.js';

// Solana agent
const keypair = Keypair.fromSecretKey(Buffer.from(process.env.AGENT_PRIVATE_KEY!, 'base64'));
const { key } = await bootstrapAgentKeySolana(keypair, 'my-agent');
// → key: "sk_live_..."  — store securely, never re-run

// EVM agent (ethers.js Wallet)
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(process.env.AGENT_PRIVATE_KEY!);
const { key: evmKey } = await bootstrapAgentKeyEvm(wallet, 'my-evm-agent');

Once you have a service key, combine it with createX402Client for a fully autonomous agent:

import { createX402Client } from '@relai-fi/x402/client';
import { createManagementClient } from '@relai-fi/x402/management';

const serviceKey = process.env.RELAI_SERVICE_KEY!;

// Pay for APIs
const client = createX402Client({
  wallets: { solana: agentWallet },
  defaultHeaders: { 'X-Service-Key': serviceKey },
});

// Manage APIs
const mgmt = createManagementClient({ serviceKey });

x402 Bridge

Bridge USDC between Solana and SKALE Base using the x402 protocol. The bridge uses a liquidity network model — instant payouts, no canonical bridge delays.

Requires a service key (sk_live_...). Get one via bootstrapAgentKeySolana or the RelAI dashboard.

How it works

Agent pays USDC on Solana (x402)
  ↓ facilitator verifies payment
Bridge pays out USDC on SKALE Base from its liquidity pool  ← instant
  ↓ async in background
Rebalance via Circle CCTP restores liquidity

Quick start

import { createManagementClient } from '@relai-fi/x402/management';
import { createX402Client } from '@relai-fi/x402/client';
import { Keypair } from '@solana/web3.js';

const serviceKey = process.env.RELAI_SERVICE_KEY!;
const mgmt = createManagementClient({ serviceKey });

// 1. Check liquidity before bridging
const balances = await mgmt.getBridgeBalances();
console.log(`SKALE Base liquidity: $${balances.skaleBase.usd} USDC`);

// 2. Get a quote
const quote = await mgmt.getBridgeQuote(10.0, 'solana');
// { inputUsd: 10, outputUsd: 9.99, feeBps: 10, direction: 'solana-to-skale' }

// 3. Bridge Solana → SKALE Base
const keypair = Keypair.fromSecretKey(Buffer.from(process.env.SOLANA_PRIVATE_KEY!, 'base64'));
const solanaWallet = {
  publicKey: keypair.publicKey,
  signTransaction: async (tx: any) => { tx.sign([keypair]); return tx; },
};

const x402 = createX402Client({
  wallets: { solana: solanaWallet },
  solanaRpcUrl: process.env.SOLANA_RPC_URL,
  defaultHeaders: { 'X-Service-Key': serviceKey },
});

const result = await mgmt.bridgeSolanaToSkale(
  10.0,                    // \$10 USDC
  '0xYourSkaleAddress',    // destination EVM address on SKALE Base
  x402,
);
// { success: true, txHash: '0x...', amountOutUsd: 9.99, explorerUrl: '...' }

Bridge SKALE Base → Solana

import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider(process.env.SKALE_RPC_URL);
const signer = new ethers.Wallet(process.env.EVM_PRIVATE_KEY!, provider);

const evmWallet = {
  address: signer.address,
  signTypedData: (params: any) => signer.signTypedData(
    params.domain, params.types, params.message
  ),
};

const x402 = createX402Client({
  wallets: { evm: evmWallet },
  defaultHeaders: { 'X-Service-Key': serviceKey },
});

const result = await mgmt.bridgeSkaleToSolana(
  5.0,                        // \$5 USDC
  'YourSolanaPublicKey',      // destination Solana address (base58)
  x402,
);
// { success: true, txHash: '...', amountOutUsd: 4.995, explorerUrl: '...' }

Bridge API reference

MethodDescription
getBridgeQuote(amount, from?)Fee and net output for a given amount
getBridgeBalances()Current USDC liquidity on Solana / SKALE Base / Base
bridgeSolanaToSkale(amount, dest, x402Client)Bridge Solana → SKALE Base
bridgeSkaleToSolana(amount, dest, x402Client)Bridge SKALE Base → Solana

Error handling

try {
  const result = await mgmt.bridgeSolanaToSkale(100.0, '0x...', x402);
} catch (err) {
  if (err.message.includes('insufficient_liquidity')) {
    // Bridge temporarily unavailable — check balances and retry later
    const { skaleBase } = await mgmt.getBridgeBalances();
    console.log(`Available: $${skaleBase.usd} USDC`);
  }
}

Fee: 0.1% (10 bps) deducted from output amount. Check getBridgeQuote for exact amounts.

Limits: min $0.000001, max $10,000 per transaction.


Utilities

import { toAtomicUnits, fromAtomicUnits } from '@relai-fi/x402/utils';

toAtomicUnits(0.05, 6);       // '50000'   (\$0.05 USDC)
toAtomicUnits(1.50, 6);       // '1500000' (\$1.50 USDC)

fromAtomicUnits('50000', 6);  // 0.05
fromAtomicUnits('1500000', 6); // 1.5

Payload Conversion (v1 ↔ v2)

import { convertV1ToV2, convertV2ToV1, networkV1ToV2 } from '@relai-fi/x402/utils';

networkV1ToV2('solana');     // 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'
networkV1ToV2('base');       // 'eip155:8453'
networkV1ToV2('avalanche');  // 'eip155:43114'
networkV1ToV2('skale-base'); // 'eip155:1187947933'
networkV1ToV2('skale-base-sepolia'); // 'eip155:324705682'

const v2Payload = convertV1ToV2(v1Payload);
const v1Payload = convertV2ToV1(v2Payload);

How It Works

Client                        Server                     Facilitator
  |                             |                             |
  |── GET /api/data ──────────>|                             |
  |<── 402 Payment Required ───|                             |
  |   (accepts: network, amount, asset)                      |
  |                             |                             |
  | SDK signs payment           |                             |
  | (EIP-3009/SPL)              |                             |
  |                             |                             |
  |── GET /api/data ──────────>|                             |
  |   X-PAYMENT: <signed>      |── settle ─────────────────>|
  |                             |<── tx hash ────────────────|
  |<── 200 OK + data ─────────|                             |
  |   PAYMENT-RESPONSE: <tx>   |                             |

MPP (Machine Payment Protocol)

MPP is an alternative payment channel that works alongside x402. Instead of the client building and signing blockchain transactions, MPP delegates signing to specialized payment providers — simpler client code, same protect() middleware on the server.

x402MPP
Payment flowClient builds tx (SPL / EIP-3009), signs, server settles via facilitatorProvider handles signing & broadcasting via challenge-response
Header protocolX-PAYMENT (base64 JSON)WWW-Authenticate: Payment / Authorization: Payment
Supported providersTempo (EVM), Solana (@solana/mpp), EVM/SKALE (built-in), Stripe

When both are enabled the server returns a 402 with both an MPP challenge (WWW-Authenticate) and the standard x402 accepts body. The client tries MPP first, then falls back to x402.


MPP Server Setup

Tempo (EVM)

import express from 'express';
import Relai from '@relai-fi/x402/server';
import { Mppx, tempo } from 'mppx/server';

const TEMPO_USDC = '0x20C000000000000000000000b9537d11c60E8b50';

const mppx = Mppx.create({
  secretKey: process.env.MPP_SECRET_KEY!,
  methods: [
    tempo.charge({
      recipient: '0xYourWallet',
      currency: TEMPO_USDC,
      decimals: 6,
    }),
  ],
});

const relai = new Relai({
  network: 'base',
  mpp: mppx,
});

const app = express();

app.get('/api/data', relai.protect({
  payTo: '0xYourWallet',
  price: 0.01,
}), (req, res) => {
  res.json({ data: 'Paid via MPP Tempo or x402' });
});

Solana (@solana/mpp)

@solana/mpp expects amounts in base units (1 000 000 = 1 USDC), while the SDK passes USD. A thin wrapper with a handler cache is needed:

import Relai from '@relai-fi/x402/server';
import { Mppx, solana } from '@solana/mpp/server';

const SOLANA_USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const DECIMALS = 6;

const mppx = Mppx.create({
  secretKey: process.env.MPP_SECRET_KEY!,
  methods: [
    solana.charge({
      recipient: 'YourSolanaWallet',
      splToken: SOLANA_USDC,
      decimals: DECIMALS,
      network: 'mainnet-beta',
      rpcUrl: process.env.SOLANA_RPC_URL,
    }),
  ],
});

// Wrap to convert USD → base units and cache handlers (mppx.charge is stateful)
const handlerCache = new Map<string, ReturnType<typeof mppx.charge>>();
const mppSolanaWrapper = {
  charge(params: Record<string, unknown>) {
    const usdAmount = parseFloat(params.amount as string);
    const baseUnits = Math.round(usdAmount * 10 ** DECIMALS).toString();
    if (!handlerCache.has(baseUnits)) {
      handlerCache.set(baseUnits, mppx.charge({ ...params, amount: baseUnits }));
    }
    return handlerCache.get(baseUnits)!;
  },
};

const relai = new Relai({
  network: 'solana',
  mpp: mppSolanaWrapper as any,
});

EVM — SKALE, Base, Polygon, Avalanche, Ethereum, Telos (built-in)

The SDK ships a built-in EVM MPP method that works with any EVM chain. Supported chains:

ChainChain IDGasNotes
SKALE Base1187947933FreeGas-free USDC micropayments
SKALE Base Sepolia324705682FreeTestnet
SKALE BITE103698795FreeGas-free, encrypted mempool
Base8453ETHLow fees (~$0.001/tx)
Polygon137POLLow fees
Avalanche43114AVAXLow fees
Ethereum1ETHHigher fees
Telos40TLOSLow fees

SKALE chains are gas-free, making them ideal for zero-cost micropayments. The same USD→base-units wrapper pattern applies:

import Relai from '@relai-fi/x402/server';
import { Mppx } from 'mppx/server';
import { evmCharge } from '@relai-fi/x402/mpp/evm-server';

const DECIMALS = 6;

const mppx = Mppx.create({
  secretKey: process.env.MPP_SECRET_KEY!,
  methods: [
    evmCharge({
      recipient: '0xYourWallet',
      tokenAddress: '0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20', // USDC on SKALE Base
      chainId: 1187947933,                                         // SKALE Base
      rpcUrl: 'https://skale-base.skalenodes.com/v1/base',
      decimals: DECIMALS,
      network: 'skale-base',
    }),
  ],
});

// Same wrapper pattern as Solana
const handlerCache = new Map<string, ReturnType<typeof mppx.charge>>();
const mppEvmWrapper = {
  charge(params: Record<string, unknown>) {
    const usdAmount = parseFloat(params.amount as string);
    const baseUnits = Math.round(usdAmount * 10 ** DECIMALS).toString();
    if (!handlerCache.has(baseUnits)) {
      handlerCache.set(baseUnits, mppx.charge({ ...params, amount: baseUnits }));
    }
    return handlerCache.get(baseUnits)!;
  },
};

const relai = new Relai({
  network: 'skale-base',
  mpp: mppEvmWrapper as any,
});

MPP Client Setup

Tempo (EVM)

import { createX402Client } from '@relai-fi/x402/client';
import { Mppx, tempo } from 'mppx/client';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount(process.env.TEMPO_PRIVATE_KEY as `0x${string}`);

const mppx = Mppx.create({
  methods: [tempo.charge({ account })],
  polyfill: false,
  onChallenge: async (challenge, { createCredential }) => {
    console.log(`Amount: ${challenge.request?.amount}`);
    return await createCredential();
  },
});

const client = createX402Client({ mpp: mppx });
const response = await client.fetch('https://api.example.com/protected');

Solana

import { createX402Client } from '@relai-fi/x402/client';
import { Mppx, solana } from '@solana/mpp/client';
import { createKeyPairSignerFromBytes } from '@solana/kit';
import bs58 from 'bs58';

const signer = await createKeyPairSignerFromBytes(
  new Uint8Array(bs58.decode(process.env.SOLANA_PRIVATE_KEY!))
);

const mppx = Mppx.create({
  methods: [solana.charge({ signer })],
  polyfill: false,
  onChallenge: async (challenge, { createCredential }) => {
    console.log(`Amount: ${challenge.request?.amount}`);
    return await createCredential();
  },
});

const client = createX402Client({ mpp: mppx });
const response = await client.fetch('https://api.example.com/protected');

EVM — SKALE, Base, Polygon, Avalanche, Ethereum, Telos (built-in)

import { createX402Client } from '@relai-fi/x402/client';
import { Mppx } from 'mppx/client';
import { evmCharge } from '@relai-fi/x402/mpp/evm-client';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);

const mppx = Mppx.create({
  methods: [evmCharge({ account })],
  polyfill: false,
  onChallenge: async (challenge, { createCredential }) => {
    const req = challenge.request as any;
    console.log(`Chain: ${req?.methodDetails?.network}, Amount: ${req?.amount}`);
    return await createCredential();
  },
});

const client = createX402Client({ mpp: mppx });
const response = await client.fetch('https://api.example.com/protected');

MPP + Plugins

All existing plugins work with MPP payments. afterSettled fires on both success and failure for both x402 and MPP:

import { freeTier, shield, circuitBreaker, refund } from '@relai-fi/x402/plugins';

const relai = new Relai({
  network: 'base',
  mpp: mppx,
  plugins: [
    shield({ healthUrl: 'https://my-api.com/health' }),
    circuitBreaker({ failureThreshold: 5 }),
    freeTier({ perBuyerLimit: 5, resetPeriod: 'daily' }),
    refund({ triggerCodes: [500, 502, 503] }),
  ],
});

MPP API Reference

Server — MppServerHandler

interface MppServerHandler {
  charge(params: Record<string, unknown>): (request: Request) => Promise<MppChargeResult>;
}

type MppChargeResult = {
  status: number;
  challenge?: Response;                            // 402 with WWW-Authenticate header
  withReceipt?: (response: Response) => Response;  // Attaches Payment-Receipt header
  receipt?: { method?: string; reference?: string; status?: string };
};

Client — MppHandler

interface MppHandler {
  createCredential(response: Response): Promise<string>;
}

Both interfaces are exported from @relai-fi/x402.


Cross-Chain Bridge Flows

The SDK supports transparent cross-chain payments via the RelAI bridge. There are two approaches depending on who manages the bridge logic — the client or the server.

x402 Auto-Bridge (client-side)

The server is a standard x402 server. The client has a wallet on a different chain and uses bridge: { enabled: true } to auto-bridge.

sequenceDiagram
    participant Client as Client (Solana)
    participant Server as Server (Base)
    participant Bridge as RelAI Bridge API
    participant Facilitator as Facilitator

    Client->>Server: GET /api/data
    Server-->>Client: 402 (accepts: Base USDC)

    Note over Client: No Base wallet → auto-bridge

    Client->>Bridge: GET /bridge/info
    Bridge-->>Client: source chains, payTo, fees

    Client->>Client: Build Solana SPL transfer to bridge payTo
    Client->>Bridge: POST /bridge/settle (sourcePayment, targetAccept)
    Bridge->>Bridge: Co-sign & broadcast Solana tx
    Bridge->>Bridge: Sign EIP-3009 authorization (Base)
    Bridge-->>Client: xPayment (base64)

    Client->>Server: GET /api/data + X-PAYMENT header
    Server->>Facilitator: POST /settle (paymentPayload)
    Facilitator->>Facilitator: Execute transferWithAuthorization on Base
    Facilitator-->>Server: tx hash
    Server-->>Client: 200 OK + data

MPP Client-Side Bridge (evmChargeWithBridge)

The server exposes a standard evm/charge MPP method. The client detects it's on a different chain and bridges transparently — the server never knows. Supports both EVM and Solana source wallets.

EVM source (e.g. Tempo → SKALE):

sequenceDiagram
    participant Client as Client (Tempo)
    participant Server as Server (SKALE)
    participant Bridge as RelAI Bridge API
    participant Facilitator as Facilitator

    Client->>Server: GET /api/data
    Server-->>Client: 402 + WWW-Authenticate: Payment (evm/charge, chainId=SKALE)

    Note over Client: Source chain ≠ target chain → bridge

    Client->>Bridge: GET /bridge/info
    Bridge-->>Client: source chains, payTo, fees

    Client->>Client: ERC-20 transfer on Tempo → bridge payTo
    Client->>Client: Sign settle message (ECDSA secp256k1)
    Client->>Bridge: POST /bridge/settle (sourceTxHash, signature, targetAccept)
    Bridge->>Bridge: Verify source tx on-chain + ecrecover signature
    Bridge->>Bridge: Sign EIP-3009 authorization (SKALE)
    Bridge-->>Client: xPayment + targetTxId=pending

    Client->>Facilitator: POST /settle (paymentPayload, paymentRequirements)
    Facilitator->>Facilitator: Execute transferWithAuthorization on SKALE
    Facilitator-->>Client: tx hash (SKALE)

    Client->>Server: GET /api/data + Authorization: Payment (hash=SKALE tx)
    Server->>Server: Verify ERC-20 Transfer on SKALE (standard evm/charge verify)
    Server-->>Client: 200 OK + data

Solana source (e.g. Solana → SKALE):

sequenceDiagram
    participant Client as Client (Solana)
    participant Server as Server (SKALE)
    participant Bridge as RelAI Bridge API
    participant Facilitator as Facilitator

    Client->>Server: GET /api/data
    Server-->>Client: 402 + WWW-Authenticate: Payment (evm/charge, chainId=SKALE)

    Note over Client: No EVM wallet on SKALE → bridge from Solana

    Client->>Bridge: GET /bridge/info
    Bridge-->>Client: source chains, payTo, feePayerSvm, fees

    Client->>Client: SPL transfer on Solana → bridge payTo (feePayer sponsored)
    Client->>Client: Sign settle message (Ed25519, base58-encoded)
    Client->>Bridge: POST /bridge/settle (sourceTxHash, signature, targetAccept)
    Bridge->>Bridge: Verify SPL transfer on-chain + Ed25519 signature
    Bridge->>Bridge: Sign EIP-3009 authorization (SKALE)
    Bridge-->>Client: xPayment + targetTxId=pending

    Client->>Facilitator: POST /settle (paymentPayload, paymentRequirements)
    Facilitator->>Facilitator: Execute transferWithAuthorization on SKALE
    Facilitator-->>Client: tx hash (SKALE)

    Client->>Server: GET /api/data + Authorization: Payment (hash=SKALE tx)
    Server->>Server: Verify ERC-20 Transfer on SKALE (standard evm/charge verify)
    Server-->>Client: 200 OK + data

MPP Server-Side Bridge (bridge/charge method)

The server explicitly exposes a bridge/charge method alongside its direct method. The client picks bridge/charge when it can't pay directly. Use this when the target chain is not EVM (e.g. Solana), or when the server wants to control bridge options.

EVM target (e.g. Tempo → SKALE):

sequenceDiagram
    participant Client as Client (Tempo)
    participant Server as Server (SKALE)
    participant Bridge as RelAI Bridge API
    participant Facilitator as Facilitator

    Client->>Server: GET /api/data
    Server->>Bridge: GET /bridge/info (auto-discover)
    Server-->>Client: 402 + WWW-Authenticate: Payment<br/>(evm/charge + bridge/charge)

    Note over Client: Can't match evm/charge → picks bridge/charge

    Client->>Client: ERC-20 transfer on Tempo → bridge payTo
    Client->>Client: Sign settle message (ECDSA secp256k1)
    Client->>Bridge: POST /bridge/settle (sourceTxHash, signature, targetAccept)
    Bridge->>Bridge: Verify source tx on-chain + ecrecover signature
    Bridge->>Bridge: Sign EIP-3009 authorization (SKALE)
    Bridge-->>Client: xPayment + targetTxId=pending

    Client->>Facilitator: POST /settle (paymentPayload, paymentRequirements)
    Facilitator->>Facilitator: Execute transferWithAuthorization on SKALE
    Facilitator-->>Client: tx hash (SKALE)

    Client->>Server: Authorization: Payment (targetTxHash=SKALE tx)
    Server->>Server: bridge/charge verify() — check ERC-20 Transfer on SKALE
    Server-->>Client: 200 OK + data + Payment-Receipt

Solana target (e.g. Tempo → Solana) — requires server-side bridge:

sequenceDiagram
    participant Client as Client (Tempo)
    participant Server as Server (Solana)
    participant Bridge as RelAI Bridge API

    Client->>Server: GET /api/data
    Server->>Bridge: GET /bridge/info (auto-discover)
    Server-->>Client: 402 + WWW-Authenticate: Payment<br/>(solana/charge + bridge/charge)

    Note over Client: No Solana wallet → picks bridge/charge

    Client->>Client: ERC-20 transfer on Tempo → bridge payTo
    Client->>Client: Sign settle message (ECDSA secp256k1)
    Client->>Bridge: POST /bridge/settle (sourceTxHash, signature, targetAccept)
    Bridge->>Bridge: Verify source tx on-chain + ecrecover signature
    Bridge->>Bridge: Build signed SPL transfer (bridge wallet → merchant)
    Bridge-->>Client: xPayment (signed Solana tx) + targetTxId=pending

    Note over Client: Solana target → broadcast directly (no facilitator)

    Client->>Client: Deserialize & sendRawTransaction on Solana
    Client->>Client: confirmTransaction

    Client->>Server: Authorization: Payment (targetTxHash=Solana signature)
    Server->>Server: bridge/charge verify() — check SPL Transfer on Solana
    Server-->>Client: 200 OK + data + Payment-Receipt

When to use which approach

ApproachServer configClient configBest for
x402 auto-bridgeStandard x402 (no change)bridge: { enabled: true }Solana client → EVM server
MPP client-sideStandard evm/charge (no change)evmChargeWithBridge()EVM client → different EVM server
MPP server-sideevm/charge + bridge/chargebridgeCharge() clientAny client → Solana server, or when server wants to control bridge options

See examples/bridge/ for runnable examples of each approach.


Payment Codes

BLIK-style one-time payment codes — pre-signed EIP-3009 tokens that can be generated in advance and redeemed later, without a wallet at redemption time. Ideal for AI agents and walletless buyers.

import {
  createPrivateKeySigner,
  generatePaymentCode,
  generatePaymentCodesBatch,
  redeemPaymentCode,
  getPaymentCode,
  cancelPaymentCode,
} from '@relai-fi/x402';
import { ethers } from 'ethers';

const config = { facilitatorUrl: 'https://relai.fi/facilitator' };

// ── Create a signer from a private key (agent / server-side) ──────────────
const signer = createPrivateKeySigner(process.env.AGENT_PRIVATE_KEY!);

// ── Generate a single payment code (\$10 USDC, expires in 24 h) ───────────
const code = await generatePaymentCode(config, signer, {
  amount:  10_000_000,       // \$10.00 in µUSDC
  network: 'base-sepolia',
});
console.log('Code:', code.code);  // "ABCD1234"

// ── Batch generate 5 codes (requires API key) ─────────────────────────────
const batch = await generatePaymentCodesBatch(config, signer, {
  amount:   5_000_000,
  network:  'base-sepolia',
  count:    5,
  apiKey:   process.env.RELAI_API_KEY!,
});

// ── Check status ──────────────────────────────────────────────────────────
const status = await getPaymentCode(config, 'ABCD1234');
console.log(status.redeemed, status.expired, status.value);

// ── Redeem (settle USDC to a payee) ──────────────────────────────────────
const result = await redeemPaymentCode(config, 'ABCD1234', {
  payee: '0xMerchantWallet',
});
console.log('Explorer:', result.explorerUrl);

// ── Cancel before use ─────────────────────────────────────────────────────
await cancelPaymentCode(config, 'ABCD1234');

generatePaymentCode options

OptionTypeDefaultDescription
amountnumberAmount in µUSDC (1 USDC = 1 000 000)
networkstring'base-sepolia'Settlement network
descriptionstringOptional note stored with the code
payeestringLock the code to a specific payee address
ttlnumber86400Expiry in seconds (default: 24 h)

RedeemResult fields

FieldTypeDescription
successbooleanSettlement succeeded
codestringThe redeemed code
l2TxHashstringOn-chain tx hash
explorerUrlstringBlock explorer link
amountstringAmount settled (µUSDC)
changestring?Remainder returned to buyer (µUSDC), if partial
changeMode'code' | 'wallet'?How change was returned
changeCodestring?New payment code for change (when changeMode === 'code')

Payment Requests

Merchant-initiated invoices — the merchant creates a payment request, shares the code or link, and any buyer (or agent) can pay it.

import {
  createPayRequest,
  getPayRequest,
  payPayRequest,
  payPayRequestWithCode,
} from '@relai-fi/x402';

const config = { facilitatorUrl: 'https://relai.fi/facilitator' };

// ── Merchant: create an invoice ───────────────────────────────────────────
const req = await createPayRequest(config, {
  to:          '0xMerchantWallet',
  amount:      5_000_000,       // \$5.00 USDC
  network:     'base-sepolia',
  description: 'Order #42',
  ttl:         3600,            // expires in 1 h
});
console.log('Invoice code:', req.code);    // "MW78SGTW"
console.log('Pay URL:', req.payUrl);       // "https://relai.fi/pay#MW78SGTW"

// ── Buyer: read the request ────────────────────────────────────────────────
const info = await getPayRequest(config, 'MW78SGTW');
console.log(`$${Number(info.amount) / 1e6} USDC → ${info.to}`);

// ── Buyer: pay with EIP-3009 signer (has wallet) ─────────────────────────
const signer = createPrivateKeySigner(process.env.BUYER_PRIVATE_KEY!);
const paid = await payPayRequest(config, 'MW78SGTW', signer);
console.log('Explorer:', paid.explorerUrl);

// ── Buyer: pay using a pre-generated payment code (no wallet at payment time)
const result = await payPayRequestWithCode(config, 'MW78SGTW', 'MYBLIK78');
console.log('Success:', result.success);
console.log('Explorer:', result.explorerUrl);

// If the code covers more than the invoice → change is returned as a new code
if (result.changeCode) {
  console.log(`Change $${Number(result.change) / 1e6} USDC → code: ${result.changeCode}`);
}

payPayRequestWithCode options

OptionTypeDefaultDescription
returnChange'code' | 'wallet''code'How to return surplus when code value > invoice
allowOverpaymentbooleantrueIf false, throws when code value ≠ invoice amount

returnChange behaviour:

ValueMechanismRequires buyer wallet?
'code' (default)Relayer pays merchant, generates a new code for the remainderNo
'wallet'PaymentSettler.settleExact() — merchant + change in one atomic txYes (from address)

Development

npm run build        # Build ESM + CJS bundles
npm run dev          # Watch mode
npm run type-check   # TypeScript checks
npm test             # Run tests

License

MIT


RelAI Facilitator · Marketplace · GitHub