P2P Trading 6.4.0

Pre-release

This version isn’t published on updates.mashdiv.com yet — the notes are available to preview, but it can’t be downloaded until it’s released.

10 September 2026

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

ESCROWOFFERSFIAT-DENOMINATED OFFERSMIGRATION

P2P Trading v6.4.0

Release Date: September 10, 2026 Tags: ESCROW, OFFERS, FIAT-DENOMINATED OFFERS, MIGRATION

Draft — the version number is assumed. The newest note in this tree is 6.3.5. This is a behaviour change carrying four new nullable columns, so it is filed as the next minor, 6.4.0. Confirm against the shipped version list before publishing and rename the file if it differs.

Overview

An offer denominated in a national currency no longer asks anybody for a fiat balance, because the platform no longer pretends to escrow cash. It escrows the offer's crypto leg instead — which on such an offer is the currency the offer is priced in — and it takes that hold from whoever owes the crypto, when the trade opens.

This is the product's founding sentence finally applied consistently: the platform never touches the fiat leg; it holds the crypto and it arbitrates. The code had been reading it as "the platform escrows the asset leg", which is the same sentence only while the asset is a coin. Post "I will buy EUR at 1.10 USDT" before this release and the taker — the person sending euros from their own bank — was refused with "Insufficient balance. Available: 0 EUR, Required: 90.909 EUR … your FIAT wallet." They were being asked to escrow money the platform cannot hold, cannot see and had no business locking. The offer was unusable from both ends.

Three things follow, and all three are in this release:

  • A fiat-denominated offer escrows the price leg, from the crypto payer: the maker on a BUY offer, the taker on a SELL. Always at trade initiation, for the trade total in the price currency.
  • It holds nothing at publish, in either direction, and requires no fiat balance and no fiat wallet — the publish path does not read a wallet at all.
  • Fiat on both legs is refused, because there is then nothing on the offer that can be escrowed.

Nothing changes for an offer denominated in a coin: same leg, same party, same moment, same amount, and the same wording on every refusal. That path is deliberately what it was.

Requires Core v6.7.6.

Update Instructions

pnpm updator

Restart the backend and ship the frontend build. Four columns are added on boot by the model sync — p2p_offers.priceWalletType, and escrowCurrency, escrowWalletType and escrowOwnerId on p2p_trades. All four are nullable; there is no migration to run, no seeder and no new setting.


Upgrade Notes

Nothing already held moves, and no existing trade changes hands

Every trade open at the moment you update has NULL in the three new escrow columns. NULL is not "unknown" — it is read as legacy, and it resolves to exactly what the old code computed: the escrow belongs to sellerId, in the offer's wallet type, in the trade's currency. Fiat included. A hold sitting in a fiat wallet's inOrder right now still releases, still refunds, still returns to its offer and still settles from a dispute, by the same arithmetic as before the update.

That is deliberate, and it is the one rule we would not trade away: the new "no fiat holds" guard applies only to opening a new hold. Settlement never refuses a fiat escrow. Money that is already locked can always be got out.

Your existing fiat-denominated offers are dormant, and one edit repairs them

An offer published before this release has no priceWalletType recorded, so the platform does not know which wallet its price leg lives in. A fiat-denominated offer in that state resolves to "no escrowable leg": it is not tradeable, and the board and the offer page say so with the reason rather than drawing a Buy button that always fails.

The repair is an edit. Open the offer, save it, and the price leg is derived and stored — the advertisement becomes tradeable in the crypto it was always quoted in. The same save also releases any old offer-level fiat hold back to the maker's spendable balance, because the offer level escrows nothing on a fiat-denominated offer now. That is a release, never a new hold, and the edit route already refuses to touch an offer with a trade in flight, so nothing a taker is relying on is unwound.

Two things worth knowing before you tell a maker to do that:

  • If the offer is priced in a national currency as well, the edit is refused while the result would stay ACTIVE or PENDING_APPROVAL — see below. Pausing and deleting it still work.
  • A fiat-denominated offer nobody repairs is harmless. It holds nothing, it refuses trades with a stated reason, and it pauses and deletes normally.

Cash-for-cash offers are refused at publish and at edit

"I will sell EUR for USD" has no leg the platform can hold, so it is refused with a 400 naming which leg failed — at publish, and on an edit that would leave such an offer live. Pausing and deleting stay possible on purpose. Refusing every write would leave a maker with a live advertisement they could not take down, which is a worse outcome than an unpublishable one.

The composer refuses the same combination in place, under both currency pickers, as soon as both legs are known, so a maker meets it while typing rather than as a server error on Publish.

If you have live cash-for-cash offers today, they were never really escrowed: whatever they hold sits in a fiat wallet the platform cannot honour against a counterparty. Pause them, then either reprice in a crypto currency or delete them.

Who releases changes on a fiat-denominated trade

Escrow is released by whoever's crypto is held. On a fiat-denominated trade that is the party buying the asset — they paid the coin and they receive the cash — and the party who declares payment sent is the asset seller, who wired the money from their own bank. The trade room now draws each action for the party who must take it, the escrow figure in the room and the padlock in the ribbon are the escrow's own currency and amount, and the dispute reasons are offered by which side of the escrow the viewer is on rather than by asset role.

:::warning{title="The two trade-room doors still authorise by asset role"} POST /api/p2p/trade/{id}/release still loads the trade by sellerId and POST /api/p2p/trade/{id}/confirm by buyerId. On a fiat-denominated trade both are the wrong party, so the room offers the action and the API refuses it. Until those doors authorise on the escrow owner and its counterparty, finish a fiat-denominated trade from the case desk — admin resolution goes through the same escrow authority and moves the money correctly. Schedule this follow-up before advertising fiat-denominated offers to your traders. :::

The fee, the capacity restore and the offer pool follow the escrow

Three quieter consequences, each of which would otherwise have been a wrong-unit figure:

  • The escrow fee is quoted on the escrowed leg. Settlement subtracts escrowFee from the escrow without converting anything, so a fee derived from a euro amount and subtracted from a USDT release would have charged a percentage of the price rather than of the trade.
  • A cancelled or expired fiat-denominated trade refunds its holder rather than being absorbed into the offer's pool. The pool is denominated in the asset leg it never held; the offer's advertised capacity is restored separately, in its own currency.
  • An offer's live-trade escrow sum counts only trades held in the offer's own currency, so a price-leg hold is never netted off an asset-leg pool.

Added

  • Added one escrow-leg resolver, used by the publish door, the edit door, the offer board, trade initiation, settlement and the trade room, so the leg, the escrowing party, the amount and the moment the hold is taken are decided in a single place instead of being re-derived from the offer's direction at each site.
  • Added p2p_offers.priceWalletType — the wallet type of the price leg, derived server-side at publish and re-derived when the price currency is edited. It is never accepted from a client: a caller who could claim EUR is ECO would point an escrow at a wallet that has never held anything.
  • Added escrowCurrency, escrowWalletType and escrowOwnerId on p2p_trades, stamped in the same transaction that takes the hold, so settlement releases against what was actually held rather than re-deriving it from roles.
  • Added support for composing an offer whose asset is a national currency, in both directions. The asset list was built from the sellable currency endpoint, whose filter reaches all three wallet groups, so the fiat group came back empty and "I'm selling EUR for USDT" could not be composed at all.
  • Added an escrow line to the offer preview, the offer page and the market board naming the currency actually held and the party holding it, plus a stated reason on an offer that has no escrowable leg.

Changed

  • Changed a fiat-denominated offer to escrow its price leg, from the crypto payer, at trade initiation — the maker on a BUY offer, the taker on a SELL — for the trade total in the price currency.
  • Changed the publish path to take a hold only where the escrowed leg is the asset leg of a SELL offer. A fiat-denominated offer performs no balance check, reads no wallet, and does not require its maker to own a fiat wallet.
  • Changed the escrow authority to refuse opening any new hold in a fiat wallet, naming the rule. Settlement is deliberately not guarded: an existing fiat hold must always be releasable.
  • Changed the settlement outcomes to be holder-relative, with unambiguous spellings alongside the old names — RELEASE_TO_COUNTERPARTY for RELEASE_TO_BUYER, REFUND_TO_ESCROW_OWNER for REFUND_TO_SELLER. The old names still work and mean the same thing.
  • Changed the escrow fee, the frozen payment-details payee, the 2FA-enrollment check and the settlement payout to follow the escrow owner rather than sellerId.
  • Changed the Learn page, the market board's escrow explainer, both trade openers and the trade room to describe the crypto and cash sides of a deal instead of the seller and buyer roles, which are inverted on a fiat-denominated offer.
  • Changed the documentation throughout to follow the framing sentence on the P2P overview: an offer may be denominated in cash, and the platform still only ever escrows crypto — with a new decision table, Which leg is escrowed, on the Creating offers guide.

Fixed

  • Fixed a taker being refused with "Insufficient balance. Available: 0 EUR, Required: 90.909 EUR … your FIAT wallet" when opening a trade against an offer denominated in a national currency. They were being asked to escrow cash, which the platform does not hold; the crypto side of that trade is the maker's, and it is now what gets held.
  • Fixed a maker being required to hold a fiat balance — and to own a fiat wallet at all — in order to publish an offer denominated in a national currency.
  • Fixed the expiry sweep restoring a fiat-denominated offer's capacity from the settled escrow figure, which is denominated in the escrow currency while capacity is counted in the asset currency. It now restores the trade's asset amount.
  • Fixed the "Settled from" fact on the offer page printing the offer's own wallet type — the fiat one on a cash-denominated offer — where it should name the wallet the escrow is actually held in.
  • Fixed the market board capping the amount field against a wallet balance read in the wrong leg's currency, and ranking two caps measured in different units against each other.