Where the tape lives — Scylla tables, retention and backups
Every table this addon writes, which of them live in ScyllaDB outside the platform's MySQL backup, what the retention job prunes and what it never touches, and exactly what deleting a market destroys.
This addon writes to two stores. Its configuration, its money and its audit trail are in MySQL. Its entire trade history, its order records and its real-user P&L ledger are in ScyllaDB, and the platform's own MySQL backup does not cover ScyllaDB at all.
That sentence is the whole reason this page exists. An operator who backs up MySQL nightly and never touches Scylla has a complete record of what every market was configured to do and no record whatsoever of what it did.
Nothing in the product backs Scylla up. Lose the node and you lose every chart, every trade, every order record and every per-fill P&L figure — permanently, with no reconstruction path, because none of it is mirrored into MySQL.
Take nodetool snapshot of the ecosystem keyspace on the same schedule as your
MySQL dump, and keep the two together so you always restore a matched pair. See
Backup and restore for the commands and the
restore procedure.
The Scylla tables
The addon reuses the Ecosystem keyspace — whatever SCYLLA_KEYSPACE names in
.env (the default is trading; most installs set it to ecosystem). It does
not create a keyspace of its own.
Its tables are created on the engine's first successful start, by
initializeAiMarketMakerTables(), with CREATE TABLE IF NOT EXISTS. You create
nothing by hand.
| Table | Partition key | Holds |
|---|---|---|
ai_bot_orders |
(market_id), clustered on created_at DESC, order_id |
Every order a bot created, with its side, type, price, amount, fill, purpose and matched counterparty bot |
ai_bot_trades |
(market_id, trade_date), clustered on trade_time DESC, trade_id |
The bot-to-bot tape, in 1e18 fixed point. Both bot ids, both order ids, price and amount |
ai_price_history |
(market_id), clustered on timestamp DESC |
Price points with volume, an is_ai_trade flag and a source |
ai_real_liquidity_orders |
(ecosystem_order_id) |
The link between an AI order and the ecosystem order it became. This is what makes a bot's resting order cancellable after a restart |
ai_bot_real_trades |
(bot_id, trade_date), clustered on trade_time DESC, trade_id |
One row per fill against a real user: price, amount, fee, maker flag, counterparty user id and realised P&L |
Three materialized views come with them:
| View | Base table | Answers |
|---|---|---|
ai_bot_orders_by_bot |
ai_bot_orders |
"What has this bot placed" |
ai_bot_open_orders |
ai_bot_orders |
"What is still OPEN on this market" |
ai_real_liquidity_orders_by_ai_order |
ai_real_liquidity_orders |
"Which ecosystem order is this AI order" |
Two more Scylla tables are not the addon's, but the addon is a heavy writer to both — they belong to Ecosystem and are shared with everything else trading that market:
candles— thirteen intervals from1mto1w. Every AI print writes all thirteen, and so does the throttled heartbeat that keeps a gated market's chart continuous. This is where your charts come from.orderbookandtrades— the depth and the tape your customers see. Both carry the addon's rows alongside genuine ones. See What your users actually see.
Starting a market runs deleteAiBotOrdersByMarket for it — bot-to-bot trades
create no persistent orders to match, so the rows only accumulate. Do not treat
that table as history; it is working state.
What lives in MySQL instead
| Table | Holds | Pruned? |
|---|---|---|
ai_market_maker |
The configuration row. marketId is unique, so one per ecosystem market |
No |
ai_market_maker_pool |
One pool per market maker: both balances, the capital basis, TVL and both P&L figures | No |
ai_bot |
The six bots, their configuration and their lifetime real-fill statistics — totalRealizedPnL, realTradesExecuted, profitableTrades, firstRealTradeAt |
No |
ai_market_maker_history |
The trail: one row per print plus every lifecycle action | Partly — see below |
ai_market_maker_engine_lease |
The database arbiter for engine leadership | No |
ai_market_maker_history.action is one of TRADE, PAUSE, RESUME,
REBALANCE, TARGET_CHANGE, DEPOSIT, WITHDRAW, START, STOP,
CONFIG_CHANGE, EMERGENCY_STOP, AUTO_PAUSE, PHASE_CHANGE, BIAS_CHANGE
or MOMENTUM_EVENT. The daily summaries are written into that same table as
CONFIG_CHANGE rows carrying details.field = "DAILY_SUMMARY", one per market
per UTC day, by the daily reset job.
The model has a beforeDestroy guard that refuses an individual delete —
"History records cannot be directly deleted - they are immutable for audit trail
integrity" — unless it is running inside a transaction or is an explicit
cascade. The retention job sidesteps it with a bulk destroy, and the market-maker
delete sidesteps it with its transaction. Nothing else can remove a row.
Retention
Admin → AI Market Maker → Settings → Trading, labelled Trade History Retention, under the Storage group. Accepted 0–3650.
processAiHistoryRetention runs daily and is the only thing in the product
that deletes history. It prunes exactly two kinds of row, both from MySQL:
action = "TRADE"— the individual prints.action = "CONFIG_CHANGE"wheredetails.field = "PRICE_DEVIATION_ALERT"— the price-sync job's alerts, at most one per market per hour but forever.
Everything else survives: daily summaries and every lifecycle audit row are never pruned. START, STOP, PAUSE, RESUME, AUTO_PAUSE, EMERGENCY_STOP, DEPOSIT, WITHDRAW, REBALANCE, TARGET_CHANGE, PHASE_CHANGE, BIAS_CHANGE and operator CONFIG_CHANGE rows are low-volume and audit-bearing, so the aggregate stays readable after the prints behind it are gone.
| Value | Effect |
|---|---|
| Blank / unset | 90 days |
0 |
Keep everything. Nothing is ever pruned |
1 |
Treated as 2 |
2–3650 |
That many days |
The floor of two days is not arbitrary: the daily summariser reads yesterday's
TRADE rows, so a shorter window would delete its own input before it ran.
Each run deletes in batches of 5,000 with a short yield between them — it shares
a thread with the one-second engine tick — and is capped at 200 passes, so a
million rows per run. A large backlog drains over several days rather than in
one lock-heavy sweep. The job is deliberately not gated on
aiMarketMakerEnabled: a disabled market maker is exactly when nobody is
watching the table.
processAiHistoryRetention never deletes from ScyllaDB. ai_bot_trades,
ai_bot_real_trades and ai_price_history grow without bound for the life of
the install. Size the node accordingly, and note that Scylla's own TTLs cover
only the shared display tables — the ecosystem trade tape expires on
ECO_TRADE_TAPE_TTL_DAYS (default 30 days) and synthetic order-book levels
expire after 120 seconds.
The volume, and why every read is bounded
The seeded daily trade budgets are 1,200 / 3,500 / 9,000 prints per market
per day for CONSERVATIVE / MODERATE / AGGRESSIVE. Each print writes a MySQL
TRADE row, a Scylla ai_bot_trades row and thirteen candle writes.
An aggressive market therefore records roughly 9,000 trades a day. Multiply by markets, then by your retention window.
That is why the analytics reads are capped and say so:
- Performance (
/admin/ai/market-maker/analytics, Performance) reads at most 5,000 history rows per request and returns atruncatedflag. On a 30-day window over an aggressive market an unbounded query would pull a quarter of a million rows to draw a few hundred chart points. When the flag is set, the window you are looking at is the most recent 5,000 rows, not the whole period — the cap drops the oldest. - Trades pages against MySQL
ai_market_maker_history, filtering before paging. - P&L period figures read Scylla
ai_bot_real_tradesas a bounded fan of partition lookups (bots × days), not a table scan.
Performance and Trades read the MySQL history table, so shortening the retention window shortens the chart. The P&L figures read Scylla and are unaffected. Two screens, two stores, one setting that moves only one of them.
What deleting a market maker destroys
There is no soft delete, no archive and no undo. marketMaker.destroy() is a
hard delete inside a transaction that also destroys the bots, the pool row and
every history row for that market maker.
Do it deliberately, and read the whole sequence below first — it credits real funds to whoever pressed the button.
In order:
- Stops the market through the engine if it is ACTIVE or PAUSED, and waits up to ten seconds for the engine to confirm. The database status is set to STOPPED regardless.
- Cancels the bots' open ecosystem orders for that symbol, in parallel, through the matching engine. The response reports how many were found, how many were cancelled and which failed.
- Withdraws both pool balances to the deleting admin's own ecosystem wallet, creating the wallet if it does not exist, and writing an ordinary wallet transaction for each side. If a wallet cannot be resolved the log says "funds will be lost" and the deletion continues.
- Runs
cleanupMarketMakerDataagainst ScyllaDB. - Deletes the MySQL rows — bots, pool, history, then the market maker — in a single transaction.
cleanupMarketMakerData deletes, for that market:
| Target | Scope |
|---|---|
ai_bot_orders |
Every row for the market |
ai_bot_trades |
Every row for the market over the last 365 days. Older partitions are not walked and survive |
ai_price_history |
Every row for the market |
ai_real_liquidity_orders |
Every row for the symbol |
orderbook |
Every level for the symbol |
Three things are worth knowing precisely about that list:
The cleanup uses the force clear, which drops every row in orderbook for the
symbol — including levels backed by genuine resting user orders. Those orders
still exist and are still open; only the aggregated depth view loses them, and
the matching engine's reconciler repairs it on its own timer. Ordinary market
stop does not do this: it clears only levels the AI wrote, matched by their
TTL.
ai_bot_real_trades is not deleted. The per-fill ledger of every trade
against a real user survives the deletion in Scylla — but the ai_bot rows that
own those partitions are gone from MySQL, so nothing in the product can read it
back. Treat the P&L record as destroyed for practical purposes while the rows
themselves persist.
Candles are not deleted. The chart for that market survives, frozen at the last price the engine published. That is deliberate — the engine also refuses to clear candles on restart, to preserve chart history.
Verifying Scylla is reachable
The addon has no health screen of its own. Three checks:
cqlsh -u <user> -p '<password>' -e "DESCRIBE KEYSPACE ecosystem;" | grep ai_You should see ai_bot_orders, ai_bot_trades, ai_price_history,
ai_real_liquidity_orders, ai_bot_real_trades and the three materialized
views. If they are absent, the engine has never successfully started.
pm2 logs backend --lines 200 | grep -iE "SCYLLA|AI_MM"On success the addon logs AI_MM Database tables initialized. On failure,
Failed to initialize database tables: ….
3. The dashboard masthead at /admin/ai/market-maker names the engine
process and its status.
What the addon does when Scylla is down
The engine calls initializeAiMarketMakerTables() during startup, after it has
claimed leadership. That call initialises Ecosystem's Scylla client first, which:
- probes the first contact point over TCP with a short timeout and, if it does
not answer, logs
"ScyllaDB unreachable at
<host:port>— skipping connection retries; ecosystem trading features are unavailable" and fails immediately rather than spending a minute on retries; - refuses outright when
SCYLLA_ENABLED="false".
Either way the engine's initialize() throws and its status becomes ERROR.
No market loads, no price advances, no order is placed. The market rows stay at
whatever status an operator last set — ACTIVE markets keep their ACTIVE badge
throughout — so the list screen looks healthy while nothing at all is running.
The six non-engine cron jobs keep running. They touch only MySQL, so they will report completed every cycle. A green cron page is not evidence that Scylla is up.
processAiMarketMakerEngine logs the first failure honestly — "Failed to
initialize engine: …". On its next pass, five seconds later, it calls
initialize() again, which returns immediately because the engine is in ERROR
and initialisation only proceeds from STOPPED. The supervisor takes that quiet
return as success and logs "AI Market Maker Engine initialized successfully".
So a cron log that says the engine came up may be describing an engine that has been in ERROR since the first attempt. Check the dashboard masthead's engine status, not the cron log.
Fix Scylla, then recover the engine one of two ways: restart the backend, or
press START on any market — the status endpoint routes through
ensureRunning(), which is the one path that clears ERROR back to STOPPED and
re-initialises.