Bicrypto 6.7.5

2 September 2026

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

PRIVACYSECURITYTWO-FACTOREMAILSMSWALLETSWITHDRAWALSNFTP2PBLOGCONFIGURATIONBUILDI18NTRANSLATIONSDEPENDENCIESIMAGES

Core v6.7.5

Release Date: September 2, 2026 Tags: PRIVACY, SECURITY, TWO-FACTOR, EMAIL, SMS, WALLETS, WITHDRAWALS, NFT, P2P, BLOG, CONFIGURATION, BUILD, I18N, TRANSLATIONS, DEPENDENCIES, IMAGES

Overview

A privacy release, a two-factor one, and a wallet-configuration one. There is no schema change, no migration, and nothing to run by hand.

Take the privacy work first, because it was already live. Pages that need no sign-in — the P2P offer board, the NFT marketplace, the copy-trading leaderboard, storefront product pages — sent anonymous visitors the first and last name on each customer's account, and the NFT top-creators board their email address as well. The update stops that but does not retrieve what is already out; Upgrade Notes says what to do about it.

Take it next if you sell SMS two-factor: the phone number those codes go to could be changed from a signed-in session alone — no password, no code — which is a way past the second factor without ever touching it. Codes delivered by email or SMS now last the five minutes the email has always promised, and are spent when used.

One value wants a decision before the rebuild. A frontend built without NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID turned every wallet screen into a whole-page 500; those screens now explain themselves, but if you sell any wallet feature, set the id first — see Upgrade Notes.

Update Instructions

pnpm updator

There is no schema change in this release — no new table, no seeder, and nothing to run by hand. Both halves of pnpm updator matter here: the privacy and two-factor fixes are server-side and need the backend restart, and the wallet and image fixes live in the browser bundle and need the frontend rebuild. Neither one alone is enough.

If you sell any wallet feature and have never set NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID, set it before the rebuild — the value is compiled into the bundle, so a restart does not pick it up. It is the only value in this release that needs a decision, and it is a ten-second check; see Upgrade Notes.

A code sent by email or SMS before the restart is not accepted after it, so anyone mid-sign-in at that moment asks for a new one. Past withdrawals pick up their transaction hash on their own, with nothing to run.


Upgrade Notes

Customer names, and creator email addresses, were readable without an account

Until this release, pages that need no sign-in sent the first and last name on each customer's account — the name on their identity document — to anyone who loaded them. The surfaces were the P2P offer board, the NFT marketplace and the collection, token, listing, activity, bid, offer and auction feeds beneath it, the NFT market socket, the copy-trading leaderboard, and storefront product and landing pages. The NFT top-creators board additionally sent the account's email address.

There is nothing to run and no record to repair — the change is in what the server sends, so it takes effect on the backend restart. What is worth your attention is that the data was already out, and installing the update does not retrieve it.

  • Treat the exposure as having run from the day you first published any of those pages until the day you install this release, and handle it under whatever disclosure rules apply to you.
  • If you would rather customers were named by a handle than by their given name, ask them to set one under Profile → Personal Info. An account without one is now shown by its given name alone, never the surname.

Check NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID before you rebuild

grep NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID .env
  • If the line is absent or empty and you sell any wallet feature — wallet sign-in, Profile → Wallet linking, the NFT marketplace's wallet actions, or Web3 Wallet & On-Chain Trading — create a free project at cloud.reown.com, set the id, and only then rebuild. The value is compiled into the browser bundle: a restart does not pick it up. The backend reads the same variable at start-up to verify wallet signatures, so restart it as well.
  • If you sell none of those, leave it empty. The frontend prints one warning and the wallet screens show a "not set up yet" card. Nothing else is affected.
  • Up to Core 6.6.1 the frontend silently fell back to a shared project id, so wallet connection could appear to work without the variable while signing still failed. If that describes your install, set your own id anyway — the shared one is not yours and can be withdrawn at any time.

The five new sentences on the wallet card ship in English in all ninety languages until the next translation pass.


Added

  • Added NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID="" to .env.example, under a new Wallet connections heading that says where the id comes from, which screens need it, and that it is compiled into the bundle at build time.
  • Added a warning from the frontend — at build, and again whenever the frontend starts — while the variable is empty, naming the screens that will show the card until it is set.

The transaction hash of a withdrawal, where the customer can reach it

A withdrawal's on-chain hash is the only evidence, outside this platform's own database, that the coins actually left. Customers could already see it for a deposit — the deposit screen has linked to a block explorer for some time — but nowhere for a withdrawal. Their history listed this platform's internal reference instead, which is not a hash and which no explorer can look up, so "where did my withdrawal actually go?" could only be answered by your support team reading it out of the admin panel.

Where no explorer is known for a chain, the hash is still shown and still copyable — just not linked. That is deliberate: a link to the wrong explorer loads a real page telling a customer their own withdrawal does not exist, which is worse than offering no link. The same caution governs withdrawals an administrator settles by hand, which are recorded against the field the platform also uses for bank wire references: such a reference becomes a link only when it has the shape of a genuine hash for that coin's chain, so a wire reference or a ticket number stays plain text. How administrators record it is unchanged.

Past withdrawals pick up the hash on their own. Self-custody withdrawals have always had theirs on file, so they become clickable as soon as the update is installed. Two sets stay without one, and neither is recoverable — payouts through your connected exchange that had already finished, because no hash was ever kept for them, and withdrawals on a chain you added yourself that were made before this update, because the explorer address is recorded on the withdrawal as it is created. On a testnet installation, older self-custody withdrawals do not say which network they settled on and so still point at the mainnet explorer.

  • Added a Transaction Hash column to the transaction history, and to the history on each wallet's own page, showing the hash with a copy button and — where the chain is one the platform recognises — a link that opens it on that chain's block explorer. Opening the row shows the same hash in full. Anything that never touched a chain, such as a fiat payout, an internal transfer or an order fill, shows a dash.
  • Added the hash to withdrawals sent through your connected exchange, which never recorded one at all — all that was kept was the exchange's own withdrawal reference. The exchange publishes the hash only once it actually broadcasts the payout, minutes after accepting it, so it is picked up on the next reconciliation run, and straight away for the exchanges that return it immediately. This covers both a customer's own withdrawal and one an administrator approves.
  • Added the network a payout went out on to the withdrawal record. Without it a testnet installation had no basis for a correct link and would have sent the customer to a mainnet explorer, which reports their transaction as non-existent.
  • Added links for chains you added yourself, using the explorer address you entered when you set the chain up. That address was previously readable only from the admin panel, so a customer paid out on a custom chain had no way to view their own transaction.

Changed

  • Changed the public pages listed in Upgrade Notes no longer send a surname or an email address at all. A single name field carries the public form of the name — the handle where there is one, otherwise the given name and an initial — and the surname is deleted before the response leaves the server.
  • Changed blog author and commenter names are now resolved by that same rule instead of being assembled in the browser from a first and a last name. The assembly had been producing correct output only because the server deleted the surname first; it was one un-rewritten response away from printing it.
  • Changed the framework moves from Next.js 16.3.3 to 16.3.4, and the one thing you can observe is that AVIF images are served again. 16.3.3 was a security release that closed a remote-code-execution hole in the image optimizer reachable through AVIF files, and AVIF output was switched off as part of that fix; this platform asks for AVIF, so on 16.3.3 every optimised image fell back to WebP. 16.3.4 is the follow-up that restores it, and a browser that accepts AVIF gets the smaller file. There is nothing to configure — the image format list is unchanged, and the security fix from 16.3.3 stays in place.

Fixed

The platform has a single answer to "what may one customer be told another is called" — the handle they chose, or their given name and an initial, never the surname. Twenty-five endpoints never asked it. They read the first and last name straight off the account and sent both.

What made this survive review is that the repair has two halves, and each looks complete without the other: a page can ask for the handle and still publish the surname, because asking is not the same as rewriting. The P2P offer board is the clearest case. It already selected the handle. It already carried a comment explaining why the email address had been taken out. Every sign of care, and the surname shipped anyway, because nothing rewrote the rows on the way out.

Measured against a live installation before the fix, one request to the NFT activity feed came back with thirty surnames, the listing feed twenty, a storefront product page fifteen, and the P2P board one for every trader holding an open offer.

  • Fixed all twenty-five, which now send the public name and delete the surname before the response leaves the server.
  • Fixed the same assembly in the pages that displayed those responses, so a surname reaching the browser in future cannot be rendered even by accident.

Beside each creator that board returned the account's email address. It also filled a field named username by joining the creator's first and last name — so the public API reported the name on someone's identity document as though it were the handle they had chosen for themselves.

  • Fixed the email address is no longer read from the database at all, and username now carries the creator's handle, falling back to the display name they set on their creator profile.

The blog author card published the account's role, which reads as the operator's name

Under every article, the author card showed the author's platform role. A new install creates its operator account as "Super" "Admin" and names its top role "Super Admin", so on a default install that line read as the operator's own first and last name — which is how it was reported. On any install it told every visitor which account holds super-admin. The role was also being sent in the article's data, not merely drawn on the page, so removing it from the page alone would not have been enough.

  • Fixed the card now shows the author's handle, or the word "Author" where they have not chosen one, and the role is no longer sent to anonymous callers.

A handle a user had chosen never reached some pages

Two queries asked for the name columns but not the handle column. Because the public name is resolved from the handle first, a column that was never fetched reads as "this person has no handle" rather than as a mistake — so those pages fell back to the given name for everybody, with nothing logged and nothing to notice.

  • Fixed the related-articles bylines under a blog post, and the binary trading leaderboard, which showed a derived mask such as "O. I." even for traders who had set a handle.

An NFT creator's portfolio page was titled "undefined undefined"

The page built its browser title, its social-preview title and its description from name fields on the creator record — where those fields do not exist, because the name lives on the account nested inside it. Every creator who had not set a display name got the literal text "undefined undefined" in the browser tab and in any shared link.

  • Fixed the page reads the account, through the same public-name rule as everywhere else, and falls back to "NFT Creator".

The withdrawal confirmation email gave the customer an identifier nobody can look up

The confirmation email sent after a self-custody withdrawal has always carried a Transaction ID line, and it has always been the wrong number. It printed this platform's own internal record identifier rather than the on-chain hash — a value no block explorer, exchange or counterparty can do anything with. A customer who took that line to their receiving exchange to prove they had sent funds was giving them something meaningless.

The email is composed from a copy of the withdrawal that was read out of the database before the payout was broadcast, and the hash is written straight to the database afterwards without refreshing that copy — so the line fell back to the internal identifier every time. This affected every chain, including batched Bitcoin, Litecoin, Dogecoin and Dash withdrawals.

  • Fixed the withdrawal is re-read before the email is composed, so the Transaction ID line now carries the real on-chain hash. If that re-read fails the email still goes out, with the identifier it had before.

On the deposit screen, the link that opens a confirmed deposit on a block explorer dropped the customer on a generic blockchain search page that finds nothing — for seven of the twenty chains the ecosystem supports. Fantom was listed in the platform's explorer table under a name the platform itself never uses, so it never matched; Base, Celo, Cronos, HECO, RSK and MO had no entry at all. Both cases fell through to the same search page.

  • Fixed all seven now open the transaction on that chain's own explorer. The other thirteen chains are unchanged, and this same table is what the new withdrawal links are built from, so deposits and withdrawals can no longer disagree about where a chain is viewed.

An SMS second factor could be redirected to someone else's phone

This is the most serious thing in this release, and it did not need the second factor at all. Ten places text a two-factor code to the phone number on the account — signing in, confirming a withdrawal, releasing P2P escrow, changing a password. That number could be changed by anyone with a signed-in session: the profile update took a new number with no password and no code, and so did the phone-verification flow and two-factor setup itself. phoneVerified was recorded but never actually consulted before sending.

So a stolen session was enough to point the number at an attacker's handset, ask the platform to send a verification code, and receive a code computed from the account's own enrolled secret — which the withdrawal and escrow-release prompts then accept.

  • Fixed while SMS two-factor is enabled, changing or clearing the account's phone number requires a current code from that number or one of the account's recovery codes, on every route that can write it — the profile update, phone verification, and both two-factor setup routes. A user who no longer has the old handset uses a recovery code.
  • Accounts on an authenticator app or email two-factor, and accounts with no second factor, are not affected: their phone number is not a delivery address for codes, and it stays freely editable. Re-saving the same number is never treated as a change.

A signed-in session alone could replace an active second factor

Turning two-factor off has always required re-proving yourself — the account password, or a current code — because stripping the second factor is exactly what a stolen session would want. Replacing it did not: any signed-in session could bind a fresh secret over an active one, and from that moment sign-in, withdrawals, P2P escrow release and password changes all answered to the replacement while the owner's factor and recovery codes went dead.

  • Fixed while a two-factor method is active, every route that can write the second factor demands a current code from the existing method or one of the account's recovery codes (API field currentOtp) before accepting a replacement. Without it the request is refused with a message that says what to do, and nothing about the existing method changes.
  • Nothing changes in how the apps are used. The web Security tab already offered only one control while two-factor was active — turn it off — and changing methods was already off-then-on; the mobile app is the same shape. What was open was the API beneath them, which accepted a replacement from any signed-in caller. (That off switch had a problem of its own, below.)

Google and wallet accounts could not turn two-factor off, and neither could the app

Disabling two-factor asks you to re-prove yourself, which is right. The screen the web profile and the mobile app both use accepted only the account password — and an account created through Continue with Google or Continue with wallet has none. Those users could not disable two-factor, could not change method (that is off-then-on), and had no way back short of an operator clearing the row by hand. The platform's other two-factor route had always accepted a current code as well, so the two doors onto the same decision disagreed, and the one every shipped client actually calls was the strict one.

The mobile app was worse off still, on every kind of account: its disable dialog collected nothing and sent {status: false} on its own, which the server refuses. Two-factor could not be switched off from the phone at all.

  • Fixed disabling accepts the account password or a current code from the enrolled method, at both routes, from one shared rule. An account with no password uses a code.
  • Fixed the web disable dialog offers both boxes, and the mobile app now collects the same proof before it sends the request — and reports what the server actually answered, instead of announcing success either way.
  • Changed an account with no password that supplies one anyway is told This account has no password set. Enter a code from your two-factor method instead, rather than Incorrect password — which was never true of somebody who has no password to get wrong.

The emailed two-factor code was refused before the user could type it

A user enabling email two-factor from Profile → Security received the code, entered it 29 seconds later and was told it was invalid — and again 18 seconds after that. The email said the code would expire in five minutes. It had expired at the next thirty-second boundary.

Codes delivered by email and SMS were computed with the same thirty-second formula an authenticator app uses, and were checked with a tolerance that depended on which part of the platform had last touched a shared setting. On a freshly restarted backend, the profile enrolment screen was the one sender that set no tolerance at all, so its codes were valid for whatever remained of the current thirty-second slot. Once any two-factor sign-in had run, every delivered code was good for between sixty and ninety seconds — still well short of what the email promised, and short enough that a slow inbox lost the code on its own.

  • Fixed a code sent by email or SMS is now valid for at least five minutes and at most ten, whether it is for enabling or disabling two-factor, signing in (by email and password, Google, a wallet, or the mobile app), resetting or changing a password, confirming a withdrawal or releasing P2P escrow. The lifetime no longer depends on what ran before it.
  • Fixed at any moment only three delivered codes are accepted for an account instead of five, so the longer lifetime does not make a code easier to guess. Authenticator-app codes keep their thirty-second step and are now checked with the two-step tolerance that sign-in always used, on every screen.

One two-factor code was good at every door, and stayed good after it was used

Every door computes the account's code from the same enrolled secret and the same time slot, and nothing marked a code as used. So the digits a user typed into the sign-in prompt were, for the rest of that code's life, also a valid withdrawal approval, a valid P2P escrow release, a valid password change, and valid proof for replacing the second factor itself.

At thirty to ninety seconds that was close to academic. Giving delivered codes the five minutes the email promises makes it five to ten, which is long enough to matter: a code read over a shoulder, forwarded in a support thread, or shown in an SMS preview on a lock screen stays a live credential at every other door until its slot rolls. RFC 6238 requires a verifier to refuse a second use of the same code, and this is that.

  • Fixed a code verified against the account's enrolled secret is spent on first use. Every later presentation of it — at that door or any other — is refused as though it were wrong: sign-in, withdrawal confirmation, P2P escrow release, password changes, and the two-factor routes themselves. The claim is a single atomic write, so two requests carrying the same code, which is the shape a replay actually takes, can never both win it.
  • Enrolment codes are deliberately not spent, and that is not an oversight. They are checked against a secret the caller supplied and still holds, before anything is bound to the account, and the enrolment flow legitimately presents the same code twice — once to verify it, once to collect the recovery codes.
  • What is stored is a hash of the code, never the code, and it expires on its own after ten minutes — long enough to outlive the longest code the platform issues.

The Wallet tab and the Swap terminal were a whole-page 500 on an unconfigured build

frontend/config/wallet.tsx threw the moment a browser loaded it if the project id was missing, under a comment that believed it was checking at build time. It never ran at build time — the wallet code is fetched on demand, so the build never evaluated it and the throw landed in the visitor's browser, where the page's error boundary painted the 500 card over everything on the route. The same build failed the same way on /dex/swap, the four NFT wallet flows, the wallet sign-in form and the Swap addon's pool detail console. Affected releases are 6.6.2 through 6.7.4.

  • Fixed a frontend built without NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID no longer throws. Each wallet screen renders a "Wallet connection is not set up yet" card in place of the wallet controls; a Super Admin, or any admin holding access.admin, is additionally shown the variable to set and that it needs a rebuild, and the browser console line now says the same instead of reporting an initialisation error. The rest of the page — the profile, its other tabs, the site chrome — stays up.
  • Fixed a wallet chunk that fails to load or throws while it starts — a deploy that rotated chunk hashes under an open tab, a blocked script, an offline reload — is caught at the wallet provider and reported as "Wallet features could not be loaded" with a reload button, instead of reaching the page's boundary. The previous fallback for this in that file could never run: in the App Router the loading component is never handed the error.
  • Fixed a browser that refuses site storage no longer takes those pages down either. Reading localStorage throws outright under "block all cookies" and in some embedded webviews, and it was being read at the same point, with the same result. The wallet now falls back to remembering nothing between visits.

The frontend build warned that 48,000 translation files belonged in the server output

Translations are served as per-route chunks from public/i18n, which the running server reads off its own disk. That directory is legitimately about 48,000 files — 504 routes across 90 locales, plus menus and cores — and it is not rot, so the number will not come down.

The reader built its candidate paths from the working directory, which Turbopack can resolve to a real location. Seeing a read of a filename it could not pin down, it concluded the whole directory belonged in the route's file trace, and then, once the directory itself became a variable, that the whole project did. Five warnings per build: one directory pattern matching 48,512 files and two matching 339,466.

The trace was wrong. public/ is read from disk by the running server and is never bundled, and this app does not build in standalone mode, so nothing consumed the trace at all.

  • Fixed the four path expressions and the read itself now carry Turbopack's documented opt-out, and the build completes with no warnings block. The reader still opens the same files from the same directory, so the translations a page receives are byte for byte what they were.