What your users actually see
The customer's side of an AI-made market — the tape, the chart, the book, which numbers are real and which are engine-generated, and exactly what happens to your money when a user fills a bot order.
Every other page in this product describes the addon from the admin side. This one describes what a customer with your platform open in front of them is looking at, and which of it is real.
You need this for two reasons. The first is that when Real Liquidity is above zero, a customer filling a bot order is trading against your pool balance, in the same transaction that pays them. The second is that support will eventually be asked who the counterparty was, and someone has to have decided the answer before the ticket arrives.
The screen
An AI-made market is an ordinary ecosystem market. There is no separate customer surface for this addon — no badge, no panel, no page.
Customers reach it from /ecosystem, which lists the ecosystem markets by
token and quote pair. Every row links to:
/trade?symbol=<BASE>-<QUOTE>&type=spot-ecoThat is the standard trading screen, with four things on it that the market maker feeds:
| Panel | Where it comes from |
|---|---|
| Recent Trades tab, beside Order Book | The Scylla trades table — the synthetic tape |
| Order Book tab | The Scylla orderbook table — synthetic depth and real depth, merged |
| The chart | The Scylla candles table, thirteen intervals |
| The header's last price, 24h change and 24h volume | The ticker, which is computed from the 1d candle the engine writes |
Open Orders and Order History are the customer's own and contain nothing of yours — bot orders are stored under the bot's id as their user id, so they can never appear in a customer's list.
The tape
Every bot-to-bot print writes a row into the ecosystem trades table, which is
exactly the table a genuine fill writes to. Those rows are what the Recent
Trades panel streams.
Two details matter:
- A synthetic print is one row, not two. A real fill has two sides and both are stored, so the tape carries a buy and a sell. AI prints are a single row with a side, because there is no counterparty to record.
- The row carries an
isAiTradeflag, and it reaches the browser. The backend selects it and returns it on thetradesstream. Nothing in the shipped UI renders it, so a customer reading the screen cannot see it — but a customer reading the WebSocket payload can. Decide how you would answer that before someone asks.
The tape expires. Rows are written with a TTL from ECO_TRADE_TAPE_TTL_DAYS,
30 days by default; 0 disables expiry. The tape is a display surface, not a
record.
The chart
Candles are written by the engine directly, not derived from the tape. Every print updates all thirteen intervals, and on ticks where trading is gated — the daily budget spent, too few bots, a global pause — a throttled heartbeat publishes the current price with zero volume every ten seconds, so the chart stays continuous without inflating reported volume.
Nothing deletes candles on stop, on restart or on delete. A stopped market's chart simply ends at the last price the engine published and stays there. That is deliberate: the engine explicitly refuses to clear candles on restart so chart history survives a deploy.
The consequence is that a market you stopped six months ago still shows a customer a full, unbroken price history that ends abruptly.
The order book
The Order Book panel reads every level for the symbol. It does not distinguish between the two kinds of level in it, and there are exactly two.
Synthetic depth
On every print the engine rewrites ten levels — five bids and five asks at 0.1%, 0.2%, 0.3%, 0.4% and 0.5% either side of the print price, each sized at 50–150% of the trade amount.
These are display rows. There is no order behind them.
- They are written with a 120-second TTL, which is what marks them as the AI's. A level with no TTL is the platform's marker for "an order backs this".
- The engine deletes only its own TTL-bearing levels before writing new ones, so a real customer's resting order is never erased from the aggregated book.
- They cannot be filled. The matching engine matches orders, not book levels. A customer whose market order "should" have hit synthetic depth simply does not match against it.
Real liquidity
realLiquidityPercent above 0 splits every print: part of it prints as a
bot-to-bot trade, and part is placed as a genuine resting LIMIT order in the
ecosystem book, priced away from the mid by the bot's own spread widened for
volatility.
Those are real orders in every sense the matching engine cares about. They rest until they fill, are cancelled, or reach their one-hour expiry. Each market may leave at most 500 of them resting, and a market at that ceiling stops placing new ones rather than force-cancelling old ones.
The ceiling is not a per-market setting: there is no column, no field on the
Configuration tab and no settings key for it, so do not go looking for one. The
only way to move it is the AI_MM_MAX_RESTING_REAL_ORDERS environment variable,
which changes the ceiling for every market on the box at once and takes
effect at the next backend restart.
They are written with userId = botId, marketMakerId, botId and a fee of
zero, and they lock no wallet — they are backed by the pool.
What happens when a user fills one
There is no house account and no netting. The pool's two balances are the
counterparty. A single database transaction locks the pool row and the
customer's two wallets with SELECT … FOR UPDATE and moves all of it at once.
A customer buys from a bot:
- The pool's base balance is debited by the fill amount.
- The pool's quote balance is credited with the cost.
- The customer's quote wallet releases its locked share, and any price improvement is refunded to their available balance.
- The customer's base wallet is credited with the asset.
A customer sells to a bot: the mirror image — the pool receives base, pays quote, and the customer's base hold is released against a quote credit.
Two guards sit on the pool side. A fill that would drive either balance below zero is refused outright:
Insufficient pool base balance: need x, have y
The whole transaction rolls back, so the customer is not paid and the order is not marked filled. Practically, an underfunded pool shows up as fills that fail, not as an overdraft. Watch the pool tab.
Fees behave differently on this path than on a user-to-user trade. The bot side
is excluded from the platform fee — a bot order is created with fee = 0 and the
fee collector skips the bot's portion — so on a bot fill the platform collects
only the customer's side.
Afterwards, and outside the money transaction so a bookkeeping failure can never
unwind a settled trade, the addon records the fill against the bot: its position,
its average entry, its lifetime realised P&L, and one row in the Scylla
ai_bot_real_trades ledger carrying the counterparty's user id.
On the customer's side it is an ordinary fill
Nothing marks it. The customer gets:
- the order in Order History, filled, with its trades;
- the wallet credit and debit in Wallets (
/finance/wallet); - two rows in Transaction history (
/finance/history), typedEXCHANGE_ORDERlike any other trade.
The settlement writes the leg's description straight onto the transaction row. On a bot fill those descriptions are literally "buyer receives base from bot", "buyer releases quote to bot + price-improvement refund", "seller releases base to bot" and "seller receives quote from bot".
Those strings are visible to the customer in their own transaction history. If your disclosure posture depends on the word not appearing there, check this before you turn real liquidity on — it is the one place the mechanism is named in customer-facing text.
Real, engine-generated, or both
| What the customer sees | Real? |
|---|---|
| Their wallet balances | Real. Ordinary platform wallets |
| Their own orders and fills | Real. Settled by the matching engine |
| A fill against a bot order | Real money, from your pool |
| Resting depth with no TTL | Real. Backed by an order — theirs, another customer's, or a bot's |
| Resting depth with a TTL | Engine-generated. Ten display levels, unfillable |
| The Recent Trades tape on a market at 0% real liquidity | Engine-generated, in full |
| The chart, the last price and the 24h change | Engine-generated. Every candle is written by the price engine |
| The 24h volume | Mixed, and mostly synthetic. Bot-to-bot prints add to candle volume exactly like real fills |
A market running at Real Liquidity 0% has a complete price history, a busy tape and a deep-looking book, and not one order on it a customer can fill. It is a display. Realised P&L on such a market is legitimately zero however busy it looks, because both sides of every print are the house.
Stopping, and what stopping does not do
Stopping a market goes through the engine: it cancels the bots' open ecosystem orders and clears the synthetic depth it wrote — matched on the TTL, so customers' resting orders are untouched.
Disabling the extension does not. The cron jobs deregister and the engine stops ticking, but nothing cancels the orders already resting in the book. They sit there, fillable, backed by a pool nothing is managing.
The order is: stop each market, confirm the book is clear, then disable the extension.
If orders are already stranded — from an older build, or from an extension switched off without stopping its markets first — the cleanup is a script, run from the project root:
pnpm eco:mm:orders # survey only, changes nothing
pnpm eco:mm:orders:clean # cancel themIt is dry-run by default. It selects only rows carrying marketMakerId and
botId, re-checking each one individually, because a customer's order is
identical apart from those two columns and cancelling one without releasing its
hold would strand their funds. It performs no MySQL writes at all — bot orders
lock no wallet, so there is nothing to refund. Run it with the backend
stopped: cancelling underneath a live matching engine races its settlement.
Deleting a market maker is more aggressive still, and it clears the entire aggregated order book for that symbol including real levels — see Where the tape lives.
When a customer asks who they traded against
Decide this before you need it, and write it down for your support team.
What is factually true, on a market with real liquidity above zero:
- They traded against the platform's own liquidity. The counterparty was the operator, not another customer.
- The fill was real. Their money moved, your money moved, and the price they got is the price the book showed.
- On a market at 0% real liquidity they traded against nothing — those prints are not fills, no customer can be on either side of them, and no customer order was matched.
What your team should not say is that the tape represents other customers'
activity. It does not, and the isAiTrade flag is in the payload that built the
screen they are looking at.
Where and how you disclose market making is a legal and commercial question this documentation cannot answer for you — it depends on your jurisdiction, your terms of service and your licensing. What it can tell you is what the software does, so that whatever you write is accurate.
Before you raise Real Liquidity
-
Run at 0% first, for a full day. You get a price series, a tape and a chart with no money at risk and nothing a customer can fill.
-
Fund the pool on both sides. A one-sided pool fills in one direction and refuses in the other. See Funding the pool.
-
Raise the percentage in steps, watching the pool's inventory skew on the dashboard as you do. Skew is how you see the market absorbing customer flow.
-
Watch the resting-order count. A market at its ceiling logs it once; a market accumulating quotes it never cancels is the failure mode that produced 704,353 open orders on one live market.
-
Confirm the daily loss limits are set before real fills can happen at all. See Risk and emergency controls.