# Connect to the realtime feed

> CryptoStruct realtime WebSocket feed — the complete self-serve connection spec in one document, for humans and coding agents.
> Canonical HTML: https://cryptostruct.com/docs/realtime · This document: https://cryptostruct.com/docs/realtime.md
> Endpoints, ports and examples are generated from the same registry as the account page; wire facts were verified against the live proxy.

Live L2 order book, trades and derivatives feeds over a plain WebSocket. Three messages get you streaming: connect to the port of your market, send a login, send a subscribe. This page has everything a self-serve customer needs — the platform docs stay the field-level reference.

Contents: What you need · Endpoints — the port selects the market · Instrument master data · Quick start in three messages · Login (type 13 → 14) · Subscribe and unsubscribe (types 11 / 12) · Messages you receive · Keep-alive, errors and reconnecting · Complete examples · For coding agents · Quick reference · Security notes

> **Public beta**
>
> Self-serve realtime access is in public beta. The feed and the protocol are the production platform behind our enterprise feeds; the self-serve path — the shop, API keys and this guide — is new. Every day bought while the beta runs is 20 % below the list price, see [pricing](https://cryptostruct.com/pricing#realtime). Found a gap in this guide or the feed? [Tell us](https://cryptostruct.com/contact?topic=realtime).

## What you need

1. An API key — created with your first realtime purchase and shown on [/account/realtime](https://cryptostruct.com/account/realtime) (one key per account; rotate it there any time).
2. Access — realtime days are sold per UTC calendar day in the [shop](https://cryptostruct.com/shop?realtime=1), either a whole market (every instrument of it) or single instruments. Paid days start on the next UTC day and the remainder of the purchase day is included free: the key validates the moment the order is paid.
3. The instrument ids you want — numeric, from the master-data export (`/api/realtime/instruments?market=<code>`, JSON or `&format=csv`: ids, codes, state, tick and lot sizes — downloaded from your access card, or fetched by any script with your API key as `Authorization: Bearer` header; see “Instrument master data” below), or from any instrument page under [/analyze/instrument/67824](https://cryptostruct.com/analyze/instrument/67824) (the id is in the URL and the header chip).
4. A WebSocket client — any library that speaks RFC 6455 and answers pings by itself (Python `websockets`, Node `ws`, `websocat` for a first smoke test, …).

> **Coming from the platform docs?**
>
> Three things differ for self-serve access: there is no host discovery (`/api/v2/marketdata` is not available — the endpoints below are static), no master-data API (`/api/exchanges`, `/api/instruments` — the “Instrument master data” section below replaces them: the same list from the browser or with your API key as a Bearer header), and authentication is the `apiKey` query parameter, not an IP whitelist. Login, subscribe, encodings and every message are identical to the [protocol reference](https://docs.cryptostruct.com/market-data-api/protocol/).

## Endpoints — the port selects the market

Each market listens on its own port; there is no market switch inside the protocol and no discovery endpoint. Pick the URL of the market you bought, append your key as `?apiKey=…`, and choose the encoding by path: `/api/v6` is JSON (text frames, human-readable — start here), `/api/v6/sbe` is Simple Binary Encoding (binary frames, for low-latency clients — see the [SBE docs](https://docs.cryptostruct.com/market-data-api/sbe/)).

| Market | Code | Location | Port | JSON endpoint | SBE endpoint | Self-check |
| --- | --- | --- | --- | --- | --- | --- |
| Binance USDT-M | `binance_swap` | London | `14004` | `ws://lon1.cryptostruct.com:14004/api/v6` | `ws://lon1.cryptostruct.com:14004/api/v6/sbe` | [/api/info](http://lon1.cryptostruct.com:14004/api/info?all=true) |
| Binance Spot | `binance_spot` | London | `14003` | `ws://lon1.cryptostruct.com:14003/api/v6` | `ws://lon1.cryptostruct.com:14003/api/v6/sbe` | [/api/info](http://lon1.cryptostruct.com:14003/api/info?all=true) |

Append ?apiKey=<your key> to the WebSocket endpoints. The self-check is plain HTTP and needs no key.

`GET http://lon1.cryptostruct.com:<port>/api/info?all=true` answers with the proxy's process id, protocol version, the two endpoints and the capabilities per topic — the same object the login response carries. Capabilities differ per market: spot markets have no mark price, index price, funding or liquidations (those topics are `null` there and never emit); USDT-M perpetuals have all of them.

## Instrument master data

Subscriptions take numeric instrument ids, so fetch the master data of your market first: every sellable (`open`/`review`) instrument with id, code, state, tick and lot size and contract specs — the same list the access card offers as a download. Over HTTP it is `GET https://cryptostruct.com/api/realtime/instruments?market=<code>` (JSON) or with `&format=csv` (CSV); `<code>` is the market code from the endpoint table. From a script or coding agent, authenticate with your realtime API key as the header `Authorization: Bearer <key>` — the same key as the feed, sent as a header here, never as a URL parameter.

**instruments.sh**

```shell
# Instrument master data for Binance USDT-M (binance_swap) as CSV — the same key as the feed,
# sent as a header (never in the URL). 404 = unknown market or key not accepted,
# 503 = try again after the Retry-After seconds.
curl -sS -H "Authorization: Bearer $CRYPTOSTRUCT_REALTIME_API_KEY" \
  "https://cryptostruct.com/api/realtime/instruments?market=binance_swap&format=csv" -o realtime_instruments_binance_swap.csv
```

**instruments.py**

```python
# Instrument master data for Binance USDT-M (binance_swap) — standard library only
import json, os, sys, urllib.error, urllib.request

URL = "https://cryptostruct.com/api/realtime/instruments?market=binance_swap"
KEY = os.environ["CRYPTOSTRUCT_REALTIME_API_KEY"]  # the same key as the WebSocket login

req = urllib.request.Request(URL, headers={"Authorization": f"Bearer {KEY}"})
try:
    with urllib.request.urlopen(req, timeout=30) as res:
        data = json.load(res)
except urllib.error.HTTPError as e:
    # 404 = unknown market or key not accepted; 503 = retry after the Retry-After seconds
    sys.exit(f"master data request failed: HTTP {e.code}")

print(data["market"]["code"], data["count"], "instruments as of", data["generated_at"])
for row in data["instruments"][:5]:
    # ticksize / lot_size are decimal STRINGS — keep them as str or Decimal, never float
    print(row["instrument_id"], row["code"], row["state"], "tick", row["ticksize"], "lot", row["lot_size"])
```

| Column | Type | Meaning |
| --- | --- | --- |
| `instrument_id` | integer | Numeric id — the value you subscribe with (`[11, instrument_id]`). |
| `code` | string | Exchange symbol, e.g. `BTCUSDT`. |
| `type` | string | Instrument class as recorded (`perpetual`, `spot`, …). |
| `base_underlying` | string or null | Base asset, e.g. `BTC`. |
| `counter_underlying` | string or null | Quote asset, e.g. `USDT`. |
| `state` | string | `open` or `review` — only sellable instruments are listed. |
| `ticksize` | decimal string or null | Minimum price increment. |
| `lot_size` | decimal string or null | Minimum quantity increment. |
| `contract_value` | decimal string or null | Contract value (derivatives). |
| `multiplier` | decimal string or null | Contract multiplier. |
| `min_order` | decimal string or null | Minimum order size. |
| `max_order` | decimal string or null | Maximum order size. |
| `exchange_id` | integer | Numeric id of the market (exchange). |
| `exchange_code` | string | Market code — the value of `?market=`. |

JSON: `{market: {exchange_id, code, name}, generated_at, count, instruments: [...]}` with these fields per row; CSV: one header line, these columns in this order.

- Decimal columns (`ticksize`, `lot_size`, `contract_value`, `multiplier`, `min_order`, `max_order`) are strings — keep them as strings or a decimal type, never floats; absent values are `null` (JSON) or empty (CSV).
- Only `open` and `review` instruments are listed — the sellable set. The list is refreshed server-side about hourly; fetch it once at start-up and re-fetch daily, not per message.
- Responses: `200` with the list; `404` when the market code is unknown OR the key is not accepted (uniform on purpose — check both); `503` with a `Retry-After` header when the access check is temporarily unavailable — retry after that many seconds.
- Answers are `Cache-Control: private` — keep them to your own systems; do not proxy them to other accounts.

## Quick start in three messages

Nothing flows after the WebSocket handshake by itself. Every session is the same sequence, and every message in both directions is a JSON array whose first element is a numeric type tag:

1. Connect to your market's endpoint, e.g. `ws://lon1.cryptostruct.com:14004/api/v6?apiKey=YOUR_API_KEY`.
2. Login — send `[13, "my-org", "my-client", "1.0.0", "client-01"]` and read the `[14, …]` response with the capabilities.
3. Subscribe — send `[11, 67824]` per instrument. The server answers with a `[5, …, "READY", ""]` state event and the current state (book snapshot, top of book, last trade, mark/index/funding where the market has them), then streams live updates.
4. Keep the connection open — the server pings every 5 s; every WebSocket library answers with a pong automatically. Subscribe to more instruments at any time.

## Login (type 13 → 14)

The first message after connecting. The four strings identify your organization, application, its version and this process instance — they are free-form, only used for logging, and may even be empty. Authentication already happened through the `apiKey` in the URL.

**login → response**

```json
[13, "my-org", "my-client", "1.0.0", "client-01"]

[14, "public-proxy-ldn-a", "3.12.1-rc", "6", {
  "depthTopic":    {"eventIdType": "ORDERED", "exchangeTimestampType": "MATCHING_ENGINE",
                    "exchangeTimestampPrecision": "MILLIS", "disabled": false},
  "topOfBookTopic": {…}, "tradesTopic": {…}, "markPriceTopic": {…}, "indexPriceTopic": {…},
  "fundingRateTopic": {…}, "liquidationsTopic": {…},
  "crossTopicBookEventId": true, "predictedFundingRate": true, "continuousFunding": false
}]
```

- A subscribe before the login is answered with the text frame `not logged in` and the connection is closed; a second login gets `already logged in`.
- There is no logout — reconnect to change the identifiers.
- SBE clients: announce schema version 5 or newer, older schemas get no `liquidationsTopic` capability.

## Subscribe and unsubscribe (types 11 / 12)

Subscriptions are per instrument and reference the numeric instrument id, not the symbol. Without the optional third element the default topics apply: depth, top of book, trades, index price, mark price and funding rate on; liquidations off.

**subscribe variants**

```json
[11, 67824]

[11, 67824, {"depthTopic": false, "topOfBookTopic": true, "tradesTopic": true,
             "markPriceTopic": true, "indexPriceTopic": true, "fundingRateTopic": true,
             "liquidationsTopic": true, "topOfBookCoalescing": false}]

[12, 67824]
```

- `topOfBookCoalescing: true` delivers only the latest top-of-book per flush instead of every change — fewer messages for slow consumers.
- After every subscribe the server sends `[5, id, ts, "READY", ""]` and the current state (snapshot, top of book, last trade, mark/index/funding where available), then live updates. An instrument cannot be subscribed twice on one connection.
- A whole-market day unlocks every instrument of that market; an instrument day unlocks exactly the bought ids. Anything else — an unknown id, an instrument of another market, an id you did not buy — does not drop the connection: you get a state event `[5, id, ts, "ERROR", "…"]` with `instrument not available on this endpoint` or `instrument not permitted for this api key`.
- Unsubscribing (`[12, id]`) is acknowledged with a state event `instrument unsubscribed`.

## Messages you receive

Every market-data event shares one header: `[type, instrumentId, "prevEventId", "eventId", adapterTimestampNs, exchangeTimestampNs, …payload]`. Timestamps are nanoseconds since the Unix epoch (`0` = not available); `adapterTimestamp` is when our adapter received the event, `exchangeTimestamp` is the venue's own clock where it has one. Prices and quantities are decimal strings — keep them as strings or parse with a decimal type, never a float. Event ids are strings too; `prevEventId` chains an update to its predecessor, so a gap means you missed data — resubscribe. The state event (type 5) is the one exception with its own, shorter header.

| Tag | Message | Direction | Payload after the header | Example (live, BTCUSDT perp) |
| --- | --- | --- | --- | --- |
| `13` | Login | client → server | [13, organization, appName, version, processId] — four free-form strings, logging only | `[13, "my-org", "my-client", "1.0.0", "client-01"]` |
| `14` | Login response | server → client | [14, service, serviceVersion, protocolVersion, {capabilities}] | `[14, "public-proxy-ldn-a", "3.12.1-rc", "6", {"depthTopic": {…}, "tradesTopic": {…}, …}]` |
| `11` | Subscribe | client → server | [11, instrumentId] or [11, instrumentId, {topic flags}] | `[11, 67824]` |
| `12` | Unsubscribe | client → server | [12, instrumentId] | `[12, 67824]` |
| `0` | Book snapshot | server → client | header + [[side, "price", "qty", orderCount], …] (full depth), trailing forceReset flag | `[0, 67824, "0", "11416095364323", …, [[0, "77979.1", "5.221", 1], [0, "77979", "0.39", 1], …]]` |
| `1` | Book update | server → client | header + [[side, "price", "qty", orderCount], …] — qty "0" deletes the level | `[1, 67824, "11416095364323", "11416095366135", …, [[0, "77907.2", "5.772", 1], …]]` |
| `2` | Trades | server → client | header + [[side, "price", "qty", "tradeId", timeNs], …] | `[2, 67824, …, [[1, "77979.1", "0.012", "8030538547", 1787937748600000000]]]` |
| `5` | Instrument state | server → client | [5, instrumentId, timestampNs, "READY" \| "ERROR", "message"] — own header | `[5, 67824, 1787937748548526562, "READY", ""]` |
| `6` | Top of book | server → client | header + [[0, "bidPrice", "bidQty", count], [1, "askPrice", "askQty", count]] | `[6, 67824, …, [[0, "77979.1", "5.221", 1], [1, "77979.2", "1.481", 1]]]` |
| `7` | Mark price | server → client | header + "price" | `[7, 67824, …, "77979.1"]` |
| `8` | Index price | server → client | header + "price" | `[8, 67824, …, "78007.70152174"]` |
| `9` | Funding rate | server → client | header + ["currentRate", nextFundingTimeNs, "predictedRate"] | `[9, 67824, …, ["0.00005913", 1787961600000000000, "0.00008026"]]` |
| `17` | Liquidations | server → client | header + [[side, "price", "qty", timeNs], …] — off by default, opt in per subscribe | `[17, 67824, …, [[1, "77812.4", "0.31", 1787937750123456789]]]` |

Order-book levels are [side, "price", "quantity", orderCount] with side 0 = bid, 1 = ask. Ignore type tags you do not know — new message types may be added.

- Book snapshot (0) carries the full depth. Rebuild your local book from it; a trailing `forceReset: true` means the venue restarted its feed — discard what you had first.
- Book update (1) carries changed levels only; quantity `"0"` deletes the level. Apply updates in order of `eventId`.
- Trades (2) are `[side, "price", "quantity", "tradeId", timeNs]` with side 0 = buy (taker bought), 1 = sell.
- Funding (9) is `["currentRate", nextFundingTimeNs, "predictedRate"]` — the current period's rate, when it settles, and the venue's prediction for the next one; venues republish it periodically, it is not a payment event.
- Liquidations (17) only arrive when subscribed with `"liquidationsTopic": true` and only on derivatives markets.

## Keep-alive, errors and reconnecting

- Heartbeat. The server sends a WebSocket ping every 5 s and closes the connection if no pong arrives within 30 s (`pong check failed`). Standard libraries answer pings automatically — you only have to keep reading from the socket.
- Connection-level notices arrive as a plain-text frame (not a JSON array) right before the close — a running session only, rejected keys never get that far (see below): `api key expired or revoked`, `not logged in`, `already logged in`, `protocol violation: <detail>`, `pong check failed`, `service shutdown`. Log them — they tell you why.
- Key rejected at the handshake. An unknown key, or a key without a paid day for the market behind that port, never gets a WebSocket: the upgrade request is answered with HTTP 401 (`text/plain`) and the body `api key rejected: <reason>`, where `<reason>` is one of `unknown api key`, `no active realtime subscription for this market today (UTC)`, `market not configured`, `invalid request`. Your library reports it as a failed connection (Python `websockets`: `InvalidHandshake`; Node `ws`: the `unexpected-response` event carries status and body); `curl -i` with the Upgrade headers shows the text. The second reason is the common case: you have no paid day for this market today (UTC) — check the port you connected to and your subscriptions on [/account/realtime](https://cryptostruct.com/account/realtime).
- Instrument-level errors come as state events `[5, id, ts, "ERROR", "…"]` and never drop the connection: `instrument not available on this endpoint`, `instrument not permitted for this api key`, `instrument permission expired or revoked`, `instrument unsubscribed`.
- Connection limit. Each key may hold a limited number of parallel connections per market (shown on your access card, 5 by default); further handshakes are refused.
- Reconnect is simple: connect, login, subscribe again. Every subscribe starts with a fresh snapshot, so no state is lost — do not try to resume by event id.
- When changes take effect. Days and instruments you buy apply at your next login (reconnect) — access starts the moment you pay (the rest of the purchase day is free, paid days begin at the next 00:00 UTC); a rotated key or a refund is enforced at the next login and by the proxy's periodic re-check (about hourly). A day ends at 00:00 UTC — extend before that to stay connected across midnight.

## Complete examples

All three log in, subscribe to BTCUSDT (instrument 67824) on Binance USDT-M and print top of book, trades and state events. Replace `YOUR_API_KEY` with the key from [/account/realtime](https://cryptostruct.com/account/realtime) — or copy the ready-made script from the access card there, which already carries your key and one of your instruments.

### Python (websockets)

**quickstart.py**

```python
# CryptoStruct realtime feed — BTCUSDT (instrument 67824) on Binance USDT-M
# pip install websockets
import asyncio, json, websockets

URL = "ws://lon1.cryptostruct.com:14004/api/v6?apiKey=YOUR_API_KEY"
INSTRUMENT = 67824  # BTCUSDT


async def recv(ws):
    raw = await ws.recv()
    if not isinstance(raw, str) or not raw.startswith("["):
        # plain-text server notice sent right before a close (e.g. the key was revoked)
        raise RuntimeError(f"server: {raw!r}")
    return json.loads(raw)


async def stream(ws):
    # 1) Login — the four strings are free-form identifiers (logging only)
    await ws.send(json.dumps([13, "my-org", "my-client", "1.0.0", "client-01"]))
    login = await recv(ws)
    print("logged in:", login[1], "capabilities:", sorted(login[4]))

    # 2) Subscribe — default topics: depth, top of book, trades, mark/index/funding
    await ws.send(json.dumps([11, INSTRUMENT]))

    # 3) Read events — the library answers the server's pings by itself
    while True:
        msg = await recv(ws)
        typ, instr = msg[0], msg[1]
        if typ == 6:  # top of book: [[side, price, qty, count], ...] (0 = bid, 1 = ask)
            bid, ask = msg[6][0], msg[6][1]
            print(f"TOB {instr}: bid {bid[1]} x {bid[2]} | ask {ask[1]} x {ask[2]}")
        elif typ == 2:  # trades: [[side, price, qty, tradeId, timeNs], ...]
            for side, px, qty, _tid, _ts in msg[6]:
                print(f"trade {instr}: {'SELL' if side else 'BUY '} {qty} @ {px}")
        elif typ == 0:  # full book snapshot
            print(f"snapshot {instr}: {len(msg[6])} levels")
        elif typ == 5:  # instrument state: READY / ERROR
            print(f"state {instr}: {msg[3]} {msg[4]}")


async def main():
    try:
        async with websockets.connect(URL, max_size=None) as ws:
            await stream(ws)
    except websockets.exceptions.InvalidHandshake as e:
        # HTTP 401 "api key rejected: <reason>" — unknown key, or no paid day for this market today
        print("handshake rejected (check the key, the port = market, and your paid days):", e)


asyncio.run(main())
```

### Node.js (ws)

**quickstart.mjs**

```javascript
// CryptoStruct realtime feed — BTCUSDT (instrument 67824) on Binance USDT-M
// npm install ws
import WebSocket from 'ws';

const URL = 'ws://lon1.cryptostruct.com:14004/api/v6?apiKey=YOUR_API_KEY';
const INSTRUMENT = 67824; // BTCUSDT

// `ws` answers the server's pings by itself (5 s cadence, 30 s tolerance).
const ws = new WebSocket(URL);

ws.on('open', () => {
  // 1) Login — the four strings are free-form identifiers (logging only)
  ws.send(JSON.stringify([13, 'my-org', 'my-client', '1.0.0', 'client-01']));
});

ws.on('unexpected-response', (_req, res) => {
  // HTTP 401 "api key rejected: <reason>" — unknown key, or no paid day for this market today
  let body = '';
  res.on('data', (chunk) => {
    body += chunk;
  });
  res.on('end', () => console.error('handshake rejected', res.statusCode, body));
});

ws.on('message', (data) => {
  const text = data.toString();
  if (!text.startsWith('[')) {
    // plain-text server notice sent right before a close (e.g. the key was revoked)
    console.error('server:', text);
    return;
  }
  const msg = JSON.parse(text);
  const [type, instr] = msg;
  if (type === 14) {
    console.log('logged in:', msg[1], 'capabilities:', Object.keys(msg[4]));
    // 2) Subscribe — default topics: depth, top of book, trades, mark/index/funding
    ws.send(JSON.stringify([11, INSTRUMENT]));
  } else if (type === 6) {
    // top of book: [[side, price, qty, count], ...] (0 = bid, 1 = ask)
    const [bid, ask] = msg[6];
    console.log(`TOB ${instr}: bid ${bid[1]} x ${bid[2]} | ask ${ask[1]} x ${ask[2]}`);
  } else if (type === 2) {
    // trades: [[side, price, qty, tradeId, timeNs], ...]
    for (const [side, px, qty] of msg[6]) {
      console.log(`trade ${instr}: ${side ? 'SELL' : 'BUY '} ${qty} @ ${px}`);
    }
  } else if (type === 5) {
    console.log(`state ${instr}: ${msg[3]} ${msg[4]}`);
  }
});

ws.on('close', (code, reason) => console.log('closed', code, reason.toString()));
```

### Smoke test without code (websocat)

**websocat**

```shell
# websocat (https://github.com/vi/websocat) — connect, then paste the two lines
websocat -t 'ws://lon1.cryptostruct.com:14004/api/v6?apiKey=YOUR_API_KEY'
[13,"my-org","websocat","1.0.0","smoke"]
[11,67824]
```

## For coding agents

This guide is also served as one self-contained markdown document at [https://cryptostruct.com/docs/realtime.md](https://cryptostruct.com/docs/realtime.md) — hand that URL to Claude Code, Cursor or any coding agent and let it implement the feed for you. Copy the prompt below, or the personalized one from your access card on [/account/realtime](https://cryptostruct.com/account/realtime) (your market, port and an instrument you bought). Put your key into the agent's environment as `CRYPTOSTRUCT_REALTIME_API_KEY` — use “Copy key” on the access card — never into the prompt.

**prompt for your coding agent**

```text
Implement a client for the CryptoStruct realtime market-data feed (WebSocket, JSON encoding).

Read the spec completely before writing code — it is self-contained:
https://cryptostruct.com/docs/realtime.md

My setup:
- Market: Binance USDT-M (`binance_swap`) at our London endpoint, port 14004. The port selects the market — connect to ws://lon1.cryptostruct.com:14004/api/v6?apiKey=<key>
- Transport is plain ws:// — there is no TLS in front of the proxy today; do not switch to a TLS scheme.
- Start with instrument 67824 (BTCUSDT). More ids (numeric, with tick/lot sizes): https://cryptostruct.com/api/realtime/instruments?market=binance_swap&format=csv — GET it with the HTTP header Authorization: Bearer <key> (the same key, taken from the environment variable below; never put it into that URL). If the spec URL above answers 404, request it with the same header.
- My API key is in the environment variable CRYPTOSTRUCT_REALTIME_API_KEY. Read it from there; never hardcode, print, log or commit it, and keep the full URL (it contains the key) out of logs and error messages.

What I want:
1. connect → login [13, "my-org", "my-client", "1.0.0", "client-01"] → wait for [14, …] → subscribe [11, 67824] → process events (book snapshot 0 / update 1, trades 2, state 5, top of book 6, mark 7, index 8, funding 9; liquidations 17 only if I ask for them).
2. Keep prices, quantities, event ids and trade ids as decimal strings — never floats. Apply book updates in eventId order; on a prevEventId gap or a snapshot with forceReset, resubscribe and rebuild the book from the fresh snapshot.
3. A text frame that does not start with "[" is a server notice — log it verbatim, then reconnect with exponential backoff (connect, login, subscribe again). An HTTP 401 at the WebSocket upgrade means the key is unknown or has no paid day for this market today (UTC) — show the response body and stop, do not retry in a loop.
4. Expect the notice `api key expired or revoked` followed by a close at 00:00 UTC when the next day is not bought — treat it as a normal stop, not a crash loop.

Follow the "For coding agents" checklist in the spec. Use a standard WebSocket library for my language (Python `websockets` and Node `ws` are known to work) and ask me before adding other dependencies.
```

> **Keep the key out of the prompt**
>
> Prompts end up in agent transcripts, logs and sometimes in commits. The prompt names the environment variable only; the key itself goes into the shell the agent runs in.

### Implementation checklist

1. Read the API key from the environment variable `CRYPTOSTRUCT_REALTIME_API_KEY`; never hardcode, print, log or commit it, and keep the full URL (it carries the key) out of logs and error messages.
2. The port selects the market — build the URL from the endpoint table above (`ws://lon1.cryptostruct.com:<port>/api/v6?apiKey=…`); there is no market switch inside the protocol and no discovery endpoint. Transport is plain `ws://` — there is no TLS in front of the proxy today, so do not try a TLS scheme.
3. Instrument ids are numeric and come from the master-data export `https://cryptostruct.com/api/realtime/instruments?market=<code>&format=csv` (or JSON without `format`) — request it with the header `Authorization: Bearer <key>` (the same key as the feed, read from `CRYPTOSTRUCT_REALTIME_API_KEY`; never in that URL), fetch it once at start-up, and never subscribe by symbol.
4. Session order: connect → send login `[13, org, app, version, processId]` → wait for `[14, …]` → send `[11, instrumentId]` per instrument (optionally with topic flags) → read events. Nothing flows before the login, and a subscribe before it closes the connection.
5. Every message in both directions is a JSON array whose first element is the numeric type tag; a text frame that does not start with `[` is a plain-text server notice sent right before a close — log it verbatim.
6. HTTP 401 at the WebSocket upgrade with the body `api key rejected: <reason>` means the key is unknown or has no paid day for this market today (UTC) — surface the body and stop; do not retry in a loop.
7. Answer the server's WebSocket pings (every 5 s) with pongs — standard libraries do this by themselves as long as you keep reading from the socket; 30 s without a pong closes the connection (`pong check failed`).
8. Keep prices, quantities, event ids and trade ids as decimal strings (or a decimal type) — never floats. Timestamps are integer nanoseconds since the Unix epoch (int64); `0` means not available.
9. Maintain the book from snapshot (0) plus updates (1) in `eventId` order; quantity `"0"` deletes a level. A `prevEventId` that does not match your last `eventId`, or a snapshot with `forceReset`, means data was missed — unsubscribe `[12, id]`, subscribe `[11, id]` and rebuild from the fresh snapshot.
10. Reconnect with exponential backoff on any close: connect, login, subscribe again — every subscribe starts with a fresh snapshot; never try to resume by event id.
11. Entitlement is per UTC calendar day: at 00:00 UTC a key without a paid next day gets the notice `api key expired or revoked` and the close — handle it as an expected stop, not an error loop. The same notice follows a key rotation or refund at the proxy's periodic re-check.
12. Instrument-level problems arrive as state events `[5, id, ts, "ERROR", "…"]` and do not drop the connection — log them per instrument and keep the session.
13. Ignore type tags you do not know — new message types may be added.
14. Respect the connection limit per key and market (5 by default): one connection per process, more instruments per connection rather than more connections.

## Quick reference

**cheat sheet**

```text
Connect    ws://lon1.cryptostruct.com:<port>/api/v6?apiKey=<key>        JSON
           ws://lon1.cryptostruct.com:<port>/api/v6/sbe?apiKey=<key>    SBE (binary)
Port       14004 = binance_swap   14003 = binance_spot
Self-check GET http://lon1.cryptostruct.com:<port>/api/info?all=true
Instruments GET https://cryptostruct.com/api/realtime/instruments?market=<code>[&format=csv]   header: Authorization: Bearer <key>
Login      [13, org, app, version, processId]   ->  [14, service, version, "6", {capabilities}]
Subscribe  [11, instrumentId]                  ->  snapshot, top of book, … + [5, id, ts, "READY", ""]
           [11, instrumentId, {topic flags}]   (depth/topOfBook/trades/markPrice/indexPrice/fundingRate/liquidations)
Unsub      [12, instrumentId]
Events     [type, instrumentId, prevEventId, eventId, adapterTsNs, exchangeTsNs, payload]
Keep-alive server ping every 5 s -> answer with pong (libraries do); 30 s tolerance
Rejected   HTTP 401 at the handshake, body "api key rejected: <reason>" (unknown key / no paid day for this market today)
Notices    plain-text frame, then close: api key expired or revoked / not logged in / service shutdown …
```

Ports today: `14004` = Binance USDT-M (`binance_swap`), `14003` = Binance Spot (`binance_spot`). Field-level details for every message, including the SBE layout, are in the [protocol reference](https://docs.cryptostruct.com/market-data-api/protocol/) and the [SBE docs](https://docs.cryptostruct.com/market-data-api/sbe/).

## Security notes

- The key is your only credential and on the WebSocket it travels in the URL query string — treat the full URL like a password: keep it out of shared configs, tickets, logs and screenshots.
- Rotate the key on [/account/realtime](https://cryptostruct.com/account/realtime) if it ever leaks; the old key stops validating at the next login and re-check. Rotation does not touch your purchased days.
- Transport is plain `ws://` today — run your client from a server you control rather than a shared network, and do not put the key into browser-side code.
- Never share one key between systems that you may want to cut off separately — one key per account is the model; use the connection limit for parallelism instead.
- On the HTTP master-data route the key travels as an `Authorization: Bearer` header — never as a query parameter there (that form is for the WebSocket only), so it stays out of URL logs.
