Bicrypto 6.7.8
Pre-releaseThis version isn’t published on updates.mashdiv.com yet — the notes are available to preview, but it can’t be downloaded until it’s released.
11 September 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
Core v6.7.8
Release Date: September 11, 2026 Tags: INSTALL, FINANCE, TRANSFERS, DEPOSITS, WITHDRAWALS, PERFORMANCE, RATE LIMITS, LOGGING, ADDONS, I18N
Overview
If you do not run the Ecosystem addon, 6.7.7 could not start, and pnpm updator
died before the schema migration ran. A core file imported an Ecosystem file at
module scope; the require chain reaches server.ts, so the absence of an optional
addon was a whole-platform boot crash rather than a missing feature. Anyone who
tried to update to 6.7.7 without Ecosystem installed and saw MODULE_NOT_FOUND was
hitting this. This release is the fix, and it is why there is a 6.7.8 at all.
The platform was slow while a bot ran, and the cause was the platform's own logging. A CPU profile taken while a bot placed and cancelled about ten orders a second put the backend at 91% of one core, with 13.3% of that busy time spent writing to the console and 28.5% in the licence layer. Node is single-threaded, so both come straight out of the event loop every other request is queued behind. Neither was producing anything anybody read.
The finance wizards stated as fact things they had not yet asked. An empty list has several causes — not fetched yet, genuinely empty, request failed, superseded by a newer click — and every step of deposit, transfer and withdraw collapsed them into one. Customers were told they own no wallets, that no currency can be deposited, and that there is nothing to transfer, while the request that would have said otherwise was still in flight or had failed with a banner saying so directly above.
A malformed RATE_LIMIT silently removed the per-IP cap from the entire
platform, login and registration included. parseInt("unlimited", 10) is NaN,
every comparison against NaN is false, and the limiter failed open.
It is all in Upgrade Notes; read it before you update.
Addons released alongside this. Work in the extension trees ships in their own notes, and several of them want this release underneath:
- Ecosystem 6.5.2 — three ScyllaDB readers were returning their first page and reporting it as the whole answer, one of which could release the collateral backing a live order. Requires Core v6.7.8 for the rate limiter that moved here.
- Hummingbot Connector 6.1.9 — the backend was billing its own queue delay to the
bot's clock and charging it as a forged signature. The two core files behind that
(
handler/Request.ts,handler/Routes.ts) change in this release; the behaviour is explained there. - Futures 6.2.5 — a cancelled order could be revived by a read taken before it was cancelled.
- AI Market Maker 6.2.6 — one line, and it belongs to the Ecosystem fix above.
- P2P Trading 6.3.7 — "top cryptocurrencies" was adding up different currencies.
Update Instructions
pnpm updatorThere is no schema change in this release — no new table, no seeder, and nothing to run by hand. Both halves matter: the licence cache, the logging changes and the rate-limit guard are server-side and need the backend restart; the deposit, transfer and withdraw wizards live in the browser bundle and need the frontend rebuild.
- Update Core first, before the addons listed above. Ecosystem 6.5.2 re-exports a module that moves into Core in this release, so Core has to be in place for the addon's own path to resolve.
- Check
RATE_LIMITandRATE_LIMIT_EXPIREin your.envbefore you restart. If either is not a positive number, you have been running with no per-IP cap or with a bucket that never expired. See Upgrade Notes. - Per-step request logs are quieter by default.
LOG_LEVEL=debugrestores them in full; nothing durable changed. See Upgrade Notes.
Upgrade Notes
An install without the Ecosystem addon could not boot 6.7.7
utils/pool-backing/custody.ts imported the rate limiter from
api/(ext)/ecosystem/deposit/util/RateLimiter at module scope. That file belongs to
an optional addon, and the import chain above it does not stop:
utils/pool-backing/custody.ts
<- utils/pool-backing/reconcile.ts
<- cron/jobs/poolBacking.ts
<- cron/index.ts
<- server.tsSo on any install without Ecosystem the process died at boot with
MODULE_NOT_FOUND, and because pnpm updator boots the backend to run the schema
migration, the update itself failed before the migration ran. Core may not depend
on an addon; an addon may always depend on core.
The limiter has moved to core at utils/chain-rate-limit.ts. The addon path still
exists and re-exports it with the identical export surface, so an install still
carrying the old file — or an older Ecosystem addon — resolves either way and
nothing needs reordering. The "one set of buckets per process" guarantee now holds
more firmly than before: the singleton lives in core, which is always present, so
the deposit scanner and the pool-backing custody reads share one budget against the
public chain endpoints whatever is installed.
This was introduced on 5 September, which is after 6.7.6 was numbered, so 6.7.7 is the released version that carries it. If your update to 6.7.7 failed, go straight to 6.7.8.
Check your rate-limit settings before restarting
RATE_LIMIT and RATE_LIMIT_EXPIRE were read with a bare parseInt. A value that
does not parse yields NaN, the limiter decides with if (used >= limit), and every
comparison against NaN is false — so a typo did not fall back to the default, it
removed the per-IP cap from the whole platform, including the login and
registration doors the limiter exists to protect. RATE_LIMIT_EXPIRE had the
mirror-image fault: a NaN TTL left the counter key with no expiry, so the bucket
filled once and refused for ever.
From this release anything that is not a finite positive number falls back to the default and prints one warning per variable per process naming the value it rejected. There is deliberately no value meaning "off".
What to do. Read the two variables in your .env now. If either is misspelled,
empty-but-present, or set to a word, your install has been running without a cap (or
with a permanently jammed one), and this release will change that on the first
restart — which is the correct behaviour, but it is a change you should not meet by
surprise.
Per-step request logs are quieter, and LOG_LEVEL=debug brings them back
A CPU profile taken while a bot placed and cancelled about ten orders a second put
the backend process at 91% of one core with 13.3% of that busy time in console
output (writeUtf8String / writeBuffer / consoleCall). Two writers accounted
for it, and neither was producing anything anyone read:
- Every HTTP request opens a live task, and
placeOrderalone reports thirteen steps — a header, ~13 step lines and a closing line, fifteen synchronous writes to a pipe per request. On Linux a write to a pipe blocks when the reader is behind, and in production the reader is the pm2 daemon. - The wallet audit logger ran
JSON.stringifyon every wallet operation and wrote ~400 bytes that duplicated, verbatim, the durable row written immediately after it. A market maker performs at least two wallet operations per order.
What is kept. The request header and the closing line with its duration still
print for every request, so traffic and timings stay visible. A step that warned or
failed still prints, because those are the ones worth reading and they are rare.
The full step breakdown of any slow request is unaffected — the SLOW line is built
from the context's own step array, not from the console handle, so it still names
exactly where the time went. And a developer watching a TTY sees no change at all;
only the non-TTY (pm2 pipe) branch is gated.
Nothing about the durable audit trail changes. walletAuditLog is still written
inside the caller's transaction, and a persistence failure still warns
unconditionally. What was removed is the console copy of a row that is already
stored.
LOG_LEVEL=debug restores both in full.
The currency route's empty answer changed shape
GET /api/finance/currency?action=transfer now always answers {from, to}. Four
paths used to short-circuit with a bare [] for every action — the three "this
wallet type is switched off" guards and the "you hold nothing funded" return — so a
transfer caller reading data.from got undefined and could not tell "no
currencies" from "the request went wrong". That is exactly how the transfer wizard
came to render a blank step with nothing on it explaining why.
Any client reading data.from / data.to is unaffected and now gets the empty case
correctly. A client that treated the transfer response as an array was already broken
on the populated path, because that path has always returned an object.
The route's declared response schema was also wrong on every path and is corrected —
it described a {status, data} envelope this route does not send, an array for an
action that returns an object, and database columns where the rows are
{value, label} picker options. Any SDK, mock or contract test generated from it was
wrong; regenerate them.
Added
- Added
utils/chain-rate-limit.tsin core, holding the per-chain token buckets that every in-process reader of the public chain endpoints draws from. The Ecosystem addon's path re-exports it unchanged. See Upgrade Notes for the boot crash this resolves. - Added
requireOptionalModuleAtLoad(module, importer), the module-scope variant of the optional-import helper. At module scope a throw is not an error a caller can handle — it aborts the importing file's own load, so one bad addon takes a core file down with it. Absence returnsnullsilently; a real defect returnsnulltoo, but loudly, at error level, naming both the module and the importer. The old barecatch (e) {}could not tell the two apart and said nothing either way. - Added five finance-wizard message keys — covering "no depositable currencies
here", "no destination wallet for this source", "no funded currencies to transfer
out", "no target currencies that could receive this", and "no wallet types are
switched on" — seeded into all 89 catalogues.
no_wallets_available_descriptionpreviously rendered as the literal text "No Wallets Available Description".
Changed
- Changed the licence route lookups to be memoised.
isLicenseExemptRouteandfindExtensionForRouterun on every HTTP request and walk static tables — 8 exempt prefixes and 19 extensions holding 36 route prefixes between them. In source that is trivial; in a shipped build it is not, because this file is obfuscated and every literal comparison runs through the string-array decoder. The profile put the licence layer at 28.5% of busy time under bot load. Both functions are pure functions of the path over tables nothing mutates, so the answer can never change within a process. The cache is bounded at 5,000 entries and cleared wholesale at the cap, because a path like/api/hb/order/<uuid>is one path per order and an unbounded map would be a memory leak with a public trigger. No security property changes: nothing here decides whether a licence is valid. - Changed
handler/Request.tsandhandler/Routes.tsto stamp each request's arrival time and to allow a signed bot route's body-schema check to be held until after its signature verifies. Both exist to serve the Hummingbot bridge and neither alters any other route; Hummingbot Connector 6.1.9 explains the failure they fix and the reasoning behind them. - Changed the transfer wizard's destination list to mirror the routes the backend
will actually execute. It derived the list as "every type except the source", which
is not the rule — FIAT and SPOT cannot reach FUTURES, and the POST refuses the pair
outright with
400 Invalid wallet type transfer. So the wizard offered Futures for a fiat or spot balance, rendered the target-currency step and the amount step for it, and only refused at the final click. - Changed both currency fetches in the transfer wizard to a last-selection-wins guard. Clicks arrive faster than round trips, and whichever request the network answered last used to win — so an empty Eco answer landing after a populated Spot one blanked a list that had just been filled in. The guard covers teardown too: pressing "Start over" mid-fetch could otherwise paint "Failed to fetch currencies" above a freshly reset form with nothing pending.
- Changed the withdraw wizard to probe its three wallet types concurrently
instead of one after another, and to wait for the config to arrive before probing
at all. The wallet types to probe are derived from settings that start empty, so
running early made
Promise.all([])resolve immediately with nothing probed and step 1 render "No Wallets Available" as a settled answer — then, when the config landed, switch to a spinner and then to wallet pills, which reads as a glitching page rather than a loading one. - Changed
blockchains/{sol,ton,tron,xmr}.ts, the BTC deposit scanner, the spot deposit intent, the price-alert mark-price loader, the custom-chain lookup and the spot custody cron to use the optional-import helpers. Beyond the boot crash, the barecatch {}they replace was flattening "addon installed but broken" into "addon absent" — leaving bindings undefined with no record, so the failure surfaced much later asx is not a functionwith nothing pointing at the cause. Two of them were also loggingCannot find module ...into the admin cron log on every run of every install without the addon; that noise is gone, while a real defect inside an installed addon still throws and still lands there. - Changed the pool-backing custody reads to return an explicit error when the Ecosystem addon is absent, rather than a zero. A figure nobody read is not a balance of nothing.
Fixed
An empty list was read as a boolean, in every finance wizard
An empty list has four causes — not fetched yet, fetched and genuinely empty, the request failed, and superseded by a newer selection — and every step collapsed them into one. The state a failed fetch leaves behind is byte-for-byte the state a genuinely empty answer leaves behind, so the wizard stated as fact that the customer owns no wallets while the error banner directly above it said the request had failed: two contradictory explanations, one of them false.
It was also a false dead-end on every single click, not just on failures. The fetches live in effects, so they start after the render that commits the selection — and that render is "a wallet is selected, the list is empty, nothing is pending", which is exactly what the empty state draws.
- Fixed by raising the in-flight flag in the same
set()that empties the list, so no render can land in between, and by giving each list its own error slot instead of sharing one with balance failures and server refusals. - Fixed the deposit method panel to treat
nullas "not asked yet" and[]as "asked, and there are none". It previously requiredloadingto be true to show skeletons, so the panel dropped out of the page entirely for one frame; and for FIAT it rendered a heading with blank space under it, because every group was gated on its own length and an empty array passed the truthiness test that would have set an error. - Fixed the transfer wizard's source-wallet change to clear everything downstream
of it. It cleared three fields and left eleven, and
toCurrencysurviving was the damaging one: the step-5 gate istransferType === "wallet" && toCurrency, so after switching source wallets the amount panel stayed mounted showing the previous wallet's available balance, the previous amount and a stale rate, beside an empty ticker. - Fixed
setFromCurrencybeing declared as returningvoidwhile its implementation was async and also fetched the balance. The one caller that needed to await it could not see that it should, and fired a second identical balance request instead.
The currency route's empty answer did not keep the shape of its full answer
Covered in Upgrade Notes.
- Fixed all four short-circuit paths to answer
{from, to}foraction=transfer. This is the backend half of the blank transfer step above. - Fixed the route's declared response schema, which matched no path in the file.
- Fixed the Eco branch to order its currencies deterministically.
A malformed rate-limit setting disabled the limiter
Covered in Upgrade Notes.
- Fixed both settings to fall back to their defaults on anything that is not a finite positive number, with one warning per variable per process — rate-limited because this runs on every request and a misconfiguration that printed a line each time would be its own outage.
The platform's own logging was taking the event loop
Covered in Upgrade Notes.
- Fixed the non-TTY per-step log to print only steps that warned or failed unless
LOG_LEVEL=debugis set. The TTY branch is untouched. - Fixed the wallet audit logger's console copy to be gated on the log level, and
the level checked before the
JSON.stringify— an argument is built whether or not the call prints it. The durable row is unaffected.