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/wallet.ts, ecosystem/utils/token-move.ts,
ecosystem/utils/evm-mover.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
At the customer's own address, on every chain and for every token type. There
is no list to consult and no shared platform contract: a NO_PERMIT token on
an EVM chain deposits to exactly the same kind of address as a PERMIT one or
the chain's native coin. The custodial contracts an older install deployed for
NO_PERMIT deposits receive nothing now; they are only a withdrawal source
while they drain.
contractType still matters, but only at withdrawal, where it decides which
movers may be used to get the token out of a gas-less address.
Flag a token PERMIT that does not implement EIP-2612, and the engine offers
it a permit mover it cannot sign; under the default auto order EIP-7702 is
tried before it and just-in-time gas after it, so the withdrawal still goes
out. Flag a permit-capable token NO_PERMIT, and it simply never gets the
permit option. On Solana, Tron, TON, Monero, MO and the UTXO chains the flag
is ignored entirely — those chains have no movers to choose between. The
mechanism is 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 some of the cases, and on EVM chains it
depends on the address model (ecosystemAddressModel, Admin → Ecosystem →
Settings → Addresses).
| Case | Address source | wallet_data.index |
|---|---|---|
EVM token, any contractType, model per_currency |
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, model per_currency |
A freshly generated HD wallet of its own, unrelated to the master wallet | 0 |
Any EVM token or coin, model per_user |
The customer's one canonical EVM key — derived from the master wallet the first time any EVM chain asks, copied for every (wallet, chain) pair after that, so the address is identical on every EVM chain | the canonical key's index |
| 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. An entry that was
written under the old shared-deposit model — a NO_PERMIT token's
{ balance } with no address — is issued an address the next time the wallet
is opened and keeps its balance.
So the master wallet is load-bearing in a narrower way than it looks: losing it
loses every deposit address that was derived from it, and disabling it stops
new ones being derived (Skipping chain <CHAIN> - Master wallet not found or not enabled) — though under per_user a customer who already holds an EVM key
is given that key without the master wallet being consulted. UTXO deposit keys,
and native-coin keys issued under per_currency, 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, and the flag plays no part there.
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 token, PERMIT or NO_PERMIT |
The source is chosen in order — the customer's own address, another customer's address, the master wallet's own token balance, a draining legacy custodial contract — and each is checked on-chain. From a customer address the token leaves by EIP-7702 (one transaction sent by the master wallet), by permit (PERMIT tokens), or by just-in-time gas (the address's own key, after the master tops it up) |
The master wallet on-chain, in native coin; recovered from the customer in the withdrawn token as the network fee when ecosystemChargeNetworkFee is on (the default) |
| 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, every token path spends the master wallet's gas. A customer's address holds no native coin, and a draining legacy custodial contract holds tokens but is not the gas payer either. Keep the master wallet funded and watch it on the chains that see token withdrawals; with the network fee charged to the customer the gas comes back in the token, but only after the coin was there to spend.
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