The ecosystem order desk

The admin orders screen at /admin/finance/order/ecosystem — where it hides in the nav, what its ScyllaDB-backed tiles and filters can and cannot do, and how to run the corrupted-order cleanup safely.

8 min readUpdated 6 August 2026orders, scylladb, cleanup, permissions, admin

This is the one screen in the product where an operator can look across customer orders on ecosystem markets. It is not under Admin → Ecosystem, and that single fact accounts for most of the support traffic about it. (The same rows also appear one customer at a time on the Ecosystem tab of a CRM record at /admin/crm/user/{id} — the same endpoint, declaring the same access.ecosystem.order and view.ecosystem.order pair, filtered to that customer and equally read-only. It answers "what has this customer traded", not "what is happening on the book".)

Where it actually is

Spot Orders          /admin/finance/order/exchange
Ecosystem Orders     /admin/finance/order/ecosystem     <- this page
Futures Orders       /admin/finance/order/futures
Binary Orders        /admin/finance/order/binary

The addon's own navigation — Overview, Blockchains, Wallets, Trading — carries no Orders entry at all. Orders live in the core finance section beside every other venue's order table, because that is where an operator looks for "orders" regardless of which engine filled them.

The entry is tagged with the ecosystem extension, but the admin menu deliberately keeps extension-tagged items visible even when the extension is off — they are shown flagged as not installed, so an operator can see what is available. Seeing Ecosystem Orders in the nav is therefore not proof the addon is enabled. The user-facing menu is the one that hides them.

Two permissions gate it, and they are not the same key:

Key Grants
access.ecosystem.order Opening the screen. Enforced on the route path itself.
view.ecosystem.order The data. Enforced on GET /api/admin/ecosystem/order.
manage.ecosystem.order The cleanup action only.

create.ecosystem.order, edit.ecosystem.order and delete.ecosystem.order are seeded and grantable, but the screen disables create, edit and delete outright — granting them changes nothing here.

The tiles, and the one thing they cannot count

Lists ecosystem orders across markets

The analytics header comes in three bands.

Band Cards
Book state Total orders · Open orders · Matched orders · Canceled orders, plus a status pie
Flow over time A stacked bar of OPEN / CLOSED / CANCELED per bucket, over 24h, 7d, 30d, 3m, 6m and 1y
Liquidity composition Limit orders · Market orders, plus a stacked bar of resting versus taking flow

The analytics run against ScyllaDB, whose aggregator here understands exactly one question: count the rows where this column equals this literal. No sums, no averages, no distinct counts, no derived ratios. There is no traded-volume tile, no fee-revenue tile and no average-fill-size tile on this screen, and there cannot be one without a different data store. Fee revenue is a MySQL question — see Trading fees.

The liquidity band is the one worth reading regularly. Limit orders add depth and market orders consume it; a limit band that collapses on one market is that market losing its own liquidity providers.

The columns

Six columns are visible in the table: Status, Symbol, Order type, Side, Price and Amount. Everything else is carried on the row's expanded view and in the view dialog — ID, User ID, Time in force, Fee, Fee currency, Average price, Filled, Remaining, Cost and the raw trades payload.

Opening a row gives a four-figure strip — filled against amount with a percentage, remaining, total cost, and fee with its currency — then sections for pricing, the individual fills, routing (walletType, marketMakerId, botId), the order and account ids, and the timeline. An order placed by the AI market maker or a trading bot carries a Bot order badge, which is the fastest way to tell an operator's own liquidity from a customer's.

What each of those figures means over an order's life — why a partly filled order still reads OPEN, what the hold is, why a market BUY stores a price that looks wrong — is The order lifecycle.

Reading from ScyllaDB changes how the table behaves

This is one of three admin data tables declared against Scylla rather than MySQL — the other two are Futures Orders (/admin/finance/order/futures) and futures positions (/admin/futures/position), both on the futures keyspace. This one uses the ecosystem keyspace, which the API resolves to SCYLLA_KEYSPACE (default trading). Four consequences you will meet in practice — they come from the shared Scylla query helper, so they hold on all three screens, not just this one:

A text filter is a prefix, never a substring. A "contains" or "starts with" filter is compiled into CQL as column LIKE 'value%'. Searching Symbol for USDT is not the same question as searching for BTC on an install full of BTC/USDT orders, and the two do not return the same rows. No secondary or SASI index is created on any column of these tables, so a LIKE restriction the cluster cannot serve comes back as a query error on the screen rather than as an empty table — treat a red banner on a text filter as "that filter is not supported here", not as "the database is down". Equality filters work because every query carries ALLOW FILTERING.

A partial User ID is turned into a range query. Anything shorter than a full 36-character uuid is padded into a lower and upper bound, so a prefix works. A full uuid becomes an equality match. A value that is neither is rejected with a 400 rather than quietly widening the result set.

Sorting is only done by the database when you have filtered on User ID. userId is the partition key. With an equality filter on it the query can carry ORDER BY; without one, the rows are fetched and sorted in the API process before the page is sliced out.

Every page load is a full scan. Both the count and the data query are issued with ALLOW FILTERING and no partition key restriction. On a large install this screen is genuinely expensive to open, which is a reason to filter it rather than to page through it.

One quiet behaviour worth knowing: the list endpoint drops rows whose symbol, amount, price or side is null before it answers, and adjusts the reported total to match. Those rows exist; you simply never see them here. The cleanup dialog below is the only place the platform will tell you how many there are.

The status filter offers Cancelled, spelled with two Ls. The matching engine writes CANCELED with one. The filter value is passed straight into the CQL WHERE clause, so selecting it returns an empty table on an install with thousands of cancelled orders. Filter on OPEN or CLOSED and read the rest.

The cleanup dialog

Removes corrupted order rows with null essential fields. Supports a dry run.

Cleanup Corrupted Orders sits in the table's top bar. It exists for one specific artefact of how Cassandra-family databases write.

A corrupted order is a Scylla upsert ghost. In CQL an UPDATE against a primary key with no row behind it does not fail — it creates the row, with the key populated and every other column null. Anything that once updated an order by a (userId, createdAt, id) triple that never existed left behind a row with a perfectly valid primary key, a null symbol, a null amount, a null price, a null cost and a null side. They are not orders. They were never placed by anyone, they hold no funds, and no customer is waiting on them.

DELETE in ScyllaDB is not a soft delete, this table has no deletedAt, and the platform takes no backup of ScyllaDB at all — the built-in database backup and mysqldump both cover MySQL only. There is no undo. Always run the scan first.

  1. Open the dialog — the red Cleanup Corrupted Orders button above the table.

  2. Scan first. The Scan First action sends { dryRun: true, limit: 10000 } and deletes nothing. It reports Total scanned and Corrupted found.

  3. Read the two numbers. If Corrupted found is 0, close the dialog — there is nothing to do, and the destructive action is not even offered.

  4. Clean up orders. The confirm action re-runs with dryRun: false and the same limit, deletes each ghost row by its primary key, and reports how many were removed. The page reloads afterwards.

  5. Re-run if the scan was saturated. limit caps how many rows are scanned, not how many are deleted. If Total scanned came back at exactly 10,000 you have looked at the first 10,000 rows and no further; run it again until a scan comes back with Corrupted found at 0.

The endpoint accepts a limit between 1 and 100,000 and refuses anything outside that range with a 400. Its response carries totalScanned, corruptedFound, deleted, errors and dryRun; the dialog shows the first three.

The scan reads the orders table with ALLOW FILTERING and no partition key, which means every node walks every partition it owns. On a busy install that is a real load event, and it is issued from the same process that serves trading. Do it during a quiet window, not while a market is active.

A role that can open this screen sees the cleanup button whether or not it holds manage.ecosystem.order — the gate is on the endpoint, not the button. A grantee without it gets a permission error at the moment of the scan.

What this screen cannot do

It is read-only by construction: create, edit and delete are all disabled, and there is no cancel action anywhere on it. You cannot cancel a customer's order from the admin panel.

The two paths that do cancel an ecosystem order are:

  • the customer's own cancel, DELETE /api/ecosystem/order/{id} with the order's createdAt as the required timestamp query parameter, or their cancel-all;
  • deleting the market, from Admin → Ecosystem → Trading → Markets, which needs delete.ecosystem.market and cancels and refunds the orders it removes as part of tearing the market's data down.

An order that is stuck — resting on a customer's money with nothing matching it — is a repair-script problem rather than a screen problem. pnpm eco:index:check answers "is the open-orders index short of the ledger?" with an exit code, and pnpm reconcile:eco-inorder releases funds held with no open order behind them. Both, and the rule about restarting the backend afterwards, are in Operations.