Developers
This exchange's read API is twelve HTTP routes: a market catalogue, candles, a health check, three server-sent event streams, four ledger reads and two composite endpoints. They are documented from the handlers that serve them, and they are the same ones the site itself calls.
Three requests, no setup
Nothing to register for and nothing to configure. In the first two, https://your-deployment stands for whichever origin is serving you this page — swap it in and they work right now. The third is plain browser JavaScript against a relative URL, so it runs from any page on this site exactly as written.
Ask whether the data path is live
Not “is the web server up” — that stays green while every price is an hour stale. This reports whether the ingest worker is writing, and answers
503when it is not.Shell curl -i 'https://your-deployment/api/health'Pull real candles
Eight intervals, up to 1,000 bars, straight off the venue. Note that
timecomes back in seconds — the charting convention, and the one unit surprise in this API.Shell curl 'https://your-deployment/api/klines?symbol=BTCUSDT&interval=1h&limit=3'Open a live stream
The order book, the trade tape and the forming 1-minute candle for one symbol. Plain server-sent events, so a browser needs no library at all.
JavaScript const stream = new EventSource('/api/stream/symbol/BTCUSDT') stream.addEventListener('snapshot', (event) => { const { ticker, book, trades } = JSON.parse(event.data) render(ticker, book, trades) }) stream.addEventListener('trade', (event) => { appendToTape(JSON.parse(event.data)) })
The whole API, on one screen
Twelve routes is a small surface, and seeing all of it at once is a more useful answer than a search box — you will know in ten seconds whether the thing you came for exists. Every row links into the reference.
| Endpoint | Auth | What it serves |
|---|---|---|
| /api/healthGET | Public | Is the ingest worker writing? 503 when it is not. |
| /api/markets/indexGET | Public | The searchable catalogue — six fields per pair, top 800 by volume. |
| /api/klinesGET | Public | Candles for one pair at one of eight intervals. |
| /api/stream/tickersGETSSE | Public | Every tradable ticker, batched, as the worker publishes them. |
| /api/stream/symbolGETSSE | Public | One symbol's book, tape and forming 1m candle. |
| /api/stream/accountGETSSE | Session | A poke when your ledger moves. Never a balance. |
| /api/account/balancesGET | Session | Spot, futures and funding wallet balances from the ledger. |
| /api/account/ordersGET | Optional | The last 40 orders, as the smallest projection that shows a status change. |
| /api/account/positionsGET | Session | Open futures positions with marks, or closed position history. |
| /api/account/activityGET | Session | The futures wallet ledger feed: margin, PnL, funding, liquidations. |
| /api/terminal/contextGET | Optional | Everything an in-place market switch needs, in one round trip. |
| /api/ai/analyzePOST | Public | Streaming market commentary grounded in this venue’s own candles. |
Every route has a client you can watch
This is the spot terminal at /trade/BTCUSDT, rendering live venue data. Five panels, five things documented in the reference — open it in another tab and you are watching these endpoints deliver.

Every panel above is a client of an endpoint below
Market header — The
snapshotevent’sticker, then everytickerevent after it. The same figures the markets table gets in batches from/api/stream/tickers.Candle chart — History from
GET /api/klinesat the selected interval, then thecandleevent folds the live 1-minute bar on top of it as trades print.Order book — The
bookevent — top 20 levels a side, roughly every 100ms. Bids descend from the best bid, asks ascend from the best ask.Trade tape — One
tradeevent per print. The row colour isside, which names the aggressor —buymeans a taker lifted the ask.Order ticket —
GET /api/terminal/contextfor the market’s real precisions and minimum notional. It validates against them and then submits through a server action — there is no HTTP endpoint that places an order.
What is behind the routes
A websocket has to outlive a request, and a request handler is gone the moment it responds. So the venue connections live in a separate process — one worker holds every socket open and writes what arrives into Redis, and the web app only ever reads. That is the entire reason there are two processes, and it is what lets one upstream connection fan out to every connected browser.
The dashed branch is the part worth knowing before you integrate. Redis is a cache here, never a dependency. Every read falls back to the venue’s own REST API, so the endpoints keep answering with the worker stopped and the cache unreachable — polled instead of streamed, and slower. Never blank, and never a frozen number wearing a fresh timestamp.
You can tell which path you are on. The status page renders /api/health as it arrives, and that endpoint names the fallback whenever the fallback is the thing carrying the site.
There are no API keys
Stated early because it decides whether this API is any use to you. Nothing in this codebase issues, stores or verifies a key, and no handler reads an Authorization header. The account routes authenticate with the same session cookie the website uses, and there is no endpoint that trades credentials for one.
So this is a browsable read API. Eight of the twelve routes answer anyone, four are practical only from a signed-in browser, and none of them writes anything. Orders are placed by server actions from the trading terminal, not over HTTP with a body you can construct — so no combination of these routes adds up to a trading client.
What authenticates today
- A NextAuth session cookie, set by signing in at /signin.
- Same origin only. No
Access-Control-Allow-Originheader is emitted anywhere, so another site’s JavaScript cannot read these responses. - Four routes answer
401without it. Two more read it when it is there and answer without it.
What would have to exist for keys to be real
- Issuance and revocation — a table and a surface, with the secret shown once and only its hash stored.
- A signing scheme — a key in a query string is a key in every access log. HMAC over method, path, body, nonce and timestamp, with a replay window on the server.
- Scopes — read and trade must be different grants, or a leaked read key spends money.
- Rate limiting — session traffic is bounded by how fast a person can click. Key traffic is not, and no counter or quota exists in this repo.
- Write endpoints — place, amend and cancel, with idempotency keys so a retry cannot double-fill.
The streams are the interesting part
Server-sent events rather than websockets, deliberately: the data travels one way, the browser reconnects by itself, and there is no protocol on top of HTTP to implement. Three rules cover almost every integration mistake made against them.
Every frame is named, so onmessage never fires
These streams emit
snapshot,book,trade,candle,ticker,tickers,ready,poke,failanderror. None of them is the defaultmessageevent, so a client written againstonmessageconnects successfully and then sits in silence.A snapshot arrives before anything live
Every stream sends a full snapshot on connect, which is why reconnection needs no replay and there are no sequence gaps to reconcile. A client that connects between two publishes renders complete instead of staring at an empty table.
Nothing in front of these routes may compress or buffer
A compressor assumes a body that ends, and these do not — its buffer grows without bound until the process is killed. The routes send
cache-control: no-transformandx-accel-buffering: no; if you put a proxy in front, exclude/api/stream/*from compression.
Run the whole thing locally
A pnpm workspace with two processes. The web app is the only one you strictly need — without Redis or the worker, every read falls back to the venue, so the site polls instead of streams and is slower. Nothing is blank.
pnpm install
cp .env.example .env # defaults work as-is
pnpm dev # the site, and with it the APIpnpm infra:up # Postgres and Redis, in Docker
pnpm db:migrate # the ledger — needed by /api/account/*
# in a second terminal
pnpm dev:marketd # the venue websocketsWhy the ports are unusual
The compose file maps both services to non-standard host ports rather than their defaults, so a local checkout cannot silently connect to another project’s database and start writing into it. The values live in the repository — the compose file and .env.example agree, so the copy step above needs no editing.
| Command | What it does |
|---|---|
| pnpm dev | The Next.js dev server — the API and the site. |
| pnpm dev:marketd | The ingest worker. Without it, reads fall back to the venue REST API. |
| pnpm infra:up | Postgres and Redis in Docker. infra:down stops them again. |
| pnpm db:migrate | Applies migrations and runs the engine cutover — the ledger. |
| pnpm test | Vitest across core, marketd and web. |
| pnpm typecheck | Typechecks every package in the workspace. |
Where every number comes from
Attribution matters more here than on most APIs, because the whole premise of this project is that the market data is real while the money is not. These are the upstream sources, and the refresh column is the cache window in the code rather than an aspiration.
| Data | Source | Refresh | Served by |
|---|---|---|---|
| Spot pairs, tickers, candles, depth, trades | Binance public REST and websocket | Streaming | /api/markets/index · /api/klines · /api/stream/* |
| Perpetual futures, funding, open interest | Binance USDT-M futures REST and websocket | Streaming · funding on a slow sweep | /api/terminal/context · /api/stream/* |
| Market cap, circulating supply, rank | CoinGecko free tier | 5 minutes | No route — server-rendered tools only |
| Fiat cross rates | Frankfurter (ECB reference rates) | 1 hour | No route — server-rendered tools only |
| Market commentary | Anthropic Claude, prompted with the candles above | On request | /api/ai/analyze |
Only the last one needs a credential, and it belongs to the operator rather than the caller: without ANTHROPIC_API_KEY in the server environment, /api/ai/analyze answers 503 and names the missing configuration rather than failing obscurely.
Balances, orders, positions and activity have no upstream at all. They are this project’s own double-entry ledger in Postgres, moved by real venue prices. How this works.
Read the reference
Every parameter each handler reads, every status code it can return, the exact shape of every response, the event names on all three streams, and the types they are built from. Written with the route files open — if a route behaves differently from what the page says, the page is the thing that is wrong.