P2P Trading 6.2.2
29 July 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
P2P Trading v6.2.2
Release Date: July 29, 2026 Tags: SECURITY, ESCROW, ABUSE PREVENTION, PRIVACY, OFFER PAGE, UI, SETTLEMENT, DISPUTES, VERIFICATION, ATTACHMENTS, ADMIN PANEL, DASHBOARD, STATISTICS, PERFORMANCE, DESIGN SYSTEM
Overview
Version 6.2.2 closes two ways a buyer could destroy a seller's offer for free, turns on the abuse limits that had silently never applied, makes every "funds released" message tell the truth, and stops the public offer page exposing a seller's exact last-login time.
It also rebuilds P2P settlement around a single escrow authority. Four payout doors — buyer release, dispute resolution, admin resolution and cancellation — each kept their own bookkeeping, which is how a completed trade could be pushed back into dispute and paid out a second time from another offer's collateral, and how a "split" dispute ruling handed the buyer the entire escrow while recording that it had been split.
Trade chat attachments — payment screenshots, bank slips, ID photos — were written into the public web root and served to anyone with the link, and public offer and trade pages returned account email addresses and administrators' internal case notes.
The first step below is a required one-off escrow reconciliation. Read P2P fee settings that charge nobody anything are now labelled as such under Changed before upgrading, too: the fee control most installs have been setting wrote a value nothing reads.
Requires Core v6.5.8.
Update Instructions
pnpm updatorThen, in this order:
1. Reconcile escrow attribution (required, once per environment)
cd backend
npm run p2p:reconcile # report only — read this first
npm run p2p:reconcile -- --apply # backfill escrow attributionAn offer's recorded escrow attribution defaults to 0. Offers created before v6.2.x hold real collateral with nothing recording it, and the new capacity rules read that figure. Until it is backfilled, an affected offer reports zero attributed escrow and will not re-advertise capacity from a cancelled trade.
The script never moves money, not even escrow it believes is stranded. Locked balance is a single shared number across exchange, futures, staking, copy trading and P2P, and historical P2P holds wrote no wallet transaction rows — so a surplus is not proof of stranded P2P escrow. Discrepancies are reported for a human to adjudicate.
2. Restart the backend
Both the corrected rate limiters and the new P2P cryptocurrencies route are picked up at boot.
3. Check the boot log for MIDDLEWARE errors
Route registration now reports, at ERROR level, any route declaring a rate limiter that does not exist. A clean P2P install logs nothing. If your install carries custom routes, any line here is a door you believed was protected and is not.
Optional: MAIL_DISABLED
A new .env switch, MAIL_DISABLED=true, stops the platform attempting any outbound delivery. Intended for load or acceptance testing, where notification traffic can otherwise exhaust the mail account's SMTP login quota and get real user mail blocked by the provider. Leave unset in production.
Upgrade Notes
Nothing below breaks an existing setup, but these now work differently and are worth knowing about:
- Cancelling or expiring a trade against a sell offer no longer returns funds to the seller's spendable balance. The collateral stays held against the offer and the capacity is re-advertised. Sellers who used cancellation as a way to free up funds should pause or edit the offer instead.
- Offer creation is limited to 5 per hour per user, and trade initiation to 20 per hour. These limits were always configured; they were simply never applied. Accounts that exceeded them before will now be refused.
- Releasing an already-completed trade now says so explicitly instead of reporting a second payout.
- Resolving an already-resolved dispute is refused instead of silently overwriting the ruling.
- A completed trade can no longer be moved into dispute. A dispute has to be raised before the trade settles.
- Cancelling a trade that is in dispute is refused for both parties. Only an administrator can close a disputed trade, as part of a ruling.
- An offer can no longer be edited or deleted while a trade against it is pending, awaiting release, or disputed. Wait for the trade to finish, or pause the offer.
- Verification requirements now apply when publishing an offer, editing one, opening a trade directly, and going through guided matching. Accounts below the required level that could previously publish or trade will be asked to verify first.
- The only P2P fee actually charged is the Escrow (Platform) Fee. An operator who set a maker, taker or dispute fee expecting it to be charged has been charging the escrow fee's default of 0.2% — check your settings.
- A payment window of zero now means never auto-cancel, rather than falling back to the platform default, and the Auto-Cancel Unpaid Trades switch is honoured.
- Trade chat attachments are no longer served from a public address. New attachments are served only to that trade's buyer and seller while signed in. Files uploaded before this release stay where they were originally written.
- Money figures are reported per currency. Screens that previously printed one combined total with a dollar sign now group by currency, and show a single combined figure only where one currency is involved.
Breaking changes
None.
Added
Administrators can set the split ratio on a dispute
- Previously: a split ruling applied a fixed division, with no way for an administrator to say what the split should be.
- Added both admin resolution screens accept an explicit buyer percentage, or an absolute buyer share, defaulting to an even 50/50.
Fallback refresh when the trade page's live connection drops
- Previously: if the trade page's live connection gave up, a release, cancellation or dispute raised by the counterparty simply never appeared, and nothing indicated the page had gone stale.
- Added the page detects a degraded connection, falls back to refreshing every 15 seconds, and shows a banner with a manual refresh.
Supported payment methods on the landing page
The landing page has always been given the top eight payment methods by popularity and then thrown them away. The grid is now shown between the top-cryptos and featured-offers sections.
Two defects were found while adding it:
- Every icon was broken. Any payment method with an icon rendered a broken image. All icons now display correctly.
- The card colours were assigned from a list of eight hardcoded English names, so anything else silently fell back to one colour. All methods now share one neutral badge, consistent with the rest of the design system.
Activity Log added to the P2P admin navigation
The P2P activity log screen existed but was not linked from anywhere and could only be reached by typing its address. It now appears in the P2P admin navigation.
Changed
P2P fee settings that charge nobody anything are now labelled as such
The fee control operators have been setting wrote a value nothing in the platform reads. Installs believed they had configured their P2P revenue and had not.
- Changed the primary fee control is now Escrow (Platform) Fee and writes the one P2P fee that is actually charged. Maker Fee, Taker Fee, Dispute Fee, Escrow Release Time and Automatic Escrow Release are relabelled "(not enforced)", grouped under Unused (not enforced), and their descriptions say plainly that nothing reads them.
- Also documented: P2P Trading Enabled, Maintenance Mode and Allow New Offers are enforced in the interface only. Anything that skips the interface is still served, so treat them as presentation rather than as a lockout.
- Check your settings: an operator who set a maker or taker fee expecting it to be charged has been charging the escrow fee's default of 0.2% instead.
Resolutions that moved no money no longer report a payout
- Changed both admin resolution screens now report what the escrow authority actually moved, rather than assuming a payout had happened. Where a ruling is recorded but nothing moves, the screen says so plainly instead of showing a released amount.
- Also changed: a dispute ruling notifies both traders directly. It previously reused the trade-cancellation wording, which named one of the traders as the canceller and reached only one side.
"Average response time" clarified
- Clarified: despite its name, this figure does not measure the seller's responsiveness. It measures the time from a trade opening to the buyer confirming payment. Only the buyer can move that clock, and the seller takes no action inside the window — escrow is locked the moment the trade is created, with no seller acceptance step. On buy offers the offer owner is the buyer, so the figure comes from unrelated trades entirely.
- Preserved: the figure is unchanged wherever it already appeared on the offer page.
- Not extended: it is deliberately not shown anywhere new. A genuine turnaround figure is now possible to build, but the existing number was left exactly as it was rather than quietly changing what an established figure means.
P2P screens moved onto the shared design system
Hardcoded green, amber, blue, red and purple styling was replaced with the platform's semantic colours across roughly ninety P2P screens. P2P now reads as the same product as the rest of the platform in both light and dark themes, and follows an operator's theme changes instead of ignoring them.
- Fixed: the landing page's offer dialog fades closed like every other dialog instead of snapping shut.
- Also removed: the admin settings page and dashboard were rebuilt on the platform's shared control and card primitives, which left eighteen P2P interface pieces with nothing referencing them — superseded chart, activity, drawer, dialog and settings-section variants, a duplicate guided-matching wizard, and several panels that had already been replaced. All were removed rather than left in place to be maintained. No user-visible change.
The maintenance sweep re-read the same records every minute
Every scheduled P2P scan loaded all matching records with no cap, and two of them filter afterwards — so records that could never qualify were re-loaded in full on every run, forever. The cost grew with the age of the install and never came back down.
- Fixed the scans are capped at 500 records per run, read only the fields they need, and are ordered so they drain rather than restarting from the same head each time. Opening a trade also no longer holds every other trade by the same seller while it works.
Guided matching loaded a trader's entire history to compute two numbers
The matching search pulled every trade and every review of every offer owner into one result just to derive a completion rate and an average rating. The cost of a search grew with the size of traders' histories rather than with the number of matches.
- Fixed reputation comes from two small grouped counts. The same pattern on the landing page's top-traders panel was fixed with it, and that panel now counts a trader's activity on both sides rather than only their sales.
Smaller changes
- Ordinary trade releases no longer raise critical-risk audit events. A successful release was recorded as both a high-risk and a critical-risk audit event, which buried genuine incidents under routine completions. Those entries are now written at low risk, and after the money has settled rather than as part of the settlement itself.
- The payment countdown corrects for a wrong browser clock. The payment deadline is enforced against the platform's clock, but the page counted down on the browser's, so a machine running a few minutes fast or slow showed a deadline that did not match the one being enforced. The page now measures the offset once on load and applies it to both the countdown and the expiry check.
- A daily "archive old trades" job that archived nothing has been removed. The admin scheduled-jobs page advertised a daily retention job that wrote to details which do not exist, so nothing was ever archived while each run reported success. It has been removed rather than left as a convincing no-op, along with a second, unused schedule list that disagreed with the real one.
- Admin trade cancellation is recorded as data, not only as a timeline entry. An administrator cancelling a trade now records the time, the actor and the reason on the trade itself, so cancellations can be searched and reported on. The confirmation also states what the escrow actually did rather than assuming a refund.
- Offer prices no longer default to dollars when the currency is unknown. An offer with no recorded price currency was displayed with a dollar label it had never been priced in — on the offer list, the offer detail page, the landing page and guided matching. The label is now omitted rather than guessed.
Fixed
Security
A completed trade could be re-disputed and its escrow paid out twice
A finished trade could be pushed back into dispute. The dispute payout kept its own bookkeeping, entirely separate from the release payout, so it did not see that the trade had already been settled and paid out a second time. The collateral for that trade was long gone, so the second payment came out of whatever else the seller had locked — in practice, another offer's collateral.
- Fixed completion is final. A completed trade can no longer be moved into dispute, and all four payout doors — buyer release, dispute resolution, admin resolution and cancellation — now share one settlement authority that records a single settlement per trade and refuses to settle the same trade twice.
- Not the same as "Releasing an already-completed trade reported a second payout" below, which was a false message on the release button and moved no money. This one moved money.
A buyer could destroy any sell offer, for free
When a trade was cancelled or expired, the seller's escrow was returned to their spendable balance. Because the collateral had then left the offer, the advertised capacity could not be given back without offering funds that were no longer held — so it was permanently lost. A buyer could open a trade for the full amount, walk away, and repeat until the offer advertised nothing. The offer stayed active and untradeable, and the seller was never told. The attack cost nothing and needed no special access.
- Fixed a cancelled trade against a live sell offer now hands the amount back to the offer and moves no money at all. The collateral was committed to the offer, not to the trade, so a buyer who abandons a trade cannot release it. Capacity is genuinely restored.
- Fallback: when the offer can no longer hold the amount — a buy offer, a deleted offer, or one no longer collateralized — the seller is refunded instead, so escrow can never be left held by nothing.
- Applies to buyer cancellation, the payment-window expiry, and admin trade cancellation.
Activating an offer from the admin panel could publish a sell offer with nothing behind it
Activation only took collateral if the offer had previously been paused. Activating or approving one that was pending approval, rejected or cancelled published a live sell offer backed by no held funds at all — and pausing that offer later released funds belonging to a different offer.
- Fixed both Activate and Approve now top the collateral up to the offer's advertised total whatever its previous status, and refuse the change if the owner cannot cover the shortfall.
Trade chat attachments were readable by anyone with the link
Payment screenshots, bank slips and ID photos posted into a trade chat were written into the public web root and served to anyone holding the link — no sign-in, no expiry, and no check that the trade had ended. A link that leaked, was logged, or was shared onward stayed live indefinitely.
- Fixed attachments are stored outside the public web root and served only to that trade's buyer and seller, signed in. Responses are marked private and non-cacheable, and a request cannot name a location outside the attachment store.
- Operators: set
P2P_ATTACHMENT_DIRto keep attachments on a separate volume. Files uploaded before this release stay where they were originally written — clear the old public P2P uploads folder if its contents matter to you.
Offer and trade pages exposed email addresses and administrators' private case notes
The marketplace listing and every single-offer page returned the offer owner's account email to anyone who loaded the page, signed in or not. Both pages are public, so this was a harvestable list of email addresses tied to named traders and their advertised amounts. The trade page went further, handing each party the other side's account email, every timeline entry the admin tooling marks as an internal Admin Note, the dispute's internal activity log, and the name of the administrator who ruled on the case.
- Fixed public offer pages return only the display name and avatar. On the trade page the emails are gone, internal timeline entries are filtered out before the page is served, the dispute is reduced to its outcome, notes and amounts, and the resolving administrator is no longer named.
Offer page leaked exact last-login times
The public offer page returned a counterparty's precise last-login moment. That is both a privacy leak and a useful signal for anyone trying to take over an account.
- Fixed the exact time is never shown. The page now shows a rough presence reading instead.
- Resolution degrades with age: 5-minute buckets under an hour, hourly under a day, daily under a month, 30-day beyond that.
- Online status: now means signed in within the last 5 minutes.
Every P2P abuse limit was inert
Five abuse limits were configured but never actually applied. In practice, offer creation, dispute creation, payment-method creation, trade-history search and trade initiation were all completely unlimited.
- Fixed all five now apply, including the one on trade initiation — the door a griefer uses to tie up a seller's liquidity.
- Two more named a limit that does not exist: messaging into a trade and uploading images to it were, as a result, completely unthrottled. Both now use the real limit.
- Two more had never declared one at all: submitting a review, and the admin button that runs the whole expiry sweep on demand. Both are now throttled.
- Prevented from recurring: the platform now reports a misconfigured limit as an error on startup, so a limit can no longer look active while being open.
Disputes could be closed by the people they were about
A trader who expected to lose a dispute could simply press Cancel — the trade ended and the case closed before an administrator had ruled on it. Separately, nothing stopped an administrator who was the buyer or the seller of a trade from deciding that trade's dispute and awarding themselves the escrow.
- Fixed cancelling a disputed trade is refused, with a message saying the case must be resolved by an administrator. Administrators can still close one as part of a ruling, but an administrator who is a party to the trade is refused.
A seller could stall a paid trade indefinitely just by keeping the chat going
The 24-hour safety net that auto-disputes a trade where the buyer declared payment but funds were never released measured from the trade's last change of any kind. Every chat message counted as a change and reset the clock, so a seller who kept talking never triggered it — and the buyer's payment sat in limbo with no automatic escalation.
- Fixed the 24 hours are measured from the moment the buyer declared payment. Trades from before this release with no recorded confirmation time fall back to the old anchor so none are skipped.
Verification requirements were not enforced on the doors that publish offers or open trades
The verification requirement was only ever checked on the counterparty-facing side. A user below the required level could publish an offer, keep re-pricing it through the edit screen, open trades directly, and go through guided matching — none of which asked.
- Fixed all four enforce the requirement, and the offer-creation wizard and offer editor show it up front rather than after the form is filled in.
Uploads trusted whatever the caller declared
Chat image upload checked only the file type stated in the request, so arbitrary content could be stored and later served back as an image, and an oversized payload was fully decoded before being rejected. Dispute evidence was worse: it stored whatever address the request supplied, so the admin dispute screen could be pointed at any origin and an administrator reviewing a case could be shown content served by whoever submitted the evidence.
- Fixed a file's actual contents are checked against the declared type, oversized payloads are refused before decoding, the stored type is the one actually written rather than the one claimed, and display names are sanitised. Evidence must be a path inside this platform's own upload store, and its type is checked before anything is written.
The P2P admin settings page opened without a valid licence
The settings screen rendered its content directly and skipped the licence check entirely, making it the one P2P admin page an unlicensed install could open.
- Fixed full-screen P2P admin screens render inside the licence gate like every other page. The admin trade detail screen was added to the same list.
A "split" dispute ruling handed the buyer the entire escrow
When an administrator resolved a dispute as SPLIT, the buyer was credited the full escrow and the seller received nothing — while the dispute record, the notifications and the admin screen all reported that the funds had been split. Split and buyer-wins ran the identical payout.
- Fixed a split ruling now genuinely divides the escrow, and the resolution form will not submit without a usable share.
Deleting, editing or expiring an offer left its collateral stranded
- Deleting released only the offer's current advertised total, which shrinks as trades consume the offer. The difference stayed locked in the owner's balance with nothing left able to free it. Deletion now releases the collateral genuinely attributed to the offer.
- Editing adjusted collateral only for offers that were already live. A pending or paused offer could have its total raised with no matching hold and then go live advertising more than had ever been escrowed, and resuming a paused offer after an edit re-locked the pre-edit amount rather than the saved one. Collateral is now recomputed from the saved configuration on every write, whatever the offer's status, and the difference is held or released as part of the same save.
- Expiring was worst: an offer idle for 30 days was flipped to Expired with no escrow release and no money record of any kind, and because the offer was then expired, nothing was left able to free it. Expiry now releases the offer's collateral first and only then marks it expired. If the release cannot be made, the offer is left alone rather than expired with funds trapped behind it.
Offers could be created that could never be traded
- A price of zero: offer creation validated no prices at all. An offer whose final price came out as zero took the seller's collateral and then failed every attempt to trade against it — leaving the seller no way to trade the offer out and recover the funds. Creation now requires a recognised pricing method and a positive final price, and a margin-priced offer is bounded to ±50%.
- A currency the platform cannot hold: a buy offer touches no wallet when it is created, so nothing checked that the chosen currency exists for the chosen wallet type. Such an offer listed normally, sat on the marketplace, and failed every trade opened against it. The currency and wallet type are now checked against the platform's real currency lists when a buy offer is created.
A dispute could be closed without the money moving
Three separate ways a case could be stamped resolved while the escrow stayed held with no door able to settle it afterwards. A resolution submitted with an outcome the platform did not recognise fell straight through — the trade was marked Completed and no payout ran. A dispute on a trade whose offer had since been deleted skipped the entire fund-handling step while the dispute was still recorded as resolved. And an administrator could mark a dispute Resolved without recording an outcome at all, closing the case while the trade stayed disputed.
- Fixed an unrecognised outcome is refused and nothing is written, resolving without an outcome is refused, and settlement now works from the trade itself — it still finds a deleted offer and falls back to the trade's own currency.
The minimum escrow fee could swallow a small trade and freeze its escrow
The escrow fee had a flat floor expressed in the traded asset. On a high-value coin a small trade could be charged a fee equal to or larger than the trade itself, leaving nothing to credit the buyer and an escrow that could not be released.
- Fixed the floor is capped at 5% of the trade, and settlement additionally clamps the fee to the buyer's proceeds so it can never exceed them.
Ecosystem trades could fail to settle, and to open
- At settlement: if the seller had no per-chain ledger entry for the traded currency, releasing funds raised a server error and abandoned the entire settlement. That trade's escrow could then never be released by anyone, including an administrator. Settlement now completes the main balances and records the missing chain ledger for reconciliation instead of failing outright.
- At initiation: opening a trade on an ecosystem buy offer looked the seller's wallet up with the wrong details, and did so outside the trade's own write. It could fail outright, or leave behind a wallet belonging to a trade that was never actually created. The lookup now takes the seller's identifier and runs alongside the trade, so it either succeeds with the trade or is undone with it.
A second trade against the same buy offer was rejected forever
The hold taken when opening a trade on a buy offer was keyed on the offer and the user rather than on the trade, so a second attempt looked like a repeat of the first. Once a user had traded against a buy offer, every later trade against that offer was refused as a duplicate — permanently.
- Fixed the hold is keyed on the trade, so each trade is independent.
An offer could be edited or deleted while a trade against it was live
The guards meant to freeze an offer while a trade against it was in flight listed statuses that do not exist, omitted the two that matter, and deliberately skipped disputed trades. A seller could re-price or shrink an offer after the buyer had declared payment, or while a dispute over that very trade was open — and an offer with an open dispute against it could be deleted outright.
- Fixed editing and deletion are both refused while any trade against the offer is pending, awaiting release, or disputed.
Offer capacity restoration disagreed between screens
Four separate places restored an offer's advertised capacity after a trade ended without a sale — user cancellation, expiry, admin cancellation and admin dispute resolution — each with a slightly different cap. The same event restored a different amount depending on which one processed it.
- Fixed all four now apply the same rules: never more than the offer's original total, and — for sell offers — never more than the collateral genuinely still held. An offer can no longer advertise liquidity that has already left it.
- Fixed: the ceiling that a cancelled trade may restore capacity up to was recorded when the offer was first traded and never moved afterwards, so a seller who raised the advertised total had the increase silently swallowed at the next cancellation. The ceiling now moves with the total whenever the offer is edited, and a first trade seeds it from the offer's real total rather than one already reduced.
Releasing an already-completed trade reported a second payout
Pressing the release button again showed "Funds released successfully" with a fresh-looking credited amount, indistinguishable from a payout that never happened.
- Not a money bug: a trade is only ever settled once, and no second payout ever occurred. The message was false, not the balance.
- Fixed a repeat press now says plainly that the trade is already settled and that no further funds moved.
A resolved dispute could be silently re-ruled
Nothing stopped an administrator recording a second outcome on an already-resolved dispute. No money moved, but the recorded outcome, the resolver, the notes and the timeline were all overwritten — so the history showed the last ruling rather than the one the funds actually followed.
- Fixed a resolved dispute is now refused. Re-opening a case is a deliberate status change rather than a silent overwrite.
An offer could only be paused and resumed once
Pausing and resuming an offer within the same second failed with "Duplicate operation detected" and rolled the entire edit back.
- Fixed pause and resume can now be used freely. Escrow safety is unaffected — a repeated request still takes no second hold.
Every trade awaiting release told both parties no payment was required
The payment tab compared the trade's status against a value the platform never uses. Every trade in the window between the buyer declaring payment and the seller releasing funds fell through to a closing message saying no payment was required — on the same page whose header said payment had been sent.
- Fixed the tab reads the real status and shows the payment state correctly throughout that window.
- Also removed: a seller-side "I've received the payment" button that submitted to a buyer-only action and could never succeed.
The payment note and receipt a buyer submitted were thrown away
The confirm-payment dialog collects a reference, a note and a receipt image, but only the reference ever reached the platform. Buyers believed they had sent proof; the seller never saw it, and it was not there in a dispute either.
- Fixed the note and a link to the receipt are posted into the trade chat as part of confirming payment, so the seller sees them immediately and they survive as dispute evidence.
Refused trade actions looked like nothing had happened
When the platform refused an action — cancelling after payment was declared, cancelling as the wrong party, releasing an already-settled trade — the button simply did nothing and no reason appeared anywhere on the page.
- Fixed the refusal is shown. The seller's cancel button on a trade awaiting payment was removed entirely, since it is always refused; pausing the offer is the real alternative and the page now says so.
Trade chat mangled what users typed, and hid its own failures
- Text was mangled: plain-text fields — chat messages, trade terms, cancellation reasons, payment instructions — were escaped on save and had their line breaks stripped. Users read escaped character codes where they had typed an apostrophe, and their paragraphs arrived as a single run-on line. Text is now stored as typed, with line breaks preserved and angle brackets removed.
- A failed load looked like an empty conversation: a chat that failed to load — including one the viewer was not a party to, and one whose stored history could not be read — rendered as a successful but empty conversation, with no error anywhere. Failures are now shown as failures, a trade that cannot be found says so, and an unreadable history is no longer quietly replaced with nothing.
- The opening message appeared to come from the wrong person: the message a buyer writes when opening a trade was stored where the chat does not read it, so it rendered as if the counterparty had sent it — the seller appeared to have opened the conversation using the buyer's own words. It is now attributed to the buyer who wrote it.
The fee quoted before a trade now matches the fee charged
The trade form calculated its own fee figure rather than the one the platform is configured to charge, and the escrow panel recomputed a fee at a flat 0.2% and labelled every trade with that rate. An operator who changed the fee had two screens disagreeing with the amount actually taken.
- Fixed both show the configured rate, or the fee recorded on the trade itself.
- Also removed: an "auto-release in 23h 45m" countdown on the escrow panel, which described a process that does not exist.
Offers everywhere advertised an amount that was never updated
The landing page, guided matching, the offer list and the offer detail page all showed a snapshot of the owner's wallet balance taken at the moment the offer was created, not what was still on offer. A fully consumed offer still advertised its original amount, and users clicked through to trade against nothing.
- Fixed all four show the offer's real remaining total.
Money totals were added up across different currencies
A trade's total is denominated in its offer's own pricing currency, but the landing statistics, top cryptocurrencies, top traders, the user dashboard, the portfolio figure, the trade list, the seller panel and the admin dashboard each summed them into one number and printed it with a dollar sign. On any install with offers priced in more than one currency, every one of those figures was arithmetic fiction.
- Fixed all of them group by currency and print a single combined figure only when a single currency is involved. Where a conversion is genuinely needed, a missing rate now contributes nothing instead of being treated as one-to-one with the dollar.
Landing page trust figures were largely invented
The public landing page advertised an average completion time, an escrow release time, a 95% satisfaction rate, "50 countries served", a 100% dispute resolution rate on installs that had never had a single dispute, a flawless success rate for every featured trader, and a trader count that counted anyone who had both bought and sold twice over.
- Fixed each of those is measured from real trades and reviews, with sample sizes behind the timings, and reads as unavailable where there is no data rather than falling back to a flattering constant. Growth figures with no prior period now read as unavailable instead of zero.
The dashboard reported figures it had never measured
- "Trading Volume" printed exactly the same number as the balance tile beside it and had nothing to do with trading, while "24h Change" and "30d Return" beside it were fixed values. Trading volume is now real completed-trade volume, 24h change is measured, and 30d return reads as unavailable rather than a confident zero.
- The portfolio chart was a fixed drawing, not your data: every account saw the identical rising curve regardless of what its portfolio had actually done, and the readout in the corner was hardcoded to a green gain — including for a portfolio that had fallen. The chart now plots the account's real series, with a working tooltip.
- Every counterparty was shown as rated zero: the per-trade counterparty rating was read from a review score that does not exist. The failure was swallowed and the rating defaulted to zero, so an unrated trader was indistinguishable from a badly rated one. Ratings now come from the real review scores, and an unreviewed user shows no rating at all.
Traders with no history were presented as flawless counterparties
A counterparty with no finished trades was shown a 100% completion rate on the trade page. On the offer page the arithmetic ran the other way — a seller's still-open trades were counted against them, so a busy seller looked unreliable and a brand-new one showed 0%. The seller card printed a flat "5 min" response time as though it had been measured, and showed a never-reviewed seller as rated zero out of five.
- Fixed completion rates count only finished trades, report no rate at all when there are none, and flag the account as new rather than inventing a score for it. The invented response time and the zero rating both read as unmeasured, and the seller's volume figure drops a currency label it could not substantiate.
- Fixed: no reputation score was ever produced on any install. The hourly trader-reputation job averaged a review score that does not exist, so it failed for every user on every run — silently, because the failure was swallowed — and the milestone notifications that follow it never fired for anyone. It now averages the three scores a review actually carries, and reputation milestones fire.
Guided matching found nothing
The guided-matching screen asked for a list of currencies that did not exist, got an empty answer, and reported no error anywhere — so it silently offered nothing.
- Fixed it now lists the currencies that actually have live, publicly visible offers behind them, most-offered first, and reports a genuine failure as a failure.
Guided matching could not open a trade, and vouched for traders it knew nothing about
- "Start Trade" always failed: the wizard carried the amount through as text, and opening a trade requires a number, so the button on every matched offer failed. The amount is now converted and checked for a positive, finite value before the trade is opened.
- Sellers were shown the worst results first: results were always ordered cheapest-first, so a seller saw the least favourable offers at the top. Sellers now see the highest prices first and buyers the lowest.
- The benefits list was invented: four positive claims were printed for every match — including "Community trusted" and "KYC Verified" — for traders with no trades and no reviews, alongside a hardcoded 5-minute response time, a completion rate that counted still-open trades as failures, and an estimated saving that subtracted a dollar price from offers quoted in other currencies. Benefits are now shown only where they are true, with a plain "New trader — no history yet" otherwise; the invented response time is gone; completion rate counts only finished trades and reports nothing when there are none; and an estimated saving appears only when every candidate is quoted in the same currency.
A failure was presented as an empty account
The dashboard, the landing page, the offer detail page and the offer editor all treated a failed request as empty data. During an outage a user with money locked in an open trade was told they had no trades, an unreachable offer reported "Offer Not Found", and an editor that could not load the payment-method list showed an empty list with no error — which reads as "you have none" and invites saving an offer with no payment method attached to it.
- Fixed each of those screens reports a failure as a failure, and no longer presents it as an empty account.
Offer list sorting returned "Offer not found", and trade history search failed outright
Choosing most sort options on the offer list produced an "Offer not found" error instead of a sorted list. Searching trade history, or sorting it by most of the available options, failed outright instead of returning results — and the summary figures at the top of that page failed with it, so the whole screen was unusable.
- Fixed every sort option works on both screens, search works, and the trade-history summary totals are computed from real trade amounts.
Admin dashboard money and growth figures were wrong by construction
The dashboard compared a partial day against a whole window, so every growth percentage was meaningless and "Active Trades" growth sat permanently at +100%. Volume counted cancelled, expired and deleted trades as realised business, and amounts in different currencies were added together.
- Fixed the panels are rebuilt on equal-length rolling windows, volume counts only completed, undeleted trades, and totals are reported per currency. A growth figure with no prior period reads as unavailable instead of +100%.
- Fixed: on the activity panel: a per-row commission lookup that could attach the wrong figure to a row, and a yearly boundary that used the server's local midnight rather than a consistent one.
The admin offer and trade screens showed data that was not there
- The offer editor corrupted the offer's stored configuration: saving an offer from the admin panel wrote its amount, price, trade-settings and location blocks double-encoded, after which every filter and search over those values matched nothing. It also wrote a pricing method the platform does not recognise, so the offer priced itself unpredictably afterwards. The editor now writes plain values and maps its "fixed" and "dynamic" choices onto the platform's real pricing methods.
- The offer detail page rendered "undefined" throughout: price, currency, amounts, margin, terms and payment window were read as flat values that do not exist on an offer — they live inside its configuration blocks. The capacity bar was also divided by the per-trade maximum rather than the offer total, and fixed-price offers were shown a fabricated "0%" margin. Every field is now read from the offer's real configuration, the capacity bar is scaled to the offer total, and a fixed-price offer shows its pricing method instead of a margin it does not have.
- Offer statistics were a fixed block of zeros: total trades, completed trades, average completion time and success rate were four hardcoded zeros presented as measurements, and the owner summary beside them did the same. Both are now computed from real trades, disputes and reviews, and read as not available when nothing has finished yet.
- The trade detail page received no live data at all: an administrator opening a trade saw no live trade data and no events, because the page asked to be subscribed with one extra detail and so never matched anything the platform publishes. The subscription now matches exactly, and the page unsubscribes cleanly when it is closed.
Admin moderation work was lost or misreported
- Flagging an offer recorded nothing: it wrote an admin note and a log line and never created the flag itself. The flag never appeared on the offer, and the admin dashboard's flagged-offer count stayed at zero no matter how many were raised. The flag is now recorded against the offer, and raising one on an offer that was flagged and later cleared restores it rather than failing.
- Two administrators working the same dispute overwrote each other's notes and evidence: adding a note or a piece of evidence read the existing list, appended to it in place and saved, with nothing holding the case while it did. Two submissions arriving together lost one another's work, and because the list was modified in place the save was sometimes dropped entirely. Both now hold the case while they write and store a fresh list, so nothing is lost.
- Moderation messages announced actions the platform had rejected: approvals, rejections, dispute notes and evidence uploads showed a success message even when the request had failed, so administrators believed changes were recorded when they were not. A failure is now surfaced as a failure on all of them — including evidence upload, which previously reported nothing at all on screen.
Admin activity and dispute lists showed the wrong labels
- Every activity row was labelled the same outcome: the activity log stamped every event with the same status regardless of what had happened, and the High/Medium/Low severity tabs ignored the priority the platform sends — each one always rendered an empty "nothing found" panel, which reads as a check that passed rather than one that never ran. Each row now carries the outcome its event actually had, and the severity tabs filter on real priority.
- Dispute priority and status pills showed raw values: both badges looked up their labels using a spelling the data never uses, so administrators read raw internal values instead of translated labels — in every language. Both now use the real values and take their colour from the shared status palette, so they match the rest of the admin panel.
Reviews could be submitted twice, and re-submitted after an administrator removed one
The one-review-per-trade check was not protected against two submissions arriving together, and it ignored reviews an administrator had removed — so a moderated review could simply be posted again.
- Fixed the trade is held while the review is written, a removed review still counts as used, and the single 1-5 star rating the form collects is stored correctly across the three scores a review record holds.
Test traffic could get the mail account throttled
Test and seed addresses on reserved domains were queued and retried like any other recipient. Delivery to them can never succeed, and each attempt is a real login to your mail provider. A burst was enough for the provider to throttle the account, which then blocks mail to genuine users.
- Fixed both email queues drop reserved-domain recipients before queuing them, and a
MAIL_DISABLEDswitch suppresses all outbound delivery. A newnpm run mail:purgereports and clears an existing backlog.
Smaller corrections
- "Never auto-cancel" and the auto-cancel kill switch did not work. An offer configured never to auto-cancel had its trades expired on the platform default anyway, because a payment window of zero was read as "not set", and the Auto-Cancel Unpaid Trades switch was never consulted at all. Zero now means never, and the switch is honoured.
- Commission records were written for fees that were never collected. A commission was recorded whenever a fee was calculated, regardless of whether collecting it succeeded — and collection legitimately does nothing for a zero fee, a Super Admin seller, or an install with no configured admin wallet. The commission is now recorded only when the fee was actually collected, and a failed collection is logged.
- Expired trades carried no expiry time and logged the wrong previous status. Trade expiry wrote its timestamp where nothing could store it, so it was silently discarded and expired trades could not be reported on by date, and the activity log recorded the previous status as "expired" every time. Expiry is now recorded on the cancellation details with a reason, and the real previous status is captured before the change is made.
- Affiliate reward amounts were computed from a fee the platform had not charged. When the seller is a Super Admin the platform fee is waived, but the referral reward base was still calculated with that fee deducted, so referrers were credited against a smaller amount than the buyer actually received. Reward bases now come from what the settlement really moved.