Bicrypto 6.7.9
Latest18 September 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
Core v6.7.9
Release Date: September 18, 2026 Tags: CRON, SCHEDULED TASKS, AVAILABILITY, DEPLOYMENT, ATTESTATION, LICENSING, MOBILE, COUNTRIES, RESIDENCE, PROFILES, CRM, ADMIN, PLATFORM HEALTH, WITHDRAWALS, STAKING, ADDONS, BLOG, FOOTER
Overview
No scheduled job ran at all on an install where the scheduler process had been stopped at boot, while the web and frontend apps reported healthy. The scheduler app inherited a variable it was never given, the backend refused that combination at boot, and pm2 stopped the app rather than restarting it. On a live install this had stopped every scheduled job, the Bitcoin deposit scanner among them, so a confirmed on-chain BTC deposit sat uncredited and looked indistinguishable from "no deposit yet".
Scheduled jobs whose code failed to load reported themselves as completed, with a 100% success rate. The scheduler could not tell "this extension is not installed on this server" from "this extension IS installed and its code threw while loading", so the Scheduled Tasks screen showed the job completing on every tick while not a line of its work had run. Seen on a live install, where all six on-chain staking jobs read green although their module had never loaded once.
The mobile app kept hiding regulated modules from customers in countries you had licensed. The residence gate covered 67 countries, and a customer recorded as "Iraq", "Bangladesh", "Nepal", "Russia" or any of the other ~180 resolved to no country at all — which is a refusal. The screens were simply not there, and when the customer went looking the app told them the platform is not licensed where they live.
If you run the Staking addon's on-chain (real) staking, read Upgrade Notes before you update.
Update Instructions
pnpm updatorThere is no schema change in this release — no new table, no seeder, and nothing to run by hand. Both halves matter: the scheduler, attestation and residence changes are server-side and need the backend restart; the footer and blog-editor changes live in the browser bundle and need the frontend rebuild.
- If you run the Staking addon's on-chain (real) staking, take this release now and read Upgrade Notes first. Staking v6.2.4 already expects a module that arrives here.
Upgrade Notes
On-chain staking needs this release
- This matters only if you run the Staking addon's on-chain (real) staking.
- Staking v6.2.4 (15 September 2026) loads the network-fee tracker from its new Core path, which does not exist on Core v6.7.8. If you have already installed Staking v6.2.4, take this Core update now; if you have not, take Core first or in the same maintenance window.
- Be aware of what the old Core does when the path is missing: the module never loads and none of the six on-chain staking jobs does any work, but on v6.7.8 they are still recorded as completed — so a green cron table over that period is not evidence that anything ran.
- After updating, open Admin → System → System Monitoring → Scheduled Tasks and check the six rows whose titles begin On-chain staking:.
Added
- Added a twelfth service, Operator Licences, to the admin dashboard's Platform Health card, which says out loud what the licence table is doing to the mobile app. It distinguishes six states: the table was never created; the table exists but could not be read; it holds no licence that is still in date (and it says whether that is because nothing was ever recorded or because everything has lapsed, with the row count); at least one regulated module has no live licence anywhere, named; a licence lapses within 30 days, naming the soonest module, its country and the days left; or full cover. Five of the six carry a remedy, printed under the row. On this card anything short of full cover shows as an amber warning rather than a red critical, and costs 5 points of the health score.
- Two things to expect on upgrade. An install that has recorded no licences starts amber, because an empty table is the shipped default-deny state and hides spot trading, futures, staking, P2P, token offerings, ecosystem wallets and copy trading from every native-app user in every country. And the check counts all seven of those modules whether or not you have bought the addons that provide six of them, so a core-only install that has attested spot trading is still told six modules reach nobody.
- Added the same report per-service on the admin system health API under
attestation, deliberately uncached there so it changes the moment you record a licence rather than being frozen until the next restart; that surface grades a missing, unreadable or wholly lapsed table as Down rather than amber. - Added one warning per restart in the backend log, the first time a signed-in app user whose country is known asks for their module list and the table holds no live row. Previously only a missing table was ever logged, and an empty one was silent everywhere.
Changed
- Changed where the two shared chain helpers live: Core now carries them in
backend/src/utils/.withdrawal-outcome.tsdecides whether a failed Solana, TON, Tron or Monero withdrawal broadcast is marked FAILED and refunded, or marked TIMEOUT and held for manual review — it errs towards TIMEOUT unless the node itself rejected the transaction, because refunding a withdrawal that did leave costs the platform the full amount.network-fee-tracker.tsrecords the network fee a TON, SOL or native-TRX withdrawal burns from the customer's own address, so the per-chain tracker does not drift above what the address really holds. Until now both files came only with the Ecosystem extension, which drops them intobackend/src/blockchains/. That is an addon folder, so on an install with the Staking addon but not Ecosystem the files were simply absent and the on-chain staking module could not load at all. The code is unchanged — the files only moved. Nothing in Core itself calls them: they are here so that every install has them. The Staking addon's on-chain engines now load the fee tracker from the new Core path, as do the Solana, TON and Tron chain services, and the Monero service loads the outcome helper. The old addon path is kept for one release cycle as a forwarder to the new one, so chain packages built before the move keep working. See Upgrade Notes. - Changed the four places a profile's country can be written — the user's own
profile save, the admin CRM add-user form, the admin CRM edit-user form and CSV user
import — to put
location.countryCodealongside the country text, through one shared helper instead of four opinions. A code sent by a client is accepted as ISO alpha-2, alpha-3 or a country name, common aliases such as "UK" and "Deutschland" included, and is stored as alpha-2; "Georgia" and "Jersey" are refused when sent as a name, because each is also somewhere else. A value that is not a country at all is now refused with "… is not a country we recognise. Pick one from the country list." instead of being saved and silently ignored. Where no code is sent — a CSV cannot carry one, and the CRM screens post only a free-text Country box — the code is worked out from that text, so existing users pick one up the next time anything saves their profile; the field is left unset only when neither the code nor the text resolves to a country. Nothing is backfilled, and nobody gains or loses access to a regulated module: the residence check already resolved that same text to that same code when it read the profile. The one place the stored code can read differently is geo restriction configured to trust the profile country, which understood only ISO codes and exact English names off a profile — once a code is stored, a profile whose country says "UK", "England" or "Deutschland" resolves to a country there too. - Changed
/api/user/modulesto separate "we do not know where you live" from "we are not licensed where you live". It used to answerNOT_ATTESTEDfor both. On a stock install the second is what every user gets — registration asks for no country, and a fresh database has no approved verification to read one from — so the manifest talked about licensing when all that was missing was the customer's own country. The unknown-country case now reportsRESIDENCE_UNKNOWN, which the customer can fix by adding a country to their profile or finishing verification. In the base product this covers the Spot trading entry, the one core module flagged as needing an attestation; the ecosystem, futures, P2P, staking, token-offering and copy-trading entries answer the same way where those addons are installed. No module becomes visible that was hidden — it stays not-visible either way — and the refusal the route itself throws has always told the two cases apart in its wording; only the manifest had not. Only the native app reads this field, so what a customer is told changes once their app build understands the new reason.
Fixed
No scheduled job ran at all — the scheduler process was stopped at boot while everything else looked healthy
ECO_PROCESS_ROLE is meant to be set per pm2 app by production.config.js and never
in .env. But an app inherits any variable the pm2 config does not set for it, and
the scheduler app set only CRON_MODE — so a value left in .env, or exported in the
shell pm2 was first started from, arrived at the scheduler as
ECO_PROCESS_ROLE=trading beside CRON_MODE=only. The backend refuses that pair at
boot (exit code 78), and because the scheduler app lists 78 among the exit codes pm2
must treat as a stop, pm2 stopped it instead of restarting it. The install was then
left with no scheduler at all while the web and frontend apps reported healthy. Not
hypothetical: on a live install this had stopped every scheduled job, the Bitcoin
deposit scanner among them, so a confirmed on-chain BTC deposit sat uncredited and
looked indistinguishable from "no deposit yet".
- Fixed by having the scheduler app in
production.config.jssetECO_PROCESS_ROLEexplicitly to an empty value — which declares no role — so a stray value in.env, or one inherited from the environment pm2 itself was started with, can no longer reach the scheduler and stop it. The boot refusal message now also names both places the value can leak in from, instead of stating that.envnever sets it.
Scheduled jobs whose code failed to load reported themselves as completed, with a 100% success rate
The scheduler could not tell "this extension is not installed on this server" from "this extension IS installed and its code threw while loading": the module loader collapsed both into the same empty answer. The job handler then returned normally, the run was recorded as a success, and the Scheduled Tasks screen showed the job completing on every tick while not a line of its work had run. Seen on a live install, where all six on-chain staking jobs read green although their module had never loaded once — the only trace was an IMPORT error buried in the process log.
- Fixed by keeping the real reason a load failed and having the scheduler use it: a job whose module is installed but broken is recorded as REFUSED instead of completed, so Admin → System → System Monitoring → Scheduled Tasks shows it refused, with the loader's own error message, what is consequently not happening, and what to do about it. Each broken module also raises one URGENT in-app and email alert to every Admin / Super Admin (Super Admin alone on a demo install), repeated every 15 minutes until the module loads. The alert is keyed to the module rather than the job, so a module feeding eight jobs sends one alert instead of eight while all eight jobs still show as refused. 64 scheduled jobs across the 20 modules the scheduler loads this way are covered — 19 of those modules belong to extensions, one (general investments) is part of the base product. An extension the operator never bought stays silent exactly as before: absence is not a fault.
The app kept hiding regulated modules from users in countries you had licensed
The residence gate carried its own country tables and between them they covered 67 countries: 67 three-letter codes and a hand-written list of 85 spelled-out names, mapping to exactly the same 67 places. A customer whose approved KYC application or profile recorded "Iraq", "Bangladesh", "Nepal", "Russia" or any of the other ~180 countries outside those lists resolved to no country at all — and unknown residence is a refusal. Spot trading, futures, staking, P2P, token offerings, ecosystem wallets and copy trading therefore stayed absent from the mobile app for those users even where the operator held a licence for exactly that country and had recorded it. Nothing failed and nothing was logged; the screens were simply not there, and when the customer went looking the app told them the platform is not licensed where they live — the one thing that was not true. Two-letter codes always resolved, so the failure hit only customers whose country had been recorded as a name or a three-letter code, which is what the mobile profile form, a free-text box, and most KYC vendors produce. The web platform is unaffected: this gate applies to native-app callers only.
- Fixed by resolving residence through the platform's single ISO 3166-1 table — 250 countries, the same data the geo-restriction console's country list is built from — so a customer's country resolves from its two-letter code, its three-letter code or its English name, for every country rather than 67 of them. The attestation form accepts the same forms when you record a licence: GB, GBR and United Kingdom all store GB. Two-letter strings that are not countries no longer slip through: "ZZ" and the "XX" unknown-country sentinel used to be stored as if they were countries, matching nobody, and are now refused when you type them. The spelled-out names Georgia and Jersey are refused on purpose — each is also somewhere else, and the value arrives from a free-text profile box where a wrong guess would grant access rather than deny it; their codes GE, GEO, JE and JEY still work.
Saving an edit to a recorded licence did nothing, with no error
The attestation console shipped with the Edit action switched on for every row, but
the endpoint behind it was never written. The table's edit form sends
PUT /api/admin/system/attestation/<id>, and no route existed at that path, so every
save 404ed. That particular failure is the one the data table shows nothing for: the
server answers a bare "Not Found", the fetch layer turns it into an error without
raising a message, and the edit form's failure path only writes to the browser
console — so the drawer stayed open with the operator's typing intact and nothing on
screen said the save had failed. The two fields that most need correcting were the ones
stranded: a mistyped country code serves the module to nobody, and an expiry renewed at
the regulator but not here stops serving that country the moment the old date passes,
with no grace period. The only workaround was to withdraw the row and record it again,
which loses the record of the original claim.
- Fixed the Edit action on Admin → System → Attestation so it saves. An edit is graded exactly like recording a new licence — the module must be one the platform serves, the country must resolve to an ISO 3166-1 code, and the expiry must still be in the future — because it makes the same claim. Only the six licence fields are writable, so an edit cannot bring a withdrawn row back by posting a cleared deletion date, or back-date when the claim was first made. One consequence to know: a row whose licence has already lapsed cannot be corrected without also giving it a future expiry, so fixing a typo on a lapsed row means renewing it at the same time. The admin audit trail now carries what changed — moving a row to a different module or country, and extending or shortening an expiry, are written onto the audit entry with the old and new values, so a licence that was quietly pushed out by a year is answerable afterwards. A withdrawn row still cannot be edited; restore it first.
Withdraw, Restore and Permanently delete failed on a single attestation row
The data table sends all three row actions to
DELETE /api/admin/system/attestation/<id>, distinguished by a query flag (nothing for
withdraw, restore=true for Restore, force=true for Permanently delete). Only the
bulk endpoint at /api/admin/system/attestation existed — the one the toolbar's
multi-select uses — so all three per-row items 404ed and raised "Could not complete
delete", "Could not complete permanent delete" or "Could not complete restore" against
a row the operator could see. With deleted rows shown it was worse: a withdrawn row's
only two actions, Restore and Permanently delete, were both dead ends.
- Fixed the per-row menu on Admin → System → Attestation: withdrawing one licence stops serving that module to residents of that country immediately, Restore puts a withdrawn row back, and Permanently delete purges it — matching what the toolbar's multi-select already did, so the same action can no longer succeed from one place and fail from the other. The audit trail already distinguishes the three, so a restore is recorded as a restore rather than as a deletion.
Norwegian users lost their country from the profile form every time they saved
The profile update route declared address, city, country and zip under location, but
not countryCode — and the request parser rewrites an undeclared string that reads like
a yes/no answer into a boolean before validation ever runs. Norway's ISO code is "NO",
the only code the country picker can send that collides with that list, so a Norwegian
who chose their country had countryCode: false written into their profile. Residence
and licensing were not affected: the same form writes the picked code into country as
well, that field was declared as a string and survived, and every reader falls back to
it. What broke was the form. The Country box came up empty on the next edit, and the
State and City boxes build their lists from the code, so they had nothing to offer
either.
- Fixed by declaring
location.countryCodeas a string on the profile update route, so the code the country picker sends is stored exactly as sent. A profile already holding the corruptedfalserepairs itself the next time that user saves: a non-string code is ignored, and the code is worked out from the country value the form still carries.
A social icon whose file had gone missing drew a broken-image mark in the site footer
The footer drew each social icon with a plain image element that had no failure path at all. An icon whose file was gone — most commonly a custom icon uploaded in Admin → Settings → Social & Links and later deleted, or not carried over in a server move — left the browser's broken-image mark, and the footer is drawn across the site: the home page, the blog, legal pages, support, finance, builder pages and both the user and admin dashboards. The icon tile in the social links editor had the same gap, so the one screen an operator would open to fix it also showed a broken picture and said nothing about why. Separately, the footer read-out in Admin → Studio carried its own hand-typed copy of the globe path, so the fallback had two definitions that could drift apart.
- Fixed so footer social icons degrade instead of breaking. An icon that fails to load is replaced by the platform's globe mark, the same mark the footer already substitutes for an icon name it does not recognise. The icon tile in that settings editor degrades the same way, falling back to the empty-icon glyph it already shows for a link with no icon, so the tile reads as "no usable icon" rather than as a broken picture. The footer's renderer, the footer's icon resolver and the footer read-out in Admin → Studio now take that globe mark from one shared definition, so a degraded icon looks identical wherever it is drawn.
The blog post preview showed a broken-image mark when the post's cover image was gone
The Preview dialog in the blog post editor drew the cover with a raw image tag and no failure path, so a post pointing at a cover file that had since been deleted showed the browser's broken-image mark — in the one dialog whose whole job is to show the author what a reader will see. Every other place the blog renders a cover already degraded to the standard placeholder; only the preview did not.
- Fixed the Preview dialog to draw the cover through the same image component the
rest of the blog uses, so a cover whose file no longer exists shows the standard
placeholder instead of the browser's broken-image mark. This covers both the admin
editor under
/admin/blog/postand the author's own editor under/blog/author/manage.