API and events
Every futures endpoint — trader and admin — with its method, path, permission key and the fields that matter, plus the three WebSocket routes and what they stream.
All futures routes live under /api/futures (trader) and /api/admin/futures
(operator), with one exception: the admin orders table is served from
/api/admin/futures/order but presented under Finance in the menu.
Admin routes carry a permission key; trader routes are gated by sign-in and, for
order placement only, by the futures_trading KYC feature.
Markets
symbol of CURRENCY/PAIRThe list endpoint returns the full row including the metadata blob, which is what the trading ticket reads for precision, limits, leverage rungs and fee rates. The by-id endpoint deliberately does not.
Orders
currency + pair filter to one symbol; type=OPEN returns only resting ordersfutures_trading KYC featuretimestamp is requiredPlacement body
{
"currency": "BTC",
"pair": "USDT",
"type": "LIMIT",
"side": "BUY",
"amount": 0.5,
"price": 100000,
"leverage": 20,
"stopLossPrice": 95000,
"takeProfitPrice": 110000
}currency, pair, type, side, amount and leverage are required.
priceis required forLIMITand ignored forMARKET— a market order is priced by walking the book at placement.stopLossPrice/takeProfitPriceare optional and are carried onto the position that the fill creates. Note the field names: a ticket sendingstopLoss/takeProfitsets no stop at all.
The response echoes the stored order with isTaker and feeRate, so a ticket
can label the charge rather than guess at it from the order type.
Failure codes worth handling
| Code | When |
|---|---|
| 400 | Market disabled, bad side or type, amount or price outside the market's limits, leverage not offered, insufficient balance, missing fee configuration |
| 404 | No such market, or the market has no metadata |
| 422 | A market order cannot be priced — no resting liquidity, or not enough depth to fill the amount |
| 503 | The Ecosystem extension is not available |
Every rejection keeps its real status code and names the thing to change. A 500 from this route means something genuinely broke.
Positions
type=OPEN_POSITIONS filters to open; type=POSITIONS_HISTORY returns everything that is not opencurrency, pair and sideEach position in the list carries two computed fields beyond the stored row:
mode— always"HEDGE". Long and short on the same symbol are independent positions, each posting its own margin.liquidationPrice—entry × (1 ∓ 0.9 ÷ leverage), derived from the engine's own full-liquidation threshold rather than stored, so the two can never drift apart.
The close response returns markPrice, margin, realizedPnl and credited.
Market data
symbol, from, to and interval are all requiredCandles come back as openTime, closeTime, open, high, low, close,
volume.
WebSocket routes
Three sockets, all under the same /api prefix your proxy already forwards.
| Route | Auth | Payload |
|---|---|---|
/api/futures/market |
none | { type, symbol } where type is orderbook, ticker or trades |
/api/futures/ticker |
none | Broadcasts all tickers on demand |
/api/futures/order |
required | The caller's own order and position updates |
The market socket validates the symbol against the database on subscribe and refuses a market that does not exist or is disabled — so switching a market off also stops its data stream for new subscribers. Subscribed streams are pushed every 500 ms.
Subscribing to trades on the market socket returns an empty array. Order book
and ticker are the live streams.
Admin — dashboard
Takes timeRange of 24h, 7d or 30d (default 7d). The payload's source
object reports whether the position store answered, whether the scan was
truncated, how many rows it read and what the cap was. Read it — a truncated scan
is a sample, not a total.
Admin — markets
Create takes token ids, not symbols, and resolves them to currency codes itself. Both tokens must exist and be active, or the call is a 404; a duplicate pair is a 409.
Admin — orders and positions
Both accept the standard list parameters (page, size, sort, filter, search). Neither has a write counterpart: there is no admin route that cancels a trader's order or closes their position.
Permission keys
Nine keys matter, and they are not all enforced in the same place. Seven are
route metadata — the permission field on the handler, checked by the API before
it runs, and the value printed beside each admin endpoint above. The other
two, access.futures.position and access.futures.order, appear on no route
at all: they are frontend page gates, exported from
(ext)/admin/futures/position/permission.ts and
(dashboard)/admin/finance/order/futures/permission.ts. They decide whether the
screen opens, not whether a request succeeds.
| Key | Enforced as | Guards |
|---|---|---|
access.futures.market |
Route and page gate | The dashboard endpoint, plus the Futures and Markets screens |
view.futures.market |
Route | Reading market rows |
create.futures.market |
Route | Creating a market |
edit.futures.market |
Route | Editing a market, and status toggles |
delete.futures.market |
Route | Deleting markets |
view.futures.position |
Route | Reading position rows |
view.futures.order |
Route | Reading order rows |
access.futures.position |
Page gate only | Opening the Positions screen |
access.futures.order |
Page gate only | Opening the Futures Orders screen under Finance |
Grant the pairs together. A role holding view.futures.position without
access.futures.position can read positions over the API but cannot open the
screen; the reverse opens a screen whose first request comes back 403.
Admin → Roles lists fifteen futures rows rather than nine. create, edit
and delete for both .order and .position are seeded and grantable, but
neither table is rendered with canCreate, canEdit or canDelete, so those
buttons never appear and the six keys gate nothing on any screen or route. There
is no admin write path for orders or positions to grant.
Super Admin short-circuits every check by role name and needs none of them. See core permissions for how keys are derived and where each one is enforced.
Numbers on the wire
Orders and positions are stored in ScyllaDB as VARINT columns holding
fixed-point values scaled by 10^18, and are de-scaled on the way out. Two
consequences for anyone consuming these endpoints:
- Amounts, prices, costs and fees arrive as strings on the raw rows and as numbers on the formatted routes. Do not assume one shape from the other.
leverageis a plain integer, not a scaled value. A 10x position stores10. Anything that de-scales it produces1e-17and renders as0.00x.