The binary order desk

Reading and reconstructing a binary contract from Admin → Order Management → Binary Orders — the statuses, the columns the screen does not show, the demo flag, and why hand-editing a status races settlement.

9 min readUpdated 6 August 2026binary, orders, settlement, disputes, admin

This is the screen you open when a customer says a contract settled wrongly. It is a reading desk: it lists every binary contract ever placed and lets you open one, and it is deliberately configured with no create, no edit and no delete row action.

The routes behind it are not so restrained. An admin holding edit.binary.order can rewrite an order's status through the API, and the bulk delete action is reachable from this very screen. Both of those touch a row that a settlement engine may be about to write. This page is what each control does and when it is safe.

Admin → Finance → Order Management → Binary Orders, at /admin/finance/order/binary. Opening the page needs access.binary.order.

The routes, and who can call them

Action Route Permission
List orders GET /api/admin/finance/order/binary view.binary.order
Read one GET /api/admin/finance/order/binary/{id} view.binary.order
Rewrite a whole order PUT /api/admin/finance/order/binary/{id} edit.binary.order
Change one order's status PUT /api/admin/finance/order/binary/{id}/status edit.binary.order
Change many statuses PUT /api/admin/finance/order/binary/status edit.binary.order
Delete one DELETE /api/admin/finance/order/binary/{id} delete.binary.order
Delete many DELETE /api/admin/finance/order/binary delete.binary.order

Every mutating route writes to the admin audit trail under the ADMIN_FIN module — see The admin panel. The list route masks user.email when the platform is in demo mode.

The table is configured canCreate: false, canEdit: false, canDelete: false, so no row carries an edit or delete action. The toolbar is a different code path: selecting rows reveals an Actions menu, and because this table declares no custom bulk actions it still offers the built-in Delete selected to anyone holding delete.binary.order.

binaryOrder is a paranoid model, so that is a soft delete — deletedAt is stamped and the rows disappear from every list. The table is configured isParanoid: false, so this screen has no deleted view and no restore button: from the admin panel the rows are simply gone.

They are recoverable through the API, which still honours the flags:

# see what was deleted
GET  /api/admin/finance/order/binary?showDeleted=true
# put them back
DELETE /api/admin/finance/order/binary?restore=true   {"ids": ["…"]}

Deleting binary orders is almost never the right move regardless. The rows are the evidence for every revenue figure and every dispute, and the wallet movements they caused are not deleted with them.

The status enum, and what each one meant for the balance

binaryOrder.status is an enum of exactly six values.

Status What happened The customer's wallet
PENDING Placed, not yet expired or not yet settled The stake is held — moved from balance into inOrder on a live order
WIN Settled in the customer's favour Stake released back to balance, plus profit credited
LOSS Settled against the customer The held stake is consumed. A Turbo partial loss can release part of it back
DRAW Settled with no winner — the close price equalled the entry Stake released, nothing credited
CANCELED Closed early by the customer Stake released less the penalty; profit is set to 0 and metadata records refundedAmount and cancelPenalty
ERROR Reserved on the model as "data fetch failed, needs manual review" Nothing. No code path in the settlement engine writes this status today

Two consequences of that last row: a row in ERROR on your install came from somewhere other than settlement, and the desk's Settlement errors backlog analytics card — which counts exactly this status — will read zero however badly settlement is going. The card to watch instead is Unsettled past expiry.

Both status routes declare an enum of PENDING, WIN, LOSS, DRAW, and the request body is validated against it, so a request carrying CANCELED or ERROR is refused with "Status must be one of the following: PENDING, WIN, LOSS, DRAW."

There is therefore no supported way to mark an order cancelled from the admin panel. Cancellation is a customer action with a refund attached — see Early cancellation and risk limits.

The columns

The desk shows eleven fields. The binaryOrder row carries more than that, and the ones it does not show are the ones you need to reconstruct a barrier trade.

On the screen:

Column Is Where
symbol The market, BTC/USDT. Free text on the row — there is no currency column list
price The entry price, captured at placement list
amount The stake list
profit The payout amount on a win, or the loss figure. Not a percentage list
side The direction — see the warning below list
status The enum above list
closePrice The settlement price. Empty on a PENDING row, where the view dialog prints "Still open" expanded / view
createdAt When the contract was placed list
closedAt The expiry instant, not when it settled list
isDemo Practice or real money expanded / view
id · user The row id and the trader (avatar, name, email) expanded / view

On the row but not on the screen — read these from an export, from GET /api/admin/finance/order/binary/{id}, or from the database:

Field Why you want it
type Which of the five order types this was. Nothing on the desk shows it
durationType TIME or TICKS. TICKS is Turbo-only
barrier The barrier price for Higher/Lower, Touch/No Touch and Turbo
strikePrice The strike for Call/Put
payoutPerPoint The per-point rate on Call/Put and Turbo
profitPercentage The payout rate frozen onto the order at placement. This is the number that decides the payout, and the only proof of what a customer was actually sold
metadata Idempotency key, the copy-trading wallet type, and on a cancelled order refundedAmount and cancelPenalty

The side column on the model has ten values — RISE, FALL, HIGHER, LOWER, TOUCH, NO_TOUCH, CALL, PUT, UP, DOWN — but the desk's filter offers only RISE and FALL, and the view dialog's badge is a single test: it paints red Fall when side === "FALL" and green Rise for everything else.

So a LOWER, NO_TOUCH, PUT or DOWN contract — every downside barrier trade in the product — appears on this screen as an upward bet. Combined with type not being shown at all, a Touch contract and a Rise/Fall contract are indistinguishable on the desk.

If you are reconstructing anything other than a Rise/Fall trade, read the row, not the screen. Open GET /api/admin/finance/order/binary/{id} or query the binary_order table directly. This matters most in exactly the situation the desk exists for: a customer disputing a barrier settlement.

isDemo and why it must come out of every revenue figure

Practice contracts never touch a wallet. Every wallet operation in the binary order service — the hold at placement, the release and credit at settlement, the refund on cancel — sits behind if (!isDemo). A demo order still creates a row, still gets a status, still carries a profit, and still appears on this desk beside real ones.

Summing profit or amount without filtering isDemo = 0 therefore produces a figure that is not money. The analytics on this page already do it correctly: every P&L card filters on isDemo: false, and demo volume is carried as its own separate card precisely so the two can be compared without ever being added together.

A binary contract is staked in the quote half of symbol — the wallet debited is the one whose currency is the pair — and binaryOrder has no column saying which asset that was. symbol is free text and the model's only association is to user.

So a book carrying 500 USDT of stakes and 0.01 BTC of stakes has no single sum, and the analytics deliberately render plain numbers rather than a dollar figure they have not earned. Do not read the KPI cards as dollars unless every one of your binary markets quotes the same asset.

The analytics tabs

Two sections of KPI cards sit above the table.

Section one — the book. Net house P&L (stakes won minus payouts paid), stakes won by the house (sum of amount where status = LOSS), payouts paid (sum of profit where status = WIN), live staked volume, demo staked volume, and outstanding payout liability — the sum of profit across live PENDING contracts, which is what you owe if every open position wins. There is a stacked-bar chart of house take against payouts over time and a ranking of the top five symbols by payouts paid.

Section two — is settlement working.

Card Counts Read it as
Settlement errors backlog status = ERROR always zero today; see the status table above
Unsettled past expiry PENDING with closedAt more than 5 minutes ago the real watchdog. Anything above zero is a contract whose expiry has passed and which neither settlement path has closed
Worst settlement lag (min) the maximum closedAtupdatedAt gap on live settled contracts a proxy — there is no settledAt column, so a later admin edit inflates that row's figure permanently
Win / Settled / Win rate live settled contracts only the customers' win rate. Compare it against the break-even rate your payouts imply

Anything on the Unsettled past expiry card sends you to Binary orders that did not settle, which is the diagnostic path — a disabled market, a deleted market, or a market maker that has stopped.

Editing an order by hand

There are two edit routes and they do different amounts of damage.

PUT /{id}/status writes the status column and nothing else. It does not compute a profit, does not credit a wallet, does not release a held stake and does not write a transaction row. Flipping a stuck PENDING contract to WIN marks it won and pays the customer nothing — turning a settlement delay into a support case about missing money.

PUT /{id} rewrites symbol, price, amount, profit, side, type, status, isDemo and closePrice in one call, with the same absence of any wallet effect. Its request schema is also narrower than the model:

  • side is validated against RISE and FALL only;
  • type against RISE_FALL only;
  • status against PENDING, WIN, LOSS, DRAW;
  • symbol, price, amount, side, status and closePrice are all required.

So this route cannot be used on a barrier order at all — a HIGHER_LOWER contract fails the request schema before anything is written, with both violations joined into one message: "Side must be one of the following: RISE, FALL.; Type must be one of the following: RISE_FALL." — and it cannot be used on a PENDING contract without inventing a close price.

A live binary contract is watched by two things at once:

  • an in-process timer armed when the order was placed, which fires at expiry and settles it immediately; and
  • processPendingOrders, a cron job that runs every 15 seconds and settles anything the timers missed, taking a Redis lock per order.

Settlement claims the row with SELECT … FOR UPDATE inside a transaction and a status = PENDING predicate, so it will not overwrite a terminal order. Your edit has no such protection. A row can move from PENDING to WIN between the moment the page loaded and the moment you press save, and the admin write lands on top of a settled contract whose wallet movements have already happened.

The safe move on a stuck contract is to fix the cause and let the engine settle it — 15 seconds is the worst case once the price feed answers. If a contract has genuinely settled wrongly, correct the customer's balance with a wallet adjustment, which is auditable and reversible, and leave the order row telling the truth about what the engine did. See Wallet administration.

Reconstructing a disputed contract

  1. Find the row. Search the desk by user email or symbol, or filter on status and createdAt. Open the row to get its id.

  2. Read the full record, not the dialog: GET /api/admin/finance/order/binary/{id}. You need type, side, barrier or strikePrice, and profitPercentage — none of which the screen shows.

  3. Confirm whether it was real money. isDemo: true ends the conversation: no wallet was touched, in either direction.

  4. Check the contract's own arithmetic. price is the entry, closePrice is the settlement price, closedAt is the expiry instant. For a Rise/Fall contract that is the whole story; for a barrier type compare closePrice and barrier, remembering that Touch/No Touch is decided by whether the barrier was ever reached during the contract, not by where the price finished.

  5. Check what they were sold. profitPercentage is the rate frozen at placement. If it disagrees with what the customer says the trading page offered, the payout configuration changed while their page was open — see Order types, durations and payouts.

  6. Check the settlement timing. closedAt versus updatedAt is how late the settlement was. A large gap points at the backstop rather than the timer, which is a settlement problem, not a pricing one.

  7. If the customer is owed money, adjust the wallet. Do not edit the order row: the row is your record of what happened, and rewriting it destroys the evidence while paying nobody.