@relai-fi/x402
Unified x402 payment SDK for Solana, Base, Polygon, Ethereum, Avalanche, Telos EVM, and SKALE Base.
The x402 protocol enables 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.
Zero gas fees. The RelAI facilitator sponsors gas — users only pay for content (USDC). Works on all supported networks.
Installation
npm install @relai-fi/x402Quick Start — Client
Create a payment-aware fetch client. Works in the browser and Node.js:
import { createX402Client } from '@relai-fi/x402/client';
const client = createX402Client({
wallets: {
solana: solanaWallet, // @solana/wallet-adapter compatible
evm: evmWallet, // wagmi/viem compatible
},
});
// 402 responses are handled automatically
const response = await client.fetch('https://api.example.com/protected');
const data = await response.json();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
All networks use USDC with 6 decimals. Gas fees are sponsored by the facilitator (RelAI or 0xGasless).
| Network | Identifier (v1) | CAIP-2 (v2) | Signing Method |
|---|---|---|---|
| Solana | solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp | SPL transfer + fee payer · smart wallet ready |
| Base | base | eip155:8453 | EIP-3009 transferWithAuthorization |
| Avalanche | avalanche | eip155:43114 | EIP-3009 transferWithAuthorization |
| Telos EVM | telos | eip155:40 | Standard ERC-20 transfer (no EIP-3009) |
| Avalanche (0xGasless) | avalanche | eip155:43114 | Custom EIP-712 (A402 domain) |
| Polygon | polygon | eip155:137 | EIP-3009 transferWithAuthorization |
| Ethereum | ethereum | eip155:1 | EIP-3009 transferWithAuthorization |
| SKALE Base | skale-base | eip155:1187947933 | EIP-3009 transferWithAuthorization |
0xGasless auto-detection. When the server returns relayerContract in payment requirements, the SDK automatically switches to the 0xGasless EIP-712 domain (name: "A402", verifyingContract: relayerContract) and sets validAfter: 0. No client code changes needed. Learn more →
Both formats accepted. The SDK accepts v1 simple names (base, polygon) and v2 CAIP-2 identifiers (eip155:8453, eip155:137). Use networkV1ToV2() and networkV2ToV1() to convert between them.
createX402Client(config)
Creates a fetch wrapper that automatically handles 402 Payment Required responses.
| Option | Type | Default | Description |
|---|---|---|---|
wallets | { solana?, evm? } | {} | Wallet adapters for each chain |
facilitatorUrl | string | RelAI facilitator | Custom facilitator endpoint |
preferredNetwork | RelaiNetwork | — | Prefer this network when multiple accepts |
solanaRpcUrl | string | mainnet-beta | Solana RPC endpoint |
evmRpcUrls | Record<string, string> | Built-in | RPC URLs per network name |
maxAmountAtomic | string | — | Safety cap on payment amount |
verbose | boolean | false | Log payment flow to console |
Wallet Interfaces
// Solana — compatible with @solana/wallet-adapter-react useWallet()
// Also works with smart wallets: Squads, Crossmint, SWIG, on-chain agents
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>;
}Smart Wallets (Solana)
The SDK fully supports Solana smart wallets — including Squads multisig, Crossmint, SWIG session keys, and on-chain agents. Any wallet that implements publicKey + signTransaction works out of the box. No SDK changes are needed on the client side.
How it works
Smart wallets on Solana (e.g. Squads vault PDAs, Crossmint MPC wallets) use off-curve public keys — addresses that are derived from a program, not a private key. The SDK handles this transparently: Associated Token Accounts are resolved with allowOffCurve: true, so PDA-based signers work the same as regular keypairs. The smart wallet's own signTransaction callback handles its internal signing logic (multisig CPI, MPC, session key delegation, etc.).
import { createX402Client } from '@relai-fi/x402/client';
import { useSquadsWallet } from '@squads-protocol/sdk'; // example
// Squads wallet implements publicKey + signTransaction — pass directly
const squadsWallet = useSquadsWallet();
const client = createX402Client({
wallets: { solana: squadsWallet },
});
// Payment signed by the Squads multisig — no extra config needed
const response = await client.fetch('https://api.example.com/protected');import { createCrossmintX402Fetch } from '@relai-fi/x402/crossmint';
import { Connection } from '@solana/web3.js';
// Crossmint signs and broadcasts — no signTransaction needed
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();Get a Crossmint API key at console.crossmint.com. Required scopes: wallets.read, wallets.create, wallets:transactions.create, wallets:transactions.sign. Full working example: crossmint-relai-x402.
Supported smart wallet providers
| Provider | Type | Notes |
|---|---|---|
| Squads Protocol | Multisig PDA | n-of-m signing via CPI — works if vault has USDC ATA |
| Crossmint | MPC / embedded | Implements standard wallet-adapter interface |
| SWIG | Session key delegation | Scoped signing with time/amount limits |
| On-chain agents | Program-controlled PDA | Any agent holding USDC and signing SPL transfers |
| Privy / Turnkey | MPC custodial | Server-side signing — key never leaves MPC threshold |
Requirement. The smart wallet's ATA for USDC must exist on-chain before the first payment. For Squads vaults, this means the vault PDA must have a funded USDC Associated Token Account. Regular spl-token create-account or any ATA creation UI works.
useRelaiPayment(config)
React hook wrapping createX402Client with state management. Config is the same as above.
| Property | Type | Description |
|---|---|---|
fetch | (input, init?) => Promise<Response> | Payment-aware fetch |
isLoading | boolean | Payment in progress |
status | 'idle' | 'pending' | 'success' | 'error' | Current state |
error | Error | null | Error details on failure |
transactionId | string | null | Tx hash/signature on success |
transactionUrl | string | null | Block explorer link |
transactionNetworkLabel | string | null | Human-readable label (e.g. "Base") |
connectedChains | { solana, evm } | Which wallets are connected |
reset | () => void | Reset state to idle |
Server SDK (Express)
Protect any Express route with x402 micropayments:
import Relai from '@relai-fi/x402/server';
const relai = new Relai({
network: 'base', // or 'solana', 'avalanche', 'skale-base'
});
// Protect any Express route with micropayments
app.get('/api/data', relai.protect({
payTo: '0xYourWallet',
price: 0.01, // $0.01 USDC
description: 'Premium data access',
}), (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',
}), handler);req.payment
| Field | Type | Description |
|---|---|---|
verified | boolean | Always true after settlement |
transactionId | string | On-chain transaction hash |
payer | string | Payer wallet address |
network | string | Network name (e.g., base) |
amount | number | Price in USD |
Stripe PayTo
Accept x402 payments directly into your Stripe Dashboard as USD - no crypto knowledge required. The stripePayTo() helper creates a Stripe PaymentIntent with a crypto deposit address per request. Payments settle on Base (USDC) and Stripe converts them to USD automatically.
import Relai, { stripePayTo } from '@relai-fi/x402/server';
const relai = new Relai({ network: 'base' });
app.get('/api/data', relai.protect({
price: 0.01,
payTo: stripePayTo(process.env.STRIPE_SECRET_KEY),
}), (req, res) => {
res.json({ data: 'paid content' });
});How It Works
Each 402 response creates a fresh Stripe PaymentIntent with payment_method_types: ["crypto"]. The deposit address from next_action.crypto_collect_deposit_details is returned as payTo. When the client pays, Stripe auto-captures the PI and settles as USD.
stripePayTo(secretKey, options?)
| Parameter | Type | Default | Description |
|---|---|---|---|
secretKey | string | - | Stripe secret key (sk_live_... or sk_test_...) |
options.network | string | base | Stripe crypto deposit network |
Requirements. Your Stripe account needs Crypto payins enabled. Store your STRIPE_SECRET_KEY in environment variables - never hardcode it.
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.5Payload 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'
const v2Payload = convertV1ToV2(v1Payload);
const v1Payload = convertV2ToV1(v2Payload);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, { stripePayTo } from '@relai-fi/x402/server';
// Utilities — payload conversion, unit helpers
import {
convertV1ToV2, convertV2ToV1,
networkV1ToV2, networkV2ToV1,
toAtomicUnits, fromAtomicUnits,
} from '@relai-fi/x402/utils';
// 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';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> | |
© 2026 RelAI. Monetize APIs with x402 Protocol.