Moving to per-user addresses
The operator's step-by-step for an existing install updating to per-user deposit addresses, EIP-7702 withdrawals and the customer-paid network fee — what the first boot does by itself, the decisions on the Settings screen, how to retire the custodial contracts, and how to verify and roll back.
This page is for an install that already has customers, addresses and — on EVM chains — custodial contracts, and is updating to the release that moves every EVM token to the customer's own address. A fresh install needs none of it; it starts on the new defaults. Deposit wallets explains the model. This page is the order to do things in.
The short version: nothing breaks on the first boot, nothing you already issued changes, and the decisions that remain are settings you can take at your own pace.
What the first boot does by itself
Update the release and restart. Four things happen with nothing to run.
-
Two settings rows are materialised from what the database holds.
ecosystemAddressModelbecomesper_currencywhen anywallet_datarow exists — an install that has handed out addresses keeps handing them out the same way — andper_userwhen none does.ecosystemCustodialModebecomesdrainwhen any custodial contract row exists andoffwhen none does. Both are written once; from then on the Settings screen owns them. -
The unique index on
wallet.addressLookupKeyis relaxed to a plain index during the schema sync. Underper_userseveral currency wallets of one customer share one address, so the key that used to be unique is now designed to repeat. An install that boots withDB_SYNC=nonemakes no schema changes at boot, so run the script once instead:node backend/scripts/relax-wallet-address-lookup.mjs --apply -
Every existing address stays exactly as it is. No customer is given a new address, no key is re-derived, and nothing moves on-chain.
-
Legacy
NO_PERMITwallets repair themselves. A wallet whose chain entry was{ balance: N }with no address — the shape a shared-contract deposit left behind — is issued a real address the next time the customer opens it, and keepsN.
Every EVM token — NO_PERMIT included — now deposits to the customer's own
address. The contracts still hold whatever they held before the update; that
balance is what step 3 is about, and it keeps serving withdrawals in the
meantime.
Step 1 — Read the Settings screen once
Admin → Ecosystem → Settings (/admin/ecosystem/settings) has four tabs.
On an upgraded install they open like this:
| Tab | Setting | What the upgraded default means |
|---|---|---|
| Addresses | ecosystemAddressModel = per_currency |
New addresses are still one per currency wallet per chain. Nothing changes for anyone until you decide otherwise in step 2. |
| Withdrawals | ecosystemEvmMover = auto |
Token withdrawals leave the customer's address by EIP-7702 where the chain allows it, else permit, else just-in-time gas. No custodial contract is needed for any of them. |
| Withdrawals | ecosystemChargeNetworkFee = on, buffer 20% |
The one default that changes what customers see straight away: a token withdrawal now debits an estimated network fee in the withdrawn token, and refunds the surplus after confirmation. Step 4 covers it. |
| Custodial wallets | ecosystemCustodialMode = drain (when contracts exist) |
The contracts are the last-resort withdrawal source and their admin page stays visible. The summary tile shows how many exist and how many are still active. |
| Diagnostics | read-only | Empty until the first token withdrawal on each chain, then the delegate contract address per chain and any chain that refused EIP-7702. |
Nothing on this screen needs saving to proceed. The rest of this page is the three decisions it holds, in the order they are worth taking.
Step 2 — Decide the address model
per_currency keeps issuing one address per currency wallet per chain, as
the install always has. Choose it and nothing changes for anyone.
per_user makes every new (wallet, chain) pair reuse the customer's one
EVM key, so the address is the same on every EVM chain. The first EVM address a
customer already holds becomes that key; anything issued afterwards copies it.
Switching never changes an existing address. A customer who already has two addresses on BSC keeps both, and continues to be shown the one each currency wallet already carries. Only newly added currencies and chains join the shared address — so on an upgraded install the two layouts coexist for as long as old wallets exist, and that is fine.
Why switch:
- Wrong-network and wrong-token sends still land on a platform-held address. A token sent on the wrong EVM chain, or to the "wrong" token's address, is recoverable because you hold the key on every chain.
- Gas dust pools per customer rather than per address, so what a just-in-time top-up leaves behind is reused by the next withdrawal.
- Fewer keys to encrypt, hold and back up.
The one operational consequence: the chain's native coin — ETH, BNB, MATIC — shares the address with the tokens. That is deliberate, and it means the platform's own gas top-ups to that address, sent by the master wallet before a just-in-time transfer, are never credited as deposits. They are the platform's gas, not the customer's money, and the deposit monitor knows the difference.
Step 3 — Retire the custodial contracts
The contracts receive nothing now, but they may still hold tokens that back customer balances. Two modes:
drain— the contracts keep serving withdrawals as the last source, after the customer's own address, other customers' addresses and the master wallet. Admin → Ecosystem → Wallets → Custodial Wallets stays in the menu, and each row carries Sweep to master wallet, which moves one token's whole balance from that contract to the chain's master wallet. Once the master wallet holds the tokens it serves withdrawals directly, in either mode.off— the menu entry is hidden and the custodial routes refuse. Any balance still sitting in a contract stops backing withdrawals.
currency. Refused while the mode is off.Creating a new contract is refused in both modes.
Convert immediately.
-
Sweep every token on every contract. On the Custodial Wallets page, run the sweep once per token each contract holds. Each sweep is one on-chain transaction paid by the master wallet, so the master wallet needs native coin on that chain first.
-
Confirm each contract reads zero on the Custodial Wallets page for every token it used to hold.
-
Set the mode to
offunder Settings → Custodial wallets.
Fall back until drained. Leave the mode on drain, check the Custodial
Wallets page weekly, and switch to off when everything reads zero. Customer
withdrawals draw the contracts down by themselves, and any balance you would
rather not wait for can be swept at any point.
Step 4 — Withdrawals and gas
auto tries EIP-7702 first, then permit for PERMIT tokens, then
just-in-time gas. Leave it there unless step 6 gives you a reason not to.
The first token withdrawal on each chain does one extra thing: it deploys the
EcosystemDelegate contract from the master wallet, once, costing one
contract deployment's gas. There is no button for this. A chain whose node
refuses type-4 transactions is remembered for 24 hours under Settings →
Diagnostics and uses the fallback mover meanwhile; the probe is repeated
after that.
So the master wallet's job has not changed: keep it funded with native coin on every chain that sees token withdrawals, exactly as before. A draining custodial contract needs no native coin of its own — the master wallet pays for every token moved out of it.
With the charge on, the customer is debited the estimated gas — with the buffer
— in the withdrawn token, alongside the amount and the platform fee, and is
refunded the surplus after confirmation; what the platform keeps is booked as
withdrawal profit. That estimate needs a USD price for both the token and the
chain's native coin. Set fee.network on any token that has no USD price, or
whose chain's native coin has none — it is on the token's edit screen under
Admin → Ecosystem → Trading → Tokens, a flat amount in token units.
Without it, those withdrawals go out with the network charge skipped and a
warning in the log, and the master wallet absorbs the gas.
Step 5 — Verify
- Open the deposit page for a
NO_PERMITtoken as a customer. It shows a permanent address with no countdown, and the same address on a second visit. - Make one small token withdrawal per chain. Open the transaction record at
Admin → Finance → Transactions and read the row's metadata:
move.kindiseip7702,permit,jit_gas,masterorcustodial,move.sourcenames where it was paid from, andnetworkFeecarriestokenAmount(the quote),actualTokenAmountandrefunded. - If
move.sourceisalternative, the private ledger at Admin → Ecosystem → Wallets → Ledgers carries the matching entry for the address that lent the token. - After the first withdrawal on each chain, Settings → Diagnostics lists the delegate contract for that chain, and lists the chain under Chains refusing EIP-7702 only if its node rejected the transaction.
- The Custodial Wallets entry appears in the admin menu while the mode is
drainand disappears when it isoff.
Step 6 — Rollback
Each decision reverses independently, and every reversal is a setting.
ecosystemAddressModelback toper_currency. Affects only addresses issued from then on; an address already shared across chains stays shared, and stays valid.ecosystemEvmMovertojit_gasif a chain misbehaves under EIP-7702. Every chain then uses the two-transaction mover, which works everywhere the platform runs.ecosystemCustodialModeback todrainif a legacy withdrawal is refused with "No funded source for … on …" after you setoff— the credit behind it is still in a contract. Sweep, then tryoffagain.
There is no way to go back to shared custodial deposits, and no need: every
address a customer already holds keeps working, and every balance the
contracts still hold keeps withdrawing under drain.
Related
- Deposit wallets — the address model, the movers and the network fee in full
- Master wallets and the vault — what the master wallet pays for now
- The admin console — the Settings screen and the legacy Custodial Wallets page
- Troubleshooting — "No funded source", chains refusing EIP-7702, skipped network fees