Skip to content

Market data

Nothing on this page needs an API key. The examples are Python, using httpx and websockets 14 or later:

Terminal window
pip install httpx "websockets>=14"

/api/markets/schedule gives the windows that have not ended yet. The order is by start time (open_ms), then by market_id. The first ones are trading now. Windows that are open or live are in it.

import httpx
BASE = "https://inflection.exchange"
markets = httpx.get(f"{BASE}/api/markets/schedule", params={"limit": 5}).json()["markets"]
for market in markets:
print(market["market_id"], market["series"], market["slug"], market["status"])

limit is the page size. The default is 100, and the route clamps it to 1 to 100. The older name count still works but is deprecated. If you send both limit and count, the route returns 400.

To read the whole schedule, page with next_after. The response is {"markets": [...], "next_after": 10350205292}. If next_after is a market id, pass it as after to read the next page. If it is null, nothing follows.

params = {"limit": 100}
schedule = []
while True:
page = httpx.get(f"{BASE}/api/markets/schedule", params=params).json()
schedule += page["markets"]
if page["next_after"] is None:
break
params["after"] = page["next_after"]
print(len(schedule), "windows")

An after that names no market returns 400. The route also returns 422 for a value outside the 64-bit integer range. If a window is created while you read, you can miss it. The next read returns it.

To read fewer windows, add asset (btc, wtioil or nvda), duration (1m or 5m), or both. Paging works the same way, within the narrower list. limit is still the size of a page. A value the route does not list returns 400.

params = {"limit": 5, "asset": "btc", "duration": "5m"}
markets = httpx.get(f"{BASE}/api/markets/schedule", params=params).json()["markets"]

Each market has a market_id, which you use everywhere else, and a slug that names the window, like btc-updown-5m-20260929145000. See Markets for the rest of the fields and how a market moves from open to settled.

A series is the line of windows that one slug belongs to. It is the slug without its time: btc-updown-5m. You subscribe to a series to follow each new window.

Nvidia and WTI trade only in the hours of their home exchange, so a series can have no windows, at the weekend for example. To see when each series opens next, read /api/markets/series. See Trading hours.

for row in httpx.get(f"{BASE}/api/markets/series").json()["series"]:
print(row["series"], row["next_open_ms"])

To list markets by series, status or asset, use /api/markets/markets. Name at least one of the three. The newest listed market comes first: the order is the time each market was listed, then market_id. To read the next page, pass the next_before of the last page as before. A before that names no market returns 400.

You can add duration (1m or 5m) to narrow the list further. duration alone returns 400. If series conflicts with asset or duration, the page is empty.

page = httpx.get(
f"{BASE}/api/markets/markets", params={"series": "btc-updown-5m", "status": "settled", "limit": 10}
).json()
print([market["market_id"] for market in page["markets"]], page["next_before"])

To keep a list of markets without asking again, fetch /api/markets/markets/list, then read the server-sent events at /api/markets/markets/stream.

/api/markets/markets/list returns {"version": int, "series": [string], "markets": [Market]}. It has the same content as the snapshot event of the stream. The server compresses it with gzip if your client accepts it. The ETag is the version, and the response has Cache-Control: public, max-age=2 and Vary: Accept-Encoding. If you send the ETag back in If-None-Match and it is still current, the answer is 304. While the list loads, the answer is 503 with a Retry-After header.

The stream sends these events:

Event Data
snapshot The version, the series and all markets. Sent first, only if you do not send after.
changed The version and the markets that changed.
snapshot_stale {"version": int}. The server then closes the stream.

Each changed event has a higher version than the one before it. To skip the snapshot, pass the version you hold as after. The stream then sends only the changed events after that version. The server keeps the last 512 changes. If after is older than that, or newer than the server has reached, the stream sends snapshot_stale and closes. Without after, the stream sends a snapshot first.

Use this pattern:

  1. Fetch /api/markets/markets/list. Keep the version.
  2. Open /api/markets/markets/stream?after=<version>. Keep the version of each changed event.
  3. If you get snapshot_stale, or the connection drops, wait a random delay of one second or more, and fetch the list again.

Do not reconnect at once. Many clients that reconnect together load the server.

import json
import random
import time
import httpx
BASE = "https://inflection.exchange"
while True:
listing = httpx.get(f"{BASE}/api/markets/markets/list").json()
version = listing["version"]
try:
with httpx.stream(
"GET", f"{BASE}/api/markets/markets/stream", params={"after": version}, timeout=None
) as stream:
event = None
for line in stream.iter_lines():
if line.startswith("event:"):
event = line.removeprefix("event:").strip()
elif line.startswith("data:") and event in ("changed", "snapshot_stale"):
if event == "snapshot_stale":
break
version = json.loads(line.removeprefix("data:"))["version"]
print("changed", version)
except httpx.TransportError:
pass
time.sleep(1 + random.random() * 4)

Connect to wss://inflection.exchange/ws/market-data. The socket sends nothing until you subscribe. Send your first subscribe within 30 seconds, or the server closes the socket.

A subscribe names one channel and the markets or series you want:

Channel What you get
book The order book. Choose a level: l1 or l2.
trades Each trade as it happens.
status A message when a market opens, closes or settles.
{"type": "subscribe", "channel": "book", "level": "l2", "series": ["btc-updown-5m"]}

The server answers with subscribed, then a snapshot of each book. Its markets list holds every market the request covers. Its rejected list holds each item the server refused, with a reason: unknown_series, unknown_market or market_closed. A window that is not trading and ended more than a few hours ago gets unknown_market.

Name a market to follow one window. Name a series to follow its trading windows and the windows that ended in about the last 2 hours. A new window arrives when it starts trading, and a market leaves your subscription after it settles. One message names at most 100 series and markets, and you can hold 200 at once. To stop, send an unsubscribe with the same names.

You can subscribe to a series that has no windows now, such as Nvidia at the weekend. The server accepts it, and sends each window when it starts trading.

At level l1, you get the best bid and the best ask of each market. Each l1 message holds the whole top of the book. Replace what you hold. Use l1 for a list of markets or a ticker.

At level l2, you get a book snapshot of every level, then a book_update with only the levels that changed. Use l2 when you need depth.

A market has one level at a time. If you subscribe again at the other level, it replaces the first, and you get a new snapshot.

Every book, book_update and l1 carries a seq. Each market counts its own book changes: 1, 2, 3, with no gaps. Follow these rules for each market:

  1. When you get a book, replace your book and keep its seq.
  2. If a book_update has a seq one above yours, apply it.
  3. If its seq is at or below yours, ignore it.
  4. If its seq is more than one above yours, you missed an update. Subscribe to the book of that market again. Then wait for the new book.

Each level in an update is [price_bps, quantity], with the new total at that price. Quantity 0 removes the level.

An l1 can skip numbers. The server sends an l1 only when the best bid or ask changes, so a jump is not a gap. Ignore an l1 at or below your seq.

This client keeps the books of a series, follows market status, and subscribes again after a gap:

import asyncio
import json
import random
import websockets
URL = "wss://inflection.exchange/ws/market-data"
class Book:
def __init__(self, snapshot):
self.seq = snapshot["seq"]
self.bids = {price: quantity for price, quantity in snapshot["bids"]}
self.asks = {price: quantity for price, quantity in snapshot["asks"]}
def apply(self, update):
"""Return False if the update shows a gap."""
if update["seq"] <= self.seq:
return True
if update["seq"] != self.seq + 1:
return False
for levels, changes in ((self.bids, update["bids"]), (self.asks, update["asks"])):
for price, quantity in changes:
if quantity == 0:
levels.pop(price, None)
else:
levels[price] = quantity
self.seq = update["seq"]
return True
def request(**fields):
return json.dumps(fields)
async def watch(series):
books = {}
async with websockets.connect(URL) as socket:
await socket.send(request(type="subscribe", channel="book", level="l2", series=[series]))
await socket.send(request(type="subscribe", channel="status", series=[series]))
async for frame in socket:
message = json.loads(frame)
market_id = message.get("market_id")
if message["type"] == "book":
books[market_id] = Book(message)
elif message["type"] == "book_update":
book = books.get(market_id)
if book is not None and not book.apply(message):
del books[market_id]
await socket.send(request(type="subscribe", channel="book", level="l2", markets=[market_id]))
elif message["type"] == "status" and message["status"] != "open":
books.pop(market_id, None)
print(market_id, message["status"], message["outcome"])
elif message["type"] == "error":
print(message["code"], message["message"])
if market_id in books:
book = books[market_id]
print(market_id, book.seq, max(book.bids, default=None), min(book.asks, default=None))
async def keep_watching(series):
while True:
wait = 0
try:
await watch(series)
except websockets.InvalidStatus as refused:
wait = int(refused.response.headers.get("Retry-After", 0))
except (websockets.ConnectionClosed, OSError):
pass
await asyncio.sleep(wait + random.uniform(1, 10))
asyncio.run(keep_watching("btc-updown-5m"))

A book holds Up prices only. A price is in basis points, so 4,900 is 49¢. A quantity is in lots, 100 to the share. See Units and prices.

To read the book for Down, subtract each Up price from 10,000 and swap the sides. A bid for Down at 4,900 is an ask for Up at 5,100. This book holds Up bids at 4,900 and 4,800, and one Up ask at 5,100:

Up Down
Bids 4,900 for 20 lots, 4,800 for 35 lots 4,900 for 15 lots
Asks 5,100 for 15 lots 5,100 for 20 lots, 5,200 for 35 lots
def down_book(book):
bids = {10_000 - price: quantity for price, quantity in book.asks.items()}
asks = {10_000 - price: quantity for price, quantity in book.bids.items()}
return bids, asks

Subscribe to trades to hear each trade. A trade is one match, and it has no snapshot:

{"type": "trade", "market_id": 10350205292, "trade_id": 1, "up_price_bps": 6000, "quantity": 6,
"taker_outcome": "down", "taker_side": "buy", "time_ms": 1700000000123}

up_price_bps is always the price of Up. taker_outcome and taker_side say what the order that took liquidity did. For a trade in Down, use 10,000 minus the Up price. The example above is a purchase of Down at 4,000, or 40¢.

taker_outcome taker_side Means
up buy Bought Up at up_price_bps.
up sell Sold Up at up_price_bps.
down buy Bought Down at 10,000 minus up_price_bps.
down sell Sold Down at 10,000 minus up_price_bps.
def describe(trade):
price = trade["up_price_bps"]
if trade["taker_outcome"] == "down":
price = 10_000 - price
action = "bought" if trade["taker_side"] == "buy" else "sold"
return f"{action} {trade['taker_outcome'].capitalize()} at {price / 100:g}¢"

A trade names no account and no order. A trade_id counts 1, 2, 3 in each market. To see your own fills, use the trading socket.

A book subscription sends nothing when a market closes. To hear it, subscribe to status as well. You get a status for each market when you subscribe, and again when it changes:

{"type": "status", "market_id": 10350205292, "series": "btc-updown-5m", "status": "settled", "outcome": "up"}

status is open, closed or settled. Here open means the market is trading. It covers open, live and halted in the markets API. The outcome is null until the market settles. Then it is up, down or void. A void means no price decided the market. See A void market.

The server can close the socket. If it does, wait a random delay. Then connect and subscribe again. A random delay stops all clients from returning at once.

Close code Why
1001 The server is restarting.
1008 You did not subscribe within 30 seconds, or you sent requests faster than the server answered.
1011 The server got no answer to a ping. Answer pings.
1013 You fell behind, or the server dropped you under load.

If the server refuses the upgrade with HTTP 503, it is not ready. Wait the seconds in Retry-After, plus a random delay. Then try again. The client above does this, and it subscribes again each time it connects.

The socket sends a snapshot only after you subscribe. To draw a page before that, read the same data over HTTP. These routes are under /api/market-data:

Route Returns
/summary The best bid and ask, the last trade and the volume of each market you name. With series, it gives the trading windows of the series and those that ended recently and have not settled.
/book The top levels of one open market’s book.
/trades One market’s trades, newest first, including past windows.
BASE = "https://inflection.exchange/api/market-data"
summary = httpx.get(f"{BASE}/summary", params={"series": "btc-updown-5m"}).json()["markets"]
print(summary[0]["bid"], summary[0]["ask"], summary[0]["last_trade"])
book = httpx.get(f"{BASE}/book", params={"market_id": summary[0]["market_id"], "depth": 10}).json()
print(book["seq"], book["bids"], book["asks"])

A summary holds the seq of the market. If you then subscribe at level l1, ignore each l1 at or below that number. The /book route returns only the top levels. To keep a whole book, subscribe at level l2.

For trade history, page back with before. Pass the lowest trade_id of the page. To join history to the live feed with no gap:

  1. Subscribe to trades.
  2. Read the history.
  3. Skip each live trade whose trade_id you already have.
market_id = summary[0]["market_id"]
trades = httpx.get(f"{BASE}/trades", params={"market_id": market_id, "limit": 100}).json()["trades"]
if trades:
older = httpx.get(
f"{BASE}/trades", params={"market_id": market_id, "before": trades[-1]["trade_id"]}
).json()["trades"]

If you ask for a market that closed a moment ago, /trades can answer 503 with Retry-After. Wait that many seconds and ask again.

The full list of routes and fields is in the HTTP reference. The socket messages are in the market data reference.

On testnet, markets resolve on Binance and Hyperliquid prices that our price reporter posts on chain. Each market names its source in the venue, symbol and candle fields. Bitcoin shows binance, BTCUSDT and 1s. Crude oil and Nvidia show hyperliquid, xyz:CL or xyz:NVDA, and 1m. A price is the open of that candle at the boundary.

When the vault prices an asset from Pyth, venue is pyth and candle is null. We don’t redistribute Pyth prices.

Our key decides testnet outcomes. See Who decides on testnet and How a market resolves.