Bicrypto 6.7.2
26 August 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
Core v6.7.2
Release Date: August 26, 2026 Tags: SECURITY, KYC, PAYMENTS, DEPOSITS, REFUNDS, COMPLIANCE, INVESTMENT, LICENSING, MOBILE, TRADING, RPC, RELIABILITY, I18N
Overview
A release about money that leaves, documents that should not, and what a published app is allowed to carry. Four things were true of this platform until now and none was visible from any screen: identity documents sat in the public web root, fifteen of the sixteen deposit gateways could not take a refund back out of a wallet, a read-only API key could reach every withdrawal route, and the switches in Admin → Finance only ever emptied a dropdown.
It also adds the machinery a published mobile app needs — a recorded licence attestation per module and country, products a native client may not reach at all, and figures a native client is not sent — plus server-side price alerts, a reported-content queue, and account deletion that actually deletes.
Three things need a decision before you update. KYC documents stay public until you run the migration. Fixed-return investment plans ship blocked in the US, the UK and the EU. Regulated modules reach nobody in the mobile app until you record an attestation. Read Upgrade Notes.
Update Instructions
pnpm updatorBoot once with database sync enabled: this release adds tables for price alerts, reported content and licence attestations. Then read Upgrade Notes — four of them need action on your part, and two change what your customers can buy.
Highlights
Why the store rules are enforced on the server
Every product this platform must not sell from a phone is already absent from the mobile build: no tab, no screen, no route. That is necessary and it is not sufficient, and the reason is worth stating because it shapes a dozen entries below.
A removal that lives only in the client is one a reviewer cannot verify and an operator can undo — and it is the exact shape that reads worse than never having removed anything. A product hidden from the interface whose endpoint still answers a mobile request is not disabled, it is concealed, and concealment of a banned product is a more serious finding than carrying one. The penalty lands on the developer account rather than on the build.
So the app cannot ask and the server will not answer. None of it is a security boundary — the header a native client sends is one any caller can decline to send — and none of it changes the web, where these products are legal in the jurisdictions you are licensed for. What it buys is a claim about the server that is true whatever a future build of the app does.
Upgrade Notes
KYC documents stay public until you run the migration
-
Deploying this build changes where new uploads go. Every document already on disk keeps its old location and its old public URL, and historical applications still point there.
-
Run the migration on each install, after the backend is deployed. It reports first and changes nothing without
--apply:node backend/scripts/kyc-documents-private.mjs node backend/scripts/kyc-documents-private.mjs --apply -
It is safe to re-run, and it copies and verifies before it removes anything.
-
If you want the store on a separate volume, set
KYC_DOCUMENT_DIRin.envbefore running it — otherwise the documents move somewhere the server does not look. -
Add that directory to your backups. Losing it means asking every verified customer to re-submit.
Regulated modules reach nobody on mobile until you attest
- A new table records which countries you hold a licence to serve, per module. It starts empty, and an empty table serves the regulated modules to nobody — trade, ecosystem, futures, P2P, staking and token offerings all disappear from the app until you list a country for them.
- That is the deliberate direction. Serving a licensed product to somebody whose country nobody knows is the case the licence exists to constrain, so unknown residence is a refusal here, not a pass.
- Go to System → Geo → Licence Attestations and record what you actually hold.
- This applies to native app clients only. The web is untouched and follows your own licensing timetable.
- Residence comes from the KYC application you approved, and from the customer's self-declared profile only as a fallback. It is deliberately not read from an IP address: a VPN must not unlock a product you are not licensed to sell.
- Wallet, markets, account, support, the help centre, the store and the news feed are not gated by this at all.
Investment plans are blocked in the US, the UK and the EU
- This applies to upgrades, not just fresh installs. If you currently sell investment plans to customers in those territories, they will be refused after you update.
- To change it, go to Finance → Investment → Compliance. Adding territories takes effect immediately; removing one of the defaults takes effect only after a Super Admin accepts the risk statement on that screen.
- The acceptance records who accepted, when, and the exact wording, and it appears in the admin audit trail.
- Existing positions are unaffected. Cancelling and withdrawing are never gated.
Stripe, PayPal and eWAY need a webhook secret
- Give each vendor the usual
https://your-site.example/api/finance/deposit/fiat/<alias>/webhookURL, then set the matching key:APP_STRIPE_WEBHOOK_SECRET— the signing secret (whsec_…) from the Stripe endpoint you create.APP_PAYPAL_WEBHOOK_ID— the webhook's ID from the PayPal developer dashboard.APP_EWAY_WEBHOOK_SECRET— the notification signing key from MYeWAY.
- Without the key the webhook is refused with 503 rather than trusted. That is deliberate: the route is public and it credits wallets.
- Nothing breaks if you skip this. You are left exactly where you were before this release, which is a customer who closes the tab not being credited.
Your API keys have narrower reach than they did
- Six entries in the API-key scope map named routes that do not exist, and every one of them was a withdrawal or a transfer. Combined with a fail-open default, the real routes behind them accepted a key holding no scopes at all.
- Review your issued API keys. A key you created as read-only could reach spot and ecosystem withdrawal, staking withdrawal, forex withdrawal, NFT transfer, ecosystem wallet transfer, P2P escrow release and ICO token release.
- After this release each of those demands its
withdrawortransferscope explicitly. An integration relying on the old behaviour will start receiving 403 until you re-scope its key.
Mobile push needs your own Firebase project, and is off until it has one
- Optional. The app builds, runs and publishes with no Firebase project at all — it simply never asks for notification permission and never receives anything.
- It has to work that way: the app ships as source, so a configuration file committed to it would be ours, registering your customers' devices against our project.
- To enable it, put
google-services.jsoninandroid/app/and, for iOS,GoogleService-Info.plistin the Runner target with an APNs key uploaded to Firebase. The Gradle output tells you which state you are in. - Then configure the server half —
FCM_PROJECT_ID,FCM_CLIENT_EMAILandFCM_PRIVATE_KEYfrom a service account in the same project. Neither half reports the other's absence. - Both files are gitignored, for the same reason
android/key.propertiesis:package_for_codecanyon.shcopiesandroid/andios/wholesale.
Give each chain a second RPC endpoint
-
Optional, and the single biggest reliability gain in this release for anyone running on free-tier public endpoints.
-
Either put a list in the key you already have, or use the new fallback key:
ETH_MAINNET_RPC="https://primary.example/v3/KEY, https://backup.example" ETH_MAINNET_RPC_FALLBACK="https://backup-1.example, https://backup-2.example" -
A single endpoint behaves exactly as it always did, including still needing a restart after an
.envchange.
Check your privacy policy and your page-builder sections
- The stub privacy policy a fresh install serves has been replaced with a template
that describes the collection this software actually performs. It is still a
template, it still contains
[SQUARE BRACKETS], and it still needs your legal entity, jurisdiction and retention periods before you rely on it. - The page-builder section templates shipped capability claims the software cannot make true — FIX 4.4 support, a Singapore colocation, SOC 2 Type II, ISO 27001, yearly PwC audits and fourteen separate 99.99% uptime SLAs. They have been replaced with neutral placeholder copy. If you have already published a page built from one of those sections, the old sentence is still on your site — the templates seed a page and do not govern it afterwards.
Added
KYC documents are served from a private store
Passports and national identity cards were written to
frontend/public/uploads/kyc/documents/ and addressed as /uploads/…. That
directory is served with no session and no permission check by two independent
routes — the backend's static handler, which runs before any authentication, and
the frontend's own handling of its public folder. A previous release replaced
guessable filenames with 192 random bits, which closed enumeration and nothing
else: anyone who obtained a URL still read the document.
- Added a document store outside the web root, at
backend/storage/kyc/documents/, overridable withKYC_DOCUMENT_DIR. - Added an authenticated serve route that allows the applicant and operators holding KYC-review permission, and answers 404 to everyone else — 403 would confirm that the applicant exists and has filed.
- Added
backend/scripts/kyc-documents-private.mjs, which moves the documents already on disk and rewrites the stored paths. Dry-run by default. - Changed the old
/uploads/kyc/path to be refused permanently by both the backend and the frontend, so a restored backup or a rolled-back deployment cannot reopen the hole. - PDFs now download rather than opening in a tab, because the browser's PDF viewer executes scripting a PDF may carry on the origin where your admin session lives. Images still display inline.
Licence attestations, per module and per country
Which countries an operator may serve a regulated product to was a question this platform had never asked. Every module reached every resident of every country, and the only geo control that existed was a block list — which answers the opposite question and, by design, lets an unknown country through.
- Added System → Geo → Licence Attestations, recording the countries you hold a licence for, per module, with the admin who recorded it.
- Added a default-deny gate in front of spot trading, ecosystem, futures, P2P, staking and token offerings for native app clients: no attestation covering the customer's country of residence, no module.
- Added the same check at the routes that commit the customer — placing an order, opening a position, staking, listing or taking a P2P offer, buying into a token sale — because withholding a screen stops a screen being drawn and stops nothing else. Exits are never gated.
- Residence is resolved from the approved KYC application first and the self-declared profile second, and never from an IP address or a device locale.
Price alerts that fire whether or not the tab is open
The chart has evaluated alerts in the browser for a long time. That is only an alert while somebody is watching, which is the case an alert exists for the absence of.
- Added server-side price alerts on spot, ecosystem and futures symbols, with the alert list, the chart lines and the trade panel all reading one record.
- Added a cron evaluator that applies the same rules the browser applies —
including the noise gate being a fraction of the price rather than an absolute,
so a pair quoted at
0.00002341is not a pair whose alerts never fire. - Added alert management from the chart itself: drag a line to re-price an alert, and edit or delete it from the panel.
- A discarded tick does not advance the baseline, so a series of small moves accumulates instead of letting the price walk through a target one sub-threshold step at a time.
The trade terminal reaches the order types the engine now holds
- Added OCO and stop tabs to the spot order form, and time-in-force and post-only controls to the Pro terminal's advanced options. Both appear only on markets where the engine can actually hold the instruction — a conditional order rests outside the matching engine and only ecosystem markets have somewhere to put one, so offering the tab elsewhere produced a refusal on every submit. Switching from an ecosystem market to a provider one with the tab open falls back to limit rather than leaving an empty panel.
- Added a sweep summary on the Pro order book: hovering a row now shows the volume-weighted average price, the base size and the quote value of everything between the touch of the book and the row under the pointer. The tint already said which levels would be crossed; a trader still had to add up a column the book had already added up.
- The summary reads the book's own running totals rather than re-summing the visible rows. On a grouped book one displayed row is several raw levels folded together, and re-summing what is on screen would quietly disagree with the depth bars beside it.
A queue for content your users report
- Added a report control on comments, posts and listings, and Blog → Reported Content for ruling on what comes in.
- Reporting hides nothing. An item stays up until somebody rules on it, which is the only version of this that cannot be used to silence a counterparty.
- Added rate limiting and de-duplication per reporter, so one person cannot file the same complaint repeatedly to make it look like many.
Deleting an account now deletes the person
user rows are soft-deleted: the account became unreachable and every column
survived it — email, password hash, given and family name, phone, avatar, wallet
address and the profile blob. Freezing an account is named in Google Play's
account-deletion policy as the thing that does not qualify.
- Added anonymisation on confirmed deletion: identity columns are overwritten, the username is released and the email becomes a per-account tombstone so the real address can register again.
- Wallets, transactions, KYC applications and support tickets are deliberately kept. Anti-money-laundering rules require them for years after an account closes, and the disclosure shown before confirming says so.
- Added a Delete account link to the site footer. The page existed and was linked from nowhere, which is not reachable by somebody who has already uninstalled the app.
Refunds and chargebacks reverse the customer's wallet
Only dLocal did this. On every other gateway the money left your merchant account and the customer kept the balance.
- Added deposit reversal to Adyen, Authorize.Net, Mollie, Paysafe, Paystack, PayU, Paytm, Stripe, PayPal and eWAY, joining dLocal.
- The reversal takes back what the customer actually received — the gross charge minus the platform fee withheld at deposit time — and writes the fee back as a platform loss.
- Where the wallet cannot cover it, whatever is there is taken, the remainder is recorded as a pending receivable, the wallet is suspended and every admin is notified. Previously this case produced a log line and no ledger entry at all.
- A partial refund is refused rather than scaled, and logged for you to handle by hand. Reversing a full deposit because 10% was refunded would take ten times too much.
- 2checkout, iPay88, Klarna, PayFast and TransFi still cannot reverse — each sends no usable refund notification — and are listed in the deposit gateway guide.
Stripe, PayPal and eWAY confirm payments independently of the browser
- Added a webhook to all three. They previously confirmed a payment only when the customer's browser came back from the vendor, so a closed tab, a dropped connection or a blocked popup meant a charge with no credit and no record.
- Both paths credit under the same idempotency key, so the webhook and the browser return cannot double-credit the same payment.
- All three refuse the webhook when their secret is not configured.
Chains fall back to a second RPC endpoint
- Added support for several endpoints per chain, as a comma-separated list in
{CHAIN}_{NETWORK}_RPCor in the new{CHAIN}_{NETWORK}_RPC_FALLBACK. - The platform orders them by measured latency, takes one out of rotation after three consecutive failures with a cooldown that backs off from 15 seconds to a five-minute ceiling, and brings it back on its own when it answers again.
- Endpoint URLs are redacted in logs and on the diagnostics screen, because the path segment of an Infura or Alchemy URL is the credential.
The mobile app can receive push notifications
PushChannel, FCMProvider, mobileDevice and the subscribe endpoint were all
built and delivering to nothing: there was no Firebase dependency in the app, no
configuration file, and not one device-token reference anywhere in it.
- Added device registration on sign-in, on a restored session, after Google sign-in and after two-factor — all four of the ways somebody arrives authenticated, not just a fresh sign-in, because a restored session is how nearly every customer arrives.
- Added re-registration when Firebase rotates the token. A registration made once and never refreshed goes stale silently — the server keeps delivering to a token nobody holds and the customer simply stops receiving anything.
- Added unregistration on logout, before the session is cleared. Without it the next person to use the handset receives the previous customer's balance and security notifications.
- Added display of notifications that arrive while the app is open, which the operating system does not do on its own.
- Firebase is optional at build time: the Gradle plugin is applied only when your configuration file is present, so an install without one builds and runs with push switched off rather than failing to compile.
The mobile app speaks more than English
It declared no supported languages, so it answered every device in English regardless of the customer's setting. That is an Apple 4.8 problem as well as a sales one.
- Added the localisation pipeline, with the generated files committed rather than left in a build directory — so the source a buyer receives resolves as it is, instead of showing unresolved references until they happen to run the generator.
- Added translations for the sign-in and registration screens, including every message that refuses somebody. A customer who cannot read why they were refused cannot fix it.
- Adding a language is adding a file: the list of supported locales is derived from the translation files present, not hand-written.
- The framework's own strings — the date picker, the text-selection menu, the buttons inside system dialogs — are translated too. Without that a fully translated app still shows English inside every built-in control.
A territory gate on fixed-return investment plans
A plan's return is a percentage typed into an admin form and paid by the operator; nothing trades, lends or stakes the customer's money. In the US, the UK and the EU, offering that is operating a collective investment scheme.
- Added a default block on the United States, the United Kingdom and the 27 EU member states, applying to upgrades as well as fresh installs.
- Added Finance → Investment → Compliance, which shows the list you saved and the list actually being enforced separately, and names any territory you asked to unblock that is not yet taking effect.
- Removing a default territory requires a Super Admin to accept a versioned risk statement; the accepting account, timestamp and exact wording are recorded from the session and appear in the audit trail.
- Applies to the core investment product, Forex Investment and AI Investments — the same settlement rule under three names. Exits are never gated.
- Added a plain statement on the purchase form that the rate is set by the operator and paid from the operator's own funds, and that the customer's capital is at risk and not covered by any compensation scheme.
Products and figures a published app never receives
- Added a catalogue of the products no published app may carry — binary options, fixed-return AI and forex investment plans, and the multi-level referral programme — with the reason for each written for a person to read.
- Added a route-level refusal for the ones an app could otherwise reach: an AI Investment request from a native client is answered 403 with that reason. Forex investment plans and the referral programme are withheld from the module manifest instead, so the app is never told they exist.
- Added a server-side refusal for two capabilities a phone is the wrong place for: buying or selling a trading strategy, and minting an API credential. A mobile session can stop, disable and revoke a bot key; it can never create one or be shown a secret.
- Added stripping of forward-looking figures from native responses — advertised APR and APY, leverage ceilings, expected returns and projected profit. Realised profit, what a position has actually earned and a filled price are the customer's own record and are untouched.
- Added stripping of
<iframe>and outbound links from operator-authored HTML served to a native client. That content is written after an app is reviewed and is the one part of a compiled app a remote-content rule still reaches. - None of these refusals names a place to go and do the thing instead. That pointer is itself the violation the rule describes.
Backups are pruned, and reach the right database
- Added retention to the database backup: the ten most recent dumps are kept
and older ones removed, configurable with
DB_BACKUP_RETAIN. Nothing pruned before, so the directory grew without bound on any install that used the feature. - Only files matching the backup's own naming pattern are considered, so a dump you copied in by hand for a restore is never deleted by a later run.
The installer configures log rotation and survives a reboot
- Added
pm2 saveafter the boot hook is installed. Without it the hook fires on reboot, finds an empty process list and brings nothing back — the site stays down until somebody signs in and starts it by hand. - Added
pm2-logrotate, capped at 10 MB × 10 files per process and compressed. Nothing rotated pm2 logs before; a once-a-minute error loop is a slow disk-full outage, and one install reached 7 GB of logs before anybody noticed.
Changed
The Pro terminal is the default
- Changed
tradingProEnabledto default on. It defaulted off, so every fresh install — and every buyer evaluating the product — landed on the classic page and judged the platform by the weaker of the two terminals it ships. - An install without the Pro module is unaffected: the trade page probes for it and falls back to the classic interface when it is not there.
A referral link no longer discloses the referrer's legal name
- Changed the public referrer lookup to return the username and avatar. It
returned
firstNameandlastName, on an unauthenticated route keyed on the referral code — so anybody holding a referral link read the referrer's legal name, and anybody enumerating ids could harvest them. - The registration screen shows the username instead. The username is the only name other users are entitled to see.
Usernames are screened, not just reserved
- Added an abuse screen on handle creation, matched by whole token after folding the common digit substitutions. A username is the one piece of user-authored text every counterparty is forced to read and it cannot be reported away.
- Matching by token rather than substring is the whole point:
Scunthorpe,assassin1andLightwaterare accepted, andfuck_youandf4ggotare not. - The list is English and deliberately short. It is a floor; the report queue above is what handles what a list cannot.
The privacy policy a fresh install serves describes this platform
- Changed the default privacy page from a four-line stub that named no data practice at all. Nothing seeds that table, so the stub was what every fresh install served — and what an app-store reviewer read.
- It remains a template, in square brackets, and still needs your legal entity, jurisdiction, retention periods and regulator before you rely on it.
Settings that must not be typed into a text box
- Changed
investmentGeoBlockListandinvestmentRiskAcknowledgementto be refused by System → Settings. That screen writes what it is given without validating it, and a block list containingUnited Statesinstead ofUSwould look like a policy and match nobody. - The risk acknowledgement records who accepted legal responsibility. Writable from a request body, it would be an acceptance an admin could attribute to somebody else, which is worse than having no record.
Smaller changes
- Changed replacing a KYC document to delete the one it replaces. It
previously could not, because a stored path proved nothing about who owned it,
so every superseded passport accumulated on disk. The migration's
_orphanedsweep collects them. - Changed the page-builder section templates to neutral placeholder copy wherever they claimed a certification, an audit, an SLA or a protocol the software does not provide.
- Changed the installed-app shortcut in the web manifest to open the dashboard rather than the marketing home page.
- Changed the off state of every switch in the product to a track that can actually be seen. The knob and the track came from one step apart on the same ground ramp — measured at 1.22:1 in dark and 1.03:1 in light, against the 3:1 that non-text controls require — so the control had no readable state. It now measures above 4.4:1 in both themes, and only the off state moved.
Fixed
A read-only API key could empty an account
The scope map that decides which routes demand a withdraw or transfer scope
was written as the URLs somebody expected rather than read off the route tree.
Six of its entries named paths that have never existed — spot and ecosystem
withdrawal, staking withdrawal, both forex withdrawals and NFT transfer. Because
an unmapped money route falls through to the permissive branch, the real
routes behind those names accepted a key holding no scopes at all.
- Fixed all six entries to the paths that exist.
- Added the money routes that were never mapped at all: ecosystem wallet-to-wallet transfer, which moves a balance to any other account; P2P escrow release; the copy-trading allocation withdrawal; and ICO token release.
- Added a check that walks the route tree and fails the build on any scope entry that matches nothing, so a prefix cannot go stale again in silence.
Operator HTML reached the browser unsanitised on first paint
Every screen that renders operator-authored HTML is a client component, and a client component still renders on the server for the first request. The sanitiser returned its input unchanged there, because it had no DOM — and React never re-processes that content on hydration, so it was never cleaned up afterwards either.
- Fixed the sanitiser to run on the server as well as in the browser.
Every switch in Admin → Finance was a dropdown filter
The listing endpoint that populates the deposit and withdraw wizard honoured the
fiatWallets and spotWallets master switches and every per-currency,
per-method and per-gateway status flag. The routes that create the
transaction re-read the same rows by id and asked none of those questions. So
switching a gateway off removed it from the wizard and left its endpoint working
for anyone posting the id directly — which is how a deposit reached the queue
titled "Deposit 100 USD by Paystack" on an install where Paystack had never been
enabled.
- Fixed the deposit, withdrawal and transfer routes to ask the same questions the wizard asks, using the same queries so the two cannot drift.
- Transferring into a disabled currency is refused; transferring out of one never is. Money must always be able to leave, or delisting an asset strands every customer holding it.
A spot stop order was submitted as something else
The spot order route had two branches, limit and market, and rejected nothing
else. A stop order therefore took neither: the balance check and the hold were
computed at the limit price, and the exchange was handed an order typed stop
with no trigger price and no limit price. The route never read the trigger level
at all, so the price the trader set never left their device.
- Fixed the route to refuse an order type it cannot honour, with a message saying why, instead of placing something else with the customer's money.
Cross-origin clients were blocked at the preflight, silently
There were two Access-Control-Allow-Headers lists and they disagreed. Only one
of them reaches a browser, and it was missing platform, client-platform,
app-version, client-device-id and Origin — so any WebView or cross-origin
browser client sending them was refused before the request was made, with nothing
logged. The native app never preflights, which is exactly why nobody noticed.
- Fixed both lists to the same union, with a check that keeps them equal.
Right-to-left locales rendered left-to-right
Direction was decided by comparing the locale to "ar", on a wrapper element
inside the page. Persian, Urdu, Hebrew, Pashto, Sindhi, Divehi and Kurdish all
rendered the wrong way round — Persian is one of the most complete locales in the
catalogue and was unusable for that one comparison.
- Fixed the check to cover every right-to-left locale.
- Changed
dirto be set on the document element, on the server. Logical CSS properties, the scrollbar side, text selection and the browser's own bidi handling all key off it, and setting it on an inner element meant the first paint was left-to-right and flipped after hydration.
The news feed disappeared from the mobile app
- Fixed the module manifest, which did not list the news module at all. The dashboard filters tiles by what the manifest carries and the feed had no other way in, so the whole feature vanished from every install the moment the tile list started asking the server.
A refunded or charged-back deposit left the customer holding the money
Covered above under Added, and worth stating as the defect it was: the platform had no way to record that money it had credited was taken back.
- Fixed the reversal amount to be gross minus fee. A 100 USD deposit with a 3 USD fee credited 97; reversing the gross would have charged the customer the fee a second time on a payment being undone.
Database backups ignored the configured port
- Fixed the backup to read
DB_PORT, which every other consumer of the database honours. On an install where MySQL is not on 3306 the dump either failed outright or — worse — connected to whatever else was listening there and backed up the wrong database.
Removed
- Removed the unverifiable capability claims from the page-builder section templates: FIX 4.4 and 5.0 gateway support, sub-400-microsecond Singapore colocation, SOC 2 Type II, ISO 27001, yearly PwC audits and fourteen 99.99% uptime SLAs. None of them exists, and an operator could not make them true by editing.