Exchange Engine 6.5.2

Latest

11 September 2026

This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.

SCYLLAORDERSADMINMATCHING-ENGINERECONCILIATIONDEPOSITSOPERATIONS

Ecosystem v6.5.2

Release Date: September 11, 2026 Tags: SCYLLA, ORDERS, ADMIN, MATCHING-ENGINE, RECONCILIATION, DEPOSITS, OPERATIONS

Overview

A read from ScyllaDB returned its first page and reported it as the whole answer. client.execute returns at most fetchSize rows — 5,000 by default — and hands back a pageState token for the rest. It does not throw, warn, or shorten the result in any visible way. Three readers in this addon took the first page and dropped the token.

One of them moves money. The inOrder reconciler counts an open order it did not read as needing zero collateral, and expected is what the surplus to release is measured against. So a missed order did not make the tool report less — it made the tool release the funds backing it. Measured on a staging box: a partially-filled BUY resting at 77,000 with 0.0362 USDT correctly held was reported as expected=0, release=0.03655300. With --apply, that would have unlocked a live order's collateral.

The admin order list was showing operators a third of their open orders. A listing filtered to status: OPEN answered with 2 rows against a count(*) of 7 — the count was right, the row read had stopped after one page of a 10,115-row table, and nothing said so.

A cancelled order could be blocked from coming back when it was supposed to. The tombstone that stops a late read reviving a cancelled order was being written on every call to the engine's eviction path — and that path is not only a cancel.

Requires Core v6.7.8. This addon's rate limiter moves into Core in that release and this version re-exports it; Core also carries the boot fix that made an install without this addon unable to start at all.

Update Instructions

pnpm updator

Update Core to 6.7.8 first. deposit/util/RateLimiter.ts becomes a re-export of @b/utils/chain-rate-limit, which ships in Core — the limiter had to move because Core's pool-backing custody reads share these buckets, and Core may not import an addon. The path and its full export surface are unchanged, so nothing that imports it needs editing.

No table is added and nothing runs by hand. The matching engine, the order listing and the Scylla readers are server-side and need the backend restart.

  • Re-run any forensic tool you have acted on before. Results produced before this release may have been drawn from a single page. See Upgrade Notes.
  • Update AI Market Maker to 6.2.6 in the same window if you run it. Its one-line change is part of the cancellation fix below, and without it the market maker's post-cancel eviction does not mark its orders as terminal.
  • Update Futures to 6.2.5 if you run it; the same class of defect was ported out of that engine before it bit.

Upgrade Notes

Your Scylla forensic tools were reading one page and stopping

With ALLOW FILTERING this is not an edge case, it is the normal case: the coordinator scans a bounded slice of the partition range per page and returns whatever matched in that slice, which can be a handful of rows, or none, while many more pages remain.

  • The inOrder reconciler (backend/scripts/reconcile-eco-inorder.mjs). Covered above. It now follows the page state, prints Read N OPEN order(s) across M page(s), and refuses to write at all if its scan did not finish. Reporting still works and shows the same exposure; only --apply is withheld. No amount of arithmetic downstream can detect a missing row — an unread open order and a released one are indistinguishable by the time the surplus is computed — so the only safe rule is that an incomplete picture may not move money.
  • The corrupted-order sweep. LIMIT 20000 read 5,000 rows, reported totalScanned: 5000, and found one of the two corrupted rows actually present. Every corrupted row past the first page was invisible to the one tool that exists to remove them — including at the default limit of 10,000, which is already twice a page. It now pages to the limit the caller asked for and reports scanTruncated when it stopped with rows still unread, in the response and in the operator's summary line.
  • The admin order list. Covered above. Worse than a short page: the sort is done in memory, so a partial set does not merely truncate the last page, it puts the wrong rows on page 1.

What to do. If you have ever run the reconciler with --apply on a table larger than one page, re-run it now in its default dry-run mode and read the new page count line. If you previously acted on a report drawn from one page, customers may have had collateral released from under live orders. The same applies to any "0 corrupted orders" verdict from the cleanup tool: re-run it, and this time the answer will tell you whether it reached the end of the table. "Found 0 out of 5,000 scanned" is the same sentence whether the table holds 5,000 rows or 500,000, and the operator's next move is opposite in the two cases.

The listing read is bounded, and the bounds are yours to set

Reading every page is correct; reading an unbounded table in one request is how a cluster goes down. The paged listing stops at 50,000 rows or 200 pages, whichever comes first, and says so — in the log and in truncated on the response — rather than serving a prefix as if it were the whole answer. All three are read per call from the environment, so they can be changed without a restart:

ECO_LIST_FETCH_SIZE=5000    # rows per driver page
ECO_LIST_SCAN_CAP=50000     # most rows one listing may accumulate
ECO_LIST_MAX_PAGES=200      # round trips, bounded independently

The page bound exists separately from the row bound because a filter matching almost nothing returns tiny pages, and a row cap alone would let one request walk an entire table five rows at a time.

Admin order pages were short, and the total was wrong

Scylla's upsert behaviour leaves ghost rows: an UPDATE against a primary key with no row behind it creates one with symbol, amount, price and side all null. The admin listing filtered those out of the page after it had been sliced, so every ghost cost one slot on the page the operator was looking at — perPage: 10 over a market with ghosts showed seven orders. The correction applied to the total had the same shape of error: it subtracted the ghosts visible on one page from a whole-table count(*), which is neither the page count nor the real total.

The filter is now handed to the reader, so the slice is taken from rows that survive it and the count is reduced by every ghost the scan actually saw. Expect page sizes and totals on that screen to change; the new ones are the correct ones.


Changed

  • Changed deposit/util/RateLimiter.ts into a re-export of Core's @b/utils/chain-rate-limit. The path and every export keep their names, so this addon's own callers and any install still carrying the old file resolve either way. "One set of buckets per process" now holds more firmly than before: the singleton lives in Core, which is always present, so the deposit scanner and Core's pool-backing custody reads share one budget against the public chain endpoints whatever is installed. Two limiters built from the same rates are two budgets, and two budgets against one endpoint is the doubling the buckets exist to prevent.
  • Changed the paged Scylla read to accept a row filter, so rows the caller cannot use are dropped before the page is cut and before the total is counted.

Fixed

Three readers took the first page as the whole answer

Covered in Upgrade Notes.

  • Fixed the filtered listing, the corrupted-order sweep and the inOrder reconciler to follow pageState to the end of the result set.
  • Fixed the two bounded readers to report when they stopped early, rather than serving a prefix silently.
  • Fixed the reconciler to refuse --apply on an incomplete scan.

The cancel tombstone was written for cancellations that had not happened

The engine's eviction path means "forget this order and refresh the book", and the tombstone that stops a late read re-adopting a cancelled order was written on every call to it. That path is not only a cancel: the AI market maker uses exactly this on orders that are still open and still fundedflattenOwnLiquidity drops the pool's own resting quotes out of the matcher before a rebalance so the rebalance cannot self-match, and the per-order sweep beside it re-reads the order and calls this only while it is live.

Tombstoning those refused the resync that is supposed to bring them back. A funded order invisible to the matcher for a minute is the same class of defect as the phantom the tombstone exists to prevent, reached from the other side.

  • Fixed by making the tombstone opt-in: the caller states that the ledger row is terminal and the funds are released, and only then is the id blocked from re-adoption. The two real cancel doors and the market maker's post-cancel eviction pass it; nothing else does.
  • Fixed the two cancel doors in this addon to pass it, both of them after the CANCELED row and the refund have landed — so the flag can only ever describe a cancellation that actually completed.

AI Market Maker 6.2.6 carries the third call site. If you run the market maker, update both together: its post-cancel eviction reaches this same method, and until it passes the flag its own cancelled orders are not marked terminal.