Practice mode and demo orders

How binaryPracticeStatus, tradingModes and the isDemo column work, why practice contracts move no money but still inflate your reports, and which queries have to exclude them.

10 min readUpdated 6 August 2026binary, practice, demo, reporting, isdemo

A practice contract is a real row in the real order table with isDemo set to true. It goes through the same validation, the same expiry timer, the same settlement pass and the same WebSocket broadcast as a live one. The only thing it never touches is a wallet.

That combination is the whole reason this page exists. Nothing about a practice order is fake enough to keep it out of a naive report, and nothing about it is real enough to belong in one. Your first revenue figure and your first win-rate figure are wrong unless you filtered isDemo.

The switch

Admin → Finance → Binary Options → Binary Settings, the Practice Mode toggle in Master Controls, beside the Binary Trading master switch. It writes one flat row:

Whether customers may place practice contracts. An absent row reads as OFF everywhere that enforces it

Three things about that row.

It is a plain settings row, stored as text. Like every settings row, its value is the string "true" or "false", not a boolean. It is not part of the binarySettings JSON document.

This screen is its only writer. No seeder creates it, nothing on Admin → System → Platform Settings exposes it, and no other admin page touches it. On a fresh install the row does not exist at all — and every reader that enforces it treats an absent row as off. The order route compares the raw value against the string "true", so a missing key refuses a demo order; the trading header does the same and hides the practice tabs. Practice trading is therefore unavailable until somebody has opened this screen and saved, which is what actually creates the row. The one reader that disagrees is this screen's own GET, which defaults an absent row to on — so on a fresh install the Practice Mode toggle displays as enabled while nothing will accept a practice order. If practice is what you want, save the screen once even though the toggle already looks right.

Its counterpart inside the JSON is global.practiceEnabled. The save route reads binaryPracticeStatus from the request body and falls back to global.practiceEnabled only when the body omits it, so the toggle on screen writes both in agreement. The order route reads only the flat row. If you ever edit the JSON by hand, global.practiceEnabled is decorative — the flat row is what refuses an order.

With the switch off, POST /api/exchange/binary/order carrying isDemo: true is refused 403, "Binary practice mode is currently disabled". Live trading is unaffected; the two switches are independent.

Per order type, on top of the switch

Each of the five order types carries its own pair of availability flags inside the settings JSON:

"orderTypes": {
  "RISE_FALL": {
    "enabled": true,
    "profitPercentage": 72,
    "tradingModes": { "demo": true, "live": true }
  }
}

Both switches on the Order Types tab, under Trading Modes, headed "Control where this order type is available". Both default to true on every type and in all three presets.

The order service resolves tradingMode as isDemo ? "demo" : "live" and refuses a mismatch with 400, "Order type X is not available in demo/live mode". So a type can be:

demo live Effect
on on the default — available everywhere
on off practice only. This is how you trial a new contract type on practice accounts before it can lose you money
off on live only. Practice traders never see it, so they cannot learn on it
off off unreachable, whatever enabled says

The check runs after the platform switch, so a practice-only type is still dead when binaryPracticeStatus is off.

What isDemo actually suppresses

isDemo is not a label the reporting layer applies afterwards. It is a hard branch through BinaryOrderService, and every path that moves value sits inside if (!isDemo). In placement order:

Guarded by !isDemo Consequence for a practice order
The Binary AI Engine per-order exposure cap not applied
The three per-user throttles — maxConcurrentOrders, maxDailyOrders, cooldownSeconds not applied. A customer cannot exhaust their daily limit on practice, and cannot use practice to dodge a cooldown
The wallet lookup and its FOR UPDATE lock no wallet is read, so no balance is required
The balance sufficiency check a practice order of any size within the market limits is accepted
walletService.hold on the stake no hold, and therefore no transaction row
Cancellation: the refund, the penalty and the transaction lookup the row goes to CANCELED and nothing else happens
Settlement: release, executeFromHold, credit and collectPlatformFee the row goes to WIN, LOSS or DRAW and no money moves in either direction
The copy-trading created / cancelled / settled hooks practice orders are never replicated to followers

The KYC gate is the mirror image: assertKycFeature(binary_trading) is skipped for practice orders on purpose — practice is the funnel that brings an unverified visitor to verification, and gating it closes the door before they reach it.

isDemo arrives from the client and the column is a Sequelize BOOLEAN, whose sanitiser maps the string "false" to false and "0" to 0 — both of which are truthy in JavaScript. The KYC gate therefore tests for true, 1, "true" or "1" explicitly rather than for truthiness, so a body carrying {"isDemo":"false"} is treated as real money and verified. Anything ambiguous is gated. Do not "simplify" this in an integration of your own.

The practice balance is in the customer's browser

There is no demo wallet, no demo balance column and no server-side reset endpoint. The figure a customer sees on their practice account is a field in the binary-trading-store Zustand store, persisted to their browser's local storage, seeded at 10,000 and deducted client-side as they trade. The Top up button in the account menu sets it back to 10,000.

That means: it is per-browser, it survives nothing but that browser, a customer can reset it as often as they like, and you cannot adjust, audit or zero it from the admin panel. It is not money and is not modelled as money.

Reporting: filter isDemo, always

Every revenue, volume, profit and win-rate figure over binary_order must carry isDemo: false. The platform's own dashboards do this in some places and not in others, so know which is which before you quote a number.

These already exclude practice:

  • The Binary Orders analytics on /admin/finance/order/binary. Net house P&L, stakes won by house, payouts paid, live staked volume and outstanding payout liability all carry isDemo: false. Demo volume is deliberately a separate tile next to the live one, so the two can be compared but never added. (Written as two named tiles rather than one chart grouped on isDemo, because a group-by renders the raw column and you would get a legend reading "0" and "1".)
  • The Binary AI Engine dashboard. Today's win rate and total platform profit are computed from binaryAiEnginePosition rows filtered isDemo: false, as is the 24-hour settlement-decision breakdown and the per-engine detail.
  • The leaderboard, both /api/exchange/binary/leaderboard and /leaderboard/me. Practice trading never ranks anybody.
  • Copy trading, which never replicates a practice order in the first place.

These do not:

The public landing page. The Binary Options feature card on your marketing page reports Total Trades and Volume as a bare count and a bare sum over binary_order with no isDemo filter and no status filter. Practice contracts, cancelled contracts and still-open contracts are all in there. It is a vanity metric — treat it as one, and do not reconcile it against anything.

The admin customer profile. /admin/crm/user/{id} computes a customer's binary total trades, win count, loss count and total profit over every row with their userId, practice included. A support agent reading "68% win rate" off that screen may be reading a practice record. Cross-check on /admin/finance/order/binary filtered to that user with the Demo column expanded.

When you write your own query or export, the shape is:

SELECT
  SUM(CASE WHEN status = 'LOSS' THEN amount ELSE 0 END) AS stakes_won,
  SUM(CASE WHEN status = 'WIN'  THEN profit ELSE 0 END) AS payouts_paid
FROM binary_order
WHERE isDemo = 0
  AND deletedAt IS NULL
  AND closedAt >= '2026-08-01';

Three qualifiers, all load-bearing. isDemo = 0 for the reason this page exists. deletedAt IS NULL because binary_order is soft-deleted and a plain SELECT picks deleted rows back up. And no currency conversion — a binary stake is denominated in the quote half of the free-text symbol column and nothing on the row records which asset that is, so a book holding BTC/USDT and ETH/EUR sums to a number with no unit. See the binary API and data reference for the full column list.

Practice orders and the Binary AI Engine

Practice contracts do reach the engine — the settlement seam calls steer() for every RISE_FALL order regardless of isDemo — but three separate gates decide whether anything is steered, and each records its own refusal reason on the position row.

Reason on the position Set by Meaning
FAIR:NOT_ENGINE_TRACKED no position row exists for the order with an engine's practiceMode at DISABLED — the default — the engine never analyses demo buckets, so practice orders get no position row and settle fair
FAIR:DEMO_PRACTICE_DISABLED the order is demo and the engine's practiceMode is DISABLED a row exists from an earlier configuration, but steering is refused
FAIR:GLOBAL_PRACTICE_MODE the platform-wide binaryAiEnginePracticeMode setting every order, live and demo alike, settles fair. Decisions are still recorded; outcomes are untouched

The per-engine setting is practiceMode on the engine row, with three values — DISABLED (the create default), SAME_AS_LIVE and CUSTOM. Leave it DISABLED and your practice traders experience an unsteered market, which is the honest default.

binaryPracticeStatus decides whether customers may place practice contracts at all. binaryAiEnginePracticeMode is the Binary AI Engine's own platform-wide shadow switch — with it on, the engine records every decision and applies none of them, to live orders too. Turning the second one on because you wanted the first is a silent, total disabling of steering.

The practical consequence for an install where practice is the only traffic — a staging box, a demo tenant, the first week before you take real money — is that the engine looks inert. Positions list, decisions are recorded, and the dashboard's live win rate stays at zero because it counts only isDemo: false rows. Nothing is broken.

What the trader sees

The mode switch lives in the balance menu in the trading header on /binary — click the balance figure. It offers two tabs, Real Account (green) and Demo Account (amber), each showing its own balance, with a Top up control on the practice side.

With binaryPracticeStatus off:

  • The two tabs are not rendered at all — the panel shows the real account only, with no hint that a practice mode exists.
  • A customer whose browser had persisted tradingMode: "demo" is switched back to real automatically once settings load. Their persisted practice balance is left alone and reappears if you switch practice back on.

The compact header used on small screens reads no settings at all. Its account dropdown lists Demo Account and its Top up button whatever binaryPracticeStatus says. A customer who selects it there gets as far as pressing the trade button and is then refused with "Binary practice mode is currently disabled".

Nothing is at risk — no order is created and no balance moves — but it is a confusing dead end, and it is where the support ticket comes from after you turn practice off. Expect it.

Turning practice off

Nothing is destroyed and nothing needs a restart, but existing rows do not change.

  1. Decide what happens to open practice contracts. They keep their expiry timers and settle normally; the switch is checked at placement only. Turning it off mid-flight strands nobody, because there is nothing to strand.

  2. Open Admin → Finance → Binary Options → Binary Settings and turn Practice Mode off in Master Controls.

  3. Save. The save writes all three rows — binaryStatus, binaryPracticeStatus and the binarySettings JSON — and then publishes a cache invalidation, so every backend worker drops its copy at once. Do not edit the row in SQL: a hand-written UPDATE reaches neither the per-process binary cache nor the settings Redis layer, and workers keep accepting practice orders until their own TTL lapses.

  4. Expect the mobile dead end described above.

  5. Leave the historical rows alone. They are isDemo = 1 forever and every correctly-written report already excludes them. Deleting them is a soft delete that changes no figure worth changing, and the admin orders table cannot show you deleted rows to undo it from.

Where practice orders still show up

Places a practice contract legitimately appears, so you are not surprised by it:

  • /admin/finance/order/binary — the table lists both. isDemo is a filterable boolean column titled Demo, marked expanded-only, so you have to expand a row or add the filter to see it. The view dialog badges each order Demo (amber) or Live (neutral).
  • The customer's own historyGET /api/exchange/binary/order does not filter isDemo, so a customer's order list for a symbol mixes both. GET /api/exchange/binary/order/last is the one that splits them, returning practiceOrders and nonPracticeOrders separately with a 30-day trend on each.
  • The binary health endpoint's orders check — it counts every PENDING row, demo included. A pile of stuck practice contracts raises the same alarm as a pile of stuck real ones. That alarm is still worth acting on: the same settlement machinery is failing either way.

One more "demo" that is not this one

The order list routes carry a demoMask on the customer's email address. That masks addresses only when the whole install runs with NEXT_PUBLIC_DEMO_STATUS=true — the flag for a public demo deployment. It has nothing to do with isDemo on an order row, and turning practice mode on or off does not affect it.