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.

8 min readUpdated 6 August 2026scylladb, backups, retention, data, deletion

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 from 1m to 1w. 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.
  • orderbook and trades — 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

Days of per-trade history to keep. 0 keeps everything

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" where details.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
23650 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 a truncated flag. 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_trades as 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.

Delete a market maker. Stops it, cancels its orders, withdraws the pool, wipes its data

In order:

  1. 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.
  2. 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.
  3. 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.
  4. Runs cleanupMarketMakerData against ScyllaDB.
  5. 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.