Bicrypto 6.5.8
28 July 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
Core v6.5.8
Release Date: July 28, 2026 Tags: SECURITY, SESSIONS, PRIVACY, COMPLIANCE, GEO RESTRICTIONS, KYC, WITHDRAWALS, 2FA, BINARY OPTIONS, MARKETS, SETTLEMENT, ADMIN, USERS, SPOT, EXCHANGE, WATCHLIST, BLOG, PERFORMANCE, UI, VERIFICATION, TRADING PRO, PAGE BUILDER, NAVIGATION, ACCESSIBILITY, BUG FIXES, WALLETS, TRANSFERS, DEPOSITS, PAYMENT GATEWAYS, EXCHANGE RATES, FIAT, TRADING FEES, PROFIT REPORTING, INVESTMENTS, MARKET NEWS, CURRENCY ICONS, SITE DESIGN, THEMING, MENUS, FOOTER, LANDING PAGES, CHARTS, DATA TABLES, FORMS, PERMISSIONS, PASSWORDS, EMAIL, ANALYTICS, SUPPORT, SEO, ERROR PAGES, AFFILIATE, LICENSING, NOTIFICATIONS, MAINTENANCE TOOLS
Overview
Version 6.5.8 is a large release: geographic access restrictions, a withdrawal 2FA policy, binary options on ecosystem markets, real enforcement for the KYC feature switches, and the site's entire appearance moved out of code and into the admin panel. It also carries full reviews of the core, of money movement, and of the admin CRM, plus roughly 54,600 lines of unreachable code removed.
Updating an existing installation? Go straight to Update Instructions below — it opens with a checklist of the three things you must do, and everything after that is clearly marked as conditional.
Two prerequisites change in this release, and neither is optional: the platform now requires Node.js 26 and a reachable Redis. A backend that finds either missing exits with a message rather than crash-looping.
A narrative of what changed and why is in Highlights, below. The itemised lists are in Added, Changed and Fixed.
Update Instructions
Three things are required, in this order. Everything after them is conditional, and each heading says when it applies.
Step 1 — Move the server to Node.js 26 (required, before updating)
Move the server to Node.js 26 before updating — 26.5.1 is the current release. Install it system-wide, as root, so that every account on the box resolves it — not with nvm.
nvm installs into $HOME/.nvm and is only ever on that one user's PATH. On a control panel — Virtualmin, cPanel, Plesk — the site runs as its own domain user, and /root is mode 700, so a domain user cannot even traverse the directory root's Node lives in.
1. Remove the distribution's Node
An older nodejs package owns /usr/bin/node, which shadows a /usr/local install for any user whose PATH prefers /usr/bin. Take it out first:
# Debian / Ubuntu
sudo apt-get purge -y nodejs npm
sudo apt-get autoremove -y
# RHEL / AlmaLinux / Rocky
sudo dnf remove -y nodejs npm2. Install Node.js 26 for every user
Either method puts node, npm and npx in a directory that is on every account's default PATH. Pick one.
NodeSource packages — integrates with the system package manager, so security updates arrive with the rest of the box:
# Debian / Ubuntu
curl -fsSL https://deb.nodesource.com/setup_26.x | sudo -E bash -
sudo apt-get install -y nodejs
# RHEL / AlmaLinux / Rocky
curl -fsSL https://rpm.nodesource.com/setup_26.x | sudo -E bash -
sudo dnf install -y nodejsOfficial tarball into /usr/local — no third-party repository, and it pins an exact version:
VERSION=v26.5.1
case "$(uname -m)" in
x86_64) ARCH=x64 ;;
aarch64|arm64) ARCH=arm64 ;;
*) echo "unsupported architecture: $(uname -m)"; exit 1 ;;
esac
cd /tmp
curl -fsSLO "https://nodejs.org/dist/$VERSION/node-$VERSION-linux-$ARCH.tar.xz"
sudo tar -xJf "node-$VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1To take whatever 26.x is current at the time you run it instead of pinning, resolve the version first:
VERSION=$(curl -fsSL https://nodejs.org/dist/index.json \
| grep -o '"v26\.[0-9.]*"' | head -1 | tr -d '"')3. Install the global tooling on top of it
pnpm and pm2 belong to whichever Node installed them. If they were installed under root's nvm they are invisible to the domain user in the same way Node was, so reinstall them against the system Node:
sudo npm install -g pnpm pm24. Verify as the account the site actually runs as
Check it from a fresh login shell — the current shell has already cached its PATH:
node -v && which node # as root
su - demo -c 'node -v; npm -v; pnpm -v; which node' # as the domain userBoth must report v26.5.1 from a path under /usr/bin or /usr/local/bin. If the domain user still reports the old version, that account has its own nvm:
grep -n "nvm\|NVM_DIR" ~/.bashrc ~/.bash_profile ~/.profileAn nvm block in those files wins over /usr/local/bin regardless of what is installed system-wide. Either delete the block, or point it at the same version with nvm install 26 && nvm alias default 26.
5. Two things that follow the runtime change
- Rebuild native modules. Compiled addons are tied to the Node ABI they were built against and will refuse to load under a different major —
uWebSockets.jsin particular. In each application directory, as the account that owns it:rm -rf node_modules && pnpm install. - Restart the process manager itself, not just the apps. A running
pm2daemon holds the Node binary it was started with, so restarting a process under it changes nothing. Runpm2 updateto respawn the daemon, thenpm2 restart all --update-env. If any process was created with an explicitinterpreterpath into an old version's directory, correct it in the ecosystem file. The same applies to a systemd unit whoseExecStartnames/usr/bin/nodeby absolute path — that path no longer exists after step 1.
If you want the old version kept on disk for a reversible switch, install nvm as well, under the domain user only, and leave the system-wide 26 in place as the default everything else resolves. nvm is a per-user convenience, not a deployment mechanism.
Step 2 — Make sure Redis is reachable (required, before updating)
If your install has been running without Redis, it will not start after this update. Install Redis first.
The backend used to fall back to an embedded per-process store whenever Redis was unreachable, and carried on as if nothing had happened. That store cannot coordinate anything between processes: it answers a lock acquisition with "yes" out of a map only that one process can see, so under pm2 cluster mode or NEXT_PUBLIC_BACKEND_THREADS every process believed it held the same lock — two matching engines over the same order book, two market makers on the same market — and it has no pub/sub at all, so a setting changed in the admin panel never reached the other processes. None of that appeared as an error. The fallback has been removed and Redis is a hard dependency.
The backend now checks for it before anything else and, if it cannot connect, prints the address it tried, the environment variables that chose it and how to install Redis, then exits with code 78. The PM2 configurations list that code in stop_exit_codes, so the app stops with the message on screen instead of crash-looping until it scrolls away.
sudo apt-get install -y redis-server
sudo systemctl enable --now redis-server
redis-cli -h 127.0.0.1 -p 6379 ping # expects: PONGThen confirm REDIS_HOST, REDIS_PORT and REDIS_PASSWORD in the repo-root .env point at it. The scheduler is a separate process now (below), and both processes must use the same Redis — that is what carries scheduling, the WebSocket relay and cache invalidation between them. Since both read the one .env, that happens by itself unless you go out of your way.
Redis going away while the platform is running is treated differently, on purpose: the backend does not exit, because dropping every in-flight request and open socket over a reconnect is worse than the reconnect. It reconnects with backoff, keeps scheduled jobs running on in-process timers meanwhile, and anything that genuinely needs Redis returns an error instead of a plausible-looking wrong answer.
Step 3 — Run the update
Do these two steps in this order. The order is not a formality.
1. Download the update from the admin panel first, while your site is still running — System → Updates, or Extensions for an addon.
2. Then, on the server:
pnpm updatorIf you have binary markets imported from the Ecosystem list, they need backfilling — they are still marked exchange-backed and will keep rejecting orders. From backend:
npm run verify:binary # report what needs attention
npm run verify:binary:fix # repair the unambiguous casesverify:binary:fix only repairs markets where the choice is unambiguous — an ecosystem market exists and an exchange one does not. Where both exist it leaves the decision to you.
After updating: two one-off repairs (run these once)
Both report before they touch anything, and both are safe to run more than once.
cd backend
npm run sync:permissions # what is missing
npm run sync:permissions -- --apply # insert itRoutes are allowed through when the caller's role holds the permission the route names — or when the caller is Super Admin. A permission that is not a row in the table cannot be attached to any role, so a route naming one is Super-Admin-only, permanently and silently. Nothing in the platform created those rows from route metadata, so every feature added since the last permission seed was affected: all of Forex Trading, geo restrictions, market news, currency icons and the Ecosystem KMS. Inserting a permission grants nobody anything — it only makes it assignable in the role editor.
npm run repair:currency-precision # what looks wrong
npm run repair:currency-precision -- --apply # set it to 8The spot-currency import read ccxt's precision as a whole number. Every supported provider reports it as a tick size (1e-8), and reading that as a whole number stops at the 1 — so BTC was stored as having one decimal place, and anything finer than 0.001 as zero. The import is fixed; this corrects rows written before it. Re-importing spot currencies from the admin panel is the exact alternative, at the cost of inserting every currency the provider offers.
A type column is added to the watchlist table on the next backend start, defaulting to SPOT. Existing rows are unaffected.
After updating: clear accumulated AI Market Maker orders (only if you run that addon)
Only if you run the AI Market Maker addon. Skip this otherwise.
That addon never cancelled its own quotes — every simulated print appended a new resting order to the real ecosystem order book and nothing reliably removed the previous one. v6.1.0 of the addon fixes that and adds a ceiling, but orders already resting do not disappear on update. One live installation reached 704,353 open orders on a single market, which was enough to stop the API binding its port at startup: the site served connection-refused while the log filled with successful startup messages.
Check first. This is read-only, writes nothing, and is safe with the platform running:
pnpm eco:mm:ordersIt reports per market how many resting orders belong to the market maker and how many belong to real users:
If bot-open is zero everywhere, you are done.
Otherwise, stop the platform first — cancelling underneath a running matching engine races its settlement, and the script refuses to run if it finds anything listening on the API port:
pnpm stop
pnpm eco:mm:orders:clean
pnpm rebuild:eco-orderbook
pnpm startDo not skip rebuild:eco-orderbook. The cleanup cancels orders but does not rewrite the aggregated book, so every price level those orders contributed is still displayed. The engine's own reconciler will not clear them either — it reconciles only levels inside the resident window, and a market large enough to need this has depth well outside it. Left alone they are phantom liquidity, and the market-BUY hold calculation reads that book.
After updating: if ecosystem startup is slow (only if you run the ecosystem)
The backend loads open orders at startup. From this release it does that with a book-ordered index — two reads per market instead of a scan of the whole orders table — but it only uses the index once the one-time backfill has been recorded as complete. The backfill runs in the background after startup and writes its marker at the very end, so a backend that keeps restarting never finishes it and every start pays the full scan again.
If your log repeats this on every boot:
[ECO_INDEX] Reading open orders from the ledger this boot: the order index has not been built yetthen build it in one pass and record it:
pnpm eco:index:check # is the index correct, and is it marked complete?
pnpm eco:index:repair # build it (also records completion)eco:index:check now reports the marker as well as the rows, because an index that is perfectly correct but unmarked is still ignored — that state reported "nothing to do" while the backend scanned on every start. If a previous repair left the index correct but unmarked, pnpm eco:index:mark records it without another full pass.
After updating: 24 new strings ship in English only (only if you run a non-English locale)
The blog post editor and the image picker add 24 interface strings, and they ship in English only. Translations fall back one namespace at a time rather than one key at a time, and both namespaces already exist in every language, so nothing supplies the English text as a stand-in: on a non-English locale those 24 strings render as their own key names. It affects the two authoring screens and the image picker, and nothing else. Run the translation manager to fill them — and back up frontend/messages/ first, because it rewrites all 90 files.
The blog admin navigation gained two grouping labels. One of them reuses the translation of "Content" each locale already carries, so it is translated everywhere; the other is English until the same run.
Background — the scheduler now runs in its own process
pnpm start brings up three PM2 apps instead of two, and there is nothing to configure. No environment variable, no second command, no .env change.
| App | Port | What it does |
|---|---|---|
backend | 4000 | Serves the API. Registers no scheduled jobs. |
frontend | 3000 | Serves the site. |
cron | 4001 | Runs every scheduled job. Serves no traffic. |
pm2 list should show all three. pm2 logs cron is where scheduled work now reports.
Every scheduled job in this platform is a queue worker, and a queue worker runs in the process that created it — on the same event loop, and in the same memory limit, that answers requests. So a job that allocates heavily stalls the site while the garbage collector catches up: the incident this change comes from measured a 1.9-second pause with the site answering nothing, arriving back to back as memory filled. Moving the scheduler to its own process gives it its own memory limit, so cron work misbehaving now recycles the scheduler and leaves the site serving. Worker threads would not have done — threads share one memory limit with the process that spawned them, which is the exact resource that runs out.
Port 4001 is an implementation detail. The scheduler binds it only because it shares an entry point with the API. Nothing should connect to it; do not point a load balancer, a health check or a firewall rule at it.
Nothing runs twice. The two halves — "the API schedules nothing" and "the scheduler schedules everything" — are set in the same file, production.config.js, so they cannot drift apart. pnpm start also reconciles what PM2 already has against that file: an app left over from a previous release, still carrying its old role, is recreated rather than silently kept. The standalone production.cron.config.js from the opt-in preview is retired and starts nothing at all, because running it alongside the current configuration would have been two schedulers over the same rows.
Surviving crashes
Crashes are already handled and need nothing from you. PM2 restarts any of the three apps that exits unexpectedly, and the scheduler additionally recycles itself if it passes 2 GB — that is the point of giving it its own process. The one deliberate exception is exit code 78, which PM2 is told not to restart: it means an unreachable Redis or a Node version that cannot load the native modules, and restarting sixteen times would only scroll the message explaining it off the screen.
Start the platform with pnpm start, and let nothing else start it. A second supervisor on top of PM2 — a systemd unit, a cron entry, or a PM2 app whose script is itself pnpm start — does not add resilience. pnpm start hands the apps to PM2 and exits, correctly, in a few seconds; anything that reads that exit as a failure will run it again, and pm2 start on apps that already exist restarts them. The result is the whole platform stopping and starting on a timer, with every exit code 0 and nothing in the log that looks like an error. If the backend needs longer to boot than the interval, the API never reaches its listen call at all and the site stays down behind a log full of successful startup messages. The backend now says so directly: repeated starts in a short window print a RESTART_LOOP warning naming the likely supervisor and the commands to confirm it.
If you installed with an older installer.sh, remove bicrypto.service
Earlier versions of the installer created and enabled a systemd unit that re-ran pnpm start every 10 seconds. If that unit is on your server, remove it — it restarts your whole platform on a timer. Check:
systemctl status bicryptoIf it exists:
sudo systemctl disable --now bicrypto.service
sudo rm -f /etc/systemd/system/bicrypto.service
sudo systemctl daemon-reloadThen start the platform once, by hand:
pnpm startThe current installer no longer writes that unit, and removes it if it finds one.
Starting the platform after a reboot
Use PM2's own boot hook — it is the only supervisor this platform supports. Run this once, as a user with sudo:
pm2 startup systemd -u $USER --hp $HOMEIt prints a sudo env ... line; run that. From then on, after any pnpm updator (or any pnpm start), record the process list so a reboot replays it:
pm2 savepm2 save writes what is currently running into ~/.pm2/dump.pm2, and the boot hook replays that dump — not production.config.js. Two consequences worth knowing:
- Run it after
pnpm updator, not before, so the dump matches what you actually ended up with. - If you deliberately run a subset —
pnpm start:backendon an API-only host, say — runpm2 saveafterwards too, or the dump still lists the apps from your last fullpnpm startand a reboot brings them back.pm2 cleardumpclears a dump you no longer want.
On Windows, pm2 startup is not supported. Install PM2 as a Windows service with pm2-installer; until then the platform must be started by hand.
If you want the old single-process arrangement, put this in the repo-root .env and restart:
CRON_MODE="inline"The cron app then disappears from pnpm start entirely and the API schedules everything again, exactly as in previous releases. This is the only supported way back; setting CRON_MODE to anything else is not a supported deployment.
Two addon limitations, and how you hear about them
A short list of jobs cannot run on the scheduler process, because they own a connection or an engine that belongs to whichever process serves trading. Those specific jobs decline; the scheduler still starts and still runs everything else. In the preview release the whole cron process refused to start in these cases — as the default that would have meant one addon's narrow limitation taking down every scheduled job, withdrawals included, on an install that looked healthy from every other angle.
A declined job is never quiet. Each one, at most once every fifteen minutes for as long as it is declining:
- writes a
CRONerror to the process log, naming the job, why, what is consequently not happening, and the fix; - posts the same line to Admin → System → Cron, against the job it concerns;
- sends an urgent in-app notification and email to every Admin and Super Admin.
The two cases today:
- Forex Trading with a live A-book execution provider. The execution reconciler and the hedge monitor drive that provider's broker connection, and the API process already has it open — two of them over one broker account replay the same fills. Both jobs decline on the scheduler and run on the API process only. Nothing declines on a B-book desk, which is every install that has not enabled an execution provider.
- AI Market Maker with the Ecosystem extension disabled. Nothing is declined here — the six database jobs (risk monitor, pool rebalancer, daily reset, analytics, price sync, history retention) all run normally. But the market maker engine can only live where the Ecosystem matching engine lives, and with Ecosystem off there is no such process, so market making is not running anywhere. You get the alert because this combination cannot make markets whichever way cron is deployed — the market maker trades Ecosystem markets. Enable Ecosystem, or disable AI Market Maker.
Exiting rather than starting is now reserved for two things that break the API process identically and that no restart fixes: an unreachable Redis, and a Node version that cannot load the native modules. Both still exit 78 and both still stop under PM2 with the message on screen.
Background — ecosystem order loading
There is no step here. The change described in Ecosystem open orders no longer scanned at boot is on by default and migrates itself: the first start after updating builds what it needs from your existing orders, records that it has done so, and every start after that skips straight to the fast path. You do not have to run anything.
It is on by default because at any real size the previous method is not the safe choice, it is a different failure — it scans every node once per market at every start, and above 50,000 orders on a market it stops loading and drops an arbitrary set of them. Below 25,000 orders on one side of one market the two methods return provably identical orders, so a small install is unaffected either way.
The server checks itself rather than trusting this. Before it loads a single order from the new index, it compares a sample of markets against your actual orders. If the index holds fewer than the orders table does — the only difference that could hide a customer's order — it says so in the log and falls back to the previous method for the life of that process, then keeps checking one market an hour while it runs. Your orders are never at stake in that fallback: the orders table is the source of truth and is what it reverts to reading. What is lost is the speed.
If that ever happens you will see FALLING BACK to the legacy open-order scan in the backend log, with the reason and these commands:
pnpm eco:index:check # read-only; prints exactly what disagrees
pnpm eco:index:repair # rebuilds the index from your ordersThen restart. You can also run pnpm eco:index:check at any time — it is read-only and exits non-zero when it disagrees, so it works as a check in a deployment script. It needs the backend to have started once on this version first, and will tell you if it has not.
To stay on the previous method deliberately, put this in the repo-root .env and restart. It is the only value that turns it off; anything else is treated as the default and logged:
ECO_BOOK_SOURCE="legacy"Two optional dials, neither of which needs setting:
ECO_BOOK_WINDOW_PER_SIDE(default 25,000) — how many orders a side the engine keeps in memory per market. Does nothing on a market smaller than that. Orders outside the window are not lost; they rest in the database and load as the price reaches them. Raise it on a large server, lower it on a small one.ECO_TRADE_TAPE_TTL_DAYS(default 30) — how long Recent Trades entries are kept.0keeps them indefinitely, at the cost of a table that grows without limit.
Highlights
The detail behind the summary at the top. Nothing here is an instruction — everything you need to do is in Update Instructions above.
Version 6.5.8 adds geographic access restrictions, a two-factor policy for withdrawals, makes the default Super Admin account deletable, and fixes a set of long-standing defects in admin table sorting, spot currency lookups and form validation.
It also makes binary options work on ecosystem markets. The admin panel has always offered ecosystem markets for binary trading and flagged which ones have an AI Market Maker — but every order placed on one was rejected. Alongside that, the binary profit report — which counted stakes collected but never the winnings paid — is corrected, and a tool is added that checks a live installation rather than a test setup.
It also makes the KYC level builder's per-feature switches real. Those switches have always been presented as compliance control — turn off Withdraw Funds on Level 1 and Level 1 users cannot withdraw — but 35 of the 37 features did nothing. Only Forex & Multi-Asset Live Trading was ever enforced. Every other switch could be flipped either way with no effect. Enforcement now exists, is shared by every addon, and is off by default so no live install changes behaviour until an administrator opts in. Eleven new features cover the addons that had no KYC representation at all.
It also ships far less obfuscated code: the obfuscated set outside the security module drops from 27 files to 4.
It also ships the result of a full review of the core — the platform without its addons. What it found was mostly about who may do what: any signed-in user could list the file path of every identity document on the platform, cross-site request protection was skipped on the path a browser uses for most of every session, a signed-out session kept working, a password-reset link could be used to delete the account instead, and eight admin screens were reachable by any signed-in user. Those and twenty-four more are in Fixed below.
It also rebuilds blog post editing, which meant one of two unrelated screens depending on who you were. They are now the same editor, on a full page of its own with no site chrome. The author-side screen also could not create a post at all — every "New Post" submission was rejected. That, the admin screens silently discarding tags, and a rewrite of the image picker both screens use are in Fixed.
It also ships a full review of the platform's money movement — wallets, transfers, deposits, spot orders and the payment gateways behind them. Every exchange rate involving a fiat wallet was the reciprocal of the true one, so a transfer into a weak-currency balance credited a fraction of a unit while the same transfer in reverse credited over a thousand times the value moved. The configured wallet transfer fee was never charged at all, on any install. Fees on withdrawals and deposits were booked the moment a request was submitted rather than when it settled, so every rejection refunded the customer in full and left the platform's credit standing. Ecosystem transfers between users minted the fee outright and never spent the sender's chain balance, and deleting a wallet from the admin panel destroyed the balance and locked that customer out of the currency permanently. On the trading side the fee on every spot sell was worked out against the quantity sold rather than the value of the sale — a 0.1% fee on 1 BTC sold at 60,000 collected 0.001 USDT instead of 60 — a market sell could execute on the exchange and leave the platform recording nothing at all, and a binary contract could be settled twice and paid twice. Those and forty more are in Fixed.
It also takes the site's appearance out of code and into the admin panel. Appearance & Design now owns the palette, typeface, corner radius, elevation and motion, the shape of the public header and footer, every menu on the site and everything the footer says — each previewed live against the real pages, and gated by its own permission so a brand or marketing role can be given the look of the site without the rest of the panel. Those choices now reach the rest of the product: the per-section and per-addon identity hues that no setting could ever change resolve to your accent, the landing pages are rebuilt with illustrations drawn from your own colours, charts move onto one shared kit, and the trade terminal and binary page — roughly 1,900 lines of fixed colour between them — follow the active theme, so a customer in light mode no longer gets a black trading screen. Two further admin screens arrive alongside them: Market News, for publishing your own desk commentary into the trading terminal's feed, and Currency Icons, which finds assets with no icon and fetches them without shell access.
It also reviews the admin CRM — the user, verification and support screens an operator lives in. A momentary failure to reach the licence server left the whole install refusing every request until someone restarted it. Ordinary administrators could open, and reset the second factor on, a Super Admin account the users table never showed them. The user export declared the wrong permission and left a spreadsheet of everyone's personal data sitting on the server. And blocking an account from its detail page changed its label without ending a single one of its sessions. Three things the platform simply lacked arrive with it: a signed-in user can change their own password — and an account created through Google or a connected wallet, which has never had one, can set a first one — fiat conversion rates come from a registry of providers rather than a single feed, and there is at last a page listing more than the five most recent investments. Alongside them: four home page sections gated on names that match nothing, which could never render; a blog whose every article shared one browser-tab title and produced no preview card when shared; and the seven error pages rebuilt onto your own palette.
Finally, a full clean-up removed roughly 54,600 lines of code that nothing could reach — and, more usefully, found a cluster of finished features that had been built and never connected to a page. Those are now wired; see Added and Fixed below. Addon-specific results are in the NFT, AI Market Maker, ICO, Staking, FAQ and P2P notes for the same release.
Upgrade Notes
The two new features are off by default, so an existing install behaves exactly as before until an admin turns them on, and exchange-backed binary markets are unchanged.
The core security work does change behaviour, deliberately, in ways a browser will not notice and a custom integration might. The four most likely to reach you:
- Every state-changing request needs the cross-site token, including on the path taken when a 15-minute access token has expired. That path used to skip the check, which is the vulnerability. A custom client that stores only the session id will start being refused until it sends the token.
- A disabled, expired or IP-restricted API key stops working. That is the point — none of those controls were enforced before. If a key carries an IP allow-list, confirm it lists the address the key is used from. An empty list still means "not configured".
- Binary stake limits now come from the binary market. If those fields were left at their defaults because they appeared to do nothing, the defaults (1 / 10,000) now apply. Review them in Admin → Binary Markets if you were relying on smaller stakes.
- Public blog responses no longer carry the author's email address, return a reduced profile, and exclude drafts. A custom blog front end reading an author's address, or listing unpublished posts through the public pages, needs updating.
Sessions issued before the update keep their old behaviour until they expire; nothing signs your users out on deploy.
Four smaller ones, all in the same spirit — a control that was documented but not enforced now is:
- Disabling two-factor authentication needs a current code or the account password.
- Resending a two-factor code needs the challenge from the login step. The old shortcut is gone.
- The media browser only lists browsable folders. It never returned only the caller's own files.
- Account-deletion links issued before the update are refused — request a new one. Password-reset links in flight still work.
Two things to be aware of on the binary side:
- Binary markets imported from the Ecosystem list need backfilling. They default to exchange-backed and will keep rejecting orders until the repair is run or the source is set in Admin → Binary Markets.
- Binary orders now require a valid payout percentage. Any integration creating orders directly must supply one; the previous silent 85% fallback is gone.
Five money changes will reach an install that relied on the previous behaviour, or a custom integration reading those figures directly:
- The configured wallet transfer fee now applies. It never did — the setting was written by the admin screen and read under a different name, so every transfer was free. The shipped default is 1%, so this changes revenue and the amount users receive on every install, not only ones that customised it. Check Settings → Wallet → Fees → Wallet Transfer Fee before updating.
- Wallet Transfer Fee now requires a Super Admin to change. An administrator with only the ordinary settings permission could change the live transfer rate before this release.
- Spot sell-side trading fees are charged on the value of the sale. They were charged on the quantity sold, which collected a fraction of a percent of what the fee schedule says. Sellers now receive the amount your published fee implies rather than fractionally more, and sell-side fee revenue rises to what was configured all along. Review your spot fee before updating if you had tuned it upwards to compensate.
- The standalone currency rate response is wrapped. It returned a bare number and now returns the platform's standard response shape, in line with every other response. A custom client reading that value directly needs updating.
- Admin profit comparison figures are per currency. The period-over-period comparison returned four numbers and now returns a value per currency for each window, because summing unrelated currencies produced a figure that meant nothing.
The ecosystem matching engine loads its open orders a new way, on every install. There is nothing to run and no data to migrate — the first start builds what it needs from your existing orders — but it is a change to the most load-bearing read in the platform, so it is listed here rather than left as a footnote. It is on by default because at any real size the previous method was the riskier one: it scanned every node once per market at every start, and past 50,000 orders on a market it stopped loading and dropped an arbitrary set of them. Below 25,000 orders on one side of a market the two return provably identical orders.
The server does not take that on trust. It compares a sample of markets against your actual orders before reading one from the new index, falls back to the previous method for the life of that process if they disagree, and keeps re-checking one market an hour. Your orders are not at stake in that fallback — the orders table is the source of truth and is what it reverts to reading. ECO_BOOK_SOURCE="legacy" keeps the previous method permanently; see Ecosystem order loading in Update Instructions.
Recent Trades for ecosystem markets is now kept for 30 days by default. It was kept forever, in a table with one entry per market and no limit, which is how a database gets slow in a way no single query explains. Nothing auditable changes: the permanent record of every fill is on the order itself and price history is in the candles. ECO_TRADE_TAPE_TTL_DAYS=0 restores unlimited retention. Trades recorded before this update stored only their buy side, so on a quiet market the list can look shorter until new trades arrive.
One appearance change is deliberate and visible: decorative per-section and per-addon colours now resolve to the accent chosen in Site Design, so a number of screens change colour on update. Those are precisely the screens no appearance setting could reach before.
And three things to know before you enable the new features.
Enforce KYC Feature Access starts refusing requests based on switches that, until now, decided nothing — so nobody has had a reason to curate them, and the defaults were applied when each level was created rather than chosen. Before turning it on:
- Open every level in Admin → CRM → KYC → Levels and review its feature list. The features footer summarises each one, including how many are enabled above their recommended level.
- Check the doors your existing customers use daily — withdrawals, deposits, transfers, trading — are enabled on the lowest level those customers hold. A user whose level omits a feature is refused that action the moment the setting is on.
- Confirm the four features that already enforced something are set the way you want: API Access, Payment Gateway Usage, NFT Purchases and Become a Trader. Until the setting is on these keep their previous rule, so this is the switch that changes their behaviour.
Changing the setting requires a Super Admin account. It needs no seeder or migration.
The eleven new features arrive disabled on levels that already exist, so no addon becomes more accessible than it is today. Enable the ones you want on each level before turning enforcement on.
Verify 2FA on Every Withdrawal adds a required step to withdrawals. The bundled web app handles it, but any custom or mobile client must be updated to verify a code first.
Geographic restrictions ship with enforcement off, no countries listed and no IP lookups configured — nothing is blocked. Before switching them on:
- Add your office and monitoring addresses to the always-allowed IP list. It is checked before every other rule and is the escape hatch if a rule goes wrong.
- Set
TRUST_PROXY=truein the backend environment if the platform sits behind nginx, Cloudflare or any reverse proxy. Without it every visitor appears to come from the proxy's own address. The policy screen warns you when it is missing. - Use the rule tester to confirm a restricted country is refused and your own is not.
Which countries you must restrict, and the wording of the notice shown to them, are decisions for your legal counsel — the platform enforces and records whatever you configure, and ships restricting nobody.
Added
Geographic Access Restrictions
A new compliance suite under Admin → System → Compliance that restricts platform access by country and records every decision as evidence. Off by default — an existing install behaves exactly as before until an administrator lists countries and turns enforcement on.
Three screens:
- Geo Restrictions: the country list. Each rule records its legal basis and citation, and can be staged inactive or scheduled to start and stop on given dates, so a compliance deadline can be prepared in advance rather than flipped by hand at midnight.
- Geo Policy: how restrictions are enforced — detection sources, VPN handling, exceptions, the visitor notice and audit retention.
- Geo Access Log: a read-only record of every refusal and bypass, with CSV export for auditors and counsel.
What can be restricted:
- Blocklist Or Allowlist: allow everyone except the countries you list, or block everyone except them.
- Whole Platform Or Single Activities: a rule can lock a country out entirely, or refuse only chosen activities — registration, sign-in, trading, deposits, withdrawals, identity verification, P2P and investments.
- Permits Override Restrictions: an explicit permit rule carves a country out of a wider restriction without deleting the underlying rule.
- Bulk Import: paste a list of countries in any form —
US,USAorUnited States— and each is reported as created, already present or unrecognised. Nothing existing is overwritten.
How a visitor's country is determined, strongest signal first:
- Verified Identity: the country on an approved KYC application. A VPN cannot change it, and a customer travelling abroad is not wrongly blocked.
- CDN Geo Headers: Cloudflare, CloudFront, Vercel, Fastly, App Engine and nginx GeoIP. Free, instant, and set by infrastructure the visitor cannot reach past.
- IP Geolocation: an optional provider (ip-api.com, ipinfo.io, ipapi.co) for deployments with no geo-aware edge, with results cached so a busy site makes very few calls.
- Profile Country: self-declared and freely editable, so it is off by default.
- VPN, Proxy And Tor: optionally refused outright.
Existing customers:
- Wind-Down Carve-Out: on by default. A restricted country's existing users can still sign in, complete verification, contact support and withdraw their balance — but cannot register, deposit or take on new exposure. A country becoming restricted is the operator's policy change, not the customer's wrongdoing, and trapping their funds behind it creates a larger problem than it solves. It can be turned off for a hard lockout where an obligation demands one.
The audit trail:
- Every Refusal Recorded: address, resolved country, which signal determined it, the page, the reason and the user where known.
- Repeats Collapsed: identical decisions from the same address are counted on one entry, so a blocked crawler cannot flood the table.
- Read-Only: nothing in the admin panel can add to, edit or delete individual entries. Retention is applied nightly and configurable; entries from the last 7 days can never be purged, so a mistyped date cannot erase an incident under investigation.
- Rules Are Durable Too: retiring a country rule archives it and permanent deletion is refused, so the period a jurisdiction was restricted stays provable.
Safety valves, because a geographic control that can lock out its own operator is worse than none:
- Always-Allowed IP List: checked before every other rule, IPv4, IPv6 and CIDR ranges. Add your office and monitoring addresses before enabling enforcement.
- Administrators Bypass: on by default, so restricting your own country cannot lock you out of the panel that would undo it.
- Exempt Pages: the restriction notice, health checks, licence activation and the geo admin screens themselves are never blocked.
- Super Admin To Disable: switching enforcement off removes a legal control platform-wide, so it needs more than the day-to-day edit permission.
- Rule Tester: dry-run the live policy against a hypothetical country, IP and activity before relying on it, or to reproduce a decision a customer is disputing. Nothing is enforced or logged.
Coverage and behaviour:
- Pages, API And Live Connections: all three. Live connections never pass through the normal request chain, so without explicit handling a refused visitor could have kept using the platform over an open connection.
- Configuration Warnings: the admin screens flag the states that silently do nothing — rules saved while enforcement is off, allowlist mode with no permitted countries, and a reverse proxy without
TRUST_PROXY, which makes every visitor appear to come from the proxy. - Visitor Notice: restricted visitors get a page naming the detected country and, if configured, a contact address — not a blank screen. The notice wording is yours to write.
Withdrawal Two-Factor Authentication
Two new admin controls under Settings → Security → Withdrawal Security, both off by default:
- Require 2FA to Withdraw: blocks withdrawals for users without an accepted second factor enabled.
- Verify 2FA on Every Withdrawal: prompts for a fresh one-time code on each submission, proving the request comes from the account holder and not a hijacked session.
- Accepted Methods: three further switches decide whether authenticator app, email and/or SMS satisfy the requirement.
- Super Admin Protected: all five settings require a Super Admin to change, same as withdrawal auto-approval.
- Enforced On All Money-Out Doors: fiat withdrawals, spot withdrawals and ecosystem withdrawals, all checked before any balance is touched.
What users see:
- Blocked Before They Start: a user without an accepted second factor gets a banner at the top of the withdrawal page naming the methods that qualify, plus a button through to their profile security settings. The Withdraw button stays disabled and its tooltip carries the same reason, so nobody fills in a full form only to be refused on submit.
- Wrong Method Named Explicitly: a user who has 2FA on but via a method you do not accept is told which method they hold and which they need, rather than a generic refusal.
- Verification Prompt On Submit: with per-withdrawal verification on, pressing Withdraw opens a six-digit code prompt instead of submitting. The withdrawal continues on its own the moment the code is accepted — no re-entering the form. Email and SMS users get a resend button.
- Security Activity Entry: each passed verification is recorded in the user's security activity feed.
Behaviour details:
- Single-Use Codes: a verified code authorises exactly one withdrawal. A captured code cannot approve a second transfer.
- Login Codes Cannot Approve Withdrawals: the two are kept separate, so neither can be presented in place of the other.
- Recovery Codes Accepted: same as login, so a lost device does not lock a user out of their funds.
- No Code Sent For Authenticator Apps: the prompt opens immediately since the code is already on the user's device.
- Fails Open When Unsatisfiable: if platform-wide 2FA is off, or every accepted method is disabled, the requirement is reported inactive and withdrawals are not blocked. The misconfiguration is logged instead of blocking every withdrawal. SMS also counts as unavailable when Twilio is not configured.
- Rate Limited: sending a code is capped at 5 per 15 minutes (SMS costs money); checking a code is 15 per 10 minutes, with a separate per-user limit of 5 attempts per 10 minutes so rotating addresses buy no extra guesses.
KYC Feature Enforcement
The per-feature switches in Admin → CRM → KYC → Levels now decide what a user may actually do. A new setting, Settings → Features → Verification → Enforce KYC Feature Access, turns that enforcement on. It is off by default, and while it is off nothing about an existing install changes.
- One Gate, Every Addon: a single shared check replaces the four different mechanisms that existed before — a hard-coded level number, "holds any approved application", each addon's own hand-rolled sequence, and, for most features, nothing at all. Which levels carry a feature is now answered in one place: the level builder.
- Super Admin To Change: switching enforcement on can refuse actions to every user whose level omits a feature, and switching it off removes a compliance control platform-wide. Like withdrawal auto-approval and the two-factor policy, it needs more than the day-to-day settings permission.
- Enforced On Every Money And Action Door: order placement across spot, ecosystem, binary and futures; deposits, withdrawals and internal transfers; forex funding, withdrawal and investment; marketplace orders and checkout; token sale participation and creation; staking entry, claims and withdrawals; P2P offer creation, editing and trading; NFT minting, listing, buying, transfer and contract deployment; merchant onboarding; copy-trading allocation and leader applications; affiliate reward claims; API key issuance; support tickets, FAQ questions, author applications and blog comments.
- Cumulative Across Levels: a user's features are the union of every approved level they hold, and an approved application whose level was deleted no longer demotes them to level 0.
- Browsing Stays Open: the "view" features are deliberately not enforced on public pages. Locking a customer out of a page they cannot yet act on tells them nothing useful, and several of those pages serve visitors with no account at all. The two exceptions are the trading bot and Hummingbot dashboards, which are private surfaces rather than public catalogues.
- Practice Modes Never Gated: demo forex accounts, practice binary orders and paper-mode trading bots stay open regardless. They are how a user decides whether to verify in the first place.
Eleven new features, covering addons that previously had no KYC representation:
| Feature | Category | Default level |
|---|---|---|
| Trading Bot Access | Trading Bot | 1 |
| Live Trading Bot Execution | Trading Bot | 2 |
| Strategy Marketplace Purchases | Trading Bot | 2 |
| Become a Strategy Seller | Trading Bot | 3 |
| Hummingbot Connector Access | Hummingbot | 1 |
| AI Investment Participation | Investment | 2 |
| Forex Live Account Opening | Trading | 2 |
| Affiliate Reward Payouts | P2P | 3 |
| Staking Withdrawals | Staking | 2 |
| NFT Transfers | NFT | 3 |
| NFT Contract Deployment | NFT | 4 |
Two new categories — Trading Bot and Hummingbot — appear in the level builder's sidebar, bringing the total to 48 features across 15 categories.
Notes on three of them:
- Staking Withdrawals should never be set above Staking Participation. Staked principal is locked until its end date and this is the only exit, so a higher bar would strand funds users were already permitted to stake. The switch's description says so in the builder.
- Live Trading Bot Execution is checked at bot creation and at start and resume — so a bot cannot be created in paper mode and later started against real balances.
- NFT Contract Deployment is separated from NFT Creation because the platform-funded deployment path spends the master wallet's gas. It is an irreversible spend of platform funds triggered by a user, which is a different risk from minting into an existing collection.
Appearance & Design: Site Design, Menus And Footer
How the public site looks used to be a code change. It is now four screens under Admin → Appearance & Design, none of which needs a developer, a rebuild or a redeploy — the site changes when you save. The group has also moved under Content, alongside the blog, media library and sliders, rather than sitting under System administration, because it is content work and it was two clicks deeper than the things it is done next to.
Site Design — the whole visual identity in one place, with a live preview of the real site beside the controls. Seven panels, ordered by how much of the site each one moves:
- Presets: a complete shipped look per preset, each shown as its own four swatches and its own corner radius before you apply it. Obsidian is what the platform ships with and is always one click away.
- Palette Studio: generate a coherent palette from a seed colour instead of picking thirty colours by hand, then adjust the seeds until it is yours.
- Colours: every colour the site uses, individually, each with its own reset.
- Type & Shape: typeface, corner radius, elevation, motion and icon shape.
- Accessibility: contrast recalculated as you edit, for every text-on-background pair in the palette, plus how far apart the chart series stay for viewers with the two common forms of colour blindness. It reports rather than blocks — an operator can still choose a low-contrast accent for a brand reason, the difference being that they now do it knowingly, and marginal pairs are flagged with a note to carry a second cue rather than relying on the colour alone.
- Navbar Layout: Classic (the header this platform has always had, and the default — an install that changes nothing gets exactly the bar it has today), Centered, Stacked and Minimal. The picker says what each one gives up — Centered has no search trigger, Minimal has no desktop navigation row at all — on the card next to a schematic of the layout, before the choice, rather than a week later when a control someone used daily turns out to be missing. Each layout declares its own height, so every page's top spacing moves with it and choosing the taller bar does not push the first line of forty page templates underneath it. The choice is already applied when the page paints, and an unrecognised one falls back to Classic so a theme exported from a newer build cannot leave an install with no header.
- Footer Layout: Columns (the footer this platform already has, and the default), Compact (a single row of logo, inline links and socials, for sites with few pages) and Centered. An unrecognised choice falls back to Columns.
Menus — rename, reorder, hide or add items in any menu on the site: the admin sidebar, the public navigation and every extension's own menu, 34 menus in one searchable list, at any depth, plus custom links of your own. Menu wording used to be fixed, so an operator who calls them Vaults rather than Wallets, or who wants one entry out of the way, had no way to say so. The filter matches an extension's own name as well as its display title.
- A Hidden Item Stays On Screen While You Edit: hidden rows are struck through with a Show button rather than vanishing from the editor. An item you cannot see is an item you cannot bring back.
- Extensions You Have Not Installed Are Still Listed: an override for an addon that is not running simply does nothing until it is — which is better than a menu you cannot find because the addon happened to be disabled the day you looked for it.
- Admin Entries Stay Admin: overrides for admin-only entries are never served to the public site, so renaming something in the sidebar cannot leak it into the public navigation.
Footer — everything the footer says. Its layout is chosen next door in Site Design, so there is one preview and one answer rather than two.
- Site Name And Tagline: these came from build-time environment variables, fixed when the app is built, which on a hosted install put the two most visible pieces of text on every page permanently out of reach. They are fields now.
- Copyright Line: free text, with {year} and {siteName} filled in for you, so it does not need an edit every January.
- Social Links: add, reorder and remove — and the list can now be emptied completely. The old footer always kept at least one entry, so "we have no social accounts" was not expressible.
- Link Columns: the footer's column headings and the links under them.
- Every Field Is Optional: each one shows the current value as its placeholder, so blank means "keep what you have" and you can always see what blank will produce.
Default Pages is rebuilt on the same shell, with the live page as its workspace — see Changed.
How it all saves:
- Only Your Changes Are Stored: a colour you never touched is not written down, and neither is a menu item you never edited. Later improvements to the shipped look and to the shipped menus still reach your site, instead of being frozen at whatever the values happened to be the day you first opened the screen. A single control puts any item back.
- Discard returns to the last saved state and Reset colours to shipped returns to the palette the platform ships. Both are deliberately separate from Save, so neither is reachable by a mis-click mid-edit.
- Import And Export write the theme out as a file, so a look can be built on a staging install and moved to the live one.
- The Preview Is The Site, not a drawing of it. What you approve is what visitors get.
- Two Administrators At Once: each screen writes only its own part, and a save that would overwrite a change someone else made in the meantime is refused with a clear conflict message rather than one editor silently winning.
- It Cannot Break The Site: the public side reads the saved chrome on every page, so it falls back to the shipped defaults rather than failing the page if anything is wrong with what is stored.
- Its Own Permission: a single new design permission gates all three screens, so a brand or marketing role can be given control of the site's look without also being given the settings page. Page Builder and Default Pages keep the general admin permission they already had. Like the other new permissions in this release it only becomes assignable once the permission seeder has run — see Update Instructions — and a Super Admin reaches the screens either way.
- Storage is created automatically on the next backend start, and the old Appearance screen redirects to Site Design.
Market News
A news feed for the trading terminal, written by your own desk and optionally topped up from a provider. Until now that feed carried only what the provider supplied, and nothing could be corrected or added.
- Where Operators Write: Admin → System → Communication Tools → Market News — headline, body, category, publication time, source link, thumbnail, and the assets a story is tagged against (up to 25). A story written here is your own desk commentary, badged Desk in the terminal.
- Where Traders Read: the terminal's news feed, and a Market News button in the Trading Pro header toolbar that opens the full view over the workspace. It opens on everything and can be narrowed to the market currently on screen. The forex terminal shows the same feed where that addon is installed.
- Matched On The Base Asset: a narrowed feed keeps the stories that name the market's base asset — by ticker or by name — in their headline or summary, plus anything an operator tagged with that symbol under Related Symbols. A desk note therefore reaches a specific market on its tag alone, without having to name the asset in its prose.
- An Empty Feed Is An Answer: wire copy is filed against the majors, so a scoped feed can legitimately come back empty. A market nothing has been written about shows nothing, with a link back to the full feed, rather than falling back to unrelated stories that merely look relevant.
- Never Overwritten: the provider sync only ever touches the stories it fetched itself, so an item written in the admin panel is never rewritten, hidden or removed by it. Edits to a provider story stick; deleting one outright brings it back on the next sync, so hiding it is the durable choice.
- Provider Sync: a new scheduled job pulls headlines every 15 minutes and prunes provider stories older than 30 days. Yours are kept indefinitely.
- Optional Provider: set
APP_FINNHUB_API_KEYin the backend environment to turn the provider feed on. Without it the job says so once and does nothing else — a missing key is a configuration state, not an incident — and operator-written stories serve the feed on their own. - Text Only, Safe Links: headlines and bodies are stored as plain text, publisher markup in a provider's summary is converted to prose on the way in, and a source or image address that is not an ordinary web link is refused, so nothing in the feed can carry markup into the terminal.
- Public: news is market data, not account data, so the feed is readable without signing in.
- Off Switch: Market News in the trading settings, on by default.
- Permissions And Storage: five new permissions — view, access, create, edit and delete. A Super Admin can open the screen immediately; no other role can be given access until they are seeded (see Update Instructions). Storage is created automatically on the next backend start.
Change Your Own Password, And Set A First One
A signed-in user can now rotate their password from their profile security settings, without going through "forgot password" and their inbox. And an account created through Google or a connected wallet — which has no password at all — can request an emailed one-time link and set one. Neither path existed before.
- The Current Password Is Re-Proven: a live session is not proof that the person holding it owns the credential. Re-asking for the existing password is what stops a borrowed session — an unattended browser, a captured token — from being turned into permanent ownership of the account.
- A Second Factor Is Demanded When One Is Enrolled: authenticator, email or SMS, with recovery codes accepted so a lost device does not block the change. Email and SMS users get the code sent from the dialog itself.
- Every Other Device Is Signed Out: a password change is the user's remedy for "someone else may be in my account", and it is worthless if the intruder's session survives it. The change ends every other session the account holds and reports how many were revoked, while keeping the device doing the rotation signed in.
- Sessions Only: an API key cannot rewrite the password of the account it belongs to.
- Rate Limited: five attempts per 15 minutes on the change itself, and five per 10 minutes on the code.
- The First-Password Link Proves Ownership Against The Inbox, not the session, because for one of these accounts there is no existing credential to re-prove against — and a hijacked social sign-in could otherwise mint a permanent password and lock the real owner out entirely. Completing the link signs the account out everywhere, which is correct for a first credential.
This is also the path an account with no password needs before it can turn its second factor off (see Fixed → Security), which until now was described but did not exist.
Currency Icons
A new screen under Admin → System → System Monitoring → Currency Icons that finds assets with no icon and fetches them. Previously this needed shell access.
- What It Reports: every currency across the platform's asset lists that has no icon file, broken down by asset class — plus icons that exist but are byte-for-byte copies of the generic placeholder, which look present in a list and render blank on the page.
- One Button: the sync fetches each missing icon as a 64×64 image, trying several public sources in turn, and reports which source supplied each one and which symbols could not be found anywhere.
- Enabled Only: most imported currencies are created disabled, so the full list is largely latent. The report can be narrowed to the assets a customer can actually see today.
- Read-Only Until You Press It: the report itself writes nothing and makes no outbound calls.
Fiat Rates From Several Providers
Fiat conversion rates were taken from one feed, and when it stopped answering the platform failed quietly. They now come from a registry of providers, in the order you set with APP_FIAT_RATES_PROVIDERS.
- One Source Going Down Is Degraded Coverage, Not An Outage: a provider that fails, or that answers with a complete rate table it stopped updating days ago, is set aside and the rest carry the conversion.
- Mirrors Do Not Vote Twice: each source records which upstream family it belongs to, so two feeds republishing the same central-bank data are not counted as independent confirmation of each other. Only cross-family agreement corroborates a rate, and a currency where the families disagree beyond a threshold is reported.
- Freshness Is Part Of The Answer: every source declares how long its data stays current, because a feed that has stopped updating still answers successfully and looks healthy.
- Reported On The System Health Screen: the row changes from OpenExchangeRates to Fiat Rates and now probes every configured provider at once, counting the rates each returns and checking how current they are. Up when every provider answers with current data, Degraded when some are down or any feed has stopped updating, and Down only when none answer. Providers that are not configured are named in the message, so the row explains its own coverage. Previously one source was checked on the strength of its API key alone, which is how the fallback feed became unreachable without anyone noticing.
One Landing Kit, And Artwork That Follows Your Palette
The core landing page and the addon landing pages — staking, ICO, NFT, P2P, forex, e-commerce, payment gateway, affiliate, copy trading, Hummingbot, investment and trading bot — are rebuilt on one set of layout blocks, with purpose-drawn illustrations replacing the previous mix of floating orbs, particle fields and grid patterns.
- Drawn From Your Colours: every fill in every illustration resolves to a colour from the site's palette, so the artwork follows whatever an operator picks in Site Design instead of sitting at a fixed hue beside it.
- Correct In Light Mode: the old decoration was drawn against a dark background and washed out on a light one.
- One Vocabulary: the same section headers, feature grids, stat rows and calls to action across every landing page, so twelve addons read as parts of one product rather than twelve separately styled sites.
- Fixed an illustrated bar labelled 74% that came to rest at 53% for anyone with the reduced-motion setting on — the number and the drawing disagreed.
A Shared Chart Kit
Charts in the admin tables and across roughly twenty dashboards now come from one shared kit instead of three separate private colour maps.
- Charts Follow The Palette: series colours are drawn from the site's own colours rather than fixed values, so they repaint when an operator changes the theme instead of staying on last year's brand.
- Six Colours, Assigned By Position: the categorical ramp is a fixed six-step sequence checked to stay distinguishable for colour-blind viewers, assigned in order rather than cycled, so the same series is the same colour on every chart that draws it.
- Price Colours And Status Colours Stay Reserved: up and down keep the platform's price colours, and red, amber and green stay meaning failure, warning and success rather than being spent on the third and fourth series of a bar chart.
- Changed the summary tiles and charts at the top of every admin table, which were a separate visual system with their own card, value styling, chart colours and loading placeholders, onto the same stat card and chart frame used everywhere else — so the numbers at the top of a table look like the numbers on a dashboard and both follow the palette.
Investment History
Investment → History lists every investment the account has ever made — amount, profit and status — a page at a time. The investment dashboard shows the five most recent investments, and the View History control beside them had nothing behind it: no link, no action, nothing happened when it was pressed. A sixth investment could not be reached from anywhere in the app.
- Fixed View History, which now leads to the new page, and appears only once there is more history than the dashboard already shows.
Binary AI Engine Per-Order Exposure Limit
- Enforced At Placement: on a market steered by a Binary AI Engine, a single stake can no longer exceed the per-order exposure cap the operator configured. The placement is refused with a message naming the limit and the currency. Until now that cap was configuration the engine displayed and nothing checked when an order arrived.
- Applies to real orders on markets an active engine manages. Practice orders are unaffected, and the check does nothing on installs without the Binary AI Engine addon or on markets no engine is steering.
Admin Settings Screens: Multi-Line Fields, Category Labels And A Working Back Button
- Multi-line Fields: a setting that holds a list or a paragraph — an IP allow-list one entry per line, a customer-facing notice — now gets a real multi-line box instead of a single-line field it does not fit in. The ICO and geographic-restriction settings screens use it already.
- Category And Requirement Beside The Label: each setting shows which category it belongs to and whether it needs an addon installed, so a switch that will do nothing on this install says so where you read it.
- Page-level Notices: a settings page can carry a banner above the form for warnings that apply to everything on it.
- Fixed the Back control on settings pages, which was a button nested inside a link. Most of the control was dead to clicks and it could not be reached from the keyboard at all. It is one properly labelled control now.
- Removed the decorative animated wash behind the settings header, whose colour was never a valid colour — it produced nothing while re-running every 15 seconds, forever.
Operator Maintenance Tools
Three more tools ship alongside the ones described in Update Instructions. Each reports what it would do and changes nothing until told to, each is safe to run more than once, and none of them touches a balance.
- Email queue purge: drains the two outbound email queues. A backlog survives a backend restart and a code change, and every retry is a real login against your mail account — enough of them and the provider throttles it, which blocks mail to genuine customers until the throttle lifts. Report-only by default, because a waiting message may be somebody's password reset.
- News text repair: strips publisher markup out of news stories already stored. A published story is never rewritten, so fixing the conversion only fixes stories arriving from now on — anything already stored keeps its tags until this is run.
- Fiat rate diagnostics: two read-only checks. One confirms conversion runs the right way up against the currencies this install has enabled — the direction that, when inverted, quoted 1 USDT as a 1365th of a naira instead of 1365 of them. The other reports which rate providers are reachable, how much each adds beyond the others, which enabled currencies nothing can price, and which ones the sources disagree about.
Filling in missing currency icons was the fourth of these and now has a screen of its own; see Currency Icons above.
Algo Trading Tab In The Trade Terminal
- Third Order-Form Tab: the trade page and Pro workspace now host an Algo tab, plus a Bots tab in the orders panel, when the Algo Trading Bots extension is installed. See the Algo Trading Bots v6.0.0 patch notes for the feature itself.
Trading Pro: Features That Were Built But Never Shown
- Command palette (Ctrl/Cmd+K). The shortcut was already wired up — but nothing ever appeared when you pressed it. The palette now opens: market search across spot, ecosystem and futures with live prices, favourites, recents and full keyboard navigation.
- Positions panel. The layout showed a placeholder that said "No open positions" unconditionally, so futures traders on Pro had no positions view even when they held positions. The real panel is now there.
- Trading analytics. A complete analytics panel existed with placeholder data and no way to open it. It is now reachable from the header toolbar and shows real trade history — equity curve, win rate, performance metrics, per-pair distribution and a 24-hour activity heatmap.
- Panel resizing. Side panels can now be dragged to width, and the existing reset action clears it.
- Mobile swipe. Swiping between mobile tabs works, and is suppressed on the chart tab so it does not fight the chart's own pan and zoom.
Binary: Error Recovery And Limit Orders
- Error recovery. The binary trading page — a live-money surface — had none. Any error in the chart, order panel or positions list blanked the entire page. A failure is now contained to the component that failed, with a retry, so the rest of the page keeps trading.
- Mobile positions. Swiping to the positions tab on mobile showed a blank screen. The panel now appears, with trade history.
- Limit orders. The limit-order engine was already live and able to place real orders, but had no interface. A Limits tab in trading settings exposes the entry form and the pending list. These triggers run in the browser and do not survive a page reload.
Page Builder Inline Text Editing
Heading and text elements are editable directly on the canvas in edit mode. Changes are grouped, so typing a sentence costs one undo step instead of one per keystroke.
NFT Public Minting Control
A collection owner can enable public minting from the collection edit page. Ownership is checked against the contract before the transaction is sent, so a non-owner gets an explanation instead of a failed transaction and a wasted gas fee.
One Blog Post Editor, As A Full Page
The admin panel and the author dashboard now use the same editor, and it takes the whole viewport with no site header, nav or footer, the way an editor should.
The layout is one screen rather than a stack or a set of tabs: the headline, URL and summary at the top of the wide column, the rich-text body under it, and every decision about the post — publish state, author, category, tags, cover image — in a sidebar beside it. A fixed bar carries the exit, the post's title, its publish state, Preview and Save, so Save is reachable from anywhere in a long article instead of only from the bottom of the page.
- Preview renders in the app, with the same styling as the published article. It used to open a bare browser window with no stylesheet, so it could only ever approximate the real page and could not follow your palette.
- Tags are picked from an existing list or typed; a name that does not exist yet is created on save. Admins can now set them at all (see Fixed).
- Unsaved work is guarded both on browser navigation and on the editor's own exit control.
- Leaving by the back control, pressing Enter in the title field and pressing Save all behave the way they would on an ordinary form.
Admin create and edit are now real pages of their own, each with its own permission, so they can be granted separately from the post list.
Two smaller things had to follow the site chrome out. The floating live-chat bubble is now suppressed on the author editor, where it would have sat over the corner of a writing surface. And the mobile navigation drawer remembers whether it is open — so entering the editor with it open left it open, and the next ordinary page briefly flashed the drawer. Both are now closed on the way in.
Binary Verification Tooling
A verification tool that checks an actual installation, not a test setup. It confirms the update applied, flags markets with no usable price source, checks that no settled order contradicts its own recorded prices, that every win paid the advertised percentage, and that the reported admin profit matches the true house result. It writes nothing and is safe to run on production. See Update Instructions.
Smaller additions
- Reserved test domains are never mailed. Addresses ending in
.invalid,.test,.exampleand.localhostcan never receive mail, and seed and test data are full of them. Nothing is attempted for those addresses at all. - Forex introducing-broker rebates. Two new affiliate reward conditions are seeded for the native forex dealing desk: Forex IB Commission Share, a partner's share of the commission a referred user pays on each forex trade, and Forex IB Volume Rebate, a per-lot rebate on referred forex volume. Live accounts only, and both arrive switched off — they pay real money, and an update that quietly starts a payout programme the operator never opted into is not an update anyone wants. Turn them on in Admin → Affiliate → Conditions.
Environment Variables
SUPERADMIN_EMAIL/SUPERADMIN_PASSWORD: set the first administrator's credentials before the initial seed instead of using the published default.APP_FINNHUB_API_KEY: turns on the Market News provider feed. Without it the feed runs entirely on what your own desk writes.APP_FIAT_RATES_PROVIDERS: which fiat rate sources to use, and in what order of preference.MAIL_DISABLED: set it and the platform stops attempting any delivery. Exercising flows that send notifications no longer generates mail — and, more to the point, no longer generates a burst of failed sign-ins against your production mail account, which is what gets a provider to throttle it and take your customers' mail down with it.CRON_MODE: leave it unset. The scheduler runs in its own process by default and both halves of that are set inproduction.config.js— see The scheduler now runs in its own process above. The single supported value isinline, which puts the scheduler back inside the API process and removes thecronapp; anything else is not a deployment shape this platform supports.ECO_BOOK_SOURCE: leave it unset. The matching engine reads the new book-ordered order index by default and verifies it against your orders as it runs — see Ecosystem open orders no longer scanned at boot below. The single value that turns it off islegacy, which restores the previous method; anything else is treated as the default and logged.ECO_BOOK_WINDOW_PER_SIDE: how many orders a side the matching engine keeps in memory per market, default 25,000. Does nothing on a market smaller than that. Orders outside the window are not lost — they rest in the database and are loaded as the price reaches them. Raise it to hold more of a deep book in memory, lower it on a small server.ECO_TRADE_TAPE_TTL_DAYS: how long entries in the ecosystem Recent Trades list are kept, default 30. Set0to keep them indefinitely — but see Recent Trades no longer scans the database below, because that list is stored one entry per market with no limit. It is display data only; the permanent record of every fill is on the order itself.MINI_REDIS_DIR(removed): the Redis fallback it configured no longer exists — see Step 2 under Update Instructions. Delete it from.envif it is there;backend/storage/mini-redis/can be deleted too.
Changed
Source code obfuscation
- Reduced the obfuscated set outside the security module from 27 files to 4, all four of them licence-related: the server entry point, the licence check, the licence configuration and its page map. Those four carry licence enforcement inline or embed key material.
- Readable Now: the web framework, request and response layers, parsers, database bootstrap, background worker, API documentation generator and their shared utilities all ship as ordinary, readable JavaScript.
Blog admin navigation
Dashboard, Posts, Authors, Categories, Tags, Comments and Settings were seven top-level entries in one bar — more than any other admin section puts in a row. They are now four: Dashboard, Content (Posts, Categories, Tags), Community (Authors, Comments) and Settings. Nothing moved out of reach: the two groups open as dropdowns on desktop and as collapsible sections on mobile.
Image picker appearance
The image upload control predates the design system and carried the whole old vocabulary: layered shadows, a coloured glow, two gradients, a pulsing ring behind the icon, and a scale effect on drag-over that moved the drop target under the cursor mid-drag. It is now flat — a hairline drop zone, the standard tinted icon tile, and the drag state expressed as a border and a tint. Its sizes were corrected at the same time, which changes the height of the control on the screens named in Fixed.
The trade terminal and the binary page follow the site theme
Both live-money trading screens were painted in fixed dark colours. A customer using light mode got a black trading screen, and neither screen responded to a configured palette at all.
- Changed the whole trade terminal and the whole binary page onto the site's theme colours — roughly 1,900 lines of hardcoded grey, green, red, black and white across the two, down to two. Both now follow the active light or dark theme and any palette an operator has set.
- Changed the Pro workspace dark theme to the same palette as the binary trade page, which is what the app-wide dark theme already resolves to. Shared elements dropped into the page — sign-in controls, notification bell, popovers — now match instead of clashing. Muted text is brighter than the old value, which sat below readable.
- Fixed the depth chart and market sparklines using hardcoded greys and dark-mode green/red regardless of the active theme.
- Fixed animation warnings on hover tints. A few colour animations became plain transitions, because the animation the platform uses cannot interpolate the colour values the theme is written in.
- Fixed full-screen loading states using pure black instead of the theme background.
Per-section and per-addon identity colours are now one accent
Extension landing pages, admin table heroes, section washes and the header's mega menu each carried their own identity hue — teal, cyan, violet, amber, emerald — written as fixed colour values that no theme setting could reach. A site with a red brand still had a teal staking page and a violet investment section, and there was nothing anywhere to change it.
- Changed decorative colour now resolves to the site's accent and follows the palette chosen in Site Design. Status colour keeps its meaning — red for failure, amber for warning, green for success, blue for information — and neutral chrome becomes muted grey.
- Fixed the mega menu in particular, where each category had its own colour — trading green, investment violet, marketplace amber, business blue — which reads as four states rather than four sections, in shades chosen for dark mode with no light equivalent, so a selected category's label was barely legible on a white background. One accent for all four now, legible in both themes, and which entry counts as active is decided by the same rule the rest of the navigation uses rather than a second one that disagreed with it.
The default page editor's preview is now the real page
Editing the home page showed nine hand-drawn imitations of the page's bands, one per section — and every one of them had drifted from what visitors actually see. One drew a "Live Markets" panel the page does not have; another listed prices from a fixed sample that was never connected to anything. An operator was arranging a picture of their site and hoping it matched.
- Changed the workspace is the live page itself, updating as you type, and picking a section scrolls the page to it. Legal and content pages use the same shell with the rich-text body as the workspace.
- Changed the section list, the field controls and the preview are one screen now, instead of a full-screen editor reached from a grid of cards, and selecting a page shows the page itself rather than a card in a grid.
The error pages, rebuilt
The seven error pages — not found, not permitted, not signed in, session expired, too many requests, server error and maintenance — were seven copies of one file differing only in a picture, three lines of text and a width. Each shipped a light and a dark screenshot and chose between them in the browser after the page had mounted, so every dark-mode visitor saw the light illustration flash first, on top of an ink scale that ignored the palette entirely.
They now use the same building blocks as the rest of the product and draw their illustration from your palette, so an error looks like your site instead of like a different website — and fourteen screenshots stop being shipped. Colour carries meaning rather than decoration: the ones that mean we failed are treated as faults, and "not found" or "not permitted" as outcomes.
The same shell replaced four error screens that were each broken in a different way:
- Fixed the dashboard's error screen having no page padding at all, so it rendered underneath the fixed header with its message printed across the logo — and its "something went wrong" treatment not applying, leaving the alert and its button in their ordinary skin.
- Fixed the site-wide and token-sale error screens rendering as a fixed, blurred full-screen overlay, which reads as a half-drawn dialog rather than as a page.
- Fixed the site-wide "not found" screen falling through to an unstyled white document with a hairline rule, ignoring the palette completely.
The Pro order ticket gained value sizing, a balance slider and pre-flight checks
The Pro workspace order ticket could only be sized by quantity, offered four fixed percentage buttons, and sent every order to the exchange before finding out whether the market would accept it.
- Order Value Sizing: size an order by the value you want to spend and the quantity is derived from it — and kept in step when the limit price moves. Sizing by quantity works exactly as before.
- Balance Slider: a slider against the balance available replaces the fixed 25/50/75/100 buttons, so any fraction is reachable rather than four of them.
- Checked Before It Is Sent: the market's minimum and maximum quantity, its minimum order value, and a missing limit or stop price are all checked in the ticket, so those refusals appear as you type instead of after a round trip.
- Balance Is A Warning, Not A Block: exceeding the balance shown is flagged rather than preventing submission. The displayed balance is a snapshot and the server is the authority on funds, so it must never be the thing that stops a trade.
A fresh install seeded overlapping referral bonuses
Every seeded reward condition arrived switched on, and several conditions share one trigger — so a single piece of activity paid under all of them at once. One deposit on a new install paid Welcome Deposit Bonus at 10%, First Deposit Reward at 5% and Deposit Commission at 2%: 17% in total, on a screen that reads 10%. Fourteen of the fifteen trigger types were in that state, and the overlap is wider than it looks because several condition types resolve to the same underlying activity.
- Changed exactly one condition per trigger — the base commission named after the trigger itself — now seeds switched on. The rest are seeded switched off: present, described, and one switch away in Admin → Affiliate → Conditions, so a stacked bonus programme is something you build deliberately instead of inheriting it.
- Applies to new installs only. An existing install keeps whatever its operator has already configured; nothing is switched off on upgrade.
Screens and badges moved onto the theme's own palette
- Changed the wallet, deposit, withdraw, transfer and history screens, along with several admin screens, off their fixed colour values and onto the theme's palette, so they follow the active theme and any custom colours and hold their contrast in both light and dark. Amount cells now use the platform's up/down colours rather than its success and failure colours, so a credit is no longer visually identical to a Completed status badge sitting in the same row, and the finance page header spacing is derived from the header's real height rather than a fixed value.
- Changed the support ticket screens from a one-off animated statistics card carrying its own gradients to the panel's standard card, and ticket importance and status onto the same colour rules as the rest of the admin panel — so an Open ticket reads as waiting on you rather than as a green, resolved-looking chip. A handful of hardcoded status colours in the users table moved with them.
- Changed the media browser's full-screen preview and hover overlays, which were drawn in literal black and white and looked correct only against a dark background, onto the site's overlay colours. A stack of redundant nested tooltip wrappers went with them.
- Changed the Draft badge in Admin → Blog → Posts from warning amber to neutral. A draft is an inert state, not a problem, and amber is what the rest of the product uses for something that needs attention. Published is unchanged.
- Fixed the bulk-action confirmation on Admin → CRM → KYC → Levels blanking its own heading, body and button while it closed, and being dismissable while the action was still running.
Performance
The blog author index loaded its largest image last
- Fixed the author grid's avatars all being deferred. One of the first row is the largest thing the page draws — the measurement the page is judged on — so the browser was told to defer exactly the image it was waiting for.
Database update at startup
Startup ran a full rewrite of every table whenever anything changed — and it detected changes by comparing the text of all 205 definition files, so editing a comment counted as a schema change. On a database of 229 tables and about 2,800 columns that cost 3m 12s on every affected boot, which in development is every time a definition is touched.
Almost none of that work was necessary: it re-issued a change for every column of every table without ever checking what the database already had, then dropped and re-added all ~280 relationships.
- Changed change detection to look at the actual definitions rather than the file text. Comment and formatting edits no longer trigger anything.
- Changed the update to touch only what actually changed, in the right order.
- Result: changing one column now takes about 0.2 seconds instead of 3m 12s, and twenty changes across the database about 3.6 seconds. The cost is now proportional to what changed rather than to the size of the database. A boot with no changes is unaffected.
- Fixed relationship keys accumulating without limit — one table had reached its 26,724th — because they were being dropped and re-added anonymously on every boot.
Two escape hatches remain: DB_SYNC=always runs a full update of every table with nothing skipped, for a database changed outside the platform (a manual change, a restored dump); DB_SYNC_DIFF=off disables only the per-column skipping.
The first boot after updating adopts the existing state automatically and does not trigger a full update if the database was already current.
Extensions startup
The Extensions phase took 5.7s on the reference install, and it is the last thing standing between a restart and a server that can take traffic.
- Fixed a fixed two-second wait before the first database connection attempt, which on every healthy install was two seconds spent waiting to connect to a database already known to be listening. That whole step now costs 56ms, down from 2,059ms. A database that accepts connections but is not yet ready is still covered by the same retries as before.
- Changed the ecosystem, forex trading and Hummingbot subsystems to start together instead of one after another. Each already handled its own failures without stopping the boot, so the phase now finishes with the slowest of the three rather than with their sum.
- Changed the matching engine to load candle history alongside its order book reconciliation rather than queueing behind it. The steps that genuinely depend on each other keep their order.
The largest fixed cost is gone outright and the remainder overlaps instead of queueing, which brings the phase to roughly two seconds and takes about four seconds off total startup.
Ecosystem open orders no longer scanned at boot
The matching engine held every open order on the platform in memory for the life of the process, and rebuilt that from the database at every start. Three separate limits followed from it, and all three arrive long before a busy market would like them to:
- Memory. Roughly 1–2 KB an order, with no cap and nothing ever evicted. A million resting orders is 1–2 GB of memory occupied before anything trades — and it is the same memory the site serves requests from.
- Processor. The whole per-market list was re-sorted on every matching pass, several times a second, to find the best price. That is work proportional to the entire book to answer a question about its first entry.
- Database. Open orders are stored by customer, so "every open order on this market" named no partition and scanned the whole cluster — once per market, one after another, at every boot. On the reference incident this was the slowest part of startup, and it grew with the order table rather than with the number of markets.
The reason it had gone unfixed is worth stating plainly, because it is a genuine database restriction rather than an oversight: a Cassandra/ScyllaDB materialised view may promote at most one column that is not already part of the source table's key, and this question needs two (status and market). No view can answer it. The only real fix is a second table the write path maintains.
- Added
open_orders_by_market, ordered on disk by price then arrival time — which is exactly the order the matching engine wants them in. Every place that creates, fills, cancels, rolls back or deletes an order now maintains it in the same atomic write as the order itself, so the two cannot drift apart. - Changed the engine to keep only the orders nearest the market price in memory, per side, per market, and to load the next ones as price moves toward them. Orders outside that window are not lost and not forgotten — they are in the database, which is where the platform's record of them has always been. Memory now depends on how actively a market trades rather than on how many orders it has ever accumulated.
- Changed the per-pass sort to be skipped when the list is already in order, which — reading from a table stored in that order — it usually is.
- Result: loading a market's open orders is two direct reads instead of a cluster-wide scan, and holding them costs the same whether the book has ten thousand orders or ten million.
This is on by default and needs nothing from you. The first start after updating builds the index from your existing orders and records that it has done so; every start after that skips straight to it. Before reading a single order from it, the server compares a sample against your actual orders and falls back to the previous method if they disagree — see Ecosystem order loading in Update Instructions above, which is the only place the detail is written down. Setting ECO_BOOK_SOURCE="legacy" keeps the previous method permanently.
The ecosystem order book is now calculated, not fetched
Alongside the change above, the matching engine's remaining database work was reviewed. It was reading the aggregated order book back from the database at the top of every matching pass — several times a second, once per market — changing that copy as it filled orders, and writing it back.
That is the wrong way round. The orders it is about to match are the book; asking the database for them is asking it to restate what the server already knows. And because two things could be writing that same figure at once — a matching pass and an order being placed — each could lose the other's change. That is the mechanism behind every "cancelled order still shows in the order book" report, and the reason a repair sweep has to run every five minutes.
- Changed the engine to work out the book from the orders it holds, and read nothing. The displayed book is still stored — subscribers need it, and the AI market maker's own quotes live only there — but it is now something the engine publishes, never something it consults.
- Fixed a level going missing for a fill that certainly happened. The book was read once per pass, so an order placed a moment later could fill against a book that predated it. Worked out from the orders themselves, a filled order's price level cannot be absent — the order put it there.
- Removed the last whole-table read of the order book. The five-minute repair sweep re-broadcast its results by loading every price level on the platform to send a handful of markets. It now loads only the markets it actually repaired. This is the same pattern that caused the startup crash fixed earlier in this release; this caller was missed at the time.
- Removed a database view of open orders that nothing has ever read. A view is not free — the database maintains it by reading the original row before every change — so it was a tax on every order created, filled and cancelled, to keep a copy nobody looked at. It is dropped automatically on update.
- Removed ~230 lines of disabled code that cancelled customers' orders whenever a wallet lookup failed. It had been switched off by a comment asking future readers not to call it, which is not a safe place to leave something like that.
Recent Trades no longer scans the database
The recent-trades list was built from two places at once: a full scan of the orders table — run for every subscriber, on every refresh, on the market feed — and the dedicated trades table. Each order's trade history was then read out of stored text, flattened, sorted, de-duplicated against itself, and finally matched against the second source using a quarter-second guess at which rows described the same trade.
All of that existed to reconstruct the sell side of a trade that was never stored: only the buy side went into the trades table.
- Changed trades to record both sides when they happen. The list is now a single direct read of the trades table, already in the right order — no scanning, no text parsing, no guessing.
- Added a retention period to that table so it cannot grow without limit. It holds one entry per market and never removed anything, which is how a database gets slow in a way no single query explains. The default is 30 days and it can be changed with
ECO_TRADE_TAPE_TTL_DAYS(0keeps everything). Nothing auditable is affected: the permanent record of every fill stays on the order itself, and price history stays in the candles.
Extensions page
The extensions list is loaded by several admin screens on every visit and spent most of its ~1s cost on an outbound licence call plus one disk check per product.
- Improved licence detection to read the licence folder once instead of once per extension, blockchain and exchange provider.
- Added a 10-minute cache on the batch update check, with simultaneous visitors sharing one lookup. The cache clears automatically when an update is applied or a product is activated or deactivated.
Nine stylesheets folded into one, and a blocking web-font request removed
- Fixed every page load fetching JetBrains Mono from a third-party font server before it was allowed to finish painting. The font ships with the app now and that request is gone entirely — one fewer external dependency between a visitor and a rendered page, and one fewer thing that stalls the site when someone else's server is slow.
- Changed nine separate stylesheets into one.
- Fixed a screen-width rule used in about eighteen places across the notification and affiliate pages that had never been registered, so every one of those rules did nothing. Pairs written as "hide below this width / show above it" simply did not work, in either direction.
- Added reduced-motion handling, which the old stylesheets had none of.
Portfolio totals made one exchange lookup per coin held
The wallet dashboard's totals asked the exchange for a separate price for every currency in the account — and the slowest of those were the currencies the exchange does not list, because the lookup had to fail before it could return. One coin's timeout slowed the whole dashboard.
- Changed spot prices to come from the platform's own market list in a single read, with the live lookup used only where that cannot answer.
- Changed the two profit/loss reads to run together rather than one after the other.
Binary settlement held a wallet locked through an email send
Every binary settlement sent the balance update to the browser, the order-completed push, the settlement email and the in-app notification while still holding the customer's wallet exclusively locked. Where the background queue is unavailable the email falls back to sending directly, so the lock was held for the length of a mail-server handshake — ten at a time from the settlement sweep, enough to stall anything else touching those wallets.
- Fixed all four now happen after the settlement has been committed.
- Also: the balance pushed to the browser is now one that has actually been saved, rather than one that might still have been undone.
Backend development no longer restarts for a one-file edit
Editing a single backend file used to restart the whole server, which on a real install means the database update, chain hydration, licence setup and matching-engine startup all run again from the top.
- Changed the edited file is now swapped in place. Live connections are not dropped.
- Fixed a failed boot leaving nothing watching for the fix. A watcher now stays alive, so the next save is picked up rather than the process sitting wedged with nobody listening.
- Applies to development only. A production install is unaffected.
Removed
A full audit removed 370 files, about 54,600 lines, that nothing could reach: components superseded by the design-system migration, exact duplicates of files still live elsewhere, orphaned tests whose subject no longer exists, scaffolding, and 18 backup copies sitting beside live money screens. Nothing that was merely unused was removed — every candidate was read and classified, and everything that turned out to be a finished-but-unwired feature was connected instead (see Added).
- Fixed a version-control rule that silently excluded real source directories — including the Algo Trading Bots audit log, which had never been committed and was missing from a fresh install.
- Fixed a dependency being reachable only by chance, so a routine install could leave the build failing.
Fixed
Security
Any signed-in user could list every identity document on the platform
The media browser is named "your own media files", and the editor that uses it hides its folder sidebar because "users only see their own files". Neither was true. Nothing records who uploaded a file, so it filtered by folder alone — and it accepted any folder. Asking for the KYC folder returned the path of every identity document uploaded to the platform; the support, disputes and P2P folders returned other customers' ticket attachments and dispute evidence. Asking for nothing at all listed the entire library. Those files are stored under the public web root and served without a sign-in, so a path is the document: passports, ID photos and selfies were one request away for any registered account.
- Fixed with a list of folders a non-admin may browse (the blog and editor images the post editor actually needs). Anything not on that list is refused — a permit-list rather than a block-list, so a folder added later for something private is not exposed by default.
A password-reset link could delete the account instead
Every emailed one-time link was interchangeable with every other. A password-reset link, which sits in the account holder's inbox for an hour and is routinely forwarded, logged by mail scanners or pasted into a support chat, was also a valid account-deletion confirmation. In the other direction an account-deletion link was a valid password reset, which is a full takeover.
- Fixed each link now carries its purpose and each screen demands its own. Reset links already in flight keep working.
An account deletion request was emailed to whoever the caller named
The request took an email address, and answered 404 when no such account existed — telling any signed-in account whether an address is registered. When the address did exist it emailed a live one-click deletion link to that person's inbox, unasked and uncapped. The recipient never requested it and the sender was never told who they had mailed.
- Fixed it now always targets the caller's own account, and is rate-limited like every other door that sends mail.
Deleting an account did not sign it out
A deleted account's live sessions kept working — and kept renewing themselves — for the session's full fourteen days. Both deletion paths were affected: the self-service one on the profile page and the emailed confirmation.
- Fixed both now end every session the account holds, the same way a ban and a password reset do.
A second door switched two-factor authentication off without asking for anything
There are two ways to toggle the second factor. The one the profile page uses requires the account password to disable it — deliberately, so that a stolen session alone cannot strip the second factor. The other one required nothing but the session. The hardened door could simply be walked around: anyone holding a borrowed session — an unattended browser, a captured token — could remove the control that exists to make a stolen password insufficient.
- Fixed the second door now re-proves identity too, accepting either a current code from the enrolled method or the account password. Either, because each covers what the other cannot: an authenticator-app user may have no password at all (social or wallet sign-in), and a user who has lost their device still has one. An account with neither is told to set a password through "forgot password" first — a real path, rather than being stranded with a second factor it can never remove. Turning 2FA back on strengthens the account and needs no extra proof.
Anyone holding a user id could make the platform text that person
The "resend code" step is necessarily open to callers who have not finished logging in — and it accepted a bare user id. So anyone with a user's id could have the platform send that person a code on demand: a billable SMS to their phone or an email to their inbox, repeatedly, with no cap. It also confirmed that the id existed and had two-factor enabled.
- Fixed it now works only from the short-lived challenge the login step issues, and is rate-limited.
Cross-site request protection was skipped for most of every session
The access token lives 15 minutes and the session 14 days, so a browser spends nearly all of a session's life with an expired access token. On that path the server renewed the session and ran the request without checking the cross-site token — and that exact combination is what a malicious third-party site can cause a browser to send. Every state-changing action was reachable from another site while a signed-in user's access token happened to be expired: transfers, withdrawals, order placement, profile changes, API-key creation.
- Fixed the check now runs on that path too. Legitimate clients are unaffected.
A signed-out session kept working
Nothing tied a signed access token to its session. Signing out, "sign out other devices", a password reset and an administrator ban all deleted the session and left the token valid for the remainder of its 15 minutes. Revocation did not revoke.
- Fixed a token whose session no longer exists is refused. Live connections, which authenticate differently, keep working.
- Fixed the bulk user-status action not ending sessions when it banned or suspended an account, while the single-user editor next door did. Banning from the users table left the account working normally until its session expired.
- Fixed the Block User button on a customer's detail page, and the status control beside it, changing the account's label and nothing else. Sign-in refuses a blocked account — but a session that already exists never goes back through sign-in, and its life was extended on every request, so a banned or suspended customer kept trading and withdrawing indefinitely while the admin page displayed them as blocked. Both doors now end every session the account holds, for any status other than Active.
Sessions never actually expired
When a session's renewal token expired, the server issued a brand-new pair instead of refusing, and every renewal extended the session again. The configured session lifetime ended nothing; a session was immortal as long as it was used.
- Fixed an expired or malformed renewal ends the session and asks for a fresh sign-in.
One account's token could be paired with another account's session
- Fixed a defence that had never run. A leaked or guessed session id could be presented alongside a different account's access token.
Form contents were checked before the sign-in check
Any stranger could read a screen's field list out of its own error message — an admin role form with an empty body answered "Name is required.; Permissions is required." with no session at all. Across a thousand-plus screens that is a map of the platform that the sign-in gate exists to withhold, and the work was being done for callers about to be refused anyway.
- Fixed form contents are now checked after sign-in and permission. This also made a class of permission bug visible for the first time — a "your form is wrong" answer from an admin screen had been indistinguishable from "you are not allowed here".
API keys ignored every control that could take them away
An API key record carries a disabled flag, an expiry date and an IP allow-list, and none of them were read. Disabling a key did nothing. An expired key worked forever. The IP allow-list that the key screens accept, store and present as a security control was never enforced. A key acts with its owner's full authority, so each of those was a credential that could not be revoked.
- Fixed everywhere at once. Keys whose owner has been banned or suspended are also refused now, and "last used" is recorded — which nothing had ever written, so every key read "never used" in the admin list.
Eight admin screens were reachable by any signed-in user
An admin screen that named no permission was open to every registered account. Eight were: the AI investment duration options, the e-commerce category and product options, the blog author and category options, the NFT onboarding status, and both reading and writing the ICO settings limits.
- Fixed by giving each the permission its neighbours already declare.
- Added a structural check over the whole platform, because the mirror of this mistake is worse: a screen that names a permission but does not require a sign-in never reaches the check at all and is fully public. Every screen is now checked automatically, rather than sampled.
- Added the same check for live connections, with the default inverted: every one must require a session unless it is one of the two deliberately public ones (the market list and the price ticker). A connection that streams a user's own orders, deposits or tickets and forgets to require a session has no symptom for whoever wrote it — their own page works — so it is the easiest place for this to go unnoticed.
Ordinary administrators could read and act on Super Admin accounts
The users table hides Super Admin rows from lesser administrators — but every per-user screen underneath it was open. Any administrator holding the ordinary permission to view a user, who knew or guessed an account id, could open a Super Admin's profile, activity log and block history, and reset their second factor — which is enough to take the account over.
- Fixed all four now answer "User not found". Not "forbidden", because refusing by name would itself confirm which accounts are Super Admins. Super Admins still see each other, and every administrator still reaches their own profile.
The user export used the wrong permission and left a copy of everyone's data on the server
The Excel export on the admin users table declared the ordinary permission for viewing a user, while the CSV export beside it correctly declared the export permission. Any administrator who could look a single customer up could therefore pull the entire user list — names, email addresses, phone numbers, roles and verification status. It also wrote the spreadsheet to a fixed location on the server, where it stayed after the request finished, and reported that location back to the caller — disclosing the deployment layout along with the file.
- Fixed the export requires the export permission, the same as the CSV one, and now streams straight to the browser under a dated filename. Nothing is written to disk.
A person-to-person transfer could change asset and still be credited one-for-one
A transfer between two accounts is meant to move the same asset. The screen always sent matching currency and wallet type, so nothing ever exposed it — but the server never checked, and the ecosystem path credits the raw amount with no conversion at all. A hand-crafted request could send one unit of a worthless token and have the recipient credited one unit of a valuable one.
- Fixed a transfer to another user is refused outright when the destination currency or wallet type differs from the source.
An administrator rejecting a withdrawal could refund more than was taken
The amount on a pending record is editable in the same save that rejects it, and the refund used whatever figure was submitted rather than what the wallet was actually debited. Rejecting a withdrawal with an inflated figure credited the customer more than the withdrawal ever took — in practice a mistyped amount, but nothing stopped a deliberate one.
- Fixed a refund is capped at the amount recorded as debited when the request was made, and a warning is logged whenever the cap applies. Withdrawals and transfers both.
The live wallet transfer fee could be changed by any administrator with the settings permission
The list of settings reserved for a Super Admin guarded an old, unused name for the wallet transfer fee rather than the one the settings screen actually writes. The platform's live transfer fee — a financial control whose neighbours in that list are all Super-Admin-only — could be changed by any administrator holding the ordinary settings permission.
- Changed Wallet Transfer Fee to require a Super Admin, the same as withdrawal auto-approval and the two-factor withdrawal policy.
Payment gateway keys were predictable, and every webhook was signed with an empty key
Merchant secret keys, payment intent ids, refund ids and payout ids were drawn from a generator whose future output can be worked out from values the platform publishes. A merchant secret that can be predicted is not a secret. Separately, every webhook the platform has ever sent was signed with an empty string rather than the merchant's secret. Any merchant verifying the signature the way the integration guide describes rejected all of them, so webhook-driven order fulfilment could never have worked — and the ones who got it working had done so by not verifying at all.
- Fixed those values now come from a cryptographic random source, and outbound webhooks carry a real signature made with the merchant's own secret.
- Applies to the Payment Gateway extension.
A value from the browser went straight into the analytics database query
The analytics and summary-figure screens took a field name from the request and put it into the query they build without checking it against anything. The escaping applied to the accompanying value only doubled single quotes, which a trailing backslash slips straight past. Both the customer-facing analytics and the admin ones were affected.
- Fixed a field name is now matched against the fields the record actually has and quoted; values escape backslashes and null bytes; and an unrecognised field comes back as a clear message naming it rather than a server error.
The legal template wizard's preview renders the details you type
The wizard builds a privacy policy or terms document from the business details an operator enters, and the review step showed the result as formatted markup with those details dropped in unfiltered.
- Fixed the preview is sanitised before it is displayed, and the review step carries a warning that it is showing generated content. The generated templates themselves are unchanged.
Phone verification could be granted to yourself
The profile is a free-form record the account owner can write — and the SMS flow stored its pending verification state inside it: the code, its expiry and the number being verified. A profile update could therefore plant a code the caller already knew and then "verify" any phone number with no SMS ever sent.
- Fixed those values are now the server's and cannot be written by the user.
- Fixed the verified flag surviving a change of number, so verifying one number and then editing the field left an arbitrary unverified number marked verified — which is where SMS two-factor codes are sent.
Email verification codes could be brute-forced
The code is six digits, it was checked without reference to any account, there was no attempt limit, and redeeming one signs you in. Worse, every resend created an additional live code instead of replacing the previous one, so each one widened the space an attacker was guessing against.
- Fixed one live code per account — issuing a new one retires the old — plus a dedicated attempt limit of 10 per 10 minutes.
- Fixed the resend and password-reset request having no cap at all. Both queue a real email to an address the caller merely typed, which made them a mail cannon pointed at a third party's inbox and at your own sending reputation. Three per 15 minutes now.
Anyone could rewrite or delete anyone's blog comment
Editing and deleting a comment had no ownership check and no permission check. Any signed-in account could silently put words in another person's mouth anywhere on the platform, or delete their comment.
- Fixed to require the comment's author or a moderator, and to answer "not found" rather than "forbidden" — confirming that a comment exists is itself information the caller has no business having.
- Fixed the edit action failing silently and reporting success when a client used the same field name that creating a comment uses.
- Changed comment text to have markup stripped on the way in. The comment box is a plain text field, so tags are never something the writer intended. Nothing renders comments as HTML today, which is why nothing was exploitable — but stored markup waits for the first thing that does. Comparisons a person actually types ("5 < 10") survive intact.
The public blog published authors' personal data
Nine publicly reachable blog screens returned the author's email address alongside their platform role, so a visitor could read which account was the Super Admin and what address it signs in with. Two of them also returned the author's raw profile record, which in practice holds a postal address — and, because of the flow described above, could hold a live phone-verification code.
- Fixed by removing the address from every public response and reducing the profile to a permit-list of published fields (bio and social links). A permit-list rather than a block-list, because the profile's shape is user-controlled: anything not named is withheld.
- Fixed the public author list serving pending and rejected applications, which is the review queue.
Unpublished blog posts were public
A draft was listed on the blog index, readable directly by its link, and shown under its category, its tag and as a "related article". A link is guessable, so "nobody has the link" was the only protection.
- Fixed across all five places.
Private pages rendered to visitors who were not signed in
Only the admin panel was guarded. Every other private page relied on noticing for itself, and most happen to show a site header with a "Sign in" button — which looks like a guard and is not one. The user profile page has no header and rendered its entire shell to a signed-out visitor: a completion ring at 0%, empty Profile and Security tabs, a "Back to Dashboard" link. A dead end that reads as a broken or empty account.
- Fixed with one redirect covering the account, finance and binary sections, which returns the visitor to where they meant to go after signing in.
- Fixed three more: the investment dashboard, the support ticket list and the blog author's post manager each rendered its account shell to a visitor — a ticket list with its counters at zero, a post manager with no posts. Their parent pages stay public, because those are pages anyone may read.
- Fixed the author application page differently, because it is the page that recruits authors and a visitor should be able to read the guidelines. It only started loading once a user existed, so a signed-out visitor got a skeleton that could never resolve — and having read nothing and accepted all three sets of terms, the Apply button then did nothing at all for them. The page now renders for visitors and Apply sends them to sign in and back.
- Fixed a signed-out visitor to the admin panel being shown the admin shell wrapped around a raw technical error with a Retry button. They are sent to sign in. The existing behaviour is kept for the case it was designed for: a caller who is signed in but lacks the permission.
Two-factor codes for email and SMS could never be accepted
- Fixed a defect in six places. The codes generated for email and SMS delivery could never match the ones the platform then checked for, silently breaking email and SMS two-factor login, resend and password reset.
The default Super Admin account can now be deleted
Three separate causes made the account created during installation behave like a permanent fixture:
- Fixed the account re-appearing after every update, because the installer's setup step ran again on each one.
- Fixed Super Admin accounts being hidden from the users table for everyone, including other Super Admins. Ordinary administrators still cannot see them.
- Changed deletion to allow removing a Super Admin, but never the last one. Single and bulk deletion both check that at least one remains and refuse with a message telling you to promote someone first.
Admin-created accounts get unique passwords
- Fixed admin user creation assigning the literal password
12345678on every install, which also failed the platform's own password policy. Each account now receives a randomly generated 20-character password, shown once in the success message.
Demo mode
- Restored the Admin role for public registrants on demo deployments, so visitors can walk the admin panel without accounts being set up by hand. Demo mode continues to block every admin create, update and delete below Super Admin.
- Fixed that escalation being decided by one environment variable and nothing else.
NEXT_PUBLIC_DEMO_STATUStravels in a copied.env— which is exactly how it reaches a live install — and nothing anywhere said so. The Admin grant is now refused outright in production whatever the flag says, and logs an error naming the flag so it is visible rather than silent. - Warning: outside production the flag still governs. Confirm it is off before going live, and treat any install where signups become administrators as one where it is set.
Every exchange rate involving a fiat wallet was upside down
The platform stores a fiat rate as how many units one US dollar buys — NGN 1365 means one dollar buys 1,365 naira — and transfers and quotes read it as the dollar value of one unit. Every rate with a fiat leg therefore came out as its own reciprocal. A USDT → NGN transfer quoted and credited one 1365th of a naira; the same transfer in the opposite direction credited 1,365 times the value moved, minting funds out of any weak-currency fiat balance. The rate shown on screen and the rate used to settle had also drifted into dividing in opposite directions, so the two disagreed with each other before the inversion was even applied.
- Fixed one shared calculation now inverts the stored figure once, centrally, and both quoting and settlement use it — so a displayed rate can no longer disagree with the rate that is actually credited.
- Applies to any transfer or quote with a fiat leg. Crypto-to-crypto rates were never affected.
The configured wallet transfer fee was never charged
Transfers looked up an old name for the fee that the settings screen does not write. The lookup returned nothing, fell through to zero, and no transfer fee was ever taken — on any install, including the shipped default of 1%.
- Fixed transfers now read Settings → Wallet → Fees → Wallet Transfer Fee, the same value the admin screen writes.
Ecosystem transfers between users created the fee out of nothing and never spent the sender's funds
On an ecosystem transfer from one user to another, the recipient was credited the full amount while the platform was separately credited the fee — so the fee was created rather than deducted. Worse, both users' per-chain balances were adjusted in memory and never saved. The sender's wallet total went down but the chain funds behind it did not, so the same funds could be sent again by the next transfer.
- Fixed the recipient is credited the amount net of the fee, split across chains in proportion to what was taken from each, and both sides' chain balances are now written as part of the same settlement.
Cancelling an ecosystem order a fraction of a second out of step created a corrupted record and left the real order open
An ecosystem order is identified by its creation timestamp as well as its id, and a caller whose timestamp was off by a millisecond — a reconstructed value, a rounded one — took a recovery path that found the real order but then wrote to the original, wrong identifier anyway. The database has no notion of "only update a record that exists": that write created one, with market, side, price, amount and cost all empty. So the cancellation reported success while the customer's real order stayed open and still holding their funds, and the install gained a broken record that the order cleanup tool exists to sweep up.
- Fixed the recovery path now takes the identifier from the record it actually found, so the status write and the book update both land on the order the customer asked to cancel.
An order could be added to the matching queue twice, and filled twice
Adding an order to the engine checked that it was not already there, then wrote its price level to the database — two round trips — and only then put it in the queue, without checking again. Two things adding the same order at once (a placement arriving while background reconciliation is running) both passed a check that had already happened, leaving two entries for one funded order. The engine would fill both, and the second settlement draws on whatever else that customer holds in the same currency.
- Fixed the check is repeated immediately before the order is added, and the price level written by the losing side is reversed.
An order could be put back into matching in the moment between its cancellation being agreed and being recorded
Cancelling takes the order out of the matching engine first, then refunds the wallet, then records the cancellation. For that brief gap the stored record still reads open — and the background reconciliation, which exists to pick up orders placed by another process, re-added anything it found open that the engine was not holding. It could therefore put a live, matchable copy of the order back while the refund was on its way to the customer, leaving it able to fill against money that had already been returned.
- Fixed an order taken out for cancellation is now marked as such until the cancellation completes, and reconciliation leaves it alone. The mark expires by itself, so a cancellation that fails midway cannot leave the order permanently invisible.
Rejected withdrawals left the platform holding a fee it had already taken
Fiat withdrawals, spot withdrawals and fiat deposit requests all credited the platform its fee the moment the request was submitted, while the payout or deposit was still pending. When one was later rejected the customer was refunded the full amount including the fee, and nothing reversed the platform's credit — so every rejected withdrawal, and every deposit that was never approved, created money out of nothing and inflated the profit report by the same figure.
- Fixed the fee is taken when the money actually moves — on a withdrawal marked complete, and on a deposit credited to a wallet. A request that is never approved now costs and earns nothing.
Payment gateway deposits charged the wrong fee, and sometimes credited the wrong wallet
- Charged twice, and the first confirmation to arrive decided the amount: several gateways recorded the after-fee amount on the pending deposit and then deducted the fee again when the payment was confirmed, so the customer paid the amount plus the fee and received the amount minus it. On dLocal, PayU and iPay88 the gateway's own server-to-server confirmation credited the full amount with no deduction while the browser-return confirmation credited the reduced amount for the same payment — and both share one duplicate-payment key, so what the customer ended up with depended entirely on which confirmation arrived first. Fixed: every path now records the gross amount and credits the net, consistently, and the withheld fee is routed to the platform, which on some of those paths it previously reached nobody at all.
- Per-currency fees were read as zero on every gateway: a gateway's fee fields accept either one figure for every currency or a different figure per currency, and the admin gateway screen writes both shapes — but every deposit path recognised only the single figure and treated a per-currency configuration as zero. An operator who set different fees per currency collected nothing on any of them, and on Stripe and PayPal the fee the gateway itself charges at checkout was then credited straight back into the customer's wallet, so the platform absorbed it. Fixed across every bundled gateway, each of which now asks the gateway's own configuration for the fee that applies to the deposit's currency.
- Stripe deposits in yen and won charged a hundred times the amount: Stripe amounts were converted with a fixed ×100 on the way out and ÷100 on the way back, but Stripe quotes yen, won and similar currencies in whole units, not hundredths — so a JPY or KRW deposit charged the customer a hundred times what they asked to deposit and credited only the amount requested. Separately, on any wallet that already existed the credit failed after the customer had already paid Stripe, so they were never credited at all; only a brand-new wallet escaped it. Fixed: conversion now handles whole-unit and three-decimal currencies correctly and refuses a fractional amount, which a percentage fee routinely produces; the credit to an existing wallet works; and currency codes coming back from Stripe are matched against the ledger regardless of case.
- eWAY used a hardcoded fee and credited every non-AUD payment to the wrong wallet: the eWAY fee was fixed at 2.9% plus a flat 30 whatever the operator had configured, with the percentage rounded to whole cents and the flat part added in units the amount did not share — wrong twice over. On confirmation the deposit's currency was read from a field that could never hold it, so it always fell back to Australian dollars, and the same mistake overwrote the stored payment details with unusable data, destroying the reference needed to reconcile the payment. Fixed: the configured fee applies, unit conversion happens once and explicitly, the pending wallet lookup is restricted to fiat wallets, and the stored payment details are read properly instead of being scrambled.
Approving a pending deposit marked it complete and credited nothing
The admin deposit and transfer update screens overwrote the record's amount, fee, description and reference on every save — including the ordinary approve or reject, which sends nothing but the new status. The amount was blanked, the credit calculation produced a non-number, the "is this above zero" check quietly passed over it, and the deposit was marked complete while the customer received nothing.
- Fixed those four fields are applied only when actually supplied, and validated when they are. A gross amount that is not a positive number is now refused outright rather than skipped in silence.
A transfer awaiting approval could be neither delivered nor refunded
A transfer that needed admin approval debited the sender and then could not be settled either way. Approval was refused because the stored record was missing fields the approval step demanded. Rejection looked for a source wallet that had never been recorded, and silently skipped the refund. On the rare occasion approval did go through, it credited the sender's own wallet instead of the destination. The money sat debited with no way for an operator to release it or return it.
- Fixed the destination wallet, both currencies and the amount computed at creation are stored with the transfer; approval settles against them; and rejection falls back to the wallet the debit was actually taken from.
Deleting a wallet destroyed the balance and locked the user out of that currency
Neither wallet delete action in the admin panel applied any rule. The ordinary delete hid the record from every lookup, putting the balance out of reach — and because the platform's one-wallet-per-user-per-currency rule ignores deleted records, no replacement could be created either. Every later deposit, transfer or payout in that currency for that user failed permanently. The permanent delete additionally erased the history that explained the balance.
- Fixed deletion is refused, single or bulk, while a wallet holds a balance or funds locked in open orders. The refusal names each wallet and what it holds, and tells you to move the balance out first. A bulk delete is all-or-nothing, so one funded wallet cannot let the rest through.
General investments matured but never paid out
The payout run for general (non-AI) investments reused the investment's own identifier as its ledger reference — a reference the original purchase had already claimed, and the platform requires them to be unique. Every run failed on the collision and rolled back, so general investments passed their end date, stayed active indefinitely and never returned the customer's money. Funding a general or forex investment was also recorded as an AI investment, so referral commission was paid under the AI-investment rule for activity that was not one, while the general and forex commission rules could never match anything.
- Fixed the payout carries its own reference, closes the investment and credits the wallet correctly, and each investment family is recorded under its own type so the matching commission rule applies.
Spot sell orders collected almost none of the trading fee
On a spot sell the trading fee is charged in the quote currency — USDT on BTC/USDT — but was worked out as a percentage of the quantity sold rather than of the sale value. Selling 1 BTC at 60,000 on a 0.1% fee collected 0.001 USDT instead of 60. Every sell-side spot fee was understated by a factor of the price, so operators collected essentially nothing on the sell side, and the same understated figure was deducted from the seller, who was credited fractionally more than they should have been.
- Fixed the sell-side fee is now taken against the value of the sale, at the price the trade actually got. Buy-side fees, which are charged in the asset bought, were correct and are unchanged.
- Applies to all three places that record a fill — order placement, the live order feed and the scheduled job that reconciles pending orders. All three derived the fee the same way, so all three were wrong and all three are fixed. See Upgrade Notes.
A market sell could execute on the exchange and then be recorded nowhere
A market sell placed from the spot terminal could go through on the exchange while the platform saved nothing at all. The proceeds to credit were worked out from a price the exchange does not set on a market order, producing a value the wallet refuses as invalid — and refusing it undid every write for that order after the coins had already been sold. The customer was left with the asset gone from the venue, no proceeds credited, and an order that looked like it had simply failed.
- Fixed proceeds now come from the value the exchange itself reports for the trade. A sell for which the exchange never reports a usable fill price now fails with an explicit message, instead of silently unwinding a trade that already happened.
- Fixed: an order that filled immediately moved balances by a cost worked out from the ticker price captured just before the order was sent, not the price the trade actually got. Any slippage between the two was absorbed by the platform or over-charged to the customer. Balances now move by the exchange's own reported value for the trade whenever it reports one.
Spot orders could stay open forever and never settle
Two separate faults left orders permanently unsettled with the customer's funds still held.
-
An order that partly filled and was then cancelled recorded the credit for the filled part and the refund for the remainder under the same reference. The ledger refuses to store one reference twice, so the whole settlement was undone and retried on every reconciliation pass, indefinitely.
-
An exchange reporting an order as closed with less than the full quantity filled — a market or immediate-or-cancel order that ran out of book — matched none of the outcomes the platform handled, so the order stayed open and was fetched from the exchange again on every pass, forever.
-
Fixed both now reach a final state. The credit and the refund carry distinct references so they can coexist, and a closed-but-short order settles as the partial fill it is.
A binary contract could be settled twice and pay out twice
The lock that stops two settlements of the same binary position running at once expired after five seconds and was never renewed — while a settlement makes several exchange calls plus a write that queues an email, so it routinely ran longer than the lock lasted. The backup sweep could then begin a second settlement of the same contract, at a different price. The duplicate-payment protection on the wallet is keyed on the outcome, so a second settlement landing on WIN where the first landed on LOSS was not recognised as a repeat, and the customer was paid again.
- Fixed the lock now lasts a minute, and settlement additionally claims the order exclusively and stops immediately if it is no longer pending — so a second attempt finds nothing left to settle rather than racing the first.
A binary contract whose settlement failed once was never settled again
If settling a binary position hit any error — a missing wallet, a database deadlock, a failed email — the order stayed marked as being handled by a timer that had already fired and would never fire again. The backup sweep skips orders in that state, so the contract stayed pending for the rest of the server process's life: the customer's stake stayed held, and neither a win nor a loss was ever paid.
- Fixed the marker is now cleared whatever the outcome, and the backup sweep ignores it entirely for anything more than two minutes past expiry — so a failed attempt no longer strands a contract that nothing will look at again.
An exchange rate-limit ban could switch off the whole spot stack with no way back
When an exchange reports an IP ban, the platform records when the ban lifts and refuses every exchange-backed operation until that moment passes — price updates, order reconciliation, spot deposits and withdrawals, and the trading screens. That time was read straight out of the exchange's free-form error text and stored with no sanity check and no expiry of its own, so a garbled or oversized value became a kill switch of arbitrary length. Throughout it the platform reported itself healthy, and nothing in the admin panel could clear it.
- Fixed the recorded unblock time is capped at 24 hours, discarded outright if it is already in the past, and stored with a matching expiry so it clears itself even if nothing else ever touches it.
- Changed the log line announcing an active ban from informational to a warning, so an install that is refusing exchange work says so at a level anyone reads.
Profit reports counted what came in and not what went out
An investment product is a bet against the house, but only the customer's leg of it was booked. Admin profit accumulated the fees collected and never the returns paid out against them, so every profit report showed gross revenue as net. An operator running generous plans could have been losing money on each settlement while the dashboard stayed comfortably positive. Referral commissions had the same shape: fees captured were added to profit and commissions paid out were never subtracted.
- Fixed a winning settlement now records a matching platform payout and a losing one a matching platform gain, across general investments, AI investments and forex investments — and claiming a referral reward writes an offsetting entry, so the report nets commissions paid out of the fees captured. This is the same correction described below for binary winnings.
A momentary failure to reach the licence server took the whole install down
A network blip during a licence check left the platform refusing every request — permanently. Nothing in the request path ever retried, so a few dropped packets or a brief outage at the licence provider took the site down and kept it down until someone noticed and restarted the backend. The install itself was licensed and paid for the whole time.
- Fixed not being able to reach the licence server is no longer treated as evidence the licence is bad. The previous verdict stands and the next check decides.
- Fixed activation needing a restart before it took effect. Activation wrote the licence, but several in-memory caches kept answering with the pre-activation verdict — including a 60-second decision cache that the redirect immediately after activation landed inside every single time, and activating from an uploaded purchase file never re-checked at all. Both activation paths now clear every cache together, best-effort, so the panel is usable the moment activation succeeds.
The platform's main text input had no visible focus indicator
This input draws a styled box around the field, and every focus, disabled and invalid style was declared on the box while the field inside it switched its own outline off. None of them ever appeared. Over 700 fields across the platform showed nothing at all when focused — sign-in, withdrawal, order and admin forms alike — so a keyboard user had no way to tell which field they were in.
- Fixed focus, disabled and invalid states are now driven by the field itself, so the ring appears where and when it should.
- Fixed: the input's built-in label was not linked to its field, so clicking a label did not focus it and screen readers announced the field as unlabelled. Fixed for every field that uses it.
- Fixed: the validation-error style shifting the field a pixel sideways the moment an error appeared.
Four home page sections could never appear
The AI investment, copy trading and affiliate sections of the public home page were gated on names that are not real entries in the extension list, so the condition was always false. The sections existed, the statistics behind them were being calculated, and nothing ever rendered them. Binary options was gated on an extension that does not exist at all.
- Fixed the three are gated on their real extension names, and binary options on its own Binary Trading setting. All four now appear when the feature is installed and on.
- Added two further sections at the same time, for Forex Trading and Managed Forex.
Six blog switches could only ever be on
A switch an administrator turned off was being read as on. Each switch was therefore pinned permanently on the moment it existed:
-
Auto-approve authors could not be turned off. Every author application was approved on arrival, so the review queue an operator thought they had enabled never received anything and anyone who applied could publish to the front page immediately.
-
Moderate comments could not be turned off. Every comment was stored as pending, including on installs that had deliberately switched moderation off — where nothing was watching the queue, so comments simply never appeared.
-
Show related posts could not be turned off.
-
Enable comments and show author bio could not be turned off on the post page.
-
Enable author applications was worse: the server never checked it at all. Closing the programme left the Apply link in the blog navigation, the application form on the page, and applications still being accepted.
-
Fixed with one shared reading on each side that understands every stored form and applies a default only when a setting has never been set. Author applications and comments are now enforced on the server too — previously turning comments off only hid the form.
A two-factor code that could not be sent locked the account out entirely
If the enrolled channel could not deliver — email two-factor switched off platform-wide after a user enrolled in it, SMTP down, Twilio not configured — the login failed before issuing the challenge. That challenge is what a recovery code is submitted against, so the account holder could not sign in at all: not with a code, and not with the recovery codes kept for exactly this situation. The message named a platform setting they have no way to change.
- Fixed a delivery failure no longer ends the attempt. The challenge is still issued and the response says the code could not be sent and what to do instead, so an authenticator code or a recovery code still completes the login. This is not a weakening — the challenge on its own grants nothing.
Enabling 2FA reported success without enabling anything
An account that had never completed setup could "enable" two-factor authentication and be told it worked, while nothing protected the login.
- Fixed to require an existing enrolment and say so plainly when there is none.
Binary stake limits came from the wrong market, in the wrong asset
An exchange-backed binary market took its minimum and maximum stake from the spot market's limits — which describe a quantity of the base asset for a spot trade, not a stake in the quote currency. The consequence, on essentially every market: the per-market minimum and maximum an administrator sets in Admin → Binary Markets did nothing at all. What applied instead was nonsense — BTC/USDT's spot minimum of 0.00001 BTC allowed a 0.00001 USDT stake, i.e. dust positions whose payout rounds away to nothing.
- Fixed to read the binary market's own limits, exactly as ecosystem-backed markets already did.
Spot currency precision was imported as one decimal place
BTC was stored as having one decimal place, and anything finer than 0.001 as zero — which is the number the admin currency screens and the public currency list present as that currency's decimals.
- Fixed, with a repair for currencies already imported. See Update Instructions.
The trading watchlist was built and never connected
The platform has had a per-user watchlist the whole time and nothing used it. The star button in the markets panel saved to the browser only, so a customer's watchlist vanished when they changed device or cleared their browser.
- Fixed by connecting the star button to the account, keeping the browser copy as an instant-response cache and as the watchlist a signed-out visitor still gets. Stars made while signed out are merged in on the next sign-in.
- Fixed removal, which could never have worked and failed on every call. Nothing noticed because nothing called it.
- Added a market family to each entry (spot / ecosystem / futures). A symbol like BTC/USDT exists on more than one, so an entry carrying only the symbol could not say which market was starred. Two quick clicks can no longer create a duplicate.
- Changed the star to be described as a toggle, which it always was — a client that retried a request it believed had failed silently un-starred the symbol.
- Fixed the watchlist storing any text it was given, so it could hold entries that are not markets at all, including markup, which the trade terminal then tried to open.
Withdraw and transfer screens reported errors for an empty account
Asking about a wallet type the account does not hold — the normal state of a new account — was answered as an error. A fresh customer's first visit to the withdraw or transfer screen filled the console with failures for a situation in which nothing is wrong.
- Fixed an empty result is not an error.
The blog's "Meet our authors" section was empty for everyone it was for
The public blog home shows a designed, translated authors section — and it required a sign-in. Every visitor, which is the entire audience for a blog, saw nothing there.
- Fixed by making it public.
- Fixed the same list including authors whose application had not been approved.
An author could not create a blog post
Every submission from Blog → My Posts → New Post was rejected before it reached the platform, because the form and the platform disagreed about which fields were required. Editing an existing post worked, so the failure looked specific to creating. Separately, adding a tag that did not already exist failed. The platform accepted only tags it already knew about, while the control above it is a free-text tag input that cannot know about a name the author has just invented. On a new post every tag was an unknown name.
- Fixed the URL is derived from the title when the form does not supply one, and a tag can be either an existing one or a new name, which is created on save. Names are matched loosely, so "Trading Bots" and "trading bots" resolve to one tag rather than colliding.
Blog post tags could not be set from the admin panel
The admin create and update screens silently discarded tags and reported success, so an administrator could not add, change or remove a post's tags at all — only the author of the post could, from the public dashboard.
- Fixed. Leaving tags out leaves the existing ones alone; sending an empty list clears them. The two are deliberately different, so a client that does not know about tags cannot wipe them.
A duplicate blog URL returned a server error instead of a clear conflict
Colliding with your own post produced a clean message; colliding with another author's post produced a generic server error with nothing to indicate which field was at fault. A URL apparently freed by moving a post to trash also read as available and then failed on save.
- Fixed everywhere: the check now covers every post including trashed ones, and returns a conflict the editor can put next to the URL field.
An author's edit could half-save
A later failure — an unknown category, a rejected tag — rolled back the tag and category changes but left the new title, body and status saved, and still returned an error. The author saw a failure and the post had changed anyway.
- Fixed the whole edit now saves or does not.
An author's blog URL was displayed but never saved
The editor has always shown a URL field on an existing post, and never saved it. Renaming a post reported success and left its URL on the original title.
- Fixed. A changed URL is cleaned up, checked for conflicts and saved; a value that cleans up to nothing leaves the existing URL alone rather than clearing it.
Blog post limits and rich text were enforced only in the browser
- Fixed the Maximum Tags Per Post setting, which was applied by the tag input and by nothing on the server.
- Fixed author-submitted titles and bodies not passing through the same markup sanitiser the admin screens use, despite being shown on the same public pages.
The image picker's most prominent control did nothing
Clicking Choose File — the one thing that looks like the way in — did nothing. The control only worked if you happened to click the empty space around the button. It appears on 19 screens, two of them shared fields that put it in every admin table form.
- Fixed.
- Fixed the same control being unusable without a mouse. Replace and Remove appeared on hover only — and a touchscreen has no hover, so removing an image was not possible at all, while for a keyboard user the buttons stayed reachable but invisible. They now appear on keyboard focus and are permanently visible on touch.
- Fixed the drop zone announcing nothing to screen readers.
An image chosen in a form was not in the form yet
The picker waited 1.2 seconds behind an animated progress bar before handing the file to the form. Nothing was uploading during it — the upload happens when the form is submitted — so the wait was invented and the bar was decoration. Choosing an image and saving immediately submitted the previous value. Leaving the screen inside that window still delivered the file. In the page builder, choosing an image and then clicking a different element within 1.2 seconds attached it to the wrong element.
- Fixed by handing the file to the form immediately.
Images the picker refused vanished without a word
A file that was too large or of the wrong type was discarded silently — no message, no error. The control simply reset, which is indistinguishable from a mis-click. Most visible on the P2P payment-method icon, which caps uploads at 1 MB.
- Fixed by saying why the file was refused.
- Fixed an image that fails to display leaving a blank bordered box with nothing to explain it.
The image picker ignored the size it was asked for
Two of its sizes had no effect at all and fell through to the largest, so the "extra small" picker in the narrow page-builder sidebar rendered taller than the panel was wide. A third produced a box too short for its own contents, which were then clipped on six screens. The aspect-ratio option had never worked.
- Fixed. Every size now behaves as named, and an unrecognised one falls back to the default rather than collapsing the box and hiding the image entirely.
- Fixed a saved image being served at full resolution on every edit form instead of an optimised version.
- Fixed the format hint always reading "JPG, PNG, GIF, WebP" regardless of what the field actually accepts.
- Fixed four screens where the field's label pointed at nothing, the same error was printed twice, or the field was given a heading it already had.
40 permissions could not be granted to anyone
Those permissions were named by screens but did not exist as rows you could assign, so every one of those screens was Super-Admin-only no matter what an administrator ticked in the role editor — all of Forex Trading, geo restrictions, market news, currency icons and the Ecosystem KMS. This affected every feature added since the last permission seed.
- Fixed with a one-off repair (see Update Instructions), plus a check that fails when a screen requires a permission nobody can hold.
Blog images were downloaded at the wrong size
- Fixed ten blog images being fetched at their largest available size regardless of how big they were shown.
- Fixed two being sized against the whole page rather than their own card.
The binary expiry error listed every second of the minute
- Fixed a message that listed all sixty valid minutes for a one-minute contract and never mentioned the seconds — which is the half of the rule that rejects most requests. It now names which half failed and shows a few examples.
KYC feature switches that decided nothing
Of the 37 features an administrator could toggle per level, 35 did nothing. Withdraw Funds — the platform's primary money-out door — was a switch wired to nothing, as were deposits, transfers, trading, binary, futures, ICO purchases, store orders, staking, P2P, NFT creation and sale, copy trading and the rest. Only forex live trading was enforced. An operator configuring levels was configuring a display.
- Fixed by routing every one of them through the shared check described under Added. Because turning that on for an install whose levels were never curated against real enforcement would revoke access from existing users, it is behind a setting that defaults off.
Four gates that ignored the level builder's answer
Each of these already refused some users, but on a basis the administrator could not see or change from the KYC screens:
- Fixed API key creation testing a hard-coded "KYC level 2 or higher". Enabling API Access on a Level 1 level still refused; disabling it on a Level 3 level still issued keys.
- Fixed the Hummingbot key gate comparing against a second, separate KYC dial on a different admin screen that the level builder knew nothing about.
- Fixed merchant onboarding and high-value NFT purchases testing only "holds any approved application", so a Level 1 user with the feature explicitly disabled passed. The NFT check additionally ran only above a USD threshold, leaving every purchase below it unchecked.
- Fixed copy-trading leader eligibility hard-coding a level, so the Become a Trader switch was never consulted.
Where a door already enforced something, the old bar stays in force while feature enforcement is off. An update that silently opens a door is worse than one that never happened, so none of these four loosens on upgrade.
Addons with no KYC gating at all
- Fixed the NFT, payment gateway, copy trading, trading bot and Hummingbot addons having no KYC check anywhere — including merchant onboarding, which let an unverified account start accepting third-party money, and strategy purchases, which move real funds between users.
- Fixed marketplace checkout being reachable without passing the product page. The only store check sat on product detail, so adding to cart from a category listing and going straight to checkout bypassed it entirely.
- Fixed P2P offer creation being ungated while the buying side was gated — so the wizard that publishes an offer and escrows funds had no check. Editing an offer had none either, which made the creation gate bypassable: publish once while entitled, then re-price indefinitely.
- Fixed wallet deposits and internal transfers having no check on either side, despite the user-facing wording for them already being written.
- Fixed the affiliate dashboard containing a KYC notice that was never actually shown — a gate that looked present and did nothing.
KYC notices told verified users to verify again
- Fixed 16 of the 17 gated pages always showing the "Complete Verification" message. A user who is approved but whose level simply omits the feature was sent to a KYC page already showing their application complete, with no indication of what to do next. The difference between "not verified" and "verified, wrong level" is now decided once, centrally.
- Fixed 10 of the 37 features having no wording at all, so their notices fell through to a generic "Complete our quick and secure verification process" without naming what was locked: forex live trading, all four NFT features, both gateway features and all three copy-trading features. A feature added without wording now fails the build rather than shipping a generic message.
- Fixed the wallet dashboard showing a KYC notice to a signed-out visitor instead of a sign-in prompt.
- Fixed the withdrawal form being the only gate that showed a raw, untranslated English message.
- Removed the duplication behind all of the above: the same logic was pasted into 16 files, so any future change would have had to be made in sixteen places or silently leave a money surface open. The browser is now never stricter than the server.
Binary trading on ecosystem markets
Binary markets now declare which price feed backs them: exchange (the centralized exchange) or ecosystem (the ecosystem market, and its AI Market Maker when one is running). Entry price, settlement price and every risk check follow that one declaration.
Before this, on an ecosystem market:
- order placement returned "Market data not found" for any pair not also listed on the centralized exchange;
- where the pair was listed on both, the entry and settlement prices came from the exchange while the chart showed the market maker — the user traded one series and settled on another;
- the binary chart was hardcoded to the exchange feed regardless of the market;
- the admin "create binary market" wizard asked whether the market came from the exchange or the ecosystem in step 1, and then never used the answer.
An ecosystem market deliberately does not fall back to the exchange even when the pair happens to be listed there. Touch/No-Touch and Turbo remain exchange-only — they settle on intra-period highs and lows, which ecosystem markets do not publish — and are refused at placement rather than accepted and mis-settled.
The binary profit report counted only half the ledger
A losing stake was credited to the platform, but a winning payout was credited to the user with no offsetting entry — so the reported profit was gross stakes collected rather than the net result. On one test run the report showed +200 for a period whose true result was −16.
- Fixed winnings are now booked against the platform as they are paid, so reports net out correctly. A platform balance that has not yet accumulated reserves will never block a user's winnings. The order is now identified in each record, so profit can be reconciled per order.
Binary settlement is priced as of expiry
- Fixed settlement reading the price at the moment it ran. The normal expiry fires on time, but the backup sweep and the one after a restart can run minutes late, settling a real position on a price the contract never saw. Settlement now uses the price as of the expiry, on both feeds.
A binary order with no payout rate paid more than the platform sells
An order could be created with no payout rate and settle on a hardcoded 85%, above the 72% default.
- Fixed creation now rejects an order without a valid rate, and orders predating this check fall back to the configured default for their type rather than a single hardcoded number.
NFT contract calls used a function the contract does not have
Every call made through one part of the NFT interface failed, including the supply-and-mint-status read on the minting page. It now matches the deployed contract, and additionally exposes the public-mint toggle and owner lookup, neither of which was available.
Trading Pro panel widths were saved but never applied
Panels could not be resized and the existing reset action had nothing to reset. Panels now carry a real width, applied only once a user actually drags one, so the default layout is unchanged.
Admin data table sorting
- Fixed server errors when sorting by a column that comes from a related record. An unsortable column is now ignored instead of crashing the page.
- Fixed sorting failing on paged lists that include a one-to-many relationship.
- Fixed the users table offering sort and filter on the KYC column, which is calculated after the list is fetched and was never sortable.
Admin data table record dialog
The expanded record panel — the one that opens from a row or a card in every admin table — was close to unreadable, and closing it from row view smeared the row's contents across the screen.
- Fixed field tiles that were invisible against the panel behind them: the tile, its border and its separation from its neighbours all vanished. They now sit a clear step off the panel in both light and dark, and their labels went from roughly 4.4:1 to 6.7:1 (dark) / 5.1:1 (light) contrast. Panel edges and dividers were on the same washed-out border and are now at full strength.
- Fixed hovering a tile making it dimmer in dark mode. Hover now strengthens the border instead.
- Fixed the closing animation in row view. The row and the panel were being morphed into each other — but a table row is roughly 1900×60 and the panel 600×700, so every value in the row was rendered tall, narrow and smeared for the whole return trip. Opening concealed it because a growing box reads as intentional; closing did not. Row view now uses an ordinary modal transition. Card view keeps its morph, where the two shapes are comparable and it reads cleanly.
- Fixed the panel vanishing instantly when dismissed, with no exit animation at all.
- Improved how quickly tables render. Every cell of every row was doing animation bookkeeping for a panel that was not open — which is the bulk of the lag when opening and closing it.
- Changed the card tiles and their loading placeholder to the same surface, so a card does not change appearance when its data arrives or when it expands.
KYC level builder appearance
The builder's four panels each painted their header differently, and three of the four put weak text on a saturated brand colour.
- Fixed the panel headers. Field Library was a solid brand fill carrying the dimmest possible icons; Field Properties was a gradient under near-black text; Level Presets was a half-strength tint; Level Settings put an invisible icon on a matching background. All four now share one treatment — a neutral surface, the accent confined to the icon, and full-strength titles.
- Fixed the vertical icon rail. Inactive icons used the dimmest colour available, which is the bulk of the "icons are hard to see" reports, and the active colour was chosen by comparing tooltips against English text — so it resolved to nothing at all in any other language. Three buttons were also given status colours for states that are not a status.
- Fixed six places where two conflicting styles were declared on the same element, so which one won was arbitrary. On the five field-library tabs the loser was the active tab's own highlight, leaving it identical to the inactive ones; on every unselected field in the canvas it was the outline, which disappeared entirely.
- Fixed four alert blocks whose text was below the readable contrast floor, plus two that gave light mode a heavier fill than dark — the inverse of everything else in the product.
- Fixed a code sample with no text colour declared at all, so it inherited near-black text in both themes.
- Fixed a loading spinner drawn in literal white, which is correct in light mode only by coincidence and wrong in dark mode and under any custom palette.
- Fixed eight duplicated styles and two hover states that did nothing.
KYC level builder text spacing
- Fixed numbers running into their labels throughout the builder — the features footer read "27Enabled 10Disabled 19Enabled Above Recommended Level", and the field inspector "IDfull-nam", "Order0", "Level1Configuration" and "Must be at least5characters". Twelve occurrences corrected.
- Fixed the field inspector's Required / Optional label being hardcoded English on an otherwise translated panel.
Spot deposit and withdrawal currency lookups
- Fixed misleading "Currency not found" errors. Exchanges disagree about which field carries the user-facing ticker, and the old code handled only some of them — so an entire shape of exchange data always failed. Lookups now handle both, case-insensitively, for every provider.
- Fixed the same error appearing when the exchange returns an empty currency list instead of an error, which happens when the credentials are missing or invalid. It now says plainly that the credentials are the problem.
- Fixed currencies being rejected on exchanges that leave the "enabled" flag unset. Only an explicit "no" now counts as disabled.
- Removed per-provider special-casing for network listings, whose fallback silently returned nothing for several providers.
Exchange connections refused with a timestamp error
Failed to initialize exchange: binance {"code":-1021,"msg":"Timestamp for this request was 1000ms ahead of the server's time."} at startup, leaving every exchange-backed feature unavailable. Syncing the server clock did not help, because the clock was not the problem — the correction for it was being applied in the wrong direction.
- Fixed the direction of the clock correction. A host running 600ms fast signed its requests 1.4 seconds ahead of Binance. Anything more than 1000ms ahead is refused outright, and widening the tolerance — already at its maximum — only helps requests arriving late, which is why raising it never made any difference. The "1000ms" in the message is Binance's fixed threshold, not the measured drift, so the wording never changed however far the clock moved.
- Fixed the correction being stored somewhere the exchange library never reads, which made it a no-op on top of being inverted.
- Fixed the first request of each connection going out uncorrected.
- Changed how the offset is measured, so network latency is no longer mistaken for clock drift, and requests are signed from a deliberate half-second behind the exchange — the only direction with room to spare.
- Added a background re-sync, so a clock that drifts after startup no longer eventually starts failing again.
Form validation
- Fixed yes/no conversion corrupting valid numeric input, so a quantity of exactly
1or0was silently rejected. The conversion now leaves text and number fields alone.
Page editor
- Fixed the editor overwriting live legal and home pages during a database outage. When a page could not be read the editor showed a default page — and the editor saves what it shows, so pressing Save replaced real content with defaults.
Home page editor controls that wrote to nothing — and one that destroyed content
Roughly half of the home-page editor's controls saved settings that nothing on the page reads. The entire Market Section and its tab, the global section's heading and stats list, the mobile-app copy, every gradient and background picker, the call-to-action subtitle and the whole SEO block. An operator could change any of them, save, see the page unchanged and have no way to find out why.
- Fixed those controls are gone. The values already stored are left untouched, so nothing is lost if a later release gives them meaning. Separately, the Features tab loaded only the first four cards and wrote back only four, while the shipped default content contains six. Opening that tab and changing anything permanently discarded cards five and six.
- Fixed. The page itself only ever displayed four, so nothing looked wrong to a visitor — the loss was invisible until you went looking for the missing entries.
A page could be saved on top of a live platform address and was then unreachable
The list of URLs a page may not use was seven hand-written entries, three of which were not platform addresses at all. Creating a page at login, register, trade, market, blog or any addon's own address succeeded and reported success — but the real page always wins, so the new one could never be opened and nothing anywhere said why.
- Fixed the list is generated from the application itself and now covers all 45 addresses the platform serves.
- Added the editor checks a URL as it is being typed and says which of the three things is wrong — reserved by the platform, already used by another page, or held by a page sitting in the trash. The last of those previously produced a raw server error on save.
A fresh install could serve a home page the editor had never shown
The starting content for the built-in pages existed in two places — the admin editor and the public page — and the two copies had drifted. The admin copy carried the extension feature sections and the whole mobile-app band; the public copy carried neither. Both create the same stored page, so which version an install ended up with depended entirely on whether an administrator opened the editor before a visitor first hit the home page. Nothing reported a problem either way.
- Fixed there is now one shared starting copy containing everything, and page titles and descriptions are read through one shared path, so the editor's preview and the live page agree.
Live connections
- Fixed phantom errors in the log from ordinary housekeeping on a connection that had just closed.
- Fixed clients that reconnected at just the wrong moment being disconnected again.
One internal transfer appeared four times in the ledger
Each internal transfer was recorded twice on each side. The money movement itself writes both legs, and two more records were being written by hand around it. Users saw both sides of a transfer duplicated in their history, the duplicates carried no balance snapshot so nothing could be reconciled from them, and the fee was reported against a record that never moved any money.
- Fixed only the two real legs are written, and the fee is reported against one of them. Records created before this update stay settleable.
The transfer screen showed a rate and a fee that were not the ones charged
The quote returned the mid-market rate while the transfer itself applied the platform's configured spread, so a user was always shown a slightly better rate than the one they received. The fee shown was assumed — nothing for moving funds between your own wallets and a flat 1% for sending to another user. Neither figure tracked the platform's actual setting, so the fee displayed matched the fee charged only by coincidence.
- Fixed the quote returns the settlement rate with Wallet Transfer Spread applied, alongside the mid-market rate and the spread percentage, so the screen can show what the spread costs rather than hiding it — and the screen is served the real configured fee and shows that.
Fiat balances were counted at face value in the portfolio total
The wallet dashboard's total, and the profit/loss history under it, read the stored fiat rate without inverting it and treated a currency with no rate as one dollar per unit. A 1,000,000 IRR balance was reported as $1,000,000, and every non-dollar fiat balance was out by the inverse of its own rate.
- Fixed fiat balances are converted properly, and a currency with no usable rate contributes zero to the total rather than its face value.
Payment records were corrupted before being read, losing settlement details
Three admin settlement screens stripped every backslash out of a payment's stored details before reading them. Any legitimately escaped character — a quote inside a bank name, an accented character, a Windows file path — broke the record, which was then read as empty, and the details the settlement depends on went with it: the original debit used to size a refund, the source wallet used to direct one, the gateway reference used to reconcile a payment. A record with no details at all failed outright rather than being treated as empty.
- Fixed the details are read exactly as stored, and a record without them is handled as empty.
Settling the second payout of a bank batch failed with a server error
One bank wire or on-chain batch legitimately settles several payouts under a single reference, but a reference must be unique platform-wide. Marking the second and later payouts complete under the same reference failed with an unexplained error — leaving the wallet already debited and the record still pending, with no way for an operator to close it.
- Fixed the reference is kept on the record when it is free, and recorded alongside it with a warning when it is not, so the settlement completes either way.
Admin balance adjustments discarded the reason, always emailed, and showed up twice
The admin wallet screen sends a reason and a notify user choice with every balance adjustment, and the server read neither. The reason never reached the ledger, and un-ticking the notification still sent the email. The internal holding record that exists only to stop an accidental double-submission was left behind marked as a completed payment for the full amount, so every admin adjustment appeared twice in the customer's history and any reconciliation built from the ledger double-counted it.
- Fixed the reason is stored on the ledger entry, the email is sent only when asked for, the holding record is removed once the adjustment lands, and an adjustment is recorded as a credit or a debit rather than a generic payment.
Admin profit tiles added different currencies into one number
The Today / This Week / This Month profit tiles summed every profit record regardless of currency, printing bitcoin plus dollars plus naira as a single figure. The "% versus last month" comparison derived from it tracked the changing currency mix rather than profit, so an operator could not tell from those tiles whether the platform had made or lost money.
- Fixed profit is reported per currency, one line each, and the period comparison is per currency too.
A customer's history in the admin panel hid every deposit and withdrawal
The platform-wide movement list deliberately excludes deposits, withdrawals, incoming transfers, binary and exchange orders, forex movements and token-sale contributions, so it is not a duplicate of six other screens. That exclusion was applied when the same list was narrowed to a single customer as well — and the customer profile presents it as their complete history, with no per-user deposits screen for the hidden rows to appear on instead. An administrator investigating a payout dispute was shown a history with every deposit and withdrawal quietly missing.
- Fixed the exclusion applies only to the platform-wide view. A single customer's history is complete.
A small spot deposit retried forever instead of failing
When the exchange's own network fee met or exceeded a small deposit, the amount left to credit came out at zero or below — which is refused — so the deposit never completed and the checking loop re-ran indefinitely, leaving a record pending forever with nothing said to the customer.
- Fixed the customer is told plainly that the deposit does not cover the network fee, the checking stops, and the record is marked failed.
The spot withdrawal confirmation email had no address and no chain
The email confirming a spot wallet withdrawal showed a blank destination address and a blank chain, on every one it sent. Those are the two things a customer checks to confirm a payout is going where they intended.
- Fixed both now appear, with a plain N/A only where the withdrawal genuinely carries neither.
The admin user detail page
Every addon section of an administrator's view of a customer — their orders, their staking, their P2P history and the rest — was gathered in one block. The first one that failed, for any reason at all (an addon not installed, a slow database, a renamed field), silently discarded every section after it, and the page then rendered the survivors with nothing to indicate the rest were ever meant to be there. The page also read the customer's role without checking one was present, so an account whose role had been deleted rendered a blank screen. The risk score was presented out of 100 while the total behind it could reach far higher; its compliance part was pinned at zero because it read a figure the page was never sent, and its "activity" part actually measured which addons the operator had installed — the same constant for every customer on the platform. The Affiliate tab never appeared for anyone, because it tested for the wrong extension name.
- Fixed each section is now gathered on its own, so one failure costs one section and that section says so. The score is bounded and each part is computed from data the page actually holds, the support figure it needed is now supplied, and the Affiliate tab appears when the extension is installed.
- Fixed the same page loading a customer's entire notification history on every visit, multiplied out against their verification applications. It now loads a bounded recent set, and ten lists it fetched in full but never displayed — it only ever showed counts and totals derived from them — are now asked for as the counts and totals.
- Fixed its confirmations, its error messages and the KYC tab label being fixed English on an otherwise translated panel.
- Changed each addon tab now leads with its headline figures rather than opening straight into a table.
Blocking and unblocking from the admin panel
A temporary block stays marked active until a scheduled sweep clears it, and that sweep runs every 15 minutes. Inside that window Unblock answered "User is not currently blocked" about an account that was still suspended, so an administrator who wanted to release someone had to wait the sweep out. Block and Unblock on the admin users table showed a green confirmation whatever happened, so the refusals that matter most — including the new "Super Admin only" and "not currently blocked" answers — reached the administrator as confirmation of something that had not happened.
- Fixed Unblock, which now clears every active block on the account in one go, and both controls show the actual reason rather than a blanket success.
- Fixed unblocking promoting accounts it had no business promoting. The account is returned to Active only when a block is what suspended it, so an account sitting inactive pending email verification is no longer activated by accident.
Summary figures failed above several customer tables
The strip of figures above a customer's tables answered with an error instead of loading. Only three kinds of record were accepted; every other list asking for it was refused — including the marketplace order list, forex investments and token-sale transactions.
- Fixed any list whose rows belong to a user is now accepted, which is the property that actually keeps a caller inside their own data. Lists with no owner are still refused.
The KYC application review screen had invisible controls
The screen painted its own page background with a fixed white, which lit the gutters in dark mode and bled through the tinted panels sitting on top of it. The score tile and the Confirm button were each given the two colour stops of a gradient with no gradient ever applied, so they had no background at all behind their white text — effectively invisible. The "no verification service configured" panel put brand-coloured text on a brand-coloured fill.
- Fixed status pills, decision buttons and surfaces now follow the same colour rules as the rest of the admin panel, in both themes.
- Fixed an unrecognised application status displaying as Pending.
- Applies to Admin → CRM → KYC → Applications. The level builder, covered above, is a different screen.
The platform announced a cache fallback that had not happened, and never came back from one that had
During startup the connection probe could expire while the server was busy and announce a fall back to the in-process store microseconds before the real connection reported ready. Operators saw "unavailable" lines in the log that were simply not true. More seriously, a single command that timed out marked the cache service down without dropping the connection — and the connection only re-announces itself after a genuine reconnect. An install that hit one slow command stayed on the in-process fallback until it was restarted, with nothing in the log to say it was still there.
- Fixed recovery is now polled, so the service is picked back up as soon as it answers. A brief down-and-up no longer reaches the log at all.
The notification Queue Items tab was permanently empty
The Queue Items tab under Admin → System → Communication Tools → Notification Service asked for a list that did not exist. An operator could see how many notifications were queued but never which ones were stuck.
- Fixed the tab lists pending and in-progress jobs, with how long each has been waiting.
The scheduled tasks screen reported every job as never having run
On deployments that run the backend across several workers, Admin → System → System Monitoring → Scheduled Tasks showed never ran, idle for every job regardless of what was actually running — the run history only exists inside the worker that does the scheduling, and any of the others could answer the screen.
- Fixed the screen reads that history from the snapshot the scheduling worker publishes, so last-run times, status and success rates are real.
- Fixed: the same screen answering any caller, signed in or not. It named a permission but never required a sign-in, so the permission was decorative — exactly the class of mistake the new structural check described under Fixed → Security now catches.
The exchange provider screen printed environment variable names that do not exist
The setup instructions on the exchange provider screen built the variable names out of translated interface labels, so they rendered as things like APP_BINANCEAPI Key. An operator following them on screen typed a name the backend never reads, and the connection silently stayed unconfigured.
- Fixed the names shown are the literal ones the backend looks for —
APP_BINANCE_API_KEY,APP_BINANCE_API_SECRETandAPP_BINANCE_API_PASSPHRASEfor Binance, and the equivalent for every other provider.
Four products showed "No Changelogs Available"
- Fixed patch notes for the Binary Engine, Hummingbot, Trading Bot and Forex Trading products coming up empty with no explanation, because those four were missing from the list that connects a product to its documentation. They now load.
The AI Investments tab was always empty in both trading terminals
A customer holding AI investments saw an empty AI Investments tab in the orders panel of the standard trade terminal and of the Pro workspace, no matter how many investments they held. Both panels were looking for the list under a name the server does not use, so both always found nothing.
- Fixed in both panels.
- Applies to installs with the AI Investment extension.
Toast notifications ignored the theme
Success, error, warning and information toasts rendered in fixed neutral greys everywhere in the app, in both light and dark mode. The styling the platform applied was being overridden, so an error toast and a success toast were the same colour on a site with a fully configured palette.
- Fixed toasts now use the site's own colours and follow whatever an operator picks in Site Design.
The site header's command palette could run the entry you were not pointing at
In the site header's Ctrl/Cmd+K palette, hovering a row painted a second highlight while the keyboard selection stayed where it was. Two rows looked active, and Enter ran the keyboard one — so the palette regularly opened something other than what the user was pointing at.
- Fixed hovering moves the selection itself, so there is only ever one active row and it is the one that runs.
- Fixed: the list scrolled itself on every pointer movement, which slid a different row under a stationary cursor and made the selection walk on its own. Scrolling is keyboard-driven only now.
- Fixed: a per-item entrance animation that ignored the reduced-motion setting, and the panel's fixed grey and white colours, which now follow the theme.
Broken images left the browser's torn-page icon
When an image failed to load, the full-screen preview kept the broken image on screen and the browser painted its own broken-image glyph beside the alt text. Visible in the admin users table among others, and a recycled table row stayed stuck on the previous row's failure.
- Fixed a failed image falls back cleanly, and the fallback resets when the row's image changes.
- Fixed: uploads whose paths were written with Windows separators by older builds, which never resolved, are repaired on display.
- Fixed: an image hosted on a domain the platform has not been told about used to take the whole page down with a generic error rather than failing on its own.
Every blog article shared one browser-tab title and produced no preview
The article page carried no page information of its own, so every article — and the blog's own "not found" page — fell back to the site-wide title. Browser tabs, bookmarks and search snippets read the same thing whatever you were reading, and pasting an article link into a chat or a social post produced a bare address with no card.
- Fixed an article now carries its own title, its summary, keywords drawn from its category and tags, its author, its publication and revision dates and its cover image. Tabs, bookmarks, search results and share previews show the article rather than the site.
Moving from one article to another showed the previous article's comments
Opening a second article showed the first one's comment thread underneath it. Nothing corrected it short of a full page reload, so a reader who browsed three articles in a row read the first one's discussion three times.
- Fixed the comment list reloads when the article changes.
On-chain NFT auctions and offers failed with a server error
There was nowhere to store a user's self-custody wallet address. Every on-chain path that needs one asked the account for an address the account could not hold and failed before doing anything — deploying an auction, settling one, bidding, and confirming an offer.
- Fixed the wallet address and the provider that supplied it are now stored against the account, added automatically on the next backend start. Those four paths work, and a user who has not linked a wallet is told to link one instead of being handed a server error.
The investment landing page advertised three plans that do not exist
With no trending plans configured, the page invented three — Growth Portfolio at 15.5%, Conservative Income at 8.2% and Aggressive Growth at 22.8% — each with its own call to action. Every one of them pointed at the same plan, and that plan does not exist, so a visitor to a financial shopfront was shown invented returns and then a "plan not found" page.
- Fixed the section renders nothing when there is nothing to show, which is what the forex and staking pages already did.
- Fixed: the metrics strip above it presenting zeros as achievements. A figure that is zero is now left out rather than published — "0 investors" is a worse claim than no claim.
Smaller corrections
- Admin table status badges disagreed with the rest of the admin panel. The badges inside tables painted fixed colours and kept their own 18-entry list of which status is which colour, one that disagreed with the list everything else uses: OFFLINE and DISABLED were neutral grey in a table and red everywhere else, PROCESSING and IN_PROGRESS amber in a table and blue everywhere else. One shared list now, so a status is the same colour wherever it appears, and the colours follow the theme. Where a badge changes colour, the table was the one that was wrong.
- Pages rendered underneath the fixed site header. The header sits over the top 64 pixels of the page and nothing owned the page frame, so any page that forgot to leave room drew its first element beneath it — a heading or a back button permanently hidden behind the bar. Shared page and editor frames now derive that clearance automatically and carry one container width, one gutter and one vertical rhythm. Applied to the e-commerce section and a set of admin and extension screens, which is where the fix is visible today; the remaining screens keep their existing spacing.
- Pagination could not be used from the keyboard. Where a list pages by clicking rather than by following a link — the FAQ, the ICO offer lists, the support ticket list and the campaign target picker — the page numbers were drawn as links with no destination, which a browser does not put in the keyboard tab order, so a keyboard user could reach page 1 and no further. Those are real buttons now; genuine links are unchanged, so middle-click and copy-link still work where the pagination is link-based.
- The notification bell's unread count disagreed with itself and was cut in half. The badge capped its number at 99+ but sat in a fixed square too narrow for three characters, while the panel header two inches away printed the raw figure uncapped — the same unread count read as a clipped "99+" on the bell and "170" in the panel. Both use one capped label now and the badge widens with the digits. The dropdown also had no border, leaving it edgeless against a dark background, and carried a second entrance animation that ignored the reduced-motion setting; type markers are icons on tinted tiles instead of emoji on saturated fills.
- The built-in pages list claimed every page had been modified two hours ago. The "Modified …" time was invented on each request — the home page always read "2 hours ago", About "1 day ago", Privacy "3 days ago", on a server installed five minutes earlier and on pages nobody had ever opened. It shows the real edited time now, or nothing at all when a page has never been edited.
- Users table avatars showed a placeholder graphic instead of initials. A customer with no profile photo got a 400×400 captioned placeholder squeezed into a 48-pixel circle — an illegible grey smudge, on most rows. The cell shows the person's initials now, the way the rest of the platform renders someone with no photo.
- A failing scheduled job reported someone else's error. Triggering a job by hand that then failed showed a misleading unauthorized or not found, because the job's own internal failure was replayed as though it were the result of the trigger. A genuine failure is now reported as one, naming the job and its message, and a job that is already running is reported as already running.
- The support page's "Try live chat" button did nothing. Pressing it produced no response of any kind — no chat window, no message, no error. It opens live chat, the same as the page's two other chat buttons.
- Support ticket counters took a full row each on phones and tablets. The four counters stacked one per row on everything short of a large screen, pushing the tickets themselves well down the page, because the size the layout named had never been defined. Two across from small screens up, four across on large ones.
- Four addon listings had no link and a placeholder version. On a fresh install, Hummingbot Connector, Forex & Multi-Asset Trading, Binary AI Engine and Algo Trading Bots appeared in the admin extensions list with no product page to open and a version of 0.0.1 — six major versions behind, for a build that was never shipped. Each now carries its product page and its real starting version. Newly seeded listings only; an existing install keeps what it has.
- Every article claimed a 5-minute read before correcting itself. The reading time was published as a fixed "5 min" and replaced a moment later by the real figure in a different format — and counting the words meant handing the whole article to the browser as a live document, which made it re-download every image in the article purely to count words around them. It is worked out once now, in one format, and fetches nothing.
- The blog comment button was English in every language. The comment box's submit button read "Post Comment" and "Submitting..." on every non-English locale; it now uses the translated wording all 90 languages already carry.
- Skipping the binary tutorial showed the completion screen. Pressing Skip opened the "you finished it" celebration, which then needed a second dismissal to get out of. Skip closes the tutorial straight away, while still recording that the customer has seen it so it does not reappear.
- Selecting a binary position gave no visible confirmation. Clicking a position in the active-positions list did not mark it as selected in any way — the selected and unselected states resolved to exactly the same appearance — so a customer could not tell which position the panel's actions would apply to. The selected row now carries a border and a tint that stay after the pointer leaves.
- The binary page's mobile header appeared only after the page finished loading. On a phone, the symbol, price, balance and account mode drew nothing at all until start-up had finished, then popped into place. It draws from the first paint.