Binary markets and where their prices come from
Managing /admin/finance/binary/market — the EXCHANGE and ECOSYSTEM price feeds, importing pairs, per-market stake limits, and how to retire a market without stranding open orders.
A binary market is a row that says "customers may buy a contract on BTC/USDT".
It is not a price feed, and it does not inherit one. Every row carries a
source column that names which of the platform's two entirely separate price
series backs it:
source |
Where the price comes from | Set by |
|---|---|---|
EXCHANGE |
your centralized exchange provider, through ccxt | the default for every row |
ECOSYSTEM |
the matching ecosystem market, and the AI Market Maker driving it when one is running | choosing Ecosystem Markets in the create wizard |
The two are different numbers for the same pair. Entry price, settlement price
and — if you run the Binary AI Engine — the price the engine steers are all
resolved from this one column, in
backend/src/api/exchange/binary/order/util/marketSource.ts. Get it wrong and
the price a customer trades against is not the price their money settles on.
When source is ECOSYSTEM and no ecosystem market exists for that pair, order
placement fails with Ecosystem market {symbol} not found — it does not
quietly read the centralized exchange, even when the same pair is listed there.
That refusal is deliberate. A market-maker pair that also trades on the CEX must
never settle on the CEX price.
The screen
Admin → Finance → Binary Options → Binary Markets
(/admin/finance/binary/market). Opening it needs access.binary.market.
The table shows:
| Column | Type | Notes |
|---|---|---|
id |
text | UUID. Only visible when you expand a row |
currency |
text | base symbol — BTC |
pair |
text | quote symbol — USDT |
minAmount |
number | smallest stake. Expanded rows only |
maxAmount |
number | largest stake. Expanded rows only |
isTrending |
boolean | merchandising badge |
isHot |
boolean | merchandising badge |
status |
boolean | inline toggle — writes straight to the API |
currency, pair and status are the priority-1 columns; isTrending and
isHot are priority 2, which still puts all five on screen at every width. The
other three are flagged expanded-only and appear when you expand a row.
The value is stored on every row and returned by
GET /api/admin/finance/binary/market/{id}, but there is no column for it in
the table and no control for it on the edit screen. The only place a human
chooses it is step 1 of the create wizard.
The update route also ignores it: PUT /api/admin/finance/binary/market/{id}
reads currency, pair, minAmount, maxAmount, isTrending, isHot and
status from the body and nothing else, so sending source changes nothing.
Changing a market's feed means deleting the row and creating it again.
To read a market's feed without touching the database, open
/api/admin/finance/binary/market/<id> while signed in as an admin — the JSON
carries source.
Adding markets
There are two routes in, and they behave differently.
The create wizard — one market, you pick the feed
/admin/finance/binary/market/create, or the Create button on the table.
Three steps:
-
Select Source — two cards, Exchange Markets and Ecosystem Markets, each with a live count of pairs you have not already added. The ecosystem card is the one that carries AI Market Maker support.
-
Select Market — the pairs available from that source, searchable. Pairs that already exist as binary markets are filtered out, so an empty list means "all of them are already added", not "none found". On the ecosystem tab each card is badged with whether an AI Market Maker and a Binary AI Engine are configured for it.
-
Configure — the Active status, Trending and Hot switches. Active defaults on, the other two off.
The wizard reads GET /api/admin/finance/binary/market/available
(view.binary.market), which returns both lists plus the pairs you already
have, and posts to POST /api/admin/finance/binary/market
(create.binary.market) with the source you chose.
Two refusals you will meet here:
- Picking Ecosystem for a pair that has no ecosystem market is rejected with
"No ecosystem market exists for
X/Y. Create the ecosystem market first, or import this pair from the exchange instead." - If the request arrives with no
sourceat all — a script, or an older client — the backend infers one, and it is deliberately conservative:ECOSYSTEMonly when the pair exists as an ecosystem market and not on the exchange. Anything else becomesEXCHANGE.
Import from Exchange — everything at once, always EXCHANGE
The Import from Exchange button at the top of the table calls
GET /api/admin/finance/binary/market/import (create.binary.market). It reads
every exchangeMarket row with status true and creates a binary market for
each pair you do not already have.
Four things about it that matter:
- Every imported row is
source: EXCHANGE. The importer only ever reads exchange markets, so it says so explicitly. It cannot produce an ecosystem market. - Everything is imported disabled (
status: false). Nothing becomes tradable until you turn it on. This is the safe default and it is deliberate — a provider with 1,500 spot pairs would otherwise list 1,500 binary markets in one click. - Swap and futures rows are skipped. Any
currencyorpaircontaining a colon (USDT:USDT) is filtered out; those are non-spot rows written by an older exchange-market importer. - The toast reports
importedandskipped, andskippedcounts both the pairs you already had and the colon rows.
With no active exchange markets at all the call answers 404, "No active exchange markets found to import" — check Finance → Trading Infrastructure → Exchange Providers first.
Stake limits
minAmount and maxAmount are per market, denominated in the quote
currency, and they are the contract for both feeds. Edit them at
/admin/finance/binary/market/{id}/edit, which is the only screen that exposes
them.
A customer whose stake falls outside them is refused at order placement with
"Amount must be between {min} and {max} {pair}".
What applies when a row carries no usable limit:
| Situation | Effective limit |
|---|---|
| row created by the wizard or the importer | the column defaults — minAmount 1, maxAmount 10000 |
| column is NULL, zero or otherwise not a positive number | DEFAULT_MIN_AMOUNT 1 and DEFAULT_MAX_AMOUNT 100000 from marketSource.ts |
The fallback accepts a value only when it is finite and greater than zero, so
setting maxAmount to 0 does not close the market — it raises the ceiling to
100,000. To stop trading on a market, turn its status off.
The two numbers also disagree by design in one place: rows written by the
platform get maxAmount 10000 from the column default, while a row whose
maxAmount is NULL (typically one that predates the column) resolves to
100,000. If a market you never edited is accepting far larger stakes than you
expected, that is the row to look at.
What ecosystem markets cannot do
Two order types are refused outright on an ECOSYSTEM market, with
"{type} orders are not available on ecosystem markets":
- Touch / No Touch
- Turbo
Both settle on intra-period highs and lows, which come from exchange OHLCV. Ecosystem markets expose no equivalent, so a position that could not be settled correctly is never opened in the first place. Rise/Fall, Higher/Lower and Call/Put are unaffected.
If you intend to run the Binary AI Engine, note that it works only when Rise/Fall is the only enabled order type — the create wizard prints a warning on step 1 when your saved binary settings enable anything else. Order types are configured on Admin → Finance → Binary Options → Binary Settings.
Enabling, disabling and retiring a market
The status toggle in the table writes to
PUT /api/admin/finance/binary/market/{id}/status (edit.binary.market) as you
click it. There is no save step.
A market with status false rejects every order attempt with
Binary market {symbol} is disabled. That is the safe way to retire a
market: the row survives, its history survives, and nothing new can be opened
against it.
The settlement path resolves the market before it settles, and a disabled market
makes that lookup throw. For an EXCHANGE-backed order the 15-second backstop
still settles it from the exchange candle, so the damage is a log line. For an
ECOSYSTEM-backed order it is worse: the backstop cannot learn the feed
either, falls back to treating the market as exchange-backed, and will settle
the position on the centralized exchange price — the exact confusion the
source column exists to prevent — or fail outright if the pair is not listed
there.
Turn a market off after its last contract has expired, not while positions are live. If you have already done it, turn it back on until the open orders have settled.
Two limits on bulk work on this screen:
- Bulk status is API-only.
PUT /api/admin/finance/binary/market/statustakes{ "ids": ["…"], "status": true }and updates them all (edit.binary.market), but the table's bulk menu offers delete selected and nothing else — there is no "disable selected" button, and the bulk restore action exists only inside the deleted-rows view, which this table never shows (see the next bullet). Flip markets one at a time from the table, or call the endpoint. - Deletion is one row at a time. There is a per-row delete route
(
DELETE /api/admin/finance/binary/market/{id},delete.binary.market) and no collection-level one. The table is not paranoid —binary_markethas no soft-delete column, so a delete is immediate and permanent. Prefer disabling.
isHot and isTrending are merchandising, not risk
Neither flag restricts anything. They do two things on the trading page:
- The market selector has All / Trending / Hot / Favorites tabs; Trending
filters on
isTrending, Hot filters onisHot, and a market that has neither gets a muted badge. GET /api/exchange/binary/marketsorts the public listisTrendingfirst, thenisHot, then alphabetically by currency — so flagging a market moves it to the top of everyone's list.
If you want to limit exposure on a market, that is minAmount, maxAmount and
status. Turning off Hot does nothing but change where it appears.
Permissions
| Key | What it opens |
|---|---|
access.binary.market |
the screen itself |
view.binary.market |
the list, one market, and the available-markets lookup |
create.binary.market |
creating a market, and Import from Exchange |
edit.binary.market |
editing, the status toggle, and bulk status |
delete.binary.market |
deleting a market |
The route itself is gated on access.binary.market alone. That key opens the
page but does not load it — the list request behind it is checked against
view.binary.market, so a role holding only access. lands on an empty table.
Grant both. Binary orders are a separate screen with its own key,
access.binary.order — see Roles and permissions.
Endpoint reference
When a market is configured and orders still are not settling, the next page is Binary orders that did not settle.