Skip to main content
Omnibook is no longer a single BTC book. Nine recurring series run in parallel: three assets × three cadences. Each series is an independent order book with its own rounds, its own strike, and its own price feed. Two integers do all the work:
  • category (1–9) identifies the series. It appears on every round object and every rounds WebSocket frame, and filters GET /v1/rounds.
  • feed (0–2) identifies the price source. It scopes GET /v1/oracle/price and appears on every oracle WebSocket frame.

The registry

Several series share one feed: all three BTC cadences settle against feed 0. Going the other way is unambiguous — a category maps to exactly one feed.
category: 0 means “unclassified”, not “all”.A round reports category 0 only in the gap before its slot’s reference data lands — a create frame can arrive that way. It is never a wildcard, and ?category=0 filters to those unclassified rounds rather than matching everything. To ask for every series, omit the parameter.
The venue API speaks in integers: market_id, round_number, category, feed. Human tickers like OB-XAU-UD-60S-260917-025111 are a display convention of the web app and are not fields on any venue endpoint. Do not parse them out of the API; derive your own labels from category.

Getting every live market

GET /v1/markets?status=trading returns live slots across all nine series in one page — one market per series while every feed is up.
The market object does not carry category. A market row tells you the book (best_yes_bid, tick_size, volume), not which asset it is. Read the round to learn the series.
So the “what is trading right now, across everything” loop is two calls deep:
Cache the market_id → category mapping for the life of the round only. market_id is recycled, and a recycled slot can come back in a different series. Re-read the round before acting on a cached mapping. See Rounds and markets.

Getting one asset

GET /v1/rounds takes an optional category filter and returns that series’ rounds newest round_number first:
A round enters this list when it opens, so within one category the first entry is the most recently opened round for that series. That makes ?category=N&limit=1 a usable “current round for this series” — but check status, because it will read frozen or settled during the gap between rounds, and the pre-created rounds waiting behind it are not listed yet.
Without ?category=, GET /v1/rounds?limit=1 is not the live round. The list interleaves all nine series by round_number, so the newest row is whichever series opened last. This was already true with one series and is badly wrong with nine. Use GET /v1/markets?status=trading for live slots.
To follow one asset across all three cadences, accept its three categories (BTC: 1, 2, 3 — gold: 4, 5, 6 — crude: 7, 8, 9). There is no “subscribe by category”: you pass market_ids on the per-market channels, and filter the global rounds channel on category yourself.

Prices, per asset

GET /v1/oracle/price takes an optional feed:
  • Omitting feed means feed 0, so every client written before multi-asset keeps its exact meaning.
  • The response does not echo the feed. Track which feed you asked for. The WebSocket oracle channel does name it — see Channels.
  • price is an integer in units of 1e-8 USD, as a decimal string. The magnitudes differ by orders of magnitude across assets: BTC around 10000000000000, gold around 424518000000, crude around 6231000000. Never infer the asset from the magnitude, and never hold these in a float.
  • fresh_source_count is how many independent sources were live behind the median. BTC medians up to five CEX perp streams; gold and WTI each come from a single QFEX underlier, so 1 is normal and healthy for them. Do not hardcode a floor of 3.
  • A feed that is not a u16 is rejected 400. A valid-but-unknown feed is not an error — it answers the zero sentinel below.

Market hours and dark feeds

BTC trades 24/7. Gold and crude do not. When a session closes, the upstream feed stops publishing and the venue stops opening new rounds for every series on that feed. The round that was already trading freezes and settles normally; nothing is stranded.
There is no session calendar on the API. GET /v1/exchange/schedule covers venue maintenance windows, not market hours. The venue deliberately reacts to the feed actually being dark rather than trusting a hardcoded timetable, so it covers upstream outages as well as scheduled closes.
The authoritative liveness check is the zero sentinel on the feed:
price: "0" means “no valid tick” — closed session, upstream outage, or a venue that has just restarted and not yet folded a tick. It is a normal 200, not an error, and no traded asset is ever really worth 0. Poll GET /v1/oracle/price?feed=N and treat price == "0" as “this asset is not opening rounds right now”.

WebSocket

The oracle and rounds channels are global. They are not per-market and take no market_ids: one subscription receives every tick from every feed and every lifecycle event from all nine series, interleaved. Scope them client-side on the feed and category fields that each frame carries. The per-market channels (orderbook_snapshot, orderbook_delta, trades) are unchanged — you name the market_ids you want, and one subscription can span markets from different assets. See Channels for payloads and filtering examples.