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.

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 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
'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 }.