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.

6 min readUpdated 6 August 2026database, mysql, scylladb, schema, support

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.contractType is NATIVE, PERMIT or NO_PERMIT.
  • ecosystem_custodial_wallet.status is ACTIVE, INACTIVE or SUSPENDED.
  • ecosystem_utxo.status is UNSPENT, LOCKED or SPENT; LOCKED means reserved for an in-flight withdrawal and therefore not re-selectable.
  • ecosystem_utxo.origin is DEPOSIT, CHANGE, CONSOLIDATION or SYNC.
  • ecosystem_market.status and ecosystem_token.status are booleans, not enums — 0 is 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.data is the master key material for a chain — on EVM chains, the HD seed every customer deposit address is derived from at lastIndex. It is the only persisted copy. There is no escrow, no second copy and no vendor recovery.
  • wallet_data.data is 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 with deletedAt set 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.