EVM chains & any other chain
Introduction
Section titled “Introduction”A chain is a parameter you pass, not an allowlist the SDK ships. The popular EVM mainnets are
built in by name (chain: 'base'), each with canonical USDC pre-filled so you never paste a
token address. Any other EVM chain works by passing a viem Chain or a bare { id, rpcUrl } —
if viem can reach the RPC, PipRail works on it.
This page covers the EVM side. The non-EVM families each have their own page (Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar, XRPL), and the cross-chain setup caveats live in Chains & tokens.
Name a built-in chain — the easy path
Section titled “Name a built-in chain — the easy path”Every gate and client takes chain. Pass one of the built-in names and the SDK fills in the
chain id, a default RPC, and the canonical token addresses:
import { requirePayment } from '@piprail/sdk'
// USDC on Base, paid to your wallet — middleware you drop in front of a route.requirePayment({ chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet' })token: 'native' pays in the chain’s own gas coin (ETH, BNB, POL, AVAX, …) — accepted on every
preset:
requirePayment({ chain: 'bnb', token: 'native', amount: '0.01', payTo: '0xYourWallet' }) // BNBAny other EVM chain — no allowlist
Section titled “Any other EVM chain — no allowlist”Don’t see your chain in the registry? Pass a viem Chain or a bare { id, rpcUrl }, plus the
exact token to be paid in. There’s no gatekeeping — the registry is a convenience, not a limit.
requirePayment({ chain: { id: 1313161554, rpcUrl: 'https://mainnet.aurora.dev' }, // any EVM chain token: { address: '0x0b9aB...4802', decimals: 6 }, // any ERC-20 — symbol is optional amount: '0.10', payTo: '0xYourWallet',})symbol on a custom token is optional (display-only metadata); address and decimals are
what the SDK actually pays and verifies against. When you pass a custom chain, the SDK still
recognises a built-in token address if its chain id matches a preset — so symbol resolution keeps
working even off the named path.
The built-in EVM registry — CHAINS
Section titled “The built-in EVM registry — CHAINS”CHAINS is the registry itself: an object keyed by chain name, each value a ChainPreset. It’s
exported so you can iterate it — build a chain picker, list what’s supported, or read a token
address — without hardcoding anything.
import { CHAINS } from '@piprail/sdk'
for (const [name, preset] of Object.entries(CHAINS)) { console.log(name, preset.chain.id, Object.keys(preset.tokens))}// → base 8453 [ 'USDC', 'EURC' ]// → bnb 56 [ 'USDC', 'USDT' ]// → … one line per built-in chainEach preset carries the underlying viem chain, an optional defaultRpc override (set only
where viem’s bundled default is unreliable), and the well-known tokens keyed by uppercase
symbol:
interface ChainPreset { chain: Chain // viem chain: id, name, native coin, default RPCs defaultRpc?: string // RPC used when the caller passes no rpcUrl tokens: Record<string, TokenInfo> // well-known tokens, keyed by UPPERCASE symbol}
interface TokenInfo { address: `0x${string}` decimals: number symbol: string}Token coverage
Section titled “Token coverage”USDC is built in on every preset except where Circle issues none; USDT where it’s Tether-native; EURC where Circle issues it. Every address was verified on-chain (symbol + decimals) before shipping. The current registry:
chain | Network | Built-in tokens |
|---|---|---|
'ethereum' | Ethereum | USDC, USDT, EURC |
'base' | Base | USDC, EURC |
'arbitrum' | Arbitrum | USDC, USDT |
'optimism' | Optimism | USDC, USDT |
'polygon' | Polygon | USDC, USDT |
'bnb' | BNB Chain | USDC, USDT |
'avalanche' | Avalanche | USDC, USDT, EURC |
'mantle' | Mantle | USDC, USDT |
'sonic' | Sonic | USDC, USDT |
'linea' | Linea | USDC, USDT |
'scroll' | Scroll | USDC, USDT |
'celo' | Celo | USDC, USDT |
'zksync' | zkSync Era | USDC, USDT |
'unichain' | Unichain | USDC, USDT |
'worldchain' | World Chain | USDC |
'sei' | Sei | USDC |
'injective' | Injective | USDC, USDT |
'hyperevm' | HyperEVM (Hyperliquid) | USDC |
'monad' | Monad | USDC |
'kaia' | Kaia (ex-Klaytn) | USDT |
token: 'native' works on all of them. For the issuer-native-vs-bridged provenance of each
USDC/USDT, the EURC EIP-712 caveat, and BNB’s 18-decimal peg tokens, see
Chains & tokens.
Resolving a chain — resolveChain
Section titled “Resolving a chain — resolveChain”resolveChain(input, rpcUrlOverride?) is the one function that normalises whatever you pass for
chain into the { chain, chainId, rpcUrl, tokens } the wallet and verifier need. The gate and
client call it for you; you rarely call it directly, but it documents exactly how the three input
shapes resolve.
import { resolveChain } from '@piprail/sdk'
const resolved = resolveChain('base')// → { chain: <viem Chain>, chainId: 8453, rpcUrl: 'https://…', tokens: { USDC, EURC } }
resolveChain(someViemChain) // a full viem ChainresolveChain({ id: 1313161554, rpcUrl: 'https://mainnet.aurora.dev' }) // a bare { id, rpcUrl }It returns a ResolvedChain:
interface ResolvedChain { chain: Chain chainId: number rpcUrl: string tokens: Record<string, TokenInfo> // built-in tokens for this chain (empty if unknown)}The three input shapes — ChainInput
Section titled “The three input shapes — ChainInput”ChainInput is the union chain accepts. How each resolves:
| Input | rpcUrl resolution | tokens |
|---|---|---|
ChainName (e.g. 'base') | rpcUrlOverride → preset defaultRpc → viem default | the preset’s tokens |
viem Chain | rpcUrlOverride → the chain’s viem default | built-in tokens if the id matches a preset, else {} |
{ id, rpcUrl } | rpcUrlOverride → the rpcUrl you passed | built-in tokens if the id matches a preset, else {} |
A bare { id, rpcUrl } may also carry an optional name (defaults to EVM <id>) and
nativeCurrency (defaults to 18-decimal ETH):
resolveChain({ id: 1313161554, rpcUrl: 'https://mainnet.aurora.dev', name: 'Aurora', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },})The rpcUrl override
Section titled “The rpcUrl override”Pass rpcUrl on the gate or client and it wins over every default. Public RPCs are
rate-limited, so production callers should always pass their own (there’s 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',})All of these are exported from @piprail/sdk:
| Type | What it is |
|---|---|
ChainName | A built-in EVM chain name — keyof typeof CHAINS. |
ChainInput | What chain accepts: a ChainName, a viem Chain, or { id, rpcUrl, name?, nativeCurrency? }. |
ResolvedChain | The normalised { chain, chainId, rpcUrl, tokens } returned by resolveChain. |
ChainPreset | One registry entry: { chain, defaultRpc?, tokens }. |
TokenInfo | A built-in token: { address, decimals, symbol }. |