P2P Trading 6.2.5
4 August 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
P2P Trading v6.2.5
Release Date: August 4, 2026 Tags: MARKETPLACE, OFFERS, PAYMENT METHODS, MIGRATION, TRADE ROOM, ADMIN, DISPUTES, ONBOARDING, ESCROW, SECURITY, TWO-FACTOR, PERMISSIONS, SETTINGS, ROUTING, DESIGN SYSTEM, WALLETS, BALANCES, PERFORMANCE, BUG-FIXES
Overview
Version 6.2.5 rebuilds P2P on both sides of the glass — the marketplace, the offer composer, the trade room and the payment-method book your traders use, and the case screens your staff rule from.
It also puts a second factor on the moment that moves money: releasing crypto can now require a one-time code from the seller's own 2FA method, and a companion setting obliges a seller to hold one before a trade can start against them. Both are off until you switch them on.
Payment methods were also split in two, and that one needs a command run after you update. A method used to be one row holding both the identity ("PayPal") and one trader's own credentials, so the marketplace filter carried an entry per trader — a hundred people accepting PayPal made a hundred "PayPal" entries. The shared half and the private half are now separate records; the filter groups on the shared one. It is in Upgrade Notes, with the repair.
Two defects in here stopped work rather than merely looking wrong. A trader could pick a payment method from the list this platform gave them and be refused by id when they saved — and the same check guards editing, so an affected offer could never be edited again, by anyone. And on the admin desk, the dispute screen showed an empty transcript and "no evidence submitted" for disputes that held both.
Read Upgrade Notes before updating. One changes what your minimum and maximum trade amounts mean and should be read first if you price in anything other than dollars, one turns a settings field that never did anything into a real cap on concurrent trades, one asks you to re-open disputes you have already ruled on, one carries a data repair to run, one needs you to look at your payment methods, one needs a permission granted, and the two-factor controls need a decision before you turn them on.
Requires Core v6.6.2.
Update Instructions
pnpm updatorThere is one data repair, and it needs running. Payment methods were split into a shared method and each trader's own details (see Upgrade Notes), and the repair attaches your existing rows to the right one:
node backend/scripts/repair-p2p-payment-rails.mjs # prints the plan
node backend/scripts/repair-p2p-payment-rails.mjs --apply # commits itIt is a dry run by default. Read the plan before applying — it names every set of names it is about to merge, and a merge is the one thing here that is awkward to undo by hand. Everything else described below is computed from records you already hold.
Four things need you afterwards. Run the payment-method repair above. Re-open the disputes you have already ruled on — the screen you ruled from was hiding the evidence, and Upgrade Notes says which outcomes to look at first. Grant the payment-method permissions in Admin → Roles or the new setup console will refuse everyone except Super Admin. And open your payment-method list once — the fix below changes which methods traders can attach to an offer, and the list will tell you whether that matches what you intended.
The two-factor controls are off when you finish, so nothing about releasing changes until you decide it should. They live in Admin → P2P → Settings → Platform, and Upgrade Notes explains the order to switch them on in.
Highlights
The marketplace asks one question instead of presenting a filter panel
The old section spread one job across four screens. A landing page carried five separate data panels, a Guided Matching wizard asked the questions the offer table already filtered on, an offer table listed everything, and a dashboard showed a portfolio chart. The wizard and the table were computed by different code against different filters, so a recommendation could vanish when you opened the full list — the two disagreed about the same market.
There is now one board at /p2p/market, and it opens on a sentence rather than a filter row: I want to buy USDT with NGN in Nigeria. Every underlined word is a control. A trader who prefers the classic filter row switches to it and the market remembers that choice per browser, because that preference belongs to the reader and not to the installation — the desk that opens the page forty times a day and the first-timer arriving from a share link are on the same install. The recommendation strip and the full board are now computed by the same code, so an offer cannot appear in one and be missing from the other.
The board also opens on the right currency. It reads the trader's own recent P2P history first, then their profile country, then a CDN geo header, then a cached IP lookup — and it says which signal answered. The old behaviour fell back to whichever fiat was busiest platform-wide, which then wrote itself into the URL, so the guess came back on every reload as though the trader had chosen it.
Requiring 2FA of the seller means working out who the seller actually is
An operator switching on a rule about sellers reasonably expects it to apply to whoever starts a trade. That is not enough here, and the reason is worth knowing, because it explains where the refusal turns up.
Which side of a trade someone takes depends on the offer they answer, not on the fact that they opened it. Answer a buy offer and you are the seller. Answer a sell offer and you are the buyer — the seller is the person who published that offer, and they are not present in the request at all. A rule applied to whoever pressed the button would therefore have left a whole class of sellers unchecked: precisely the makers whose offers sit on the board waiting to be taken, who are the ones holding escrow the longest.
So the requirement is applied to the seller of each trade, whichever side that turns out to be, which means it is checked in two places — when a trader takes a buy offer, and when a trader publishes a sell offer. A maker hears about it on their own offer form rather than through a stranger's failed trade. A taker who answers a sell offer whose maker has no second factor is told only that the offer is unavailable: another trader's security setup is not their business.
A trader's record outranks their rating, and the profile page is built that way
The new profile page could have opened on a five-star widget. It does not, and the reason is a rule this product has always run on rather than a matter of taste.
A rating is an opinion, averaged. The record is what happened. A five-star seller who takes forty minutes to release escrow is a worse trade than a four-star seller who takes fifty seconds, and no star widget can say that — it is the single fact most likely to cost your traders an afternoon, and it is not in the rating at all.
So the page leads with the record: completed trades against finished ones, the completion rate, how long they take to release escrow, and how much they currently have listed. The rating is still there, below, with its three axes drawn on one shared scale so a trader who is slow but honest reads differently from one who is fast but hard to reach. That comparison is the reason the axes are collected separately, and three separate star rows are exactly where it disappears.
The same principle decides what the page claims. A green shield reading Verified is a statement about a person; the check behind it confirms an email address. It now says Email verified, and a note at the foot of the page states what the record does and does not cover, for the reader who is about to send money to a stranger and is right to be sceptical.
The defect that stopped offers being created
A trader could pick a payment method from the list this platform gave them, and be refused by id when they saved. The refusal named a method the admin screen was badging Global · Active, and no action available to the trader cleared it — deselecting the method was the only way to save an offer at all. The same check guards editing, so an offer already carrying one of these could never be edited again, by anyone.
The admin desk was reading a case file with pages missing
Three screens decide where escrow goes — a trade, a dispute and an offer — and each was quietly withholding part of what it held.
The cause is one mistake made in several places. Some columns on these records are stored as JSON: a dispute's conversation, its evidence and its note trail; a trade's settlement record; an offer's price, limits and payment window. On this platform's database engine those columns come back as text rather than as structured data, and every one of these screens tested them with a check that is false for text. So the code did what it was written to do — it fell through to "there is nothing here" — and rendered an empty list. Nothing errored. Nothing was logged. Every request answered normally. An operator opening a dispute saw no evidence submitted and an empty transcript, and ruled on it. An operator who had just awarded an escrow saw no record of what had moved or who had decided it. An offer awaiting approval showed no price, no trade limits and no payment window.
The activity log was wrong in a different way, and worth stating separately because it looks like the same symptom. Its search box and its four severity tabs filtered whatever page the browser happened to be holding — the twenty most recent entries — so a search for a dispute from last week answered "No activities found", which is indistinguishable from it never having happened. There was no way to reach the twenty-first entry at all.
The screens themselves were built the way admin screens used to be built: tabs. On the trade case the conversation was tab three, the amount was tab one and the order of events was tab two, so weighing "they say they paid at 14:32" against a receipt and against the payment window took two tab switches per comparison — and then the decision was made in a dialog that read "Are you sure you want to resolve this trade for the buyer?", naming neither the amount, nor the currency, nor the party. The note written into the permanent record was the same hardcoded sentence on every trade ever resolved, which is what an audit of those decisions had to work from.
All four are rebuilt on the shape the trade room already uses: one screen, the way out top-left, the conversation permanently beside the case, and the decision made in place with the money on screen while you authorise it. Buttons say what they do — Release 0.0142 BTC to Jane Doe — and a projection shows what each party receives, net of fee, before anything moves.
One capability was also simply unreachable: splitting a disputed escrow. The server has accepted a percentage split since it was written, the dispute screen offered it, and the trade screen — the one an operator arrives at from a trade — offered only "resolve for buyer" and "resolve for seller". A genuinely shared-fault case could only be settled by picking a winner.
Upgrade Notes
"Max Active Trades" is a real control now — check what yours is set to
Maximum Open Trades per Trader at /admin/p2p/settings caps how many trades one person may have running at the same time. It is a new capability, but not a new setting key: p2pMaxActiveTrades sat on that page for a long time being read by nothing, and was taken down when the dead controls were removed. It is back because it now has an enforcement path behind it.
That means a number you typed into it before, when it did nothing, will start binding.
- Look at the field before you update if you ever touched it. The row you saved is still in the database and is now enforced. It used to ship as
10; if you saved that page while it was on screen, your platform now allows each trader ten concurrent trades. - If you never saved it, nothing changes. It defaults to 0, which means no limit, and that is what an absent row resolves to. This is a deliberate change from the old default — restoring the reader must not quietly throttle a marketplace that never asked for a cap.
- 0 is how you turn it off, not how you ban trading. The platform's other rule, one live trade per offer, is unaffected and still applies either way.
Your minimum and maximum trade amounts are dollars, and now behave like it
Read this one first if you run your marketplace in anything other than US dollars. It does not ask you to repair data — it changes what a number you already typed means, and the number is a trading limit.
Minimum Trade Amount and Maximum Trade Amount at /admin/p2p/settings were labelled with a dollar sign and then compared, unconverted, against whatever currency an offer was priced in. So a 100000 entered as one hundred thousand dollars was enforced as one hundred thousand naira — about $73 — and the refusal handed the trader the operator's own figure with their currency code on it: "platform maximum of 100000 NGN". The floor failed the other way and in silence: a 10 meant to stop trades under ten dollars stopped trades under ten naira, about $0.007, so the control that exists to keep dust off the board was doing nothing at all. A euro-only platform was about 1.1× out and nobody noticed for a long time.
Both figures are converted per offer now, at the same rates the rest of the platform uses.
- Re-read both values. They are US dollars. If you set them meaning your local currency, they now permit far more than you intended — on a naira board a
100000maximum has gone from roughly $73 a trade to a genuine $100,000 a trade. Nothing warns you, because from the platform's side the value did not change; only the arithmetic around it did. - Check that the currencies your traders price in have a rate, under Finance → Currencies. While either bound is set, an offer priced in a currency with no usable rate is now refused rather than quietly exempted, and the message names the currency. A platform with neither bound set never needs a rate and is unaffected.
- If you never touched these two fields, you have nothing to do. They are only read when they hold a value, and a platform that has not saved them has no platform-wide limits at all — before this change or after it.
Re-open the disputes you have already ruled on
The admin dispute screen showed "No evidence submitted" and an empty conversation for disputes that held both, and no internal note trail even where colleagues had left notes. It did not fail; it rendered an empty list, at HTTP 200, with nothing in any log. So a ruling made on that screen was made without the receipt the buyer attached, without the messages either party sent, and without whatever the last person to look at the case had written down.
The material was never lost — it was in the record the whole time and only the reading of it was broken. It is all visible now.
- Fixed all four dispute endpoints — the one that loads the case and the three that write to it — reading those columns through one shared reader instead of four copies of the same broken check. The write routes were affected too, so evidence an admin uploaded appeared to vanish the moment it was stored, and a reload showed the same nothing.
- Review any dispute you resolved against a buyer for lack of proof. That is the outcome this defect biases: evidence the buyer had supplied was invisible to the person deciding. Disputes are listed under Admin → P2P → Disputes; the ones to look at are those resolved SELLER_WINS or CANCELLED. Every one of them now shows what was actually filed.
- Escrow that has already settled cannot be re-settled from this screen, by design. A ruling you want to reverse is a manual wallet correction, so establish first whether the evidence would actually have changed the outcome.
An admin awarding an escrow could not see what had moved
Separately, and in the same shape: after resolving a trade the screen showed the ordinary Completed badge and nothing else. The settlement record — what the buyer was credited, what the seller was refunded, the fee taken, and which admin decided it — was written correctly on every resolution and then served as empty.
- Fixed the trade case screen showing that record. It also now distinguishes a resolution that moved money from one the escrow authority declined because the hold had already been settled. Those answer identically over the wire apart from a flag, and the old screen said "action completed successfully" to both — so an operator could close a case believing they had paid somebody who was never paid.
- Review any trade whose escrow you resolved and where the recipient later said they were not paid. The screen now reports it plainly; there was previously no way to tell from the interface.
Traders could be refused a payment method the platform had just offered them
Payment methods carry two independent markers: an owner, and a Global flag. Nothing kept them in agreement, and four parts of the platform disagreed about which one made a method shared. The list a trader picks from accepted either marker. The check that runs when they save accepted only the missing-owner one. Any method flagged Global while still carrying an owner therefore sat in the gap — offered to every trader, accepted from none of them. Publishing an offer failed with "Invalid payment method IDs: …" naming a method the trader had just chosen from this platform's own list, on a row the admin screen showed as Global · Active. There was no correction available to them: removing the method was the only way to save. Because the same check guards editing, an offer already carrying such a method could not be edited again by anybody — not its maker, not you.
- Changed: the list and the check now use one definition. A method is attachable if it is flagged Global, or has no owner, or belongs to the trader attaching it. Methods belonging to another trader and not flagged Global stay refused, exactly as before — those hold that person's real account details.
Two-factor on escrow release, and the order to switch it on in
Both controls are off after updating and nothing changes until you turn them on. Require 2FA to Sell stops a trade starting against a seller who has no second factor. Require a 2FA Code to Release Crypto then asks for a fresh code on every release. The second is only offered once the first is on, because the first is what makes the second safe: it is what guarantees the person who will have to answer the challenge is able to.
Turning the release requirement on affects trades that are already running. A seller holding a buyer's paid escrow who has no 2FA enabled cannot release until they enable it. The trade room tells them so and links them to their security settings before they confirm anything, and their escrow is untouched while they sort it out. If they never do, the trade opens a dispute by itself after 24 hours and you settle it from the admin desk as normal — admin dispute resolution is deliberately not subject to this requirement, so a seller can never freeze a trade by switching their own 2FA off. No funds are at risk in any of these paths, but each stuck trade costs somebody's time, which is the argument for enabling the first control and giving sellers a while to enrol before you enable the second.
- Added: both keys sit on the Super-Admin-protected list, the same class as the withdrawal two-factor switches. An admin without that role who changes either one has their whole P2P settings save refused, naming the key.
- Enable Require 2FA to Sell first and leave it on its own for as long as your sellers need to enrol. Only then enable the release requirement.
- If two-factor authentication is off platform-wide, or none of your enabled methods can actually be delivered — SMS with no SMS provider configured, for instance — both P2P requirements are ignored rather than refusing every trade and release on the platform. A warning is written to the log saying so.
One payment method, not one per trader
A payment method used to be a single row carrying two different things: the identity — "PayPal" — and one trader's own credentials. So every trader who accepted PayPal created another row named PayPal.
The marketplace's payment filter is built by grouping the methods attached to live offers, keyed on the row. On a platform with a thousand traders accepting PayPal, that filter had a thousand entries in it, every one of them reading "PayPal" — and a taker who picked one narrowed the board to a single maker without being told that is what they had done. The same was true of every method anybody used.
The two halves are now separate records. A method is shared — its name, and the fields it asks a buyer for. A trader's details are their own, private, and copied into the trade exactly as before. The filter groups on the method, so PayPal is one entry however many people accept it.
- Changed, and this is what the repair does: your existing rows are grouped by name with spelling, case and punctuation ignored, so "PayPal", "Paypal" and "pay pal" become one method rather than three. Each owner keeps their own details on it. The plan names every merge before it runs.
- Nothing about offers or trades is touched. An offer still points at the trader's row, and a trade still copies that row's details at the moment it opens, exactly as before — so no live offer changes and no settled trade is rewritten.
- Field shapes ship for the methods most operators already have — bank transfer, SEPA, PayPal, Wise, Revolut, Zelle, Cash App, UPI, IMPS/NEFT, Pix, M-Pesa, Mercado Pago, GCash, Papara and cash in person. Anything else has its fields inferred from what traders using it have actually filled in, and you can correct any of it afterwards.
- A method a trader defines does not go straight into the public filter. It works immediately, it is findable by name by the next trader who needs it, and it shows on the offers that carry it — but it enters the filter every visitor sees only once more than one maker is using it, or when you list it yourself in Admin → P2P → Payment Rails.
The new payment-method setup console is gated, and no role holds its permission yet
The setup console (below) creates catalogue payment methods in one pass. Its endpoints declare view.p2p.payment_method and create.p2p.payment_method — the same keys the existing payment-method screens use.
- Changed: on a fresh install those keys are held by nobody. Permission gates ship closed, and Super Admin bypasses them by name, so the button renders for every P2P admin and the Create step then refuses everyone except a Super Admin.
Added
A dispute now says how long it has been, and what to send
"Under review" reads identically at hour two and at hour thirty, and the trader's money is frozen behind it. The only way to find out whether anything was happening was to open a support ticket — a second queue, staffed by the same people, asking about the first one. And the panel said a member of staff was reading the conversation without saying what makes a case winnable, so disputes arrived as an assertion and a screenshot and the desk spent its first reply asking for the same three things every time.
- Added a clock beside Under review, counting from the moment the dispute was opened, against a target you set: Dispute Review Target (hours) at
/admin/p2p/settings, 48 hours by default. Both traders see how long it has been and how much of the window is left; past it, the room says the case is overdue. Set it to 0 to show the elapsed time and state no target. - Added an evidence checklist in the dispute panel — bank statement as a PDF, a screen recording of the banking app's transaction history, and anything else that supports them — sitting beside the chat box that is where the evidence actually goes.
- The chat stays open for the whole dispute, as it always has. Worth saying plainly because a good share of cases are a misunderstanding the two parties settle between themselves, and nothing about the review stops them doing that.
:::warning{title="The review target is a promise, not a mechanism"} Nothing resolves a dispute when the clock runs out, and nothing should — an escrow decided by a timer rather than by a person would pay somebody out on silence alone. Set the target to what your desk can actually meet: a target you miss is worse than none, because the screen tells the trader you missed it. :::
A trader can withdraw a dispute they opened
Disputes get raised on a timer. A buyer who has paid and is watching a countdown files one the moment the seller goes quiet — and then the payment lands, or the seller answers, or the transfer that was "on its way" arrives. The case stayed open regardless: the trade sat in DISPUTED, nobody could release, and the only way out was for one of your staff to read a case that had already resolved itself.
- Added a Withdraw dispute button in the trade room's under-review panel. The trade returns to the status it held before and the seller can release normally.
- Only the trader who filed it, and only while the case is still
PENDING. The moment an admin moves it toIN_PROGRESSthe button is gone — a case cannot be pulled out from under the person reading it, including by a filer who has been talked into withdrawing. - Nothing is unwound, because nothing had moved. Filing a dispute writes a row and changes a status; it takes no hold and releases no escrow. The record survives in full: the trade keeps its dispute stamp, the timeline keeps both the opening and the withdrawal, and the case keeps its evidence with a resolution reading
WITHDRAWN. - It is not a waiver. The same trader can file a fresh dispute if the matter turns out not to be settled after all — which is what makes offering the button safe, because the worst case is being back where they started.
DELETE /api/p2p/trade/{id}/dispute. Withdrawn cases leave your queue with the outcome WITHDRAWN, so a case that vanishes is accounted for rather than missed.
The payment reference can be optional, where your traders do not get one
Confirming a payment made a buyer type the reference their own bank gave them, always. Not every payment method produces one a person can read back — cash, some wallet transfers and several mobile-money flows give the payer nothing to quote — and on those the field was an unanswerable question standing between a buyer who really had paid and the confirmation that protects them. A trade that expires while its buyer hunts for a reference number that does not exist is the worst outcome in the product: the money is gone, the escrow has returned to the seller, and an EXPIRED trade cannot be disputed.
- Added Require a Payment Reference at
/admin/p2p/settings, on by default. Leave it on for bank transfers, where the reference is what lets a seller match an incoming credit to a trade. Turn it off where your traders are not given one. - The trade room follows it in both places: the reference row in the payment panel says whether it is a rule or a courtesy, and the confirm form marks the field required or optional. A panel that says "must be included" beside a form that accepts a blank is how a trader learns to stop reading the panel.
- Fixed, as part of this, the requirement existing only in the browser. It was an
ifin the confirm form, so anything that was not that form could confirm a payment with no reference at all and you had no way to know.POST /api/p2p/trade/{id}/confirmnow applies the setting itself.
A cap on how many trades one trader can have running at once
Until now the only concurrency rule was one live trade per offer. That stops somebody opening the same offer twice and stops nothing else — a trader could hold any number of simultaneous trades as long as each was against a different offer, tying up a dozen sellers' escrow at once with no obligation to pay any of them.
- Added Maximum Open Trades per Trader at
/admin/p2p/settings. Set it to the number of trades one person may have running at the same time, or 0 for no limit. See Upgrade Notes — the setting key is an old one that used to do nothing, so check yours before you update. - It counts only the trades a trader opened themselves. A maker is a participant in every trade taken against their offer, and counting those would cap your best market makers for being popular — fill five of their offers and they could not trade anywhere on the platform.
- A disputed trade does not count. A trader cannot close a dispute; that needs your staff. Counting it would mean raising a dispute costs a trader their ability to trade until somebody reaches the case, which makes the limit a penalty for using the dispute system.
- The trade button says so before it is pressed. A trader at their limit reads how many they are holding and what to do about it, rather than filling in an amount and being refused afterwards.
- Enforced inside the same serializable transaction that opens the trade, so two requests arriving together cannot both slip through on the same stale count.
One marketplace, at /p2p/market
- Added a market board that opens on a plain-language sentence — I want to buy USDT with NGN in Nigeria — where every underlined word is a control, each with its own search.
- Added a switch between that sentence and a classic filter row, remembered per browser. Both are always available; neither is an operator setting any more.
- Added a recommendation strip that delegates to the same query as the full board, so an offer can never be recommended and then missing from the list.
- Added a traders view of the same market, for buying from a person rather than from a price.
- Added an escrow explainer on the board itself, where a first-time buyer is deciding whether this is safe.
An offer composer that shows what you are publishing
- Added /p2p/offer/new, a single page with a live preview of the actual offer card beside the form. The eight-step wizard it replaces did collect the right fields, but nothing was visible until step nine, so every decision was made blind.
- Added a forecast panel that prices the offer as configured, against the current market.
A trades page that replaces the dashboard
- Added /p2p/trades, carrying live trades, history and the figures the separate dashboard existed to show. Nobody opens a P2P product to look at a portfolio chart; they open it because a trade is running and they want to know where it stands.
A page for the offers a trader has posted, at /p2p/offers
Publishing an offer and dealing with a trade are two different jobs on two different clocks. An offer sits there earning nothing until somebody takes it, and the questions are all tuning — is it live, is it priced right, how much of my balance is committed, why is nobody taking it. A trade has a counterparty waiting, a payment window counting down and money in escrow. Sharing one page put the unhurried thing and the urgent thing in competition for the same screen, and the offers half — being a switch rather than an address — could not be linked to at all.
- Added /p2p/offers and a My offers entry in the P2P navigation. It carries a row of figures about the board rather than about the trading record: how many offers are live out of how many posted, how much capital is held against them, how often they have been taken, and how many are in a state only the trader can clear.
- Added a status filter that offers only the states this account is actually in, so there is no tab that resolves to nothing.
- Added three facts to each offer that the old list had nowhere to put: the capital held in escrow against it, how many trades have been opened against it, and the visibility and taker requirements that were set in the composer and then never mentioned again. The last of those is usually the answer to "why is nobody taking my offer" — a link-only offer and a live one looked identical.
- Added a Draft state to that list with a name and an action. Drafts were already returned to this list, because it is the only place a saved draft is reachable from, but the list had no case for them: they rendered the raw word "DRAFT" beside a button labelled Edit.
A rebuilt trade room
- Added a single-column room with a status ribbon, the countdown, payment instructions, chat and the one action available at that moment — replacing a tab strip that hid the next step behind whichever tab you were not on.
Two-factor verification when crypto is released
- Added a Require a 2FA Code to Release Crypto setting. With it on, the seller confirms every release with a fresh one-time code from whichever method they already use — authenticator app, email or SMS. Holding a live session is no longer enough to hand a buyer's escrow away.
- Added a Require 2FA to Sell setting, so a trade cannot start against a seller who has no second factor and nobody ends up holding a buyer's payment without the means to release it. It applies to whichever trader takes the selling side: the taker of a buy offer, and the maker of a sell offer, who is refused at the offer form.
- Added the verification step to the trade room itself. The seller presses Release once and confirms in place; the release then completes on its own. Authenticator users are sent nothing, because the code is already on their device.
- Added a notice in place of the release button for a seller who cannot yet meet the requirement, linking them to their security settings. They learn they are blocked before they confirm the money arrived, rather than from a refusal immediately after.
- Added recovery codes as an accepted answer to the release challenge, so a seller who has lost their device is not left holding a trade that has already been paid for.
- Added a record to the trader's activity trail every time a release code is accepted, and a matching entry against the trade in the P2P audit log.
A trader has a profile now, at /p2p/trader/[id]
Every surface in this section named a counterparty and none of them let you look one up. The board, the traders lens, the offer permalink and the trade room each printed a name, an avatar and a one-line trust summary, and that summary was the end of the road. There was no way to see the rest of what somebody had on the board, no way to get from "that went well" to "what else are they offering", and no way to read a single word anyone had written about them — reviews had been collected since this product shipped and were rendered nowhere at all.
- Added a profile page for any trader, opening on their record — completed trades against finished ones, completion rate, how long they take to release escrow, and how many offers they have live. Below it: their volume per asset with an average trade size, everything they currently have listed, and the reviews other traders have left them.
- Added the three rating axes — communication, speed and trust — on one shared scale. They are collected separately and were being averaged into a single number before anybody saw them, which loses the distinction between a trader who is slow but honest and one who is fast but hard to reach.
- Added the viewer's own history with that person, directly under their name. It is the strongest thing a peer-to-peer product can tell somebody, and the page now answers in every case: how many trades you have completed with them and when the last one was, that you have never traded with them, or that signing in will tell you.
- Added a route in from every place a trader is named: their card on the Traders lens, the identity block on an offer, and the conversation header inside a live trade — which is where "who am I actually dealing with" gets asked.
- The page says nothing the counterparty has not already put on the board. No email address, no exact last-seen instant, and reviewers appear under a given name and an initial rather than in full.
- An account nobody has rated reads "Not rated yet", never zero stars, and an unrated axis draws no bar rather than an empty one. A brand-new trader still gets the whole record, with each empty figure named in words — "None yet", "No record", "Never measured" — because a blank invites the reader to fill it in favourably, and on this screen that is somebody's money. It also means their live listings are stated, which a suppressed record could never do.
- A review left without a comment takes one line and says so, rather than a quotation mark around a silence nobody expressed. Where a reviewer's three axes disagree, the page prints them — "happy, except about speed" is a thing only that reader said.
An explainer at /p2p/learn
- Added a guide written for someone who has not traded peer-to-peer before, with drawn diagrams of the escrow flow, the two sides of a trade and what the countdown means.
- Added the same step diagrams above the offer composer, so a first-time maker sees the whole trade before publishing into it.
A payment-method setup console for the operator
- Added Admin → P2P → Payment Methods → Setup, which creates the catalogue rails for your market in one pass, skipping any that already exist and reporting the outcome of every row. Every method it writes is global and ownerless.
Splitting a disputed escrow, from the trade screen
- Added the split outcome to the trade case screen, with a slider and a live projection of what each side receives. The server has accepted a percentage split since it was written and only the dispute screen could send one, so an operator who arrived from a trade could settle a shared-fault case only by picking a winner.
- Added a projection to every ruling on both screens: what the buyer receives net of fee, what the seller receives, the fee taken and the status the trade lands in — on screen while you authorise it, rather than in a dialog that covers the figures at the moment they should be re-read.
- Added a required explanation to every ruling, in the operator's own words. Resolving from the trade screen previously wrote the same hardcoded sentence into the permanent record on every trade, which is what an audit of those decisions had to work from.
Things the admin screens could not say before
- Added each party's dispute record — how many they have filed, and how many have been filed against them — to the trade and dispute case screens. A completion rate alone decides nothing; a trader who has filed nine disputes and had none filed against them is a different witness from one with the reverse, and neither fact was reachable from these screens.
- Added a warning when a trade or dispute is closed but its escrow is still held. That is a stuck payout, and it was indistinguishable from a normal settled trade.
- Added a warning when an offer advertises more than it escrows, which would let a taker open a trade against collateral that is not there.
- Added a warning when an offer has no payment method attached — a taker would have no way to pay it, so it cannot function as listed.
- Added the payment window a trade is actually running against, and whether auto-cancel is switched on. With it off nothing closes an overdue trade on its own, and an operator looking at one hours past its window needs to know which of those two situations they are in.
- Added the buyer's own claimed payment reference and the moment they confirmed it, beside the buyer. It was recorded on every trade and shown nowhere, so an operator deciding "did they pay?" could not see what the buyer said they had paid with.
- Added claiming a dispute — I'm taking this — and messaging both parties, to the dispute screen. Both existed and were reachable from no screen, so the only way to say anything to the traders was to write it into the ruling and rule.
- Added a stale-case warning when a dispute has sat unclaimed for more than a day with both parties waiting and the escrow frozen.
Getting between a dispute and the trade it is about
A dispute and its trade are one piece of work read from two screens — the trade holds the money, the payment window and the two traders' own conversation; the dispute holds the claim and the evidence — and ruling on one almost always means reading the other. There was no way through in either direction. The trade list showed a Disputed badge that went nowhere, the dispute list printed the trade's id as unclickable text, and the detail screens each hid the link in the page footer, below everything.
- Added the counterpart to the pinned bar on both case screens: Open the dispute appears on a disputed trade, Open the trade on every dispute. The bar does not scroll away, so the jump is available while you are reading rather than after you have scrolled past the whole case.
- Added links to both lists — the trade id on a dispute row, and the dispute on a disputed trade's record — replacing text that could only be selected and pasted into the address bar.
Endpoints behind the new screens
- Added market board, recommendations, traders and locale endpoints, an offer forecast endpoint, and a spendable-balance endpoint. The locale endpoint reports which signal produced the currency and the country it suggested, so the board can say what it assumed rather than applying it silently.
- Added category, severity and search filters to the activity endpoint, so the activity log can query the whole table rather than filtering whatever page it happened to receive.
Changed
A maker can see where their own offer ranks
The market board hid your own offers from you. The reason was sound as far as it went — you cannot trade with yourself, so a trade button on one of your own rows would have been refused — but removing the row to disable a button took with it the answer to the question a maker opens this page for. "Where does my price sit against everyone else's" was unanswerable from the one screen that knows, and the only way to find out was to sign out and count.
Your own offers stay out of the two places that recommend somebody to deal with — Find me someone and the live board on the landing page — because every card there is an invitation to open a trade the platform would then refuse. The Traders lens leaves them out too: it is a list of people to deal with, one card each, with no ranking to read off it. The seller count above the board still counts counterparties only, so you are never included in the number of people you can trade with.
- Changed the All offers board to keep your own offers, marked Your offer and carrying their position. They sit in the ranking exactly where a buyer sees them, so #1 means #1.
- Added a line above the board reading Your offer is #3 of 18 by best price. It names the sort, because a position means nothing without the ordering it is a position in — an offer that leads on trust can sit well down the list on price, and it is the second number that tells you whether to move it.
- Added a position that survives falling off the page. The board loads 24 rows, so an offer at #40 has no row on screen — and that is precisely the maker who needs telling. The line reports the rank anyway and says the offer is below what is shown.
- Changed the action on your own rows from a trade button to Edit, which opens the offer at its price field.
- Changed the board's average price, which used to depend on who was asking. It was computed with the viewer's own offers taken out, so a maker judging their price compared it against a market no buyer would ever see — and it disagreed with the forecast the offer composer prints for that same board. One figure now, for everyone.
The old P2P addresses now redirect
- Changed six routes into redirects so existing links, bookmarks and search results still land somewhere useful: /p2p/dashboard and /p2p/trade go to /p2p/trades, /p2p/guided-matching to /p2p/market, /p2p/offer to the offer list of the market, /p2p/offer/create to /p2p/offer/new, and /p2p/guide to /p2p/learn. Each keeps the visitor's language.
The market filter style is the trader's choice, not a setting
- Changed the Market Filter Style field out of P2P settings. Guided sentence or classic filter row is a property of the reader, so the market carries the switch and remembers each browser's answer. An existing stored value is still read, as the default a browser sees before it expresses a preference of its own.
- Removed two further settings that selected which panels appeared on the old landing page. Those panels were replaced by one live board and nothing had read either key since.
The payment-method book
- Changed the trader's payment-method page to full width, with the platform's shared rails and the trader's own methods presented as distinct things rather than one merged list.
Adding a payment method is a page, and it asks two questions
Adding a method used to be one question in a small dialog: invent a name, then fill in a free-form list of boxes labelled Label and Value — the names of the data structure rather than of the thing being asked for. A first-time maker had no way to know what to type, and inventing the name is what produced a thousand rows called PayPal.
It is now /p2p/payment-methods/new, a page with an address, and it asks which method before it asks for anything else.
- Added a searchable list of methods to pick from — the platform's own, plus any other traders have defined. Searching finds a method by any spelling that would have collided with it, so "pay pal" and "PAYPAL" both find PayPal.
- Changed the second step to ask for that method's own fields, by name. PayPal asks for an email. A bank transfer asks for the bank, the account number and the account name. Nobody types "Label" again.
- Added a live preview of the buyer's side beside the form, drawn as the panel that actually appears in the trade room and updating as the trader types. The two rows the trade adds by itself — the exact amount and the payment reference — are shown as such, so nobody adds an "Amount" field of their own.
- Added the fallback, last on the page rather than first: define a method the list does not have. The trader names it and names its fields, and both are then shared — the next trader to use it fills in values for the same fields and cannot change them. Somebody who needs different fields creates a differently-named method.
- Added a check on that name. One that already exists hands the trader the existing method rather than refusing them, and a near miss — "Kuda" against "Kuda Bank" — is offered as a suggestion they can accept or overrule. Neither merges anything on their behalf.
- Changed the same form to open in a panel inside the offer builder, which cannot navigate away mid-draft. It is the same form, so a method added there is a method like any other; a separate cut-down copy is what let the duplicates in.
- Changed editing a saved method to show that method's fields with the values already in them, rather than a raw list of pairs. The method itself cannot be swapped from there — offers already point at it — and the screen says so.
- Added Admin → P2P → Payment Rails, where the shared methods live: which appear in the public filter, what fields each asks for, and which ones traders defined themselves. Retiring one is a switch rather than a delete, so nobody's saved details go with it.
Moving between the two halves of "my stuff"
- Changed the header of My trades and My offers to carry a labelled link to the other. Splitting one destination into two makes it possible to be on the wrong one.
- Changed the main action on My trades from Post an offer to Browse offers. Posting is the main action on the offers page now, one click away; the thing that starts a trade is the market.
Offer settings
- Changed the longest payment window the offer composer offers from two days to one day. Editing has always rejected anything above one day, so a two-day offer could be published and then never saved again — the same permanent-lock shape as the payment-method defect above, reached by a different route.
The market opens on a currency it can justify
- Changed the opening currency to come from the trader's own recent P2P history, then their profile country, then a CDN geo header, then a cached IP lookup — and to report which one answered. Previously an unresolved currency fell back to the busiest fiat on the platform and then wrote itself into the address bar, so the guess returned on every reload looking like a deliberate choice.
Cancelling a trade was drawn as a link
The trade room's footer carries three controls: raise a dispute, cancel the trade, and go back to your trades. Cancel is the only one of them that does anything — the other two go somewhere — and it was drawn with no border, no fill and no colour of its own, so it read as ordinary text sitting beside a link. A buyer who had decided not to go through with a trade was left hunting for the way out among the ways back. It is outlined rather than filled because cancelling is only ever offered to the buyer before a payment has been confirmed: nothing has moved yet, so the control owes clarity rather than alarm.
- Changed cancel into a bordered button, a step larger than anything beside it and the only bordered control in that row.
- Changed it to show a spinner while the cancellation is in flight, and to ignore a second click landing in the same instant as the first. It previously only dimmed, and two clicks arriving together could both be sent.
The admin case screens
- Changed the trade, dispute and offer screens from tab strips to one screen each, with the conversation permanently beside the case rather than behind a tab. Weighing a claim against the evidence and against the amount previously took two tab switches per comparison.
- Changed every irreversible action from a confirmation dialog to an inline commitment strip. The button now names the amount and the party — Release 0.0142 BTC to Jane Doe — because a generic Confirm gives the person pressing it no chance to notice the figure is wrong, and a screenshot of one proves nothing about what they believed they were authorising.
- Changed the way out to the top-left of every admin P2P screen, pinned, alongside the breadcrumb. It was previously the far right of an unpinned row, so scrolling to read a long conversation took the only exit off screen along with the status.
- Changed every money figure on these screens to the same formatter the trader sees. Amounts arrived as pre-formatted text built by joining a raw number to a currency code, so the operator and the customer read the same trade in two different formats — and a fee of 0.00000052 was printed as 5.2e-7.
- Changed the moderation controls on an offer to show only those its status actually accepts. All six were offered on every offer, so Pause was live on a rejected one and could only ever fail.
- Changed the reason given when rejecting or taking down an offer from optional to required. The maker is shown it, and it is the only explanation they get.
- Changed the save control on the offer editor and the payment-method editor into a footer bar that stays in place. On forms this long it left the viewport as soon as you started editing.
The activity log searches the log
- Changed the search box, the category filter and the severity tabs from filtering the twenty most recent entries in the browser to querying the whole table. Searching for a dispute from last week returned "No activities found", which is indistinguishable from it never having happened, and there was no way to reach the twenty-first entry at all.
- Changed the four severity tabs into a filter row with a count, because four tabs over one list that each narrow the same list are a filter wearing navigation's clothes — and they hid the count, so Critical looked identical whether it held nothing or everything.
The dashboard asked the exchange for a price once per coin, and waited for each
Opening the P2P dashboard valued a trader's wallets by asking the configured exchange for a ticker — one live network request per currency held, each one waiting for the last to come back. Measured against Binance from this platform: 240 milliseconds per coin, and the coins the exchange does not list were the slowest of all, because their request has to fail before it can return. A trader holding two currencies paid about a third of a second for it. One holding thirty-one paid seven tenths of a second, on a screen that refreshes itself.
- Changed all three dashboard reads — the stat tiles, the dashboard body and the portfolio panel — to price every wallet in one pass against the exchange listing the platform already refreshes in the background. The three screens answered in 310, 344 and 319 milliseconds before; they answer in 8, 10 and 5 now, and for the trader with thirty-one wallets in 19, 19 and 11.
- Changed the price behind those totals from a live ticker to that listing, which can be up to two minutes old. A portfolio total is not a quote, and this is the source the wallet page has always used — so the P2P balance and the wallet balance now agree, which they could not before, because they were reading two different prices taken at two different instants.
- Changed completed-trade volume, the offer-price rate lookup and the admin trade aggregates to resolve every currency in one lookup rather than one query per currency inside a loop.
The admin dashboard asked the same questions twice
- Changed the admin P2P dashboard to stop re-asking for figures it already held. It issued thirty-six queries to draw one page and thirteen were redundant: three offer counts a grouped count over the same table had already answered, a trade count that was two of its neighbours added together, eight separate aggregates over the dispute table where one answers all eight, and five over the trade table likewise. Nineteen now, in two rounds instead of five.
Fixed
The platform's trade limits were dollars everywhere except in the comparison
The Minimum and Maximum Trade Amount settings carried a dollar sign and were then compared, unconverted, against amounts in whatever currency an offer was priced in. On a naira marketplace a 100000 maximum meant one hundred thousand naira — about $73 — so an operator who meant one hundred thousand dollars had capped a single trade at the price of a phone, and 99.9% of the range they thought they had offered was unreachable. Traders were refused with the operator's own figure carrying their currency code: "platform maximum of 100000 NGN", a number that was never naira.
The floor failed the other way and made no noise at all. A 10 meant to keep trades under ten dollars off the board stopped trades under ten naira, about $0.007 — so the control existed, read as configured on the settings page, and prevented nothing. A euro-only platform was about 1.1× out, which is why this survived so long.
- Fixed both bounds being converted into each offer's own currency before any comparison, at the same rates the rest of the platform uses. See Upgrade Notes — the values you already saved now mean what they say, which on a non-dollar platform is a real change in what your marketplace permits.
- Fixed the refusals naming a figure a trader can act on. They now state the bound in the offer's currency and, where they differ, the dollar figure it came from.
- Fixed the offer form and the offer page showing the converted bound. The permalink gets it from the server, so a signed-out visitor reading a shared offer sees the real ceiling rather than a dollar figure with the wrong currency beside it.
- Fixed a maximum that was never stated being measured against the offer's crypto total rather than its value. Two figures in different units were being compared as though they were the same one.
- Fixed a saved limit of
0refusing every trade on the platform rather than meaning "no limit", and a limit row holding the texttrue— which some platforms carry from an older settings bug — silently removing the limit instead. - A currency with no exchange rate on file can no longer have a dollar limit applied to it, so offers priced in it are refused while a limit is set, and the message names the currency. A platform with no limits set never needs a rate.
"You receive" was the seller's figure, not the buyer's
The escrow fee comes out of the crypto, not out of the cash. The escrow authority takes the whole traded amount from the seller's hold and credits the buyer that amount minus the fee, so the two sides of a trade are not the same number — and every screen that printed the buyer's side printed the seller's.
On an offer priced at 64,000 EUR per BTC, a buyer entering 640 EUR read You receive 0.01 BTC. What reached their wallet was 0.0099. The fee was correct, recorded on the trade from the moment it opened and printed in the trade room's own facts row; it was only the figure the buyer was deciding on that ignored it. Nothing between the offer page and settlement ever restated it, so the shortfall first became visible in a wallet balance after the trade had finished — which is where it reads as a missing payment rather than as a fee, and arrives at your desk as one.
The fee itself is unchanged. Every "You receive" figure on the platform will drop by the fee the moment you update, and nobody is being charged anything new: the number was always going to be deducted, and the screen has stopped omitting it. No settled trade paid out differently from what was recorded on it.
- Fixed the offer page, the trade room and the trades list showing a buyer what they will actually be credited. A seller's side is unchanged and still shows the gross, which is what really leaves their wallet.
- Fixed the offer page not naming the fee at all. It now states the amount, says that it comes out of the crypto rather than being added to the cash, and shows the subtraction — a net figure with no working is indistinguishable from a wrong one.
- Fixed the offer page quoting a fee it was not entitled to compute. The terms come from the same code that charges it, so a figure quoted before a trade and the figure taken when it settles cannot drift apart — which is what the screen this replaced did, deriving its quote from a settings key the platform does not read.
"Active 26 hr ago" on a trader who was mid-trade with you
Presence was read from the last time somebody signed in, and from nothing else. Sessions on this platform last fourteen days, so a trader who signed in on Monday and has been dealing every day since carried Monday's timestamp all week — and the market told everyone they had been away for days.
Reported from a live install, and the card contradicted itself in the space of two lines: "Active 26 hr ago — expect a reply when they're back", directly underneath "You've traded with them 1 time · last 6 min ago". The sentence a reader acts on was the wrong one, and it costs a trade every time: it steers somebody away from a counterparty who is sitting right there.
- Fixed presence being the later of when a trader last signed in and when they last did something — confirmed a payment, released an escrow, or published an offer. It applies everywhere presence is shown: the trader cards, the market board and the offer page.
- Fixed the Online now filter reading the same signal as the badge beside it. It filtered on the sign-in while the card was labelled from it too, so both were stale together; now that the label is live, a filter left on the old signal would have hidden the very traders it exists to find.
- Only actions the record attributes to one person count. A trade's own last-modified time is deliberately not among them: it moves when the counterparty sends a message, and it moves when the auto-cancel job expires the trade at three in the morning — either would report somebody as present on activity that was not theirs. Chat is the known gap, so presence can still read slightly older than the truth, never newer.
An offer's limits could be edited past the platform's, and it saved
Creating an offer checked it against the platform minimum and maximum. Editing your own offer checked nothing. So a maker could publish an offer inside the limits, re-open it, move the minimum below the platform's floor or the maximum above its ceiling, press Save, and it saved — and the offer then sat on the board advertising a range the platform would not settle.
That made both settings advisory in practice. A limit enforced when an offer is created and not when it is changed is one edit away from whatever the maker prefers, and the edit screen gave no sign that anything was being skipped.
- Fixed the owner's edit applying the same platform bounds as creation, and the same per-currency dust floors, which it also had no check for. It runs only when the amounts or the price are actually being changed, so editing terms or payment methods is unaffected.
- Fixed the check reading the offer as it will be saved rather than the fields the request happened to send, so a partial edit cannot slip past it.
An offer could not be published, or ever edited again, when it used a shared payment method
- Fixed the check that runs when an offer is saved refusing payment methods that this platform had itself offered the trader. See Upgrade Notes — this affected both publishing and editing, and the editing case was permanent.
- Fixed the same disagreement existing in three separate copies of the rule. There is now one definition, used by offer creation, draft saving and offer editing alike, so the three cannot drift apart again.
The trade room opened well below the top of the page
- Fixed the trade room clearing the navigation bar twice. The page shell already reserved that space and the room reserved it again, so every trade opened 128px down the page, with the status ribbon then jumping back up on the first scroll.
The offer editor did not follow the rest of P2P
- Fixed the edit screen still using the previous design, so a trader who published an offer through the new composer and then edited it was moved between two visibly different products.
A signed-out visitor lost their place
- Fixed the sign-in return path accepting addresses that could send a visitor off the platform after signing in, and dropping the language prefix so a trader signing in from a translated page returned to the English one.
Publishing an offer sent the trader to their trade history
Two places in the product sent a trader to their own offers by address — the moment after publishing one, and the failure screen of the offer editor. Both pointed at the offers view of the trades page, which was not an address: which half of that page you were looking at was held in the browser, and the page never read the part of the link that named it. So a maker who had just published landed on their finished trades, which is the one screen with nothing to do with what they had just done.
- Fixed both by giving offers a page of their own. The link now names a route rather than a view, so it arrives where it says.
The marketplace payment filter listed one entry per trader
The filter on the market board is built from the payment methods attached to live offers. Because a method row was one trader's own, the filter grew an entry for each of them — a hundred traders accepting PayPal produced a hundred entries reading "PayPal", and picking one silently narrowed the board to that trader's offers.
- Fixed by grouping the filter on the shared method rather than on the row. The chips on an offer card now name the same thing the filter does, so clicking one filters the board instead of narrowing it to a single maker.
- Fixed a bookmarked or shared board link continuing to work. Those carry the old identifiers, and answering them with an empty board would read as "nobody accepts this" rather than as a stale link.
A payment method could be saved and then silently disappear
There were three copies of the payment-method form: the real one on the payment-method page, and a cut-down copy inside each of the two offer screens — because a trader on a platform with no shared payment methods cannot publish anything unless they can add one without leaving the form. The copies had drifted, and each had lost guards the original still had.
Four field labels are reserved, because a trade's payment instructions are built by laying the trader's own fields over the method's own name, instructions and clearing time. A field named name therefore does not sit beside the method name in the trade room — it replaces it, and every trade opened against that method is titled with whatever was typed. The real form refuses those four and explains why. The two copies dropped them without saying so: the trader watched the field save and then vanish.
- Fixed by making the three one form. A rule now applies wherever a payment method is created.
- Fixed three further guards missing from the offer-screen copies: a name too short was sent to the server and returned as an error attached to no field; the same label typed twice silently kept only the second value; and there was no limit on the number of fields, so anything past the twentieth was discarded by the server with nothing on screen saying so.
Empty and error states across the section
- Fixed trade and offer screens reporting a failure as though the record did not exist. A request that failed and a record that is genuinely missing now say different things, and an expired session says so rather than presenting itself as a missing trade.
- Fixed amounts in crypto being formatted as though they were fiat on screens that showed both.
An offer awaiting approval showed no price, no limits and no payment window
- Fixed the admin offer screen rendering its price, trade limits, payment window, region rules and taker requirements as blank. Those are stored in the same kind of column as the dispute material above and were read the same broken way, so a moderator approving an offer was approving one whose terms they could not see. All of them are visible now.
Admin screens stamped in the wrong timezone
- Fixed the trade conversation and the activity log rendering every timestamp in the server's timezone. The stamp was built where the data was fetched and printed unchanged, so an operator in Lagos read a Frankfurt clock — on the screens whose whole purpose is establishing what happened when, relative to a payment window. Times are formatted for whoever is reading them.
Money figures were stamped with a currency the offer never recorded
- Fixed the admin trade, dispute and offer screens defaulting an unrecorded price currency to USD. An offer that never recorded one was presented to the deciding operator as dollars, on the screen where they choose who receives it. An unrecorded currency now shows the figure with no symbol and says why, which is recoverable in a way a wrongly-labelled amount is not.
Actions that could only ever fail
- Fixed the trade screen offering Cancel trade on an expired trade. The server refuses that outright, so the button was a red control that could only return an error.
- Fixed an internal admin note appearing twice — once in the chronology and once in the notes panel — because the note panel re-derived "is this a note?" in the browser with a text match that also caught anything else containing the word.
- Fixed an unattributed message in a trade or dispute conversation being rendered on the buyer's side in the buyer's colour. On a transcript that gets screenshotted into a chargeback, quietly attributing an unsigned line to one of the two parties is the worst mistake available; unknown now reads as unknown.
- Fixed the conversation scrolling itself to the bottom on every refresh, which pulled an operator reading back through the evidence away from where they were.
Messages vanishing from a trade conversation
A trade's chat, its status events and the operator's broadcasts all live in one record on the trade, and every one of those writers read that whole record, added its line and wrote the whole thing back. Two people typing at the same time is not an unusual event on a chat — it is the normal one — and the second write was computed from a copy taken before the first had landed, so it deleted the first message outright. Nothing failed: both senders got a success, and each saw their own line appear in their own browser. Measured against the live API before the fix: ten messages sent at once between the two traders, all accepted, one still on the record afterwards.
That is the whole of the reported symptom. Each person could see what they had just sent and nothing else, the conversation appeared to reset on reload — the reload was showing the last writer's copy, which was all that had survived — and a support broadcast could delete the trader's message it was replying to.
- Fixed the lost messages. Every writer now reads and writes the record under one lock, so an overlapping message waits and is added to what is already there rather than replacing it. The money actions (confirm, release, cancel, dispute, resolve, expiry) already worked this way; the chat, image uploads and the operator's broadcast did not, which made the most frequent writer the only unsafe one.
- Fixed every line in the transcript being signed "User", on both sides of every conversation ever held. The name was read from the session, which carries an id and not a name, so the placeholder fired every single time. New messages are signed properly, and — because no write-side fix can reach what is already stored — the name is also worked out when a transcript is read, so conversations from before this release read correctly too.
- Fixed the trade room replacing the conversation with a single message the moment anybody spoke. On a trade with nothing said yet, the first message to arrive over the live connection was treated as the entire transcript, so the history on screen was discarded. Refreshing brought it back, and the next message threw it away again.
- Fixed an operator's broadcast being accepted without the length and content checks a trader's message goes through — the one writer with elevated permission was the one with no limit — and the trade screen reporting "that trade does not exist" and "your message is too long" both as an internal server error.
A cancelled trade left its dispute open on the desk
- Fixed cancelling a disputed trade leaving the dispute in the queue. Cancelling settles the escrow, so there is nothing left to rule on, but the case stayed on the desk forever — attached to a trade the ruling screen then refuses — and went on ageing against the 24-hour target on the dashboard. The dispute is now closed with the cancellation recorded as its outcome. It is recorded as cancelled, not as either side winning, because nobody won: the trade was unwound and the money went back where it came from.
A dispute made the trader's own trade disappear
- Fixed a disputed trade being filed under History on the trader's trades page. Raising a dispute moved the trade out of Live trades, so the page told somebody whose escrow was frozen and whose case was still open that they had no trades running, and filed the one unfinished trade they had under a heading that says the opposite. A dispute is a live trade — the escrow is still held and the outcome can still go either way — so it now sits at the top of the live list, in the same alarm colour, with See the dispute on it. The "active trades" figure on the trades page and the dashboard counts it too; both previously disagreed with the list printed directly beneath them.
A failed refresh replaced a record you were reading
- Fixed the trade, dispute, offer and payment-method screens blanking the record whenever a reload failed. Every action on these screens ends with a reload, so the state that produced a full-page error was usually "your ruling succeeded and the refresh after it blipped" — replacing a settled case with a red box at the exact moment the operator needed to see what had happened. A record that can still be drawn stays drawn, and the failed refresh is reported as what it is.
- Fixed the payment-method editor answering a failed load by sending the operator back to the list, so a blipped request looked like the record had been deleted and a genuinely missing one gave no way to tell the difference. It also no longer loses the whole record to one unreadable custom field.
A missing record reported itself as a server fault
- Fixed the admin trade endpoint rewriting its own "no such trade" answer as an internal error, so a stale link read as an outage and offered a retry that could only fail again.
Broken avatars in the activity log
- Fixed the activity log falling back to an image path that does not exist in this platform, so every entry by a user without an avatar rendered the browser's broken-image icon. It falls back to their initials.
The offer editor discarded four errors into a count
- Fixed the error summary on the offer editor reporting "basic tab: 2 errors" and stopping there, on a six-section form. Every problem is now listed in full, and each one is a link that opens the section holding it — the messages existed all along and only the summary refused to print them.
Total Balance was three different numbers on one dashboard
A wallet holds two amounts: the balance available to spend, and the amount locked in open orders. They are separate pots — placing an order moves money out of the first and into the second — so what a trader owns is the two added together. The P2P dashboard did it three different ways. The stat tile subtracted the locked amount from the available one, which takes it off a second time; the dashboard body and the portfolio panel counted only the available amount, which leaves it out. One label, three figures, none of them matching the wallet page.
The subtraction was the damaging one. It can fall below zero, and a wallet that came out negative was dropped from the total entirely — so a trader who had committed most of a currency to an open order did not merely read low, they watched that wallet vanish from their balance. Measured here: a trader with a $376 hold was shown $3,186.51, $3,563.22 and $3,563.24 on the three screens. All three now read $3,938.63.
P2P's own trading code had the rule written down twice, in the offer composer and in trade initiation: available is the balance, and subtracting the locked amount double-counts it. Only the dashboards disagreed.
- Fixed the three totals onto what the trader owns — available plus locked — which is what the wallet page, the profit summary and the platform's wallet service have always reported.
- Fixed the per-wallet available figure the dashboard returns, which had the locked amount deducted from it a second time for the same reason.
The balance total dropped currencies it could not price, and said nothing
- Fixed the Total Balance tile counting a currency it has no rate for as nothing, with no indication on screen. The trading-volume figure beside it has always named the currencies it could not convert; the balance now does the same, so a total that is missing something reads as the lower bound it is rather than as a complete figure.
- Fixed completed-trade volume reporting an offer priced in a crypto currency as unconvertible. The rate lookup consulted the fiat table alone, so an offer priced in USDT contributed nothing and was listed as unconverted; it now resolves through the same fiat-then-exchange path the rest of the platform prices with.
The dispute rate could not measure what it is named for
The admin dashboard's dispute rate divided the disputes currently open by every trade ever completed. A desk that resolves its cases promptly drives the numerator to zero and reads 0% for ever, however many disputes it has handled; a desk that lets them sit reads high on its backlog rather than on any rate. It also fed the health grade printed above it.
- Fixed the dispute rate to compare disputes raised in a window against trades opened in that same window, and to publish both counts beside it so the figure can be checked instead of taken on trust.
- Removed the health grade. It was four pairs of hardcoded thresholds — 5% and 2% dispute rate, five and two high-priority cases, 20% and 10% pending offers, ten and five flagged offers — published as Excellent, Good, Fair or Needs Attention with nothing behind them an operator could see, set or derive. A grade nobody can configure is an opinion wearing a measurement's clothes.
The currency mix showed ten currencies and did not say so
- Fixed the currency distribution declaring its own limit. It returns the ten most traded currencies, which a chart drew as though it were the entire market; it now carries the number of currencies, the overall total and how many fell outside the ten, so a tail that has been cut can be seen to have been cut.