Skip to content

Chains & tokens

PipRail charges and pays the same way everywhere — requirePayment({ chain, token, amount, payTo }) to charge, new PipRailClient({ chain, wallet }).fetch(url) to pay. A single chain value selects the chain, its tokens, and the settlement family behind it. There is no allowlist: the popular chains ship as presets for convenience, and any other EVM chain works by passing its details directly.

Every major EVM chain plus a range of non-EVM families, each reached through the same chain parameter:

Familychain values
EVMethereum, base, arbitrum, optimism, polygon, bnb, avalanche, mantle, sonic, linea, scroll, celo, zksync, unichain, worldchain, sei, injective, hyperevm, monad, kaiaplus any other EVM chain by viem Chain or { id, rpcUrl }
Solanasolana
TONton
Trontron
NEARnear
Suisui
Aptosaptos
Algorandalgorand
Stellarstellar
XRP Ledgerxrpl

The authoritative, always-current list and the per-chain receive prerequisites live in Chains overview. Mainnets only — there are no testnet presets.

// On the server you charge; pass your own receiving address as payTo.
requirePayment({ chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet' }) // EVM preset
requirePayment({ chain: 'solana', token: 'USDC', amount: '0.10', payTo: 'YourSolanaAddr' })

You name a token by symbol and PipRail resolves the canonical contract for that chain. The coverage follows a simple rule:

TokenWhere it’s built in
USDCAlmost everywhere Circle issues it natively.
USDTMost chains — omitted where the chain’s “USDT” is a bridged LayerZero/USDT0 token rather than Tether-native.
EURCWhere Circle issues it — on EVM that’s Ethereum, Base, and Avalanche; also Stellar.
RLUSDXRP Ledger.
native coinValid on every family via token: 'native'.

The native coin (ETH, SOL, XRP, …) is the chain’s gas token and is always a valid payment asset. Anything else works by address — there’s no allowlist for tokens either:

requirePayment({ chain: 'base', token: 'native', amount: '0.10', payTo: '0xYourWallet' })
requirePayment({
chain: 'arbitrum',
token: { address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', decimals: 6 },
amount: '0.10',
payTo: '0xYourWallet',
})

The custom-token shape is per-family — an EVM { address, decimals }, a Solana { mint, decimals }, a TON { master, decimals }, and so on. The full set of shapes is on Chains overview.

Most chains are zero-setup: install, name the chain, add a wallet. A few have one-time caveats worth knowing before you ship — the peer library to install, what the recipient must do to receive a token, and which native coin pays gas. Each family page has the full detail.

ChainInstall (lazy peer)To receive, the recipient needs…Gas / notes
EVM (all presets)viem (core dep)nothingnative coin (ETH/BNB/POL/AVAX/…)
Solana@solana/web3.js @solana/spl-token bs58nothing (the SDK auto-creates the token account)SOL
TON@ton/ton @ton/core @ton/cryptonothing (jetton wallet auto-deploys on first receipt)TON · needs a free toncenter RPC key (@tonapibot on Telegram, in rpcUrl); ships USD₮, no native USDC
Trontronwebnothingreal TRX for energy/bandwidth; ships USD₮ (native TRX also works)
NEARnear-api-jsa NEP-141 token (USDC/USDT): one-time storage_deposit (~0.00125 NEAR). Native NEAR: nothingNEAR · set accountId
Sui@mysten/suinothingSUI · USDC only (no native USDT)
Aptos@aptos-labs/ts-sdknothing (primary FA store auto-creates)APT
AlgorandalgosdkUSDC: one-time ASA opt-in (a 0-amount self-transfer). Native ALGO: nothingALGO · USDC only
Stellar@stellar/stellar-sdkfor USDC/EURC: a trustline + the account funded above its base reserve. Native XLM: nothingXLM · USDC + EURC
XRP Ledgerxrplfor USDC/RLUSD: a trustline + the account activated (≥1 XRP reserve). Native XRP: nothingXRP · USDC + RLUSD

A recipient that isn’t set up surfaces as a typed RecipientNotReadyError (or a RECIPIENT_NOT_READY blocker from planPayment()) with the exact fix — never a silent failure. The base-reserve XLM/XRP is locked, not spent.

CHAINS is the exported registry of built-in EVM presets. Each preset carries the underlying viem Chain and a tokens map keyed by uppercase symbol, with the canonical address and decimals pre-filled — so you never paste a token address for a supported chain:

import { CHAINS } from '@piprail/sdk'
CHAINS.base.tokens.USDC
// → { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, symbol: 'USDC' }

A ChainPreset is shaped like this:

interface ChainPreset {
chain: Chain // the viem chain (id, name, native coin, RPCs)
defaultRpc?: string // override only where viem's default is unreliable
tokens: Record<string, TokenInfo> // canonical tokens, keyed by UPPERCASE symbol
}
interface TokenInfo {
address: `0x${string}`
decimals: number
symbol: string
}

Iterate CHAINS to build a chain picker, or read a preset directly when you want the canonical address for a known chain. ChainName is the union of built-in preset keys ('base' | 'ethereum' | …).

You’re not limited to the presets. For an EVM chain PipRail doesn’t ship, pass a viem Chain or a bare { id, rpcUrl }, plus the token you want paid in. If viem can reach the RPC, PipRail works on it:

requirePayment({
chain: { id: 1313161554, rpcUrl: 'https://mainnet.aurora.dev' },
token: { address: '0xB12BFcA5A55806AaF64E99521918A4bf0fC40802', decimals: 6 },
amount: '0.10',
payTo: '0xYourWallet',
})

The accepted chain types are captured by ChainInput:

FormUse when
ChainName ('base', …)A built-in EVM preset.
viem ChainAn EVM chain you already have a viem definition for.
{ id, rpcUrl, name?, nativeCurrency? }Any EVM chain by id + RPC. Native coin defaults to 18-decimal ETH; override nativeCurrency if it isn’t.

When you pass a raw viem Chain or { id, rpcUrl } that happens to match a built-in chain id, symbol resolution still works — the preset’s tokens are looked up by chain id.

The public default RPC on every chain is rate-limited. Pass your own rpcUrl in production. There is no separate API-key field — fold any key into the URL:

requirePayment({
chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet',
rpcUrl: 'https://base-mainnet.example.com/v2/YOUR_KEY',
})

Each entry in a multi-rail accept[] can carry its own rpcUrl. A few non-EVM families effectively require a keyed endpoint (TON’s keyless toncenter is too slow for verify()) — those caveats are on each chain’s page under Chains.