# Self-Service Access

There are two ways to reach the CryptoStruct realtime market data feed.
**Managed access** is what [Accessing Data](accessing-data.md) describes:
you run a Backend installation, look up hosts through the master data
API, and your server's IP is whitelisted for the markets you use.

**Self-service access** needs none of that. You buy access in the
[shop](https://cryptostruct.com/shop?realtime=1), get an API key, and
connect to a fixed endpoint. This page covers what is different — the
wire protocol itself is the same one documented in
[Protocol](protocol.md).

> [!NOTE]
> Self-service access is currently in public beta. The feed and the
> protocol are the production platform behind the managed feeds; the
> self-service path — shop, API keys and per-day access — is the new
> part.

---

## What differs from managed access

{.compact}

| Topic                  | Managed access                                          | Self-service access                                                      |
|------------------------|---------------------------------------------------------|--------------------------------------------------------------------------|
| Instrument master data | `/api/exchanges` and `/api/instruments` on `<api-host>` | `/api/realtime/instruments?market=<code>` on `cryptostruct.com`          |
| Host discovery         | `/api/v2/marketdata?instruments=…` returns `hosts[]`    | None. One fixed endpoint per market                                      |
| Authentication         | None; your server's IP is whitelisted                   | An API key, passed as the `apiKey` query parameter                       |
| Entitlement            | Agreed per contract                                     | Bought per UTC calendar day, per market or per instrument                |
| Wire protocol          | [Protocol](protocol.md)                                 | Identical — same messages, same encodings                                |

> [!NOTE]
> Everything from the login onwards is unchanged: message types, topic
> flags, event payloads, JSON and [SBE](sbe.md) encodings.
> [Protocol](protocol.md) stays the field-level reference for
> self-service clients too.

---

## Getting access

Realtime access is sold in the shop per **UTC calendar day**, either as a
whole market (every instrument of that market) or as single instruments.

1. **Buy days.** Pick a market or a set of instruments and a duration at
   [cryptostruct.com/shop](https://cryptostruct.com/shop?realtime=1).
   Paid days always start on the **next UTC day**, and the remainder of
   the purchase day is included free — your key validates the moment the
   order is paid. Rates are on the
   [pricing page](https://cryptostruct.com/pricing#realtime).
2. **Get your API key.** It is created with your first realtime purchase
   and shown at
   [cryptostruct.com/account/realtime](https://cryptostruct.com/account/realtime),
   together with your current and upcoming days. There is one key per
   account, and you can rotate it there at any time — rotation does not
   affect the days you already bought.
3. **Look up instrument IDs.** Subscriptions take numeric instrument IDs.
   Fetch them from the master data export described below.

A day ends at 00:00 UTC. Extend before that if you want to stay connected
across midnight.

---

## Endpoints

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 and append your key as `?apiKey=…`.

{.compact}

| Market         | Code           | Port    | JSON endpoint                             | SBE endpoint                                  |
|----------------|----------------|---------|-------------------------------------------|-----------------------------------------------|
| Binance USDT-M | `binance_swap` | `14004` | `ws://lon1.cryptostruct.com:14004/api/v6` | `ws://lon1.cryptostruct.com:14004/api/v6/sbe` |
| Binance Spot   | `binance_spot` | `14003` | `ws://lon1.cryptostruct.com:14003/api/v6` | `ws://lon1.cryptostruct.com:14003/api/v6/sbe` |

The encoding is selected by path: `/api/v6` is JSON over text frames and
the place to start, `/api/v6/sbe` is [SBE](sbe.md) over binary frames for
low-latency clients. A complete URL looks like this:

```!#
ws://lon1.cryptostruct.com:14004/api/v6?apiKey=<your key>
```

`lon1` is the delivery endpoint, not the source: the data is captured at
the venue in Tokyo and delivered at our London endpoint. Further
locations are available through managed access.

> [!WARNING]
> The self-service endpoints are plain `ws://` today — there is no TLS in
> front of the proxy. Run your client from a server you control, and keep
> the key out of browser-side code.

### Checking an endpoint

The `/api/info` endpoint of a port is plain HTTP and needs no key:

```!#
http://lon1.cryptostruct.com:14004/api/info?all=true
```

It answers with the proxy's process ID, the protocol version, both
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 rate or liquidations, so those topics
are `null` there and never emit. USDT-M perpetuals have all seven topics.

---

## Instrument master data

Self-service access has no Backend installation and therefore no
`/api/exchanges` or `/api/instruments`. The equivalent is a per-market
export of every sellable instrument, served by the shop:

```!#
GET https://cryptostruct.com/api/realtime/instruments?market=<code>
GET https://cryptostruct.com/api/realtime/instruments?market=<code>&format=csv
```

`<code>` is the market code from the endpoint table. Authenticate with
your realtime API key as an `Authorization: Bearer` header — the same key
as the feed, but sent as a header here, never as a URL parameter.

```bash
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
```

The JSON form is an envelope
`{market: {exchange_id, code, name}, generated_at, count, instruments: [...]}`
with the fields below per row. The CSV form carries one header line with
the same columns in the same order.

{.compact}

| Column               | Type                    | Meaning                                                     |
|----------------------|-------------------------|-------------------------------------------------------------|
| `instrument_id`      | integer                 | Numeric ID — the value you subscribe with (`[11, 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_code`      | string                  | Market code — the value of `?market=`                        |

The decimal columns are **strings**. Keep them as strings or parse them
with a decimal type, never a float. Absent values are `null` in JSON
and empty in CSV.

Only `open` and `review` instruments are listed — the sellable set. The
list is refreshed server-side about hourly; fetch it at start-up and
re-fetch daily, not per message.

{.compact}

| Status | Meaning                                                                                                         |
|--------|-----------------------------------------------------------------------------------------------------------------|
| `200`  | The instrument list                                                                                             |
| `404`  | The market code is unknown, or the key was not accepted — the two are deliberately indistinguishable, check both |
| `503`  | The access check is temporarily unavailable; the response carries a `Retry-After` header                        |

---

## Connecting

The server sends nothing until you log in and subscribe. Every session is
the same sequence:

1. Connect to the endpoint of your market, with your key in `?apiKey=`.
2. Send the login `[13, organization, application_name,
   application_version, process_id]` and read the `[14, …]` response with
   the capabilities. The four strings are free-form and used for logging
   only — authentication already happened through the key in the URL.
3. Send `[11, instrumentId]` per instrument. The server answers with a
   `[5, id, ts, "READY", ""]` state event and the current state, then
   streams live updates.
4. Keep reading. The server pings every 5 s; standard WebSocket libraries
   answer with a pong by themselves.

The optional third element of the subscribe message selects topics; the
defaults are listed under [Subscription](protocol.md#subscription).
Liquidations are off by default and have to be requested explicitly.

A smoke test without writing code, using
[websocat](https://github.com/vi/websocat) — connect, then paste the two
lines:

```bash
websocat -t 'ws://lon1.cryptostruct.com:14004/api/v6?apiKey=<your key>'
[13,"my-org","websocat","1.0.0","smoke"]
[11,67824]
```

The same in Python:

```python
# pip install websockets
import asyncio, json, websockets

URL = "ws://lon1.cryptostruct.com:14004/api/v6?apiKey=<your 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
        raise RuntimeError(f"server: {raw!r}")
    return json.loads(raw)


async def main():
    try:
        async with websockets.connect(URL, max_size=None) as ws:
            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]))

            await ws.send(json.dumps([11, INSTRUMENT]))
            while True:
                msg = await recv(ws)
                print(msg[0], msg[1])
    except websockets.exceptions.InvalidHandshake as e:
        # HTTP 401 — unknown key, or no paid day for this market today
        print("handshake rejected:", e)


asyncio.run(main())
```

> [!TIP]
> The payload of every message type, the topic flags and the capability
> fields are documented in [Protocol](protocol.md); the binary layout is
> in [SBE Encoding](sbe.md). Prices, quantities, event IDs and trade IDs
> are decimal strings, and timestamps are nanoseconds since the Unix
> epoch.

---

## 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.

### 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` and a `text/plain` body:

```!#
api key rejected: <reason>
```

{.compact}

| Reason                                                            | Meaning                                                    |
|-------------------------------------------------------------------|------------------------------------------------------------|
| `unknown api key`                                                 | The key does not exist — check for truncation or rotation   |
| `no active realtime subscription for this market today (UTC)`     | No paid day for the market behind this port today           |
| `market not configured`                                           | The endpoint could not be resolved to a market              |
| `invalid request`                                                 | The access check was called with an unusable request        |

Client libraries surface this as a failed connection rather than a
WebSocket close: Python `websockets` raises `InvalidHandshake`, Node `ws`
emits `unexpected-response` with the status and the body. The second
reason is the common case — verify the port you connected to and your
days at
[/account/realtime](https://cryptostruct.com/account/realtime). Do not
retry a rejected handshake in a tight loop.

### Notices and instrument errors

Connection-level notices arrive as a plain-text frame — not a JSON array
— immediately before the close, on a running session only:
`api key expired or revoked`, `not logged in`, `already logged in`,
`protocol violation: <detail>`, `pong check failed`, `service shutdown`.
Log them verbatim; they say why the connection ended.

Instrument-level problems 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` and
`instrument unsubscribed`. A whole-market day unlocks every instrument of
that market; an instrument day unlocks exactly the IDs you bought.

### Connection limit

Each key may hold a limited number of parallel connections per market —
five by default. The current value is shown on your access card. Further
handshakes are refused.

### Reconnecting

Connect, log in, subscribe again. Every subscribe starts with a fresh
snapshot, so no state is lost — do not try to resume by event ID. The
same applies mid-session: on a `prevEventId` gap, or a
[snapshot](protocol.md#snapshot) with `forceReset`, unsubscribe and
resubscribe the instrument and rebuild the book from the new snapshot.

### When access changes take effect

Days and instruments you buy apply at your next login, so reconnect after
a purchase. A rotated key or a refund is enforced at the next login and
by the proxy's periodic re-check, which runs about hourly. Expect the
notice `api key expired or revoked` followed by a close at 00:00 UTC when
the next day is not bought — this is the expected end of an unextended
day.

---

## Security notes

- On the WebSocket the key travels in the URL query string. Treat the
  full URL like a password: keep it out of shared configs, tickets, logs
  and screenshots.
- On the master data route the key is an `Authorization: Bearer` header
  and must never be put into that URL, so it stays out of access logs.
- Rotate the key at
  [/account/realtime](https://cryptostruct.com/account/realtime) if it
  ever leaks. The old key stops validating at the next login and
  re-check; your purchased days are unaffected.
- One key per account is the model. Do not share a key between systems
  you may want to cut off separately — use the connection limit for
  parallelism instead.

---

## See Also

- [Accessing Data](accessing-data.md). Managed access: master data API,
  host discovery and IP whitelisting.
- [Protocol](protocol.md). Market data protocol specification.
- [SBE Encoding](sbe.md). Simple Binary Encoding for market data.
