Skip to content

EVM chains & any other chain

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.

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' }) // BNB

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.

An invalid token is rejected up front, at config time, with a typed WrongFamilyError — never a raw viem error later on the verify path: a non-0x token address (e.g. a Tron T… string), or another family’s token shape (a Solana { mint }, a Stellar { issuer, code }) on an EVM chain. A valid address is checksum-normalized for you. This mirrors how a non-0x payTo is rejected at config time.

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', 'FDUSD', 'USD1', 'U' ]
// → … one line per built-in chain

Each 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
}

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:

chainNetworkBuilt-in tokens
'ethereum'EthereumUSDC, USDT, EURC
'base'BaseUSDC, EURC
'arbitrum'ArbitrumUSDC, USDT
'optimism'OptimismUSDC, USDT
'polygon'PolygonUSDC, USDT
'bnb'BNB ChainUSDC, USDT, FDUSD, USD1, U
'avalanche'AvalancheUSDC, USDT, EURC
'mantle'MantleUSDC, USDT
'sonic'SonicUSDC, USDT
'linea'LineaUSDC, USDT
'scroll'ScrollUSDC, USDT
'celo'CeloUSDC, USDT
'zksync'zkSync EraUSDC, USDT
'unichain'UnichainUSDC, USDT
'worldchain'World ChainUSDC
'sei'SeiUSDC
'injective'InjectiveUSDC, USDT
'hyperevm'HyperEVM (Hyperliquid)USDC
'monad'MonadUSDC
'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.

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 Chain
resolveChain({ 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)
}

ChainInput is the union chain accepts. How each resolves:

InputrpcUrl resolutiontokens
ChainName (e.g. 'base')rpcUrlOverride → preset defaultRpc → viem defaultthe preset’s tokens
viem ChainrpcUrlOverride → the chain’s viem defaultbuilt-in tokens if the id matches a preset, else {}
{ id, rpcUrl }rpcUrlOverride → the rpcUrl you passedbuilt-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 },
})

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:

TypeWhat it is
ChainNameA built-in EVM chain name — keyof typeof CHAINS.
ChainInputWhat chain accepts: a ChainName, a viem Chain, or { id, rpcUrl, name?, nativeCurrency? }.
ResolvedChainThe normalised { chain, chainId, rpcUrl, tokens } returned by resolveChain.
ChainPresetOne registry entry: { chain, defaultRpc?, tokens }.
TokenInfoA built-in token: { address, decimals, symbol }.