Bicrypto 6.6.4
12 August 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
Core v6.6.4
Release Date: August 12, 2026 Tags: SECURITY, TWO-FACTOR, SETTINGS, WITHDRAWALS, INVESTMENTS, PAYMENTS, WALLETS, ECOSYSTEM, WEB3, RATE-LIMITS, WEBSOCKETS, RELIABILITY, API, ACCESSIBILITY, BUG-FIXES
Overview
A settings-defaults release, and the defaults decided whether security was enforced. Nothing has ever written a settings row until somebody saves the admin panel, and the backend read a missing row as off while the panel drew it as on. Thirteen switches disagreed with themselves that way. The worst is that a user who had enrolled two-factor authentication was signed in without being challenged for it — the security page said Enabled, the login accepted a password alone.
A second panel-versus-platform disagreement is repaired with it, and this one decided whether money left. Two switches answered "should withdrawals be auto-approved" — the current setting and an older one whose name reads backwards — and an install could show the plainly-named switch off while the older one was auto-approving every withdrawal. There is now one switch, and saving it writes both rows.
Two more repairs sit alongside those. Investments the platform never took money for were being paid out by the settlement runs, principal included. And the per-address rate limit had a window that could not close, so anything that did not fall silent for a full minute got one allowance per period of activity rather than one per minute — which is what refused trading bots far below their budget.
Read the Upgrade Notes before updating. Two entries need you to go and look at records you already hold, one changes behaviour on any install that has never saved its settings, and one asks you to run a short script.
Update Instructions
pnpm updatorRestart the backend afterwards. pnpm updator carries a database repair for
installs running the Staking addon — see Staking v6.2.0; it is applied for
you and skipped on installs without it.
Two settings migrations are not applied for you, because each reads a value
only your database knows. One command runs both, in order, from the folder you
updated in — it reports and changes nothing until you add :apply:
pnpm db:migrate:6.6.4 # report only
pnpm db:migrate:6.6.4:apply # writeRun the report first and read it. Both are safe to run twice — a second run costs a read and changes nothing.
Neither changes what your platform does. The first never changes withdrawal behaviour; the second preserves your current email-verification behaviour across a default that has changed — read the email-verification entry in the Upgrade Notes before you skip it, because skipping it on an install whose mail is not configured can lock out every sign-in.
Any address currently locked out by the rate limiter is released at the next window boundary rather than needing to be cleared by hand.
Operators running Ecosystem should read Ecosystem v6.4.0 as well. Several repairs in that release run during startup and one of them wants a read-only check afterwards.
Upgrade Notes
A password reset could leave an account with no way back in
The most serious repair in this release. When a customer with two-factor authentication completed a password reset, the platform changed the password and revoked every session and API key first, then tried to send the 2FA code, and returned an error if that send failed. The error carried no challenge token — and a recovery code can only be redeemed against one.
So the account was left with a password the customer may not have written down, no live session anywhere, and no route back in. Three ordinary conditions reached it: a mail queue that was down, an account enrolled for SMS on an install with no SMS provider configured, and an SMS enrolment with no phone number on file.
The challenge is now issued before delivery is attempted, and a delivery failure is reported alongside it rather than instead of it. Issuing the token grants nothing on its own — a valid code or an unused recovery code is still required — so the customer can fall back to their recovery codes exactly as intended.
- Fixed a password reset revoking every session and then refusing to issue the 2FA challenge when the code could not be delivered, which left the account unreachable.
- Changed the reset response to say whether the code was actually sent and, when it was not, why — and to point the customer at their authenticator app or recovery codes.
- If a customer has been locked out after a password reset, they can now complete it with a recovery code. Nothing on your side needs repairing; the path simply exists again.
Email verification now defaults on — run the backfill before you restart
The email-verification switch had the same panel-versus-platform disagreement as the 2FA switches below: the admin panel drew it on, seven backend readers treated a missing row as off, and nothing has ever written that row. All seven now read a missing row as on, which is the intended product.
That is a behaviour change for anyone upgrading. An install that has been running for a year with no row would, on the first restart after updating, start demanding a verification code from every new signup and — through the login gate — from every existing unverified account. On an install whose mail transport is not configured, that is a lockout.
backfill-verify-email-status.mjs prevents it. Where there is no row and the
install already has users, it writes an explicit false: the row then says what
your platform was already doing, and the admin panel finally shows the truth. A
brand-new install with no users deliberately gets no row and lands on the new
default, where nobody can be locked out. An install that already has the row is
never touched in either direction.
- Changed an absent email-verification setting to mean on, matching the admin panel.
- Run
pnpm db:migrate:6.6.4:applybefore you restart if your install has users and you have never saved that switch. Run it without:applyfirst to see what it would write. - Fixed the customer-facing resend verification email control being refused with "Email verification is not enabled on this platform" on any install that had never saved the switch — including when the AI support agent offered to resend it.
- If you want verification on, turn it on in System → Settings after confirming a test email actually arrives. The Email Service check on the platform health screen now tells you when it cannot.
Anyone could make the platform email a verification code to a stranger
The login route checked whether an account's email was verified before it checked the password. Two problems came out of that one ordering.
A caller who typed any unverified address and any password at all made the platform queue a real verification email to its owner. The platform keeps exactly one live code per account, so issuing a new one retires the previous one — which means a stranger could repeatedly retire the code the owner was part-way through typing and make their verification impossible to finish. That send was also budgeted as a login attempt (thirty per address per fifteen minutes) while the identical send on the resend endpoint is capped at three.
The same ordering answered "User email not verified" where every other failure answers "Incorrect email or password", so one unauthenticated request revealed whether an address held an unverified account.
The check now runs after the password is verified. What a customer with the right password sees is unchanged — the same message and the same fresh code, which is exactly who needs it. Everybody else gets the ordinary sign-in failure, and their attempt now counts against the per-account lockout, which the old ordering skipped entirely.
- Fixed an unauthenticated caller being able to trigger verification emails to any unverified address, and to invalidate a code its owner was in the middle of using.
- Fixed the login response distinguishing an unverified account from a wrong password, which let addresses be tested one request at a time.
- Fixed unverified accounts having no brute-force ceiling, because the old ordering returned before the per-account lockout counted the attempt.
- Changed verification emails sent from the login route to share the resend endpoint's budget, so the two doors cannot be played against each other. The slot is handed straight back on the overwhelming majority of sign-ins, which send no mail at all.
Two-factor authentication was not being enforced on an install that never saved its settings
Check this first if you rely on 2FA. The settings table starts empty and nothing seeds it, so every switch on the admin panel has a value only once somebody presses Save on that page. The panel draws the four two-factor switches as on; the backend read a missing row as off. On an install where nobody had opened System → Settings and saved, that meant users who had enrolled an authenticator app, an email code or an SMS code were signed in on a password alone — no challenge, no error, and a security page still reporting the method as enabled.
The same disagreement reached twelve other switches. Spot and fiat wallets read as switched off, so wallet lists came back empty and spot orders were refused; investment purchases returned a permission error while the whole Investment section stayed browsable; email verification was skipped; and the payment gateway currency list had nothing in it. In each case the panel showed the feature on.
A missing row now means what the panel says it means. An explicit false still
switches a feature off — this changes nothing on an install that has saved its
settings.
- Fixed two-factor authentication being skipped at login, at password reset and at every step-up challenge on an install with no saved settings, for users who had enrolled a method.
- Fixed twelve further switches — spot wallets, fiat wallets, investments, email verification, KYC display and deposit expiry among them — behaving as off while the panel drew them as on.
- Open System → Settings and save it once after updating. It is not required, but it turns every default into a stored value, so the panel and the backend can no longer drift apart again.
- If you deliberately run with a feature off and have never saved the panel, that feature is now on. Set it explicitly before you update, or immediately after.
Investments nobody paid for were being paid out
An investment opened from the admin table — Finance → Investments → Add, and the AI Investments equivalent — inserted an ACTIVE position without taking anything from a wallet. The admin settlement buttons already refused those by name. The settlement runs did not: they looked the payout wallet up by currency rather than by the debit, found the customer's ordinary wallet, and credited principal plus profit into it on the first run after the end date. That is money the platform never received leaving the platform.
The same lookup caused a second failure in the opposite direction. A plan's currency and wallet type can be edited after customers have bought under it, and resolving the payout from today's plan sent every in-flight settlement looking for a wallet that never funded it — throwing on every run, for ever, with the customer's principal held.
Settlement now resolves the wallet from the funding debit and pays nothing when there is no debit. Admin-created active investments take the principal properly, so new ones are settleable.
- Fixed the settlement runs crediting principal and profit for investments that never debited a wallet.
- Fixed an investment stranded for ever when its plan's currency or wallet type was edited after purchase.
- Fixed cancelling an investment refunding into a wallet chosen by currency rather than the one the principal came out of. This covers Forex investments too — the cancel and refund door is shared.
- Fixed an investment held for ever because the wallet it was bought from had since been removed. The debit is proof the money left, so the wallet is put back and the payout goes into it.
- Changed an investment that cannot be settled is now reported once and then counted, instead of writing the same alert for every such position on every run. A busy install was producing thousands of identical error lines a day, which buried the runs that had genuinely failed.
- Go and look at your investment history if you have ever added a position from the admin table. Any that reached its end date was paid out of nothing; the transaction ledger will show a credit with no matching debit against the same reference.
- Admin-added ACTIVE positions now debit the customer's wallet and are refused if it cannot cover them. Recording settled history — completed, cancelled, rejected — still moves no money.
The custody dashboard reported cover the platform does not hold
An ECO wallet records the same money three times: the spendable balance, which is the authority, and two per-chain trackers that follow it. A debit floors those trackers at zero — correctly, since money reaches an ECO wallet through paths that never touch a chain — but the refund added the whole amount back. So a debit and its own refund left the trackers higher than they started, and nothing ever brought them down.
The ordinary failed-withdrawal path reaches it, and so does rejecting a withdrawal
from the admin panel. The custody dashboard publishes the sum of those trackers as
onChain, and derives coverage, shortfall and the overall custody state from it —
so every failed withdrawal quietly inflated the figure an operator uses to decide
whether the platform is solvent against its own chains. The customer's spendable
balance was never wrong.
A refund now restores only what the debit was actually able to take, and records it at the time so the two can never drift again.
-
Fixed a refunded ECO debit inflating the per-chain custody trackers, which overstated
onChain, coverage and the custody state on the admin dashboard. -
Repair the rows written before this with the reconciliation script, which reports by default and writes only with
--apply:cd backend tsx -r dotenv/config -r ./module-alias-setup.ts scripts/reconcile-eco-chain-trackers.ts dotenv_config_path=../.envIt never touches a customer's spendable balance — only the trackers behind the custody figure. Scope it to one account with
--user <userId>. -
Re-read your custody page afterwards. If it has been reporting healthy coverage on an install with many failed or rejected withdrawals, the corrected figure may be lower than you have been used to seeing.
A portfolio holding ecosystem tokens was reported at a fraction of its value
The platform prices a customer's holdings from two places: the connected exchange's listing, and — for tokens that trade only on this install's own ecosystem markets — the ecosystem order book. The second lookup asked for a price under the plain currency code, while the order book files every price under its trading pair. The two never matched, and a lookup that finds nothing is not an error: the holding was priced at zero and added to the total as such.
Only tokens that are not also listed on the connected exchange were affected, which is why the result looked like a plausible number rather than an obviously broken one. On one reported account a portfolio of $7,698.61 was shown as $513.41.
The same figure is written into the daily portfolio snapshot, and that snapshot is what the 24-hour change is measured against. More than one part of the platform writes that row, and they did not agree — one priced ecosystem tokens, another did not — so whichever screen the customer opened last decided what was stored. A customer who checked the website and then the app was told their portfolio had fallen 93% in a day, on an account where nothing had moved.
- Fixed ecosystem tokens that are not also listed on the connected exchange being valued at zero in the portfolio total, in the 24-hour change, and in the per-currency rates any client reads to value holdings itself.
- Fixed the daily portfolio snapshot being overwritten with that undervalued figure, which then became the baseline the next day's 24-hour change was measured from.
- If customers reported their balance collapsing, nothing on their account needs repairing. This was only ever the reported figure — spendable balances, orders and withdrawals all read the wallet itself and were never wrong.
- Today's snapshot is corrected the next time it is read, so the 24-hour figure is right again within a day of updating. Days already stored are not rebuilt: the 28-day portfolio chart stays understated for any day the undervaluing writer ran last, and past prices are not retained anywhere that would allow those days to be recalculated.
Moving your own money to your own wallet could be refused
A transfer out of an ECO wallet checked the per-chain trackers rather than the balance, and refused with "Insufficient chain balance" when they did not cover the amount. Those trackers are only written by the chain paths, so an ECO wallet funded any other way — a spot fill, an admin adjustment, P2P profit — has an empty map while the customer genuinely holds the funds.
The result was a customer being told they could not move their own money into their own spot wallet, on a balance the withdrawal door would happily have sent to a stranger on another chain, because the withdrawal door reads the authority and this one did not.
- Fixed an ECO transfer being refused on the per-chain record rather than the spendable balance, which blocked transfers out of any ECO wallet that was not funded on-chain.
- Changed the per-chain record to be updated as bookkeeping, with anything it cannot account for written to the log rather than refusing the customer.
Klarna deposits were credited and then reported as failures
A Klarna deposit credited the wallet and then failed to close its own record, because the closing write tried to reuse a reference the credit had just claimed and references are unique. The handler answered Klarna with a server error; Klarna retried; the credit was correctly recognised as already done and skipped — and the same write failed again. So the deposit was never acknowledged, its record stayed PENDING, and the customer's wallet had already been funded.
- Fixed a completed Klarna deposit being left pending beside a funded wallet, and answering the gateway with an error until it gave up retrying.
- If customers have reported missing Klarna deposits, check their wallet balance before refunding — the money is most likely already there, and the pending record is the only thing that was wrong.
Callers that were being over-limited will now get their full allowance
This restores throughput that was being withheld, so a bot or integration that
had quietly settled into a lower request rate can now issue up to its configured
limit again. That is the intended budget, not a new one — but if you had raised
RATE_LIMIT or HB_RATE_LIMIT to work around the symptom, the raised value is
now genuinely available and worth reviewing back down.
- Fixed the write-request window never expiring for a caller that keeps sending, so budgets now renew every window as documented.
- If bots on your install were being refused, check whether every visitor is
sharing one bucket as well — the backend log warns when most requests resolve
to the same client address, which means the forwarding headers from your proxy
are not trusted and
TRUST_PROXY_CIDRSneeds the proxy's address.
Two switches decided whether withdrawals were auto-approved, and the panel could show the losing one
Check this if you have never touched the withdrawal switches. Two settings
answer one question. withdrawAutoApprove is the current one; withdrawApproval
is an older key whose name reads backwards — it stores true to mean auto-
approve, that is, no approval required. The platform reads the current key if it
has ever been saved, and falls back to the older one if it has not.
Both had a switch on Settings → Wallet → Security, side by side, with nothing tying them together — and a switch whose row has never been saved draws itself off rather than saying it is unset. So an install could show Auto-Approve Withdrawals (Legacy) on and Auto-Approve Withdrawals off while withdrawals were, in fact, going out with nobody looking at them. The switch bearing the plain name was the one telling you the wrong thing, and it is the one an operator checks.
The legacy switch is gone. Saving the remaining one writes both rows, so they can no longer disagree.
pnpm db:migrate:6.6.4 # report only
pnpm db:migrate:6.6.4:apply # write- Removed the Auto-Approve Withdrawals (Legacy) switch from the admin panel. The key it wrote is still read as a fallback, and is still Super Admin-only.
- Changed saving Auto-Approve Withdrawals to write the legacy row to the same value, so the fallback can never resolve to something the panel is not showing.
- Run the migration above, then restart the backend. It copies whatever your platform is already doing into the visible switch, so it never changes withdrawal behaviour — it only makes the panel agree with it. Running it twice does nothing. Skipping it is safe as long as you open Settings → Wallet and save the switch in the position you want.
Added
A health endpoint that answers
The documentation has always pointed load balancers, uptime monitors and the
environment tooling at GET /api/health. Nothing served it. A perfectly healthy
install answered 404, so a monitor configured the documented way reported the
platform as down, and the deployment scripts that pre-flight against it refused
to run.
- Added
GET /api/health. It needs no authentication, answers 200 while the backend can serve and reach its database and 503 when it cannot, and reports the state of Redis and the database separately in the body. It is cheap enough to poll continuously and writes no audit entry, so it will not fill your logs.
Changed
Operational alerts no longer mail every visitor on a demo install
Demo mode hands the Admin role to everybody who signs up, so buyers can look around the admin panel before purchasing. Every alert addressed to "all admins" therefore meant all users. One disputed P2P trade, one refused scheduled job or one flagged audit event on a demo install with four thousand signups sent four thousand emails and four thousand push notifications — from the same account that has to deliver password resets.
On a demo install those alerts now reach Super Admin only. On every other install nothing changes, and nothing is hidden from anyone in either case: the alert row, the audit log and the dashboards are written regardless of who was mailed.
- Changed operational alert emails and push notifications to Super Admin only while demo mode is on.
The Forex withdrawals tile is gated on the permission its page actually uses
The Operations dashboard tile for pending Forex withdrawals checked a permission that no route declares, so nothing enforced it and the next permissions rebuild would have deleted it — taking the tile away from every role that had been granted it. It now checks the same permission as the page it links to, which means an operator can no longer be shown a queue they cannot open, or hold the page and never be told there is work waiting in it.
- Changed the pending Forex withdrawals tile to gate on
editrather than a key nothing enforced. Roles that hold the withdrawals page already hold it.
Two Web3 Trading settings had two editors, and only one of them checked the value
Token Allowlist Mode and Require KYC to Swap appeared both in the Web3 Trading console and on Settings → Features. Platform settings accepts any value of the right shape — it is a generic screen and has no idea what a token allowlist mode is — while the Web3 Trading console validates against the modes the platform can actually act on. Saved from the wrong screen, the allowlist mode could be parked on a value no part of the platform recognises, and what an unrecognised allowlist mode permits is a customer swapping into a token nobody curated. A swap cannot be undone.
Both now live only in Swap → Settings, where they are checked. Web3 Trading itself stays on Settings → Features and cannot move: the Swap console does not open while the addon is switched off, so it can never be the screen that switches it on.
- Changed Token Allowlist Mode and Require KYC to Swap to the Web3 Trading console only. Platform settings now refuses both by name and says where they live. Your current values are untouched.
Fixed
Opening a second tab silently killed the first one's live updates
A live connection was recorded once per person per screen, so the moment somebody opened the same screen in a second tab the newer connection replaced the older one in the platform's records. The first tab was never told. It stayed open, kept saying Live, and simply stopped receiving anything.
It did not need two tabs, either. Two different screens can share one connection — the P2P trade room and the admin case file for the same trade are one example — so an operator who was also a party to a trade lost the room by opening the case file. And because the record was shared, one tab leaving a screen cancelled the other tab's updates: navigating away from a page in one tab cut the feed to the page you were reading in another.
The platform now keeps every connection a person has on a screen, each with its own list of what it asked for. Updates go to all of them, closing one leaves the others alone, and per-person work — a deposit address monitor, a chat room membership, a bot terminal's poller — is only torn down when the last one goes.
- Fixed a second connection to the same screen replacing the first, which left the first open and permanently silent.
- Fixed one tab unsubscribing cancelling the updates of every other tab that person had on the same screen.
- Fixed closing one tab tearing down per-person background work that the remaining tabs still needed — deposit chain monitoring, P2P trade rooms, copy trading feeds, futures and market-maker panels, and the bot console.
- Fixed the market data poller running for the life of the process after the last viewer left, because the departing connection was counted as an audience.
A connection keepalive was logged as a client error, twice a minute, for ever
Browsers send a periodic are you still there to every live screen. No part of
the platform answered it, and the market screens logged each one as a malformed
subscription — Invalid message structure: payload is missing — every thirty
seconds, per connection, with nothing recorded about who sent it.
The platform now answers the keepalive itself, before any screen sees it, in both of the two formats in use. Screens that answered it themselves are untouched.
- Fixed connection keepalives being logged as client errors on every market screen.
- Changed frames a screen genuinely cannot act on to be logged once a minute at most, naming the screen, the signed-in user and their address — so the log can answer who is sending it.
An admin request could unwind before its work had finished
The gate that checks an administrator's permission was not waiting for the work behind it. On the roughly 1,150 admin routes that declare a permission, the gate reported success as soon as the handler started, so a handler that failed afterwards could not be seen to have failed: the request unwound early, and the console and the admin audit trail both recorded a green outcome for a request that answered with an error.
- Fixed the admin permission gate returning before its handler had finished, which recorded failed admin actions as successful in the audit trail.
The transactions list counted one P2P settlement twice
Settling a P2P trade on an Ecosystem wallet writes the buyer two rows: the credit that moves the money, and a second row that syncs the per-chain ledger so the buyer can withdraw what they were just paid. The second row moves no balance, but it carries the same amount, the same wallet and the same instant — so one settlement read as two credits in Finance → History, and the Analytics tab added it up twice.
- Fixed the per-chain sync row appearing in the transactions list and being summed in the analytics totals. The credit that moved the money is unchanged and still listed.
The history page announced its own title twice, and the market page had no main region
/finance/history drew its heading and then had the table draw the same string
again, so the page had two top-level headings and a screen reader read the title
out twice. The market page mounted its own header and content with no page layout
above it, so the document had no main region at all.
Neither changes what anything looks like — the text, the sizes and the spacing are identical.
- Fixed a duplicated page title on the transactions history screen.
- Fixed the market page having no main landmark.
- Added a way for any page that draws its own heading above a table to have the table's title rendered below it rather than as a second page title.
Every email check reported success without sending anything
Three separate checks told an operator that mail was working when none of them had reached a mail server. Send test email and the Email service probe both wait only for the message to be accepted onto the queue — the login to the mail provider happens later, in the background — and both then reported "Email sent successfully". An operator with a wrong password or a throttled account read a green result and went looking for the problem somewhere else.
Worse, all three ignored MAIL_DISABLED. With that set in .env the platform
drops every message before it is queued, and the checks still passed — which is
exactly the state in which an operator turns email verification on, sees a healthy
Email Service, and locks out every new signup.
- Fixed the test-email controls claiming a message was sent when it had only been queued. They now say queued, and say that delivery has not been confirmed.
- Fixed all three checks passing while
MAIL_DISABLEDwas set. The platform health screen now reports the Email Service as down and names the switch, and the test-email buttons refuse with the same explanation instead of reporting success. - Changed the configuration probe to say it checked credentials and attempted no delivery, rather than letting a green tick imply mail is arriving.
A wallet showed a second, smaller balance that was not spendable
An ECO wallet's response carried its spendable balance and, inside the deposit address list, a per-chain figure under the same name. That second number is a custody tracker: it only moves when money arrives or leaves over a chain, so on a wallet funded any other way it sits below the real balance by design — and nothing on the response said which of the two a customer could actually spend.
- Fixed the per-chain tracker being returned on customer wallet responses, where it read as a second and smaller amount of their own money. Admin custody screens are served separately and still show it.
- Fixed the deposit-address list being documented as a list of plain strings when it has always been a per-chain object, so generated API clients described a shape the platform never returned.
"Resend verification email" reported a mail failure for a settings problem
Pressing Verify beside Email Verified on the profile page raised two notices at once: the real one, naming the reason, and a second reading Failed to Send Email — please try again later stacked on top of it. The second is the one people read and repeat, and it names the wrong cause. At least one operator went through their whole mail configuration looking for a delivery fault that did not exist, while the actual answer — a platform switch — had been on screen the whole time underneath a sentence about trying again later.
- Fixed the profile page's resend control showing a generic mail failure over the specific reason. It now shows the reason alone, and a successful send is confirmed once instead of twice.
Three messages in the interface stopped mid-sentence
Three strings were cut off where they were written, so the interface said "KYC verification is currently" and "You don't have any" and stopped. The withdraw screen's empty state was the most consequential: a customer with no withdrawable balance was shown a fragment instead of being told to deposit or transfer first.
- Fixed three truncated messages on the KYC and withdraw screens, and a payment method label that was rendering in lower case.
Rate limits stopped renewing for anyone who did not pause
The per-address cap on state-changing requests counts against a window that is supposed to end a fixed number of seconds after it opens. Instead, each allowed request restarted that countdown. For bursty browser traffic the difference is invisible, because a browser idles constantly and the window closes during the gaps. For anything steady it was the whole behaviour: the count kept climbing and the window kept receding, so the caller was refused as soon as it reached the limit and stayed refused until it fell completely silent for the full window length.
Refusals did not extend the window, which is the only reason callers ever recovered — and it is why the symptom looked so unlike its cause. The lockouts arrived in bursts that had no relationship to the current request rate, and a caller well inside its budget could be refused while a busier one was not.
- Fixed the write-request window being extended by every allowed request. The window now ends a fixed time after the first request in it, so each caller gets its full allowance every window instead of one allowance per period of continuous activity.
- Fixed a counter that could be left with no expiry at all — previously an interruption at the wrong moment could strand an address in a refused state indefinitely. A later request now repairs it.
- Changed the remaining-requests figure reported back to callers to be measured after the request is counted rather than before, so two requests arriving together no longer both report the same figure and overstate what is left.