# Realtime channels

Public market-data channels, their JSON and Protobuf payloads, how to connect and subscribe without a token, recovery after reconnects, and the market overview.

Public market data streams over the realtime WebSocket without an account or token. Use a snapshot or history request to establish state, then apply the channel's updates. Connection behavior shared with private channels is described in the [WebSocket session model](https://testnet.polyester.com/docs/developer-docs/shared-concepts/websocket-session-model).

## Public channels

`<symbolId>` is the numeric Pair ID from [GetSpotConfig](https://testnet.polyester.com/docs/developer-docs/market-data/pairs-and-trading-rules). Each channel exists in the encodings listed; a JSON channel has no suffix unless shown, and a Protobuf channel ends in `:proto`.

- **Order book updates** · JSON, Protobuf\
  One update of a depth tier; Protobuf `orderbook.v1.OrderBookDelta`.
- **[Trades](https://testnet.polyester.com/docs/api-docs/streams/public-spot-market-trades)** · JSON, Protobuf\
  An array of trades; Protobuf `marketdata.v1.MarketTradeBatch`.
- **Candles** · JSON, Protobuf\
  The current candle of one timeframe; Protobuf `marketdata.v1.CandlePoint`.
- **Market overview** · Protobuf only\
  Changed rows of every Pair; `marketoverview.v1.MarketOverviewBatch`.
- **Order book heatmap** · JSON, Protobuf\
  One heatmap bucket; `marketdata.v1.HeatmapLiveBucket`, JSON as ProtoJSON.

The channel names:

```text
public:spot:orderbook:deltas:depth:<depth>:<symbolId>:json
public:spot:orderbook:deltas:depth:<depth>:<symbolId>:proto
public:spot:market:trades:<symbolId>
public:spot:market:trades:<symbolId>:proto
public:spot:market:candles:<timeframe>:<symbolId>
public:spot:market:candles:<timeframe>:<symbolId>:proto
public:spot:market_overview:updates:proto
public:spot:market:heatmap:<interval>:<symbolId>
public:spot:market:heatmap:<interval>:<symbolId>:proto
```

- `<depth>` is a published order book tier: `1`, `20`, `50`, `200`, or `500`. Both order book channels need their suffix. See [Order book](https://testnet.polyester.com/docs/developer-docs/market-data/order-book).
- `<timeframe>` is a REST candle timeframe such as `1m`, `3m`, or `1mo`. See [Candles](https://testnet.polyester.com/docs/developer-docs/market-data/candles).
- `<interval>` is a heatmap interval: `1s`, `1m`, `5m`, or `1h`. Read history with [`HeatmapService.GetOrderbookHeatmap`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.HeatmapService/GetOrderbookHeatmap).

Trades are described in [Trades](https://testnet.polyester.com/docs/developer-docs/market-data/trades).

> **Choose the payload by channel**
>
> JSON channels carry decimal strings, as REST does. Protobuf channels carry scaled integers, as ConnectRPC does; decode them with the scales in [Scaled integers](https://testnet.polyester.com/docs/developer-docs/connectrpc/scaled-integers). The market overview has no JSON channel.

## Connect and subscribe

Official SDKs connect, subscribe, and recover for you. A custom client speaks the Centrifugo client protocol over WebSocket.

1. Open the connection

   Connect to `wss://<API host>/connection/websocket`, for example `wss://api.testnet.polyester.com/connection/websocket`. See [Environments and hosts](https://testnet.polyester.com/docs/developer-docs/getting-started/environments-and-hosts). Public channels need no connection token.

2. Pick the client protocol

   The default protocol is JSON and carries only JSON channels. To receive Protobuf channels, open the WebSocket with the `centrifuge-protobuf` subprotocol and use the Protobuf client protocol. A JSON connection that subscribes to a Protobuf channel is closed with code `3506` when the first binary publication arrives.

3. Send connect, then subscribe

   Send a `connect` command, then one `subscribe` per channel. Public subscriptions need no subscription token.

   ```json
   {"id":1,"connect":{}}
   {"id":2,"subscribe":{"channel":"public:spot:market:trades:1","recover":true}}
   ```

4. Answer pings

   The `connect` reply carries `"ping":25,"pong":true`: the server sends an empty ping about every 25 seconds and expects a reply. In the JSON protocol, answer each `{}` with `{}`.

5. Store the stream position

   Each subscribe reply carries the channel's `epoch` and `offset`, and each publication its `offset`. Keep the latest pair per channel to recover after a reconnect.

The server answered the commands above with:

```json
{"id":1,"connect":{"client":"56cd50db-63da-4728-9e2f-bfe893f717a3","version":"6.6.0 OSS","ping":25,"pong":true}}
{"id":2,"subscribe":{"recoverable":true,"epoch":"1792667891","offset":1621617,"positioned":true,"was_recovering":true}}
{"push":{"channel":"public:spot:market:trades:1","pub":{"data":{"events":[{"symbol":"BTC-USDT","matchId":"2190","side":"SELL","price":"76075.09","qty":"0.00131449","executedAt":"2026-11-05T21:37:52.340853094Z"},{"symbol":"BTC-USDT","matchId":"2191","side":"SELL","price":"76075.08","qty":"0.01307918","executedAt":"2026-11-05T21:37:52.340853094Z"}]},"offset":1621618}}}
```

A subscribe command for a channel name that does not match a pattern above, such as an order book channel without its `:json` suffix, is rejected with error `107` (`bad request`).

## Recover after a reconnect

Delivery is at least once. After a reconnect, resubscribe with `recover: true` and the `epoch` and `offset` you stored:

```json
{
	"id": 4,
	"subscribe": {
		"channel": "public:spot:market:trades:1",
		"recover": true,
		"epoch": "1792667891",
		"offset": 1621618
	}
}
```

- With `"recovered":true`, the reply's `publications` array replays what you missed, in order. Apply them as usual, then continue with live publications.
- Without `recovered`, or with a different `epoch`, missed publications could not be recovered. Reload state over REST or ConnectRPC: a new order book snapshot, an `afterTradeId` backfill of trades, or a candle history read. Then continue with live publications.

Channels keep a limited history of recent publications, so a short disconnect usually recovers and a long one does not.

Apply state the same way after a replay as after a live publication: order book updates by book sequence, trades by `matchId`, candles by `tsSec`, and overview rows by `symbolId`.

## Market overview

The market overview is a ticker row per Pair: last price, 24h change, high, low, and volume, the best bid and ask, and the index price. Read all rows with [`GET /v1/spot/markets/overview`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/markets/overview) or [`MarketOverviewService.ListMarketOverview`](https://testnet.polyester.com/docs/api-docs/connect/marketoverview.v1.MarketOverviewService/ListMarketOverview), then apply updates from `public:spot:market_overview:updates:proto`.

## REST JSON

```http
GET https://api.testnet.polyester.com/v1/spot/markets/overview?symbols=BTC-USDT
```

```json
{
	"nextPageToken": "",
	"markets": [
		{
			"symbol": "BTC-USDT",
			"lastPrice": "75907.92",
			"lastTradeTsNs": "1793913814242553034",
			"change24hBps": -19,
			"high24h": "76566.37",
			"low24h": "75675.89",
			"volume24hBase": "6.30453704",
			"volume24hQuote": "479470.859709",
			"volume24hUsd": "478675.643863",
			"bestBid": "75923.1",
			"bestBidQty": "0.90602069",
			"bestAsk": "75953.48",
			"bestAskQty": "0.87235094",
			"indexPrice": "75935.234625758",
			"listedTsNs": "1793880813827104000",
			"sparklines": []
		}
	]
}
```

## ConnectRPC ProtoJSON

```http
POST https://api.testnet.polyester.com/marketoverview.v1.MarketOverviewService/ListMarketOverview
Content-Type: application/json

{"symbolId": [1]}
```

```json
{
	"markets": [
		{
			"symbolId": 1,
			"lastPriceTicks": "75877570000000",
			"lastTradeTsNs": "1793913933075819794",
			"change24hBps": -23,
			"high24hTicks": "76566370000000",
			"low24hTicks": "75675890000000",
			"volume24hBaseScaled": "665070087",
			"volume24hQuoteScaled": "505746706733",
			"volume24hUsdScaled": "504894996912",
			"listedTsNs": "1793880813827104000",
			"bestBidTicks": "75847220000000",
			"bestBidQtyScaled": "89693218",
			"bestAskTicks": "75877570000000",
			"bestAskQtyScaled": "87114076",
			"indexPriceTicks": "75859331815060"
		}
	]
}
```

Every parameter is listed in the [API reference](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/markets/overview). The points that differ between the two APIs, or are easy to miss:

- REST filters with `symbols`, repeated once per Pair; ConnectRPC with `symbolId`. An empty filter returns every Pair, including disabled ones, and a filter takes at most 2000 Pairs. REST skips a symbol it cannot use without an error: an unknown symbol, a comma-joined list, or a disabled Pair returns no row. ConnectRPC rejects an unknown `symbolId` with HTTP 400 and returns disabled Pairs.
- `limit` defaults to `50` rows per page, with a maximum of `2000`.
- `orderBy` is `CHANGE_24H_BPS`, `VOLUME_24H_USD`, `LAST_PRICE`, or `DATE_ADDED`, and `sort` is `ASC` or `DESC`. Without `orderBy`, rows are sorted by 24h USD volume, descending. ConnectRPC uses the prefixed enum names, such as `ORDER_BY_CHANGE_24H_BPS` and `SORT_DESC`.
- `sparklineIntervals` is `1h`, `24h`, `1w`, or `1mo`, repeated for several. It defaults to `24h` when `includeSparklines` is set. Sparkline close prices are ordered newest first.

### 24h change

`change24hBps` is the change from the 24h open to the last price, in basis points, truncated toward zero: `-19` is `-0.19%`. It has minute granularity:

- The 24h open is the price at the start of the minute that contains the time 24 hours ago.
- If the Pair first traded later than that, for example because it was listed less than 24 hours ago, the 24h open is its first trade.
- A Pair with no trade reports `0`, and its `lastTradeTsNs` is `0` too; ConnectRPC JSON omits both. A Pair that has just started trading reports `0` until its first minute of trading has ended.

To tell "no trade yet" from "no move", check `lastTradeTsNs`. Sorting by `CHANGE_24H_BPS` uses the absolute value of the change.

### Volume and prices

- `volume24hBase` and `volume24hQuote` are rolling 24h volumes. On ConnectRPC, base volume uses the base Asset's `marketDataVolumeScale` and quote volume the Pair's `quoteQuantityScale`; both are omitted if the value exceeds the signed 64-bit range.
- `volume24hUsd` applies the current quote-to-USD conversion to the quote volume; on ConnectRPC it is scaled by 1e6. USDT and USDC count at USD parity when no conversion is available. It is omitted for other quote Assets without a current conversion, and those Pairs sort last by USD volume.
- REST sends `"0"` for `lastPrice`, `high24h`, `low24h`, and the best bid and ask of a Pair without them; ConnectRPC JSON omits them.
- `indexPrice` is the current multi-venue index price; ConnectRPC reports `0` when no fresh index price is available.
- Prices on ConnectRPC use the fixed scale of 9; best bid and ask quantities use the Pair's `baseQuantityScale`.

### Overview updates

`public:spot:market_overview:updates:proto` is one channel for every Pair. It publishes at a fixed cadence, and each `MarketOverviewBatch` holds the complete current row of each Pair that changed since the previous batch, plus the batch time `tsNs`. Pairs that did not change are not included. Replace your row for each `symbolId` in the batch. Sparklines are not streamed; read them with `ListMarketOverview`.

## Rate limits and SDKs

Quotas for reads and realtime subscriptions are not published yet; the published quotas are in [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits). For payload-specific guidance, see [WebSocket JSON](https://testnet.polyester.com/docs/developer-docs/rest/websocket-json) and [WebSocket Protobuf](https://testnet.polyester.com/docs/developer-docs/connectrpc/websocket-protobuf). Every message type is in the [API reference](https://testnet.polyester.com/docs/api-docs).
