Where ecosystem data lives
Every MySQL table the Ecosystem addon owns or writes, which ones soft-delete, what is in ScyllaDB and therefore in no SQL dump, and the joins support work actually needs.
Support work on this addon is SQL work. Find this customer's deposit address. Was that withdrawal really debited. Which wallet holds the fee revenue. None of those has a screen that answers it directly, and all three are one query away once you know where the rows are.
This page is the map. Every table name here was read off a model or a migration script in this repository.
The tables the addon owns
Eight tables, all prefixed ecosystem_.
| Table | Holds | Soft-deletes |
|---|---|---|
ecosystem_master_wallet |
One row per chain: chain, currency, address, balance, the encrypted data blob, status, lastIndex |
No |
ecosystem_custodial_wallet |
Shared EVM contracts for non-permit tokens: masterWalletId, address, chain, network, status |
Yes |
ecosystem_token |
Every tradable asset: contract, name, currency, chain, network, type, decimals, precision, contractType, plus JSON limits and fee |
Yes |
ecosystem_market |
Trading pairs: currency, pair, isTrending, isHot, status, JSON metadata |
Yes |
ecosystem_private_ledger |
walletId, index, currency, chain, network, offchainDifference |
Yes |
ecosystem_utxo |
The unspent-output set: walletId, transactionId, index, amount, script, status, lockedTxId, origin |
Yes |
ecosystem_blockchain |
The licensed chain addons: productId, name, chain, version, status |
No |
ecosystem_custom_chain |
Operator-defined EVM chains: chain, name, chainId, currency, decimals, network, rpcUrl, rpcWssUrl, explorer fields, confirmations, precision, status |
No |
Enumerated columns worth knowing before you write a WHERE:
ecosystem_token.contractTypeisNATIVE,PERMITorNO_PERMIT.ecosystem_custodial_wallet.statusisACTIVE,INACTIVEorSUSPENDED.ecosystem_utxo.statusisUNSPENT,LOCKEDorSPENT;LOCKEDmeans reserved for an in-flight withdrawal and therefore not re-selectable.ecosystem_utxo.originisDEPOSIT,CHANGE,CONSOLIDATIONorSYNC.ecosystem_market.statusandecosystem_token.statusare booleans, not enums —0is disabled.
ecosystem_master_wallet and ecosystem_blockchain carry no timestamp columns
at all. There is no createdAt and no updatedAt on either, so there is no way
to ask when a master wallet's balance was last refreshed. The balance is whatever
the last successful refresh wrote.
The core tables it writes
The addon does not have its own wallet table. It writes the platform's.
| Table | The addon's stake in it | Soft-deletes |
|---|---|---|
wallet |
Rows with type = 'ECO'. Carries balance, inOrder, the JSON address map and addressLookupKey |
Yes |
wallet_data |
One row per (walletId, currency, chain) — the per-chain balance, the derivation index, and the encrypted data blob |
No |
transaction |
Deposits, withdrawals, transfers, fees and refunds against ECO wallets. Singular — the table is transaction, not transactions |
Yes |
transaction_ledger_applied |
Schema-only idempotency guard for private-ledger decrements, unique on (transactionId, walletId, currency, chain). Created by a migration script; no backend code writes or reads it |
No |
admin_profit |
One row per collected platform fee, linked by transactionId |
No |
wallet_audit_log |
The balance-change audit trail behind every wallet-service operation | No |
engine_lease |
One row, id = 'ecosystem-matching', naming the process that owns the matching engine |
— |
wallet.address is a JSON map keyed by chain:
{
"ETH": { "address": "0x…", "network": "mainnet", "balance": 0.42 },
"BSC": { "address": "0x…", "network": "mainnet", "balance": 0 }
}addressLookupKey beside it is the SHA-256 of the wallet's primary (first)
address. It exists so that "does this withdrawal destination belong to a customer
on this install?" is an indexed lookup rather than a scan of every ECO wallet.
Because it only covers the first address, a destination matching a
secondary-chain address falls back to a slower path — which is why
internal transfers still work for every chain.
They can legitimately disagree: a balance credited by P2P or by a transfer lands
on wallet.balance without a matching per-chain movement. Code that must not
over-spend reads wallet.balance. Do not treat a wallet_data row as the
customer's balance.
What is not in MySQL at all
Orders, the candle history, the aggregated order book, the trade tape, the
book-ordered open-orders index and resting stop orders all live in ScyllaDB,
in the keyspace named by SCYLLA_KEYSPACE (default trading). They are not in
the platform's built-in database backup, they are not in mysqldump, and no SQL
query will ever find them. If you run Ecosystem, you own ScyllaDB's backups.
The tables in that keyspace:
| Table | Holds |
|---|---|
orders |
Every order, partitioned by userId, clustered by createdAt DESC, id |
open_orders_by_market |
The book-ordered index of OPEN orders, partitioned by (symbol, side), clustered by price, createdAt, id |
orderbook |
The aggregated depth per (symbol, side) and price |
candles |
OHLCV per (symbol, interval, createdAt) |
trades |
The public trade tape per symbol, including AI market-maker fills flagged isAiTrade |
stop_orders |
Untriggered stop-limit and stop-market orders — a separate table the matcher never reads |
eco_index_state |
A tiny durable key/value; currently just "has the index backfill run?" |
Plus materialized views the platform maintains itself — orders_by_symbol,
latest_candles, orderbook_by_symbol and stop_orders_by_status. The full
column-level shape of each, and the tuning knobs that govern how the engine reads
them, are in ScyllaDB schema and engine tuning.
The practical consequence for support: a question about an order, a fill, a
candle or the book cannot be answered from the SQL console. Use the
order desk or cqlsh against the trading keyspace.
Key material
Two columns in MySQL hold encrypted private keys.
ecosystem_master_wallet.datais the master key material for a chain — on EVM chains, the HD seed every customer deposit address is derived from atlastIndex. It is the only persisted copy. There is no escrow, no second copy and no vendor recovery.wallet_data.datais the per-address key material for a customer's own deposit address, for the chain families that generate their own keys rather than deriving them.
Both are encrypted with the vault key, which lives in .env as
ENCRYPTED_ENCRYPTION_KEY and is unusable without its passphrase.
The blobs above are ciphertext. Restore the database onto a box with a different
ENCRYPTED_ENCRYPTION_KEY and every private key on the install is permanently
unreadable — the coins are still on-chain and nobody can sign for them. Store the
database dump, the .env and the passphrase separately, and test the restore.
See Master wallets and the vault.
Joins that answer real questions
Every deposit address a customer has
ecosystem_token.currency is the key that ties an ECO wallet to the chains it
should hold addresses on: a wallet for USDT gets an address on every chain that
has a USDT token row which is both enabled and on the network that chain
is configured to run on.
SELECT w.id AS walletId, w.currency, w.balance, w.inOrder,
wd.chain, wd.balance AS chainBalance, wd.`index`
FROM wallet w
JOIN user u ON u.id = w.userId
JOIN wallet_data wd ON wd.walletId = w.id AND wd.currency = w.currency
WHERE u.email = 'customer@example.com'
AND w.type = 'ECO';If a chain you expect is missing from the result, compare it against the token list for that currency:
SELECT currency, chain, network, contractType, status
FROM ecosystem_token
WHERE currency = 'USDT';Address generation applies two filters to that list, and a row has to survive
both. status = 0 disables the row. Then the row's network has to agree with
that chain's <CHAIN>_NETWORK in .env — on an EVM chain a variable that is not
set at all rejects every row, while the chains with their own services (BTC,
LTC, DOGE, DASH, XMR, TON, SOL, TRON) skip the network check
entirely. A network holding the chain's own name counts as that chain's
mainnet.
The backend treats the second filter as the more likely cause, and says so: when
nothing survives, the error is No enabled tokens found for USDT followed by
Enabled USDT token(s) exist but none run on the configured network. Read the
first sentence alone and you will go looking for a disabled row that is not
there — check network against <CHAIN>_NETWORK before you change any
status.
Was this withdrawal really debited
SELECT t.id, t.type, t.status, t.amount, t.fee, t.trxId, t.referenceId,
t.createdAt, t.updatedAt
FROM `transaction` t
WHERE t.walletId = '<walletId>'
ORDER BY t.createdAt DESC
LIMIT 50;trxId is the on-chain hash once one exists; txHashPending holds a broadcast
hash that has not been confirmed yet. referenceId is how a derived row points
back at the row it came from — a refund at the failed withdrawal, a fee at the
operation that charged it — so it is the column to join on when you are following
one event through several rows.
metadata on an ECO withdrawal carries the destination address, the chain, the
token's contract type, the contract, the decimals and the fee breakdown as JSON.
The destination address is not only there: the same row's description is written
as Pending withdrawal of <amount> <currency> to <toAddress>, so a plain
description LIKE '%<address>%' finds the row without parsing JSON. Everything
else in that list — chain, contract type, fee breakdown — exists only in
metadata.
Which wallet holds the fee revenue
Platform fees are not held in a special treasury table. They are credited to
the Super Admin's own wallet of the same type and currency, and recorded in
admin_profit alongside.
SELECT ap.type, ap.currency, ap.chain, SUM(ap.amount) AS total
FROM admin_profit ap
GROUP BY ap.type, ap.currency, ap.chain
ORDER BY total DESC;To find the balance itself, look for type = 'ECO' wallets belonging to the
Super Admin user. Note that admin_profit never soft-deletes, so it is a
complete record, while the wallet it credits is a live balance that can be spent
or transferred like any other.
Both admin_profit and wallet are per-currency, and neither carries a price.
SUM(amount) over mixed currencies adds BTC to USDT and produces a number with
two decimal places and no meaning. Group by currency, always.
Which "missing" rows are only hidden
Before concluding that a row was deleted, check which kind of table you are looking at.
- Soft-deleting:
wallet,transaction,user,ecosystem_token,ecosystem_market,ecosystem_private_ledger,ecosystem_utxo,ecosystem_custodial_wallet. A row can be present withdeletedAtset and invisible to every screen and every ordinary query. - Hard-deleting:
wallet_data,admin_profit,wallet_audit_log,transaction_ledger_applied,ecosystem_master_wallet,ecosystem_blockchain,ecosystem_custom_chain. Gone is gone.
That split has one sharp edge. wallet soft-deletes and wallet_data does not,
so deleting a wallet leaves its per-chain rows — including the encrypted key
material — behind, while removing a wallet_data row destroys the only copy of
that address's key.
Related
- The private ledger —
ecosystem_private_ledgerand what its number does - The ecosystem order desk — the screen over the ScyllaDB order tables
- Operations — backups, repair scripts, and what each store costs to lose
- Master wallets and the vault — the key material and the passphrase
- Environment reference —
SCYLLA_KEYSPACEand the vault variables