Market data
Nothing on this page needs an API key. The examples are Python, using httpx
and websockets 14 or later:
pip install httpx "websockets>=14"List markets
Section titled “List markets”/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"])Keep a market list up to date
Section titled “Keep a market list up to date”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:
- Fetch
/api/markets/markets/list. Keep theversion. - Open
/api/markets/markets/stream?after=<version>. Keep theversionof eachchangedevent. - 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 jsonimport randomimport 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 and subscribe
Section titled “Connect and subscribe”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.
Choose L1 or L2
Section titled “Choose L1 or L2”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.
Build a book
Section titled “Build a book”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:
- When you get a
book, replace your book and keep itsseq. - If a
book_updatehas aseqone above yours, apply it. - If its
seqis at or below yours, ignore it. - If its
seqis more than one above yours, you missed an update. Subscribe to the book of that market again. Then wait for the newbook.
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 asyncioimport jsonimport 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"))Read prices
Section titled “Read prices”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, asksRead trades
Section titled “Read trades”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.
Follow market status
Section titled “Follow market status”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.
Reconnect
Section titled “Reconnect”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.
Show a page at once
Section titled “Show a page at once”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:
- Subscribe to
trades. - Read the history.
- Skip each live trade whose
trade_idyou 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.
Where prices come from
Section titled “Where prices come from”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.