Chain families: what actually differs per chain
Per-chain behaviour rather than per-chain configuration — which deposit monitor runs, where the address comes from, how deep a confirmation is, who signs a withdrawal and who pays the fee.
Supported blockchains covers how a chain is configured. This page covers how a chain behaves — the differences that decide what a deposit problem looks like, why a withdrawal needs gas from one place on one chain and another place on the next, and which chains can do things the others cannot.
Everything here comes from ecosystem/utils/chains.ts,
ecosystem/utils/custody.ts, ecosystem/utils/wallet.ts and the deposit-monitor
factory.
Which deposit monitor runs
Seven monitor implementations ship. createMonitor picks one from the chain
symbol alone, in this order — the first match wins:
| Order | Monitor | Chains |
|---|---|---|
| 1 | UTXODeposits |
BTC, LTC, DOGE, DASH |
| 2 | SolanaDeposits |
SOL |
| 3 | TronDeposits |
TRON |
| 4 | MoneroDeposits |
XMR |
| 5 | TonDeposits |
TON |
| 6 | MODeposits |
MO, only for a non-NATIVE token |
| 7 | EVMDeposits |
everything else |
Two consequences of "everything else". A native MO deposit is watched by
EVMDeposits, not MODeposits — the MO-specific monitor exists for its tokens
only. And every custom EVM chain you add from the admin panel falls into the
same default branch with no per-chain code, which is exactly why adding one needs
no code change.
The same factory is used by the live deposit-page session and by the background scanner, so the two can never disagree about what watches an address.
Where a deposit lands
One list decides whether a customer is given their own address or a shared
platform contract. NON_CUSTODIAL_CHAINS holds nine symbols:
SOL TRON TON XMR MO own wallet service, per-user address
BTC LTC DOGE DASH no contracts at all, per-user addressA deposit uses a shared custodial contract only when both are true: the
token's contractType is NO_PERMIT, and its chain is not on that list.
A custodial wallet is a deployed contract, and there are none on Solana, Tron,
TON, Monero, MO or any UTXO chain. Mis-flagging a token there as NO_PERMIT
does not break deposits — the platform still issues the user's own address —
because the rule is a conjunction, not a contractType test. On an EVM chain the
same mis-flag is a real problem, and it is covered in
Deposit wallets.
Confirmation depth
The crediting watchdog compares an on-chain transaction's depth against the
chain's configured confirmations, and credits nothing below it.
| Chain | Required confirmations |
|---|---|
BTC |
3 |
LTC |
6 |
DOGE |
6 |
DASH |
6 |
| Everything else, including every EVM chain | 12 (the fallback) |
| Custom EVM chains | whatever the chain row says, default 12 |
Only the four UTXO chains name a depth of their own. Every other built-in chain leaves it unset and inherits 12 from the verification job's default. Custom EVM chains are the exception that can be tuned: the create form at Admin → Ecosystem → Blockchains → Custom EVM Chains has a Confirmations field, defaulting to 12 and accepting 1 to 1000.
"The deposit shows as pending and never credits" on a fast chain is usually this number: twelve blocks is a long wait on some networks and a short one on others, and there is no per-chain override for the built-ins.
Where the address comes from
This is the difference operators get wrong most often, because "HD-derived from the master wallet" is only true for one of the five cases.
| Case | Address source | wallet_data.index |
|---|---|---|
EVM token, PERMIT |
The master wallet's mnemonic, child at lastIndex + 1; the master wallet's lastIndex is incremented in the same transaction |
the derived index |
EVM coin, NATIVE |
A freshly generated HD wallet of its own, unrelated to the master wallet | 0 |
EVM token, NO_PERMIT |
No address is stored; a custodial contract is assigned per deposit session | — |
| UTXO chain | A freshly generated P2PKH key per wallet and chain | 0 |
SOL, TRON, TON, XMR |
The chain service's own createWallet() |
0 |
Every one of these private keys is encrypted with the vault key and written to
wallet_data. The encrypted blob is the only persisted copy.
So the master wallet is load-bearing in a narrower way than it looks: losing it
loses every PERMIT deposit address on its chain, and disabling it stops new
ones being derived (Skipping chain <CHAIN> - Master wallet not found or not enabled). Native and UTXO deposit keys are independent of it.
Tron is the special case worth naming: PERMIT and NO_PERMIT TRC20 tokens
both route to the user's own TRON address, because Tron has neither EVM permit
semantics nor custodial contracts.
Who signs, and who pays the network fee
| Chain and type | Signer | Network fee paid by |
|---|---|---|
EVM NATIVE |
The user's own address | The user — actual gas is deducted from their balance after the receipt |
EVM PERMIT |
The token contract, transaction sent by the master wallet | The master wallet |
EVM NO_PERMIT |
The custodial contract that holds enough of the token, transaction sent by the master wallet | The master wallet |
| UTXO | Each input signed with its own wallet's key, in one PSBT | Deducted from the inputs; one fee per batched transaction |
SOL native |
The user's own SOL address | The user — the amount is reduced to reserve the fee if needed |
SOL SPL token |
The user's own SOL keypair, decrypted from wallet_data — it is the transfer authority; the master wallet only co-signs |
The master wallet, as the transaction's fee payer |
TRON |
The address holding the coins — the user's own, or another pooled address when theirs cannot cover it | That address, in TRX, topped up from the master wallet when short |
TON, XMR |
The user's own wallet from wallet_data |
That wallet |
Three operational notes fall out of this table.
A native withdrawal does not need a master wallet. The EVM path resolves only
token metadata for NATIVE, deliberately, so a chain with no master wallet can
still process native withdrawals. Everything else on that chain cannot.
On EVM, both token paths spend the master wallet's gas. Custodial contracts hold the tokens; they are not the gas payer. Keep the master wallet funded, and watch it on the chains that see token withdrawals.
Tron requeues rather than fails when the master wallet is short. If the
signing address needs TRX for fees and the master wallet cannot cover the top-up,
the withdrawal is left PENDING with WITHDRAWAL_REQUEUED and retried later
rather than failed and refunded. A queue of Tron withdrawals that never moves is
a master wallet with no TRX.
Which chains can deploy a token
Deployment reads a bundled contract for the chain. A chain with no contract
entry answers Smart contract file not found for chain <CHAIN> and can only
import an existing asset.
| Can deploy | Contract name recorded |
|---|---|
ETH, POLYGON, FTM, OPTIMISM, ARBITRUM, BASE, CELO, MO, and every custom EVM chain |
ERC20 |
BSC |
BEP20 |
HECO |
HRC20 |
CRONOS |
CRC20 |
SOL |
SPL |
Import-only: TRON, RSK, BTC, LTC, DOGE, DASH, XMR, TON.
Deployed EVM tokens use the bundled contract, which supports permit, so they are
always recorded as contractType: PERMIT.
Rate limits and self-termination
The non-EVM chains defend themselves against provider limits in ways that look like outages if you do not know about them.
Tron stops its own monitor. Both the TRX and the TRC20 deposit monitors count
consecutive errors and stop themselves at ten, logging
Max consecutive errors (10) reached for <address>, stopping monitor. On mainnet
without TRON_API_KEY, anonymous TronGrid quotas will reach ten errors quickly,
and once stopped the monitor does not restart on its own — the address is picked
up again on the next deposit-page session or background sweep.
TON serialises everything to roughly one request per second. Wallet
operations go through a static queue with a one-second delay between calls, which
throttles deposit polling and the withdrawal confirmation loop alike. That loop
polls ten times at ten-second intervals and then gives up with
Transaction hash could not be retrieved after 10 retries. TON deposit
monitoring polls on a 60-second base interval, doubles that interval on each
consecutive failure up to sixteen times, and stops itself at ten consecutive
errors with Too many consecutive errors for <address>, stopping deposit monitoring. Set the Toncenter API key for the active network.
Monero's wallet RPC is a startup gate. If get_version fails the whole chain
is disabled rather than degraded.
BlockCypher's anonymous quota cannot sustain polling. Roughly 3 requests per
second and 100 per hour, which is why BLOCKCYPHER_TOKEN is effectively required
on DOGE and DASH — they have no other working provider.
Licensing and the chain service module
Solana, Tron, TON and Monero are separate products, and each is gated twice.
- The licence. Activation writes
lic/<productId>.lic; without it the status toggle refuses with 403. Product IDs: Solana54514052, Tron54577641, Monero54578959, TON55715370. All four ship withstatus: false. - The code. Each chain's service module has to be present in the install.
The second gate is the one that surprises people, because a valid licence does not put the code on disk. The diagnostics check for it explicitly and report:
Chain service installed: no
The SOL blockchain addon code is not installed (@b/blockchains/sol)Every flow on that chain is dead in that state — address generation, deposits, withdrawals and balances alike — regardless of what the licence says. Run Admin → Ecosystem → Blockchains → Requirements for the chain and read this row before debugging anything else on it.
Related
- Supported blockchains — configuring each family
- Deposit wallets and custody — the deposit and withdrawal flows in full
- Running the UTXO chains — the UTXO family in depth
- Environment reference — every variable named here