Supported blockchains
The four chain families Ecosystem custodies — built-in EVM, UTXO, licensed non-EVM and operator-defined custom EVM — how to enable one, and the diagnostics console that names the missing variable.
Ecosystem groups chains into four families. Which family a chain belongs to
decides how it is enabled, what it needs in .env, whether it needs a separate
licence, and how deposits reach a customer.
| Family | Chains | Enabled by |
|---|---|---|
| Built-in EVM | ETH, BSC, POLYGON, FTM, OPTIMISM, ARBITRUM, BASE, CELO, RSK, HECO, CRONOS, MO | Setting its .env variables |
| UTXO | BTC, LTC, DOGE, DASH | Setting its .env variables |
| Licensed non-EVM | SOL, TRON, TON, XMR | Buying the blockchain addon, activating its licence, enabling the row |
| Custom EVM | Anything you add | Admin → Ecosystem → Custom EVM Chains |
A chain being "supported" means the addon can derive addresses on it, watch for deposits, and sign withdrawals. It does not mean it is on — every chain is off until you configure it, and the platform ships with none configured.
The naming convention
Built-in chains read their configuration from .env using a mechanical pattern.
Get the pattern right and there is nothing else to learn per chain.
ETH_NETWORK="mainnet"
ETH_MAINNET_RPC="https://ethereum-rpc.publicnode.com"
ETH_MAINNET_RPC_WSS="wss://..."
ETH_EXPLORER_API_KEY="..."<SYMBOL>_NETWORK selects the network. The runtime then reads
<SYMBOL>_<NETWORK>_RPC and <SYMBOL>_<NETWORK>_RPC_WSS — the network name
uppercased — for that network only. <SYMBOL>_EXPLORER_API_KEY is the per-chain
Etherscan key: it is tried before the global ETHERSCAN_API_KEY, not instead
of it, so a stale per-chain key no longer shadows a working global one.
The valid network names are per chain and not guessable:
| Chain | Networks | Chain IDs |
|---|---|---|
| ETH | mainnet, sepolia |
1, 11155111 |
| BSC | mainnet, testnet |
56, 97 |
| POLYGON | matic, amoy |
137, 80002 |
| FTM | mainnet, testnet |
250, 4002 |
| OPTIMISM | mainnet, sepolia |
10, 11155420 |
| ARBITRUM | mainnet, sepolia |
42161, 421614 |
| BASE | mainnet, sepolia |
8453, 84532 |
| CELO | mainnet, sepolia |
42220, 11142220 |
| RSK | mainnet, testnet |
30, 31 |
| HECO | mainnet, testnet |
128, 256 |
| CRONOS | mainnet |
25 |
| MO | mainnet, testnet |
7860, 7862 |
Polygon's mainnet is called matic, not mainnet. Setting
POLYGON_NETWORK="mainnet" means the runtime reads POLYGON_MAINNET_RPC, which
is not a valid network, and provider initialisation fails with "Chain ID not
found".
ARBIRUM_MAINNET_RPC — missing the second T — is still read by the admin
balance endpoint and the legacy health check, but not by the real provider path.
Set only the typo and health reports Arbitrum as Up while every deposit and
withdrawal on it is broken. Always set ARBITRUM_MAINNET_RPC.
alfajores is not a supported CELO network. CELO_ALFAJORES_RPC is read by
nothing.
Enabling a built-in EVM chain
-
Pick the network and add the variables to the project root
.env.BSC_NETWORK="mainnet" BSC_MAINNET_RPC="https://bsc-dataseed.binance.org" BSC_MAINNET_RPC_WSS="wss://bsc-ws-node.nariox.org:443"The WSS endpoint is optional. Without it, token-deposit monitors fall back to HTTP polling rather than realtime events.
-
Add a transaction-history provider key — unless the chain is keyless. Native-deposit detection and the admin token-holders view both go through the explorer providers. Most chains have a free keyless provider behind them and need nothing here; BSC, FTM, CRONOS, HECO and Polygon's Amoy testnet are the ones that do not. An Etherscan V2 key still buys the richest data on the chains its free tier covers:
ETHERSCAN_API_KEY="..."The key may hold several comma-separated keys, and the same is true of every other provider key. See Explorer coverage is not uniform for which chains need what.
-
Restart the backend. Provider instances are constructed at module load, so nothing you just wrote is in effect yet.
pm2 restart backend -
Run the diagnostics. Admin → Ecosystem → Blockchains → Requirements, select the chain, and run its test. Fix what it names before moving on.
-
Create the master wallet for the chain — see Master wallets. Until one exists, withdrawals on the chain cannot be signed.
Explorer coverage is not uniform
One Etherscan V2 key does not cover every chain. Its free tier was cut on 2025-11-22 and is now paid-only for BSC (56 and 97), OP Mainnet (10 and 11155420), Base (8453 and 84532) and Avalanche; Gnosis moves to paid on 2026-09-01. FTM (chain id 250) and CRONOS (25) are not on the V2 chainlist at all — Fantom migrated to Sonic — and HECO is effectively sunset.
That is why the provider order is decided per chain rather than globally. Each chain leads with a provider that is actually free for it, and any keyless provider that can serve the chain is appended to the end of whatever order applies, so the chain still has a last resort:
| Chain | Built-in order | Works with no keys at all |
|---|---|---|
| ETH, POLYGON, ARBITRUM, CELO | Etherscan, Blockscout, then the keyed fallbacks | Yes — except Polygon Amoy |
| BASE, OPTIMISM | Blockscout first, Etherscan behind it | Yes |
| RSK | Blockscout, then Etherscan | Yes |
| MO | Etherscan against MO's own explorer host, then Blockscout if you self-host one | Yes |
| FTM | Ankr, Moralis, Covalent, Etherscan | No |
| BSC | NodeReal, Ankr, Moralis, Covalent, Etherscan | No |
| CRONOS, HECO | The generic order — but no provider indexes either chain | No |
Five chains have no keyless option at all. There is no hosted Blockscout
instance and no Routescan coverage for BSC (56 and 97), FTM (250 and 4002),
CRONOS (25), HECO (128 and 256) or Polygon's Amoy testnet (80002). BSC is the one
of them where a keyed provider is the normal answer for a production install: on
mainnet set NODEREAL_API_KEY, which is free for BSC; on testnet NodeReal and
Ankr do not index chain id 97 at all, so use MORALIS_API_KEY or
COVALENT_API_KEY. A paid Etherscan plan also works, and a self-hosted instance
named in BSC_BLOCKSCOUT_HOST is the only way to make BSC keyless.
RSK is served keylessly on both networks. It goes through the same provider
dispatcher as every other chain, and Blockscout resolves the host from the chain
id — 30 to rootstock.blockscout.com, 31 to rootstock-testnet.blockscout.com —
so RSK_NETWORK="testnet" reads testnet. Earlier releases called a hardcoded
mainnet Blockscout endpoint whose response shape the parser rejected; that is
fixed.
The RPC block scan underneath all of this is a floor, not a substitute. When the explorer path fails, the native-deposit monitor switches to walking blocks over the chain's own RPC — at most 50 blocks per poll, forward only. It catches deposits arriving from then on; it does not recover the 24 hours of lookback the explorer path gives.
Setting keys, and pinning an order
The built-in defaults already lead each chain with a provider that is free for it, so on most installations the answer is set nothing. Reach for the following only when the diagnostics console tells you to.
One key, everywhere. The common case. An Etherscan V2 key covers every chain its free tier still includes, and is ignored on the ones it does not:
ETHERSCAN_API_KEY="your_key"Several keys, rotated. Any provider key may hold a comma-separated list. The dispatcher moves to the next key when one is rejected, out of quota, or on a plan that excludes the chain — so a revoked key costs one attempt rather than the chain:
ETHERSCAN_API_KEY="key_one,key_two,key_three"A key for one chain only. Prefix any provider key with the chain symbol. The chain-scoped keys are tried first and the global key stays behind them as a spare, so a paid key can serve the one chain that needs it without you buying a plan for the rest:
ETHERSCAN_API_KEY="free_key"
BSC_ETHERSCAN_API_KEY="paid_key"
NODEREAL_API_KEY="your_key"
BSC_NODEREAL_API_KEY="a_different_key"Every provider takes both forms — <CHAIN>_ANKR_API_KEY and ANKR_API_KEY,
<CHAIN>_MORALIS_API_KEY and MORALIS_API_KEY, and so on. <CHAIN>_EXPLORER_API_KEY
is the older name for the Etherscan one and still works.
Pinning the order for a chain. Only worth doing when you know something the defaults do not — that your Moralis plan is faster than your Etherscan one, say:
TRANSACTION_PROVIDERS_BSC="nodereal,covalent"TRANSACTION_PROVIDERS sets one order for every chain at once and is the blunter
instrument; a per-chain variable beats it. Whichever applies, any keyless
provider that can serve the chain is still appended to the end as a last resort.
Set TRANSACTION_PROVIDERS_STRICT="true" if you would rather a pinned order be
the whole story and have the chain fail when it is exhausted.
Raising a keyless rate limit. Blockscout and Routescan serve without a credential, but the anonymous tier is throttled per IP — an installation polling many deposit addresses will meet it. Both accept an optional key that lifts the limit, and neither stops working without one:
BLOCKSCOUT_API_KEY="your_key"Giving a keyless-less chain a keyless provider. The only way, and it means running the instance yourself:
BSC_BLOCKSCOUT_HOST="blockscout.example.com"Every one of these needs pm2 restart backend before it takes effect, and the
full variable list — including the per-attempt timeout and record limit — is in
the environment reference.
Checking it worked
Admin → Ecosystem → Blockchains → Requirements, select the chain, run its test. The explorer check lists every provider in the order the runtime will try them, and says which one is actually serving the chain, where that order came from, and how many keys each provider has. A provider marked cooling down is one the circuit breaker is skipping after a recent failure — the tooltip says why. no key on a keyless provider is impossible by construction; if you see a provider skipped for a missing key, that is the one to set.
UTXO chains
Bitcoin, Litecoin, Dogecoin and Dash use a provider abstraction rather than an
RPC URL. <SYMBOL>_NODE selects it.
| Chain | Default provider | What actually works |
|---|---|---|
| BTC | mempool |
mempool, blockcypher, node (self-hosted Bitcoin Core) |
| LTC | mempool |
mempool (litecoinspace.org), blockcypher |
| DOGE | blockcypher |
BlockCypher only |
| DASH | blockcypher |
BlockCypher only |
The factory falls back silently. An unrecognised value uses the chain
default; node on anything but BTC falls back to BlockCypher, because
self-hosted node support is Bitcoin-only; mempool on DOGE or DASH falls back
to BlockCypher too.
Two things to know before you point Bitcoin anywhere but mainnet. BlockCypher
only serves mainnet and testnet3, so BTC_NETWORK=testnet4 with
BTC_NODE=blockcypher cannot work. And deposit addresses are generated for
the network configured at the time — flip BTC_NETWORK after users have
addresses and those addresses are invalid and must be regenerated.
Running a self-hosted Bitcoin Core node unlocks realtime zero-confirmation
detection over ZMQ. The presence of BTC_ZMQ_RAWTX is the on/off switch for the
whole ZMQ service, and it only initialises when BTC_NODE=node. Setting it
under any other provider does nothing.
Licensed non-EVM chains
Solana, Tron, TON and Monero are separate products. Each is gated twice, and both gates must pass.
-
Buy and activate the blockchain addon. Activation writes
lic/<productId>.lic— an encrypted, machine-bound file. Without it the status toggle refuses with a 403. -
Enable the row. Admin → Ecosystem → Blockchains lists the seeded chains with their product IDs. Enabling flips
ecosystem_blockchain.status. -
Add the chain's variables — each family has its own names, listed in the environment reference.
-
Restart, then run the diagnostics.
The seeded product IDs are Solana 54514052, Tron 54577641, Monero 54578959
and TON 55715370. Every one of them ships with status: false.
Beyond the licence, each of these chains needs its service module to be present in the install. If the addon code was never extracted, the diagnostics report "Chain service installed: no" and every flow on that chain is dead regardless of the licence.
A few per-chain facts that cost people days:
SOL_NETWORKselects the cluster, and gets it wrong quietly. Anything other thanmainnetortestnet— including unset, and including the plausible-lookingmainnet-beta— falls through to devnet. The public cluster endpoint is used unlessSOL_<NETWORK>_RPCnames your own;SOLANA_RPC_URLis a different key, read only by an admin cost estimate.- Tron throws at construction on a bad network.
TRON_NETWORKmust bemainnet,shastaornile; anything else is a total outage, not a degradation. On mainnet,TRON_API_KEYis effectively mandatory — the deposit monitor stops itself after ten consecutive errors, and anonymous TronGrid quotas will get you there. - TON's anonymous rate limit is about one request per second, which throttles both deposit polling and the ten-attempt withdrawal confirmation loop. Set the Toncenter API key for the active network.
- Monero needs both of its daemons, and each one fails differently. If
monero-wallet-rpcdoes not answerget_versionwhen the service starts, the chain is marked inactive in that process: wallet creation, balance reads and transaction history refuse until it answers. Withdrawals do not check that: while the wallet RPC is down a new one is still accepted and debited, then fails and is refunded.monerodmust be fully synchronised and reachable from both sides: deposit detection talks only to the wallet RPC, which syncs through its own daemon connection, while the backend's own connection prices withdrawals. So if only the backend's path is broken — a wrong port inXMR_DAEMON_RPC_URL, say — deposits keep crediting while a new withdrawal is refused on the withdraw form before anything is debited ("Failed to withdraw: Failed to estimate Monero fee: …") and one already queued fails and is refunded. Withmoneroditself down, both stop. Startmonero-wallet-rpcwith--daemon-address, and with--daemon-loginwhenmonerodrequires an RPC login: they decide which daemon every wallet it opens syncs against. Without the login, each session's refresh fails with-38 no connection to daemonand the backend has to re-point that wallet withset_daemonand retry — which fails too unlessXMR_RPC_USER/XMR_RPC_PASSWORDare the daemon's login. A wallet RPC that cannot reach a daemon the backend itself can reach is the reverse case: no deposit is detected, and withdrawals are accepted and debited first, then fail at the wallet refresh and are refunded. - One
monero-wallet-rpchas one wallet open at a time, and every backend process shares it. The platform takes a cluster-wide lock for each session — open a customer's wallet, check its address is the one on record, read or send, close — so the web and cron processes never read each other's wallet, and a session that finds the wrong wallet open refuses instead of crediting anyone. Two consequences for you: restart the web and cron processes together after an update (an old build beside a new one ignores the lock), and never callopen_wallet,close_walletor any other wallet method by hand against the live wallet RPC — an open or close swaps the wallet under whatever session is running, and any other call acts on whichever customer's wallet happens to be open. To look inside a customer's wallet, copy its files and open the copy with a secondmonero-wallet-rpcon another port. - A withdrawal waits at most 90 seconds for the wallet RPC, then goes back to
PENDINGwith "deferred, nothing was sent" in the log, and the withdrawal watchdog, which runs every five minutes, hands it back to the queue — it is never failed for being busy. One that keeps being refused because its wallet file is not the wallet on record (or has no recorded address) is failed and refunded afterXMR_WITHDRAWAL_REFUSAL_FAIL_AFTER_HOURS(24 hours by default), with anALERTin the log; a withdrawal that may already have been broadcast is never failed or refunded automatically. Whatever failed it, a refunded XMR withdrawal's row ends up described as "Refund of" and its amount: the reason is in the backend log and in the customer's failure email, not on the row — see Monero withdrawals. - Switching Monero off does not stop its withdrawals. Nothing on the XMR withdrawal path reads the chain's row, and a process whose Monero service is already active never reads it again until it restarts. To refuse new XMR withdrawals, disable the XMR token while the chain is still on — the token's toggle and edit form refuse every change while the chain's row is off, and only the bulk token-status API, which no admin screen calls, still accepts one. Rows already queued are still sent.
Custom EVM chains
- Symbols may not shadow a built-in chain and must be unique
- Two chains may not claim the same EVM chain ID
- Confirmations defaults to 12 and accepts 1 to 1000
- Test before you save — a mismatched chain ID fails later, further from the cause
Any EVM-compatible chain can be added from data alone, with no code change: Admin → Ecosystem → Blockchains → Custom EVM Chains.
You supply the symbol, display name, EVM chain ID, native currency, RPC URL and optionally a WebSocket URL, explorer URL and explorer API key. Creating the row hydrates the chain into the live registry immediately and backfills its native coin as an Ecosystem token — a native currency has no smart contract, so it cannot be added through the token import flow and is created here instead.
The RPC URL must be an http:// or https:// address whose host is a full
domain name (with a suffix such as .org) or an IP address. A bare name such
as http://localhost:8545 is refused when you save, so reach a node on the
same machine as http://127.0.0.1:8545.
With Instant Convert installed, that native token counts as a new withdrawal chain for its currency. That matters when the currency already has another enabled withdrawal chain (BTC or ETH as the gas coin of an EVM chain, say) and Convert has touched it on Ecosystem balances (a convert order in it, or a Convert house balance of it). Then creating or enabling the chain, or changing its native currency to that one, asks you to confirm closing Convert for the currency, and a native token the server would enable on its own, when it starts or after a chain is saved, stays disabled instead. A currency with no other enabled chain is never asked about. See Enabling a second chain later.
Three guards apply. You cannot shadow a built-in symbol (ETH, BSC, SOL,
BTC and the rest are reserved), symbols must be unique, and two chains may not
claim the same EVM chain ID — providers pin the ID at construction, so a
duplicate means one of them is misconfigured.
The chain's .env keys are then managed automatically: <SYMBOL>_NETWORK,
<SYMBOL>_<NETWORK>_RPC, <SYMBOL>_<NETWORK>_RPC_WSS and
<SYMBOL>_EXPLORER_API_KEY are written into process.env from the database at
boot. Editing them by hand in .env is pointless — they are rewritten on every
reload.
Custom chains do not inherit any global provider key — not
ETHERSCAN_API_KEY, not ANKR_API_KEY, none of them. A first-party key sent to
a third-party explorer turns a working keyless request into an "Invalid API Key"
rejection. What they do get is the chain's own explorer URL, used keylessly,
plus a hosted Blockscout or Routescan instance if one exists for their chain id.
With no explorer configured and no keyless instance, native deposits are found by
RPC block scanning only.
The requirements and diagnostics console
Admin → Ecosystem → Blockchains → Requirements is the page that answers "why is this chain not working". For every chain it lists:
- every
.envkey the runtime actually reads, marked required or optional, with the condition under which an optional key becomes required, and whether it is currently set (secrets are never displayed, only their presence); - the non-environment prerequisites — vault unlocked, master wallet present and enabled, active tokens on the current network, licence valid;
- chain-specific warnings, including the name traps above and every silent provider fallback;
- keys that are dead — read by no code path and safe to remove.
The test result is deliberately strict: it passes only when every check passed or was skipped and no platform flow is reported broken. An RPC that answers is not a pass if the vault is locked or the master wallet is missing, because a customer still cannot withdraw.