Master wallets and the vault
How Ecosystem encrypts private keys, what the master wallet actually pays for, why it is not a reserve, and the two ways an install loses every wallet it has.
Every private key this addon creates — master wallets, per-user deposit
addresses, custodial secrets — is encrypted with one key. That key is itself
encrypted with a passphrase, stored in .env, and held in memory only while the
process runs. The vault is the whole security model, so it comes first.
The vault
ENCRYPTED_ENCRYPTION_KEY is a random 32-byte key, AES-256-GCM-encrypted under
a passphrase-derived key (PBKDF2, 100,000 iterations, SHA-512) and stored as
four colon-separated hex parts: IV, auth tag, ciphertext, salt. Generate it once,
before you create anything:
node ./scripts/kms/generate.mjsAt boot, if both ENCRYPTED_ENCRYPTION_KEY and ENCRYPTION_KEY_PASSPHRASE are
present, the backend decrypts the key into memory and the vault is unlocked. If
the passphrase is absent, the vault starts locked and stays locked until an
administrator supplies it.
The admin console masthead at /admin/ecosystem shows the state directly: a
green Vault Active badge, or an Initiate Vault button that opens the
passphrase prompt. The unlock is per process and per restart — it is not
persisted anywhere.
What a locked vault breaks
Everything that touches key material, and nothing else:
| Blocked while locked | Still works |
|---|---|
| Creating master wallets | Trading — orders, fills, the order book |
| Generating user deposit addresses | Existing balances and internal transfers |
| Deploying tokens and custodial contracts | Market and token administration |
| Signing withdrawals | Deposit detection (crediting is queued) |
That last row is the one that catches people out. Deposits are still noticed; they simply cannot be credited into a wallet that has no address yet, and a new user hitting the deposit page gets an error rather than an address.
The passphrase trade-off
Setting ENCRYPTION_KEY_PASSPHRASE in .env buys unattended restarts —
withdrawals resume by themselves after a deploy or a crash. It costs you the
property that the passphrase never touches disk. Leaving it out means an
operator must be available after every restart, including the ones nobody
planned.
Most single-operator installs set it. Installs with a real on-call rotation usually do not. Either is defensible; drifting into the second by accident is not — an install that silently stopped signing withdrawals three days ago looks identical to a quiet week.
Losing the passphrase. The key cannot be decrypted, so nothing encrypted with it can be read. Every deposit address on the platform becomes unusable and the coins at those addresses are unreachable.
Regenerating the key. Running the generator against an .env where
ENCRYPTED_ENCRYPTION_KEY has been cleared produces a new key. Existing wallet
blobs were encrypted with the old one and will never decrypt again.
There is no recovery, no support workaround, and no partial recovery. pnpm env-manager refuses to edit either variable for this reason. Back up .env
somewhere your database backup does not live, and keep the passphrase somewhere
else again.
What the master wallet actually is
One master wallet per chain. It is created from the admin panel, its key
material is generated on the server, encrypted immediately, and stored in
ecosystem_master_wallet.data. That encrypted blob is the only persisted copy —
nothing is ever written to a plaintext file on disk, deliberately, because any
file-read or backup leak would drain every wallet on the install.
The master wallet does four jobs:
- Pays gas for token withdrawals. When a customer withdraws a permit-capable token, the tokens move out of the customer's own address and the master wallet acts only as gas payer.
- Deploys token contracts. Token deployment is signed by the master wallet.
- Deploys custodial contracts. Each custodial wallet contract is constructed with the master wallet's address as its owner.
- Holds the chain's native coin for the above.
It does not hold customer funds. Customer coins live at each customer's own HD-derived deposit address. The admin overview therefore reports master balances as a per-chain gas figure, never as a reserve to compare against customer liability — that comparison would be meaningless on this platform, and shipping it would be a confident wrong number on the one page that must not carry one.
The practical consequence: an empty master wallet does not mean the platform is insolvent. It means token withdrawals on that chain will fail until you top it up with native coin.
Creating one
-
Unlock the vault first. Creating a wallet encrypts its key material immediately, and a locked vault fails the operation partway.
-
Go to Admin → Ecosystem → Wallets → Master Wallets and create a wallet, choosing the chain.
Generates, encrypts and stores a master wallet for a chain -
One per chain, enforced. A second attempt on the same chain returns 409. An unknown or disabled chain is rejected with 400 before any key material is generated — enable the chain first, then create its wallet.
-
Fund it with the chain's native coin. ETH on Ethereum, BNB on BSC, MATIC on Polygon, TRX on Tron, SOL on Solana, and so on.
-
Back up the platform. The wallet exists in exactly two places now: the database row, and whatever encrypts it in
.env. A database backup without the.envis not a backup of this wallet.
How the key is generated depends on the chain family. EVM chains get a random HD wallet — mnemonic, extended keys, chain code and derivation path are all stored in the encrypted blob, because per-user deposit addresses are derived from it. UTXO chains get a UTXO wallet. Solana, Tron, TON and Monero each delegate to their own chain service, which must be installed for the creation to succeed at all.
There is no "reveal seed phrase" screen. The encrypted blob is the record. If your operational model requires an offline copy of the seed, take it from the database and decrypt it deliberately — do not expect the UI to hand it over.
Managing them
The master wallet list shows chain, currency, address, balance and status. Balances are fetched live and cached in Redis for one minute, so a top-up will not appear instantly.
Disabling a master wallet is a real operational lever, not a soft delete. The diagnostics treat a disabled wallet the same as a missing one and downgrade the chain's withdrawal readiness to failed. Use it to stop signing on a chain you believe is compromised.
Deleting one is destructive in a way the confirmation dialog cannot fully convey: per-user deposit addresses on EVM chains are derived from the master wallet's HD material. Losing the master wallet row loses the ability to derive — and therefore to spend from — every address under it.
Keeping gas topped up
A per-chain routine worth having:
- Watch the chain health section of
/admin/ecosystem. A chain whose gas payer is empty is flagged there, because token withdrawals on it will fail. - Native-coin withdrawals do not need the master wallet — they are signed by the customer's own deposit address and the network fee comes out of the balance being withdrawn. So a chain with an empty master wallet can still process native withdrawals while token withdrawals queue up behind an insufficient-gas error. The symptom is selective, not total.
- Custodial wallets need their own native balance as well, for the tokens they hold. See Deposit wallets.
- Deploying a token or a custodial contract costs considerably more gas than a transfer. Fund before you deploy; an out-of-gas deployment returns a 400 and wastes the attempt.
Related
- Deposit wallets — where customer coins actually sit
- Blockchains — enabling the chain before you create its wallet
- Environment reference — every variable named here
- Troubleshooting — locked vaults, failed signing, stuck payouts