Binary options: switching it on and the global limits

The two settings rows that turn binary trading on, the nine-tab settings console behind them, the per-user order limits, and what else has to be true before /binary works.

6 min readUpdated 6 August 2026binary, settings, kyc, limits, markets

Binary options is part of the core product, not an addon — there is no extension row for it and nothing to license. It ships switched on at the settings level and switched off in practice, because it has no markets until you create some.

Two admin screens own it:

Screen Path Page permission
Binary Markets /admin/finance/binary/market access.binary.market
Binary Settings /admin/finance/binary/settings access.binary.settings

Both sit under Admin → Finance → Binary Options. Customer orders are listed separately under Order Management → Binary Orders (/admin/finance/order/binary, permission access.binary.order).

Payouts, order types and duration ladders have their own page: Binary order types, durations and payout percentages.

The two switches, and where they live

binaryStatus and binaryPracticeStatus are two ordinary flat rows in the settings table. They are not part of the settings JSON blob, and this is the only screen in the whole product that writes them — they do not appear on Admin → System → Platform Settings.

Key What it gates
binaryStatus The master switch. Off, the /binary menu entry disappears, the landing page tile disappears, the footer link disappears, and the order route answers 403 "Binary trading is currently disabled". It is also what the backend checks at boot before arming expiry timers for pending orders.
binaryPracticeStatus Demo/practice trading. Off, a demo order answers 403 "Binary practice mode is currently disabled" while live trading carries on.

Both are written by the two switches at the top of /admin/finance/binary/settings.

Because binaryStatus is a plain settings row, it is also the row the platform's route catalogue uses to decide whether /binary is reachable at all — the same mechanism the support assistant uses when deciding whether to send a customer there.

The JSON blob below carries its own global.enabled and global.practiceEnabled fields, and the order service checks global.enabled as a second gate (answering 400 rather than 403 when it is false). The master switch on screen writes only binaryStatus; the save route falls back to global.enabled only when the request carries no binaryStatus at all. Both ship true, and all three presets set them true, so in practice binaryStatus is the switch — but if you ever edit the blob directly, make the two agree.

On boot, and only on the main thread, the backend sweeps every already-expired binary order and installs an in-process expiry timer for the rest — but it does that only when binaryStatus is already true. Switching the master switch on at runtime does not retroactively arm those timers. Restart the backend after turning binary trading on for the first time.

Everything else is one JSON row

The remaining configuration — order types, barriers, durations, cancellation rules, risk limits, chart choice — is stored as a single binarySettings row holding one JSON document. There is no binaryMaxDailyOrders key, no binaryChartType key, and nothing you can find by searching the settings table for a field name you saw on screen.

Binary settings are cached per process, and the settings table has its own Redis layer in front of it. A hand-written UPDATE on the row updates neither. The save button on the settings screen invalidates the binary cache across every worker and then clears the global settings cache — without that announcement, other workers keep paying out on the old profit percentages until their own TTL lapses. Change it from the screen.

The settings console

/admin/finance/binary/settings is one page with nine groups:

Tab What it configures
Global Settings Master switches and the per-user trading limits below.
Chart & Display display.chartType.
Order Types The five order types, whether each is enabled, its base payout, and demo/live availability.
Barrier Levels Barrier and strike ladders with their own payout percentages.
Durations The expiry ladder and per-duration payout adjustments.
Cancellation Whether customers may close early, how close to expiry, and the penalty.
Risk Management riskManagement.dailyLossLimit and riskManagement.winRateAlert.
Payout Optimizer A calculator. It computes break-even win rates and platform edge from what you have configured and can apply a suggestion. It saves nothing on its own.
Presets Three shipped templates — conservative, balanced, aggressive — each returned with its own validation result.
Returns the settings, both switches, a validation result and whether the install is on defaults
Returns the three shipped presets with their validation
Validates and saves all three rows, then invalidates the caches

Reading and writing are different permissions. access.binary.settings gates the GET and the presets list; edit.binary.settings gates the PUT. A role with only the first sees the whole console populated and gets a permission error when it presses Save. Grant both, or neither. The co-located frontend contract for this screen records access.binary.settings.

isDefault: true means nobody has ever saved

The GET returns isDefault: true when no binarySettings row has ever been written. The console shows a banner for it, and it is worth taking seriously: the install is running live on DEFAULT_BINARY_SETTINGS — RISE/FALL only, 72% payout, a three-second cooldown, and every other order type off. Nothing is broken, but nothing has been decided either. Open the console and press Save once so the row exists and you know what it contains.

The validation object

Both GET and PUT return a validation object with valid, errors and warnings. Errors block the save — the PUT answers 400 and writes nothing. Warnings do not; they are advice, each carrying a level (info, warning, danger), a category, a message, a suggestion and often the exact field.

Errors are structural: fewer than one concurrent order allowed, a payout outside 0–1000%, an enabled order type with no barrier or strike levels, a negative barrier distance, no durations at all, no enabled duration, a duration under one minute.

Warnings are commercial. The ones worth acting on:

Level Warning
danger No durations are enabled — trading will not work.
danger Higher/Lower's widest enabled barrier is over 2% away, which allows near-guaranteed wins.
warning The same barrier over 1% away — probably too easy.
warning Average payout across enabled types is over 90% and may not be sustainable.
warning Turbo's maxDuration is longer than 5 minutes.
info No daily loss limit is configured (riskManagement.dailyLossLimit is 0, the shipped default, which means disabled).
info Every enabled order type carries the same payout, so nothing differentiates them by difficulty.

The global limits

All five live under global and all five are shipped defaults you should review before you take live money.

Key Default What it does
global.maxConcurrentOrders 10 Most open (PENDING) live orders one user may hold at once. Exceeding it answers 429. 0 disables the limit.
global.maxDailyOrders 100 Most live orders one user may place in a rolling 24-hour window. 429 when exceeded. 0 disables it.
global.cooldownSeconds 3 Minimum gap between one user's consecutive live orders. 429 with the remaining seconds. 0 disables it.
global.orderExpirationBuffer 30 Seconds before expiry within which a new order is refused, with a 400 naming the buffer and the time remaining.
global.cancelExpirationBuffer 60 Published to the trading page so the client can hide the cancel button near expiry.

The three shipped presets move all five together, which is the fastest way to see what a coherent set looks like:

Conservative Balanced (the default) Aggressive
maxConcurrentOrders 5 10 20
maxDailyOrders 50 100 200
cooldownSeconds 5 3 0
orderExpirationBuffer 45 30 15
cancelExpirationBuffer 90 60 30

A preset rewrites the whole settings document, including every payout and the entire duration ladder — not just this table. Read the list of changes the Presets tab shows you before you apply one.

maxConcurrentOrders, maxDailyOrders and cooldownSeconds are skipped for demo orders, and skipped for copy-trading replication orders (which are placed by the server at the leader's cadence and bounded by the follower's allocation instead). A user cannot exhaust their daily limit on practice trades, and they cannot use practice trades to work around a cooldown either.

global.cancelExpirationBuffer is returned by the public binary settings endpoint the trading page reads, but the server-side refusal to cancel comes from the Cancellation tab's per-type minTimeBeforeExpirySeconds, not from this value. Set both, and set them consistently — otherwise the button disappears at one moment and the server refuses at another.

Chart & Display

display.chartType has exactly two values.

Value Requires Effect
CHART_ENGINE The Chart Engine addon, licensed The default. Positions are drawn directly on the chart with P/L zones, entry markers and countdown timers, so the trading page hides its separate positions sidebar.
TRADINGVIEW Nothing The TradingView chart, with the positions sidebar shown beside it.

If the Chart Engine licence comes back revoked or expired from the six-hourly licence heartbeat, the platform rewrites display.chartType inside the binarySettings row to TRADINGVIEW — because the alternative is a chart area that renders nothing. If your binary chart changed on its own, check the Chart Engine licence first. See Licences and activation.

What else has to be true before /binary works

binaryStatus is necessary and not sufficient. The customer-facing page requires all of:

  1. A signed-in user. /binary is not public.
  2. binaryStatus = true.
  3. The KYC feature binary_trading, when KYC feature enforcement is on. The order route asserts it — but only for real-money orders. A demo order never touches a funded wallet, and practice mode is the funnel that brings unverified users to verification, so gating it would close the door before they reach it. The check is keyed on the values that actually persist as demo (true, 1, "true", "1"), so anything ambiguous is treated as real money and gated.
  4. At least one active binary market.

If customers report the menu entry missing, check 2. If they report the page loading and every real-money order refused, check 3 against the verification level builder at /admin/crm/kyc/level and the Enforce KYC Feature Access switch on Platform settings.

Binary markets

/admin/finance/binary/market, permission access.binary.market for the page.

A binary market is a currency/pair with its own limits and a price source. The row carries:

Column Meaning
currency Base symbol — BTC, ETH.
pair Quote symbol — USDT, USD.
minAmount · maxAmount Stake bounds for this market.
source EXCHANGE or ECOSYSTEM — which price feed backs it.
isTrending · isHot Display badges on the customer's market list.
status Whether the market is live. Toggled inline from the list.
Lists binary markets
Creates a binary market
Lists exchange and ecosystem markets that could be added
Bulk-imports every active spot market as a binary market
Turns markets on or off in bulk
Edits one market
Deletes one market

Available is the picker: it returns the exchange and ecosystem markets you could add, and flags for each whether it already has an AI market maker or a Binary AI Engine attached. Import is the bulk action: it takes every active spot exchange market, skips anything already present as a binary market, skips swap and futures rows (a pair like USDT:USDT), and creates the rest in one transaction, reporting how many it imported and how many it skipped.

EXCHANGE reads the centralised exchange provider's price. ECOSYSTEM reads your own ecosystem market — which may be driven by an AI market maker. Those are different price series, and entry price, settlement price and the Binary AI Engine must all agree on which one to read. It defaults to EXCHANGE so existing rows keep their behaviour. pnpm --filter backend verify:binary audits this across your real data and can backfill unambiguous rows; see Repair scripts.

One trap on the settings screen itself

If the GET fails — a network blip, an expired session, a permission problem — the console does not stay empty. It falls back to a client-side copy of the defaults, sets both switches to on, and shows an error toast. That fallback copy is not identical to the server's defaults (its cooldown is 0 rather than 3, and its base payouts are higher), so pressing Save on a screen that failed to load writes a configuration you did not choose.

If you see "Failed to load settings", reload the page. Do not save through it.