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.
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:
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 carryisDemo: 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 onisDemo, 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
binaryAiEnginePositionrows filteredisDemo: false, as is the 24-hour settlement-decision breakdown and the per-engine detail. - The leaderboard, both
/api/exchange/binary/leaderboardand/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.
-
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.
-
Open Admin → Finance → Binary Options → Binary Settings and turn Practice Mode off in Master Controls.
-
Save. The save writes all three rows —
binaryStatus,binaryPracticeStatusand thebinarySettingsJSON — and then publishes a cache invalidation, so every backend worker drops its copy at once. Do not edit the row in SQL: a hand-writtenUPDATEreaches neither the per-process binary cache nor the settings Redis layer, and workers keep accepting practice orders until their own TTL lapses. -
Expect the mobile dead end described above.
-
Leave the historical rows alone. They are
isDemo = 1forever 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.isDemois 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 history —
GET /api/exchange/binary/orderdoes not filterisDemo, so a customer's order list for a symbol mixes both.GET /api/exchange/binary/order/lastis the one that splits them, returningpracticeOrdersandnonPracticeOrdersseparately with a 30-day trend on each. - The binary health endpoint's
orderscheck — it counts everyPENDINGrow, 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.