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, kaia, robinhood, plus 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, but 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, because 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, because 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, so 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.