> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dexploit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket: OHLCV

> Live candle bars pushed as they're built — mid-bar updates and final closes for any pool, any timeframe.

The OHLCV WebSocket emits one frame per bar update for each `(pool, timeframe)` you subscribe to. You get mid-bar updates while a bar is open (`is_closed: false`) and a final update on close (`is_closed: true`).

This is the right channel when you want to render a live chart or trigger logic on bar close. For one-shot history, use [`/api/v1/candles`](/api-reference) and [`/api/v1/candles/latest`](/api-reference). For raw per-swap data, use [WebSocket: swaps](/streaming/websocket).

## Endpoint and auth

```
wss://ws.dexploit.dev/ws/ohlcv
```

Authenticate either via Bearer header (server) or query string (browser) — `/ws/ohlcv` accepts **both**, same as [`/ws/swaps`](/streaming/websocket):

```
Authorization: Bearer ohlcv_live_sk_<your_key>

# or, for browsers:
wss://ws.dexploit.dev/ws/ohlcv?api_key=ohlcv_live_sk_<your_key>
```

## Subscribe

After the connection is up the server sends a hello frame:

```json theme={null}
{ "client_id": "8caba129-bdab-…", "data_inception": "2026-05-02T00:03:00Z", "status": "connected" }
```

Then send a `subscribe`. **Both `pair_addresses` and `timeframes` are required** — the server will reply with `{"error": "invalid subscribe: timeframes empty"}` (or similar) if either is missing.

```json theme={null}
{
  "type": "subscribe",
  "pair_addresses": ["HdqYz5GVuWgNXbE6fBkgCYXUSPGoyco5yYLBaL5ZzKAR"],
  "timeframes": ["1s", "1m"]
}
```

Server ack:

```json theme={null}
{ "status": "subscribed", "op": "subscribed", "pairs": 1, "timeframes": ["1s", "1m"], "subjects": 2 }
```

This first frame is the one-shot, backward-compatible path: a client that only ever sends it sees **zero** change from before. `pairs` is the distinct pool count, `subjects` is the number of live `(pool, timeframe)` pairs, and `op` mirrors `status`.

<Note>
  A **subject** is one `(pair_address, timeframe)` pair — the unit the stream actually fans out on. A subscribe of 3 pools × 2 timeframes is 3 pairs but **6 subjects**.
</Note>

## Add / remove / unsubscribe (mid-stream)

Once you're subscribed you can change the watched symbol set **without reconnecting** by sending more frames on the same socket. This is what lets a chart or watchlist UI swap the active pool, or add/drop timeframes, in place.

Every mutating frame carries an `op`. Each `(pair_address, timeframe)` pair it expands to is a **subject**; the ack's `pairs`/`timeframes`/`subjects` always report the **new total live set after the op** — not just the delta.

| `op`          | Effect                                                                                                               | Ack `op`       |
| ------------- | -------------------------------------------------------------------------------------------------------------------- | -------------- |
| `add`         | Union the new `(pair, timeframe)` subjects into the live set. Bars for the added symbols start arriving immediately. | `added`        |
| `remove`      | Drop those subjects. Bars for removed symbols stop.                                                                  | `removed`      |
| `unsubscribe` | Clear **all** subjects. `pair_addresses` / `timeframes` are optional and ignored. Totals go to `0`.                  | `unsubscribed` |
| `subscribe`   | **Replace** the entire live set with the new one (reset). Atomic — see note below.                                   | `subscribed`   |

### Add symbols

```json theme={null}
{ "op": "add", "pair_addresses": ["38tqb1K…"], "timeframes": ["1m"] }
```

Ack with the new totals (now 2 pools × the union of timeframes):

```json theme={null}
{ "status": "added", "op": "added", "pairs": 2, "timeframes": ["1m", "1s"], "subjects": 3 }
```

### Remove symbols

```json theme={null}
{ "op": "remove", "pair_addresses": ["38tqb1K…"], "timeframes": ["1m"] }
```

```json theme={null}
{ "status": "removed", "op": "removed", "pairs": 1, "timeframes": ["1m", "1s"], "subjects": 2 }
```

### Unsubscribe (clear everything)

```json theme={null}
{ "op": "unsubscribe" }
```

```json theme={null}
{ "status": "unsubscribed", "op": "unsubscribed", "pairs": 0, "timeframes": [], "subjects": 0 }
```

### Replace the whole set

A later frame with `op: "subscribe"` (or with **no** `op` — absent defaults to `subscribe`) **replaces** the entire live set with the new one:

```json theme={null}
{ "op": "subscribe", "pair_addresses": ["HdqYz5GVu…"], "timeframes": ["5m"] }
```

```json theme={null}
{ "status": "subscribed", "op": "subscribed", "pairs": 1, "timeframes": ["5m"], "subjects": 1 }
```

<Warning>
  **This reset behaviour is the opposite of the [swaps WebSocket](/streaming/websocket).** On `/ws/ohlcv`, **to widen** the set you must use `op: "add"` — sending another `subscribe` (or an op-absent frame) **discards** what you had. The frame's `op` field is what the server reads; the legacy `type` field is ignored.
</Warning>

The replace is **atomic**: if the new set would exceed the [per-connection subject cap](#limits-and-errors), the replace is rejected and your **prior** subscription stays fully intact.

## Frame shape

```json theme={null}
{
  "type": "ohlcv",
  "pair_address": "HdqYz5GVuWgNXbE6fBkgCYXUSPGoyco5yYLBaL5ZzKAR",
  "token_address": "BsZoKtYtP3V2xJnqonEaYXXaQdLv8kPM45aRbt23oPEJ",
  "timeframe": "1m",
  "timestamp": "2026-05-20T12:20:00+00:00",
  "open":  7.553982e-7,
  "high":  7.613724e-7,
  "low":   7.553982e-7,
  "close": 7.613724e-7,
  "volume_sol":   2058053069,
  "volume_token": 2713479778451,
  "trade_count":   5,
  "buy_count":     5,
  "sell_count":    0,
  "unique_traders": 5,
  "is_closed": false
}
```

| Field            | Notes                                                                                                                                                                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pair_address`   | The on-chain pool/LP account. **Note the naming:** OHLCV frames key the pool as `pair_address`, but [swaps WebSocket](/streaming/websocket) frames key the *same* on-chain pool as `pool_address`. Same account, different field name across the two streams. |
| `timestamp`      | Bar **open** time, ISO 8601. The bar covers `[timestamp, timestamp + timeframe)`.                                                                                                                                                                             |
| `is_closed`      | `false` for interim mid-bar pushes; `true` on the bar's final tick. A given `(pair, timeframe, timestamp)` may emit multiple `is_closed: false` frames followed by one `is_closed: true` — the latter is authoritative.                                       |
| `volume_sol`     | **Lamports**, despite the name. Divide by `1e9` for SOL.                                                                                                                                                                                                      |
| `volume_token`   | Token base units. Divide by `10 ** quote_decimals` for human-readable.                                                                                                                                                                                        |
| `unique_traders` | Distinct wallets that traded in this bar so far. Grows monotonically across mid-bar frames.                                                                                                                                                                   |

## Limits and errors

Two independent limits apply:

| Limit                        | Bounds                                                                          | Env                           | Default |
| ---------------------------- | ------------------------------------------------------------------------------- | ----------------------------- | ------- |
| `pair_addresses` per frame   | The pool count of a **single** `subscribe` / `add` / `subscribe`-replace frame. | `MAX_PAIRS_PER_SUBSCRIBE`     | 1000    |
| Live subjects per connection | The **cumulative** live `(pool × timeframe)` set across all your `add`s.        | `MAX_SUBJECTS_PER_CONNECTION` | 5000    |

Mid-stream errors are **non-fatal** — the socket stays open and a later valid frame still works. (Only an error on the **initial** subscribe frame closes the connection, as before.)

* **Bad frame / unknown op** → `{ "error": "invalid subscribe: <detail>" }` and the socket stays open. An unknown op comes back as `{ "error": "invalid subscribe: unknown op: <op>" }`.
* **Per-frame pool cap** → `{ "error": "invalid subscribe: too many pair_addresses; max 1000" }`.
* **Per-connection subject cap** → an `add` or `subscribe`-replace that would push the live set past the cap is rejected **atomically** — no partial apply, and your existing subjects keep delivering:

```json theme={null}
{ "error": "subject cap reached for this connection; cap 5000", "cap": 5000 }
```

## Minimal TypeScript client

```typescript theme={null}
import WebSocket from 'ws'; // browser: use the global WebSocket

const URL    = 'wss://ws.dexploit.dev/ws/ohlcv';
const API    = 'ohlcv_live_sk_<your_key>';
const PAIR   = '<pool_address>';
const TFS    = ['1m'];

let backoff = 1000;

function connect() {
  const ws = new WebSocket(URL, { headers: { Authorization: `Bearer ${API}` } });

  ws.on('open', () => {
    backoff = 1000;
    ws.send(JSON.stringify({
      type: 'subscribe',
      pair_addresses: [PAIR],
      timeframes: TFS,
    }));
  });

  ws.on('message', (raw) => {
    const f = JSON.parse(raw.toString());
    if (f.type !== 'ohlcv') return; // skip hello + subscribe ack + errors
    const tag = f.is_closed ? 'CLOSED' : 'open  ';
    const volSol = f.volume_sol / 1e9;
    console.log(
      `${tag} ${f.timeframe}@${f.timestamp}  o=${f.open.toExponential(3)} c=${f.close.toExponential(3)} ` +
      `vol=${volSol.toFixed(3)} SOL (${f.trade_count} trades)`,
    );
  });

  ws.on('close', () => {
    setTimeout(connect, backoff);
    backoff = Math.min(backoff * 2, 60_000);
  });

  ws.on('error', () => { /* close fires next */ });
}

connect();
```

If you're rendering a live chart, key your local bar store by `(pair_address, timeframe, timestamp)` and *overwrite* the entry on every frame for that key — the latest frame is always the freshest snapshot of that bar. When `is_closed: true` arrives, that bar is final.

### Changing the watched set at runtime

A watchlist or chart UI that switches the active pool sends `add` / `remove` on the **same** open socket — no reconnect. Helpers, and an ack handler that reads back the new live totals:

```typescript theme={null}
// `ws` is the open socket from above.

// Start watching another pool (keeps the existing ones):
const addPair = (pair: string, tfs = ['1m']) =>
  ws.send(JSON.stringify({ op: 'add', pair_addresses: [pair], timeframes: tfs }));

// Stop watching a pool:
const removePair = (pair: string, tfs = ['1m']) =>
  ws.send(JSON.stringify({ op: 'remove', pair_addresses: [pair], timeframes: tfs }));

// Drop everything (e.g. on view teardown):
const clearAll = () => ws.send(JSON.stringify({ op: 'unsubscribe' }));

// In your message handler, branch on the ack/error frames:
ws.on('message', (raw) => {
  const f = JSON.parse(raw.toString());
  switch (f.op ?? f.status) {            // 'subscribed' | 'added' | 'removed' | 'unsubscribed'
    case 'added':
    case 'removed':
    case 'unsubscribed':
    case 'subscribed':
      // f.pairs / f.subjects are the NEW live totals after the op.
      console.log(`live set: ${f.pairs} pools, ${f.subjects} subjects`);
      return;
  }
  if (f.error) {                          // non-fatal mid-stream — socket stays open
    console.warn('op rejected:', f.error, f.cap ? `(cap ${f.cap})` : '');
    return;
  }
  // …otherwise it's an `ohlcv` bar frame — handle as above.
});
```

Use `op: 'subscribe'` (or an op-absent frame) only when you intend to **reset** the whole set — it discards the current subscription. To widen, always use `op: 'add'`.

For reconnect strategy, gap-filling against `/api/v1/candles`, and keepalive guidance, see [Reconnect & backpressure](/streaming/reconnect-backpressure).

## Programmatic spec

The full message contract for this channel — and the other two WebSocket streams — is published as an [AsyncAPI 3.0 document](https://github.com/1tzkaos/docs/blob/main/api-reference/asyncapi.json) you can feed into client generators, validators, or an LLM.


## AsyncAPI

````yaml api-reference/asyncapi.json ohlcv
id: ohlcv
title: OHLCV candle stream
description: >-
  Live OHLCV bars for one or more pools / timeframes. Bars are emitted as
  they're built — `is_closed: false` mid-bar, `is_closed: true` on the final
  tick.
servers:
  - id: production
    protocol: wss
    host: ws.dexploit.dev
    bindings: []
    variables: []
address: /ws/ohlcv
parameters: []
bindings: []
operations:
  - &ref_3
    id: receiveOhlcvOhlcv
    title: Receive ohlcv ohlcv
    description: Candle update
    type: send
    messages:
      - &ref_8
        id: ohlcv
        payload:
          - name: Candle update
            description: >-
              One bar update for a subscribed (pool, timeframe). NOTE:
              `volume_sol` is in **lamports** despite the name (divide by 1e9
              for SOL); `volume_token` is in base units (divide by
              10**quote_decimals).
            type: object
            properties:
              - name: const
                type: string
                description: ohlcv
                required: false
              - name: pair_address
                type: string
                required: false
              - name: token_address
                type: string
                required: false
              - name: timeframe
                type: string
                enumValues:
                  - 1s
                  - 30s
                  - 1m
                  - 5m
                  - 15m
                  - 1h
                  - 4h
                  - 1d
                required: false
              - name: timestamp
                type: string
                description: >-
                  Bar open time. ISO 8601 — different from the `/ws/swaps`
                  epoch-seconds convention.
                required: false
              - name: open
                type: number
                required: false
              - name: high
                type: number
                required: false
              - name: low
                type: number
                required: false
              - name: close
                type: number
                required: false
              - name: volume_sol
                type: integer
                description: '**Lamports**, despite the name. Divide by 1e9.'
                required: false
              - name: volume_token
                type: integer
                description: Token base units.
                required: false
              - name: trade_count
                type: integer
                required: false
              - name: buy_count
                type: integer
                required: false
              - name: sell_count
                type: integer
                required: false
              - name: unique_traders
                type: integer
                required: false
              - name: is_closed
                type: boolean
                description: >-
                  `false` for mid-bar updates, `true` on the final tick for that
                  bar. A `(pair, timeframe, timestamp)` triple emitted with
                  `is_closed: true` is the authoritative bar — earlier
                  `is_closed: false` frames for that same triple are interim.
                required: false
        headers: []
        jsonPayloadSchema:
          type: object
          properties:
            type:
              const: ohlcv
              x-parser-schema-id: <anonymous-schema-110>
            pair_address:
              type: string
              x-parser-schema-id: <anonymous-schema-111>
            token_address:
              type: string
              x-parser-schema-id: <anonymous-schema-112>
            timeframe: &ref_1
              type: string
              enum:
                - 1s
                - 30s
                - 1m
                - 5m
                - 15m
                - 1h
                - 4h
                - 1d
              x-parser-schema-id: Timeframe
            timestamp:
              type: string
              format: date-time
              description: >-
                Bar open time. ISO 8601 — different from the `/ws/swaps`
                epoch-seconds convention.
              x-parser-schema-id: <anonymous-schema-113>
            open:
              type: number
              x-parser-schema-id: <anonymous-schema-114>
            high:
              type: number
              x-parser-schema-id: <anonymous-schema-115>
            low:
              type: number
              x-parser-schema-id: <anonymous-schema-116>
            close:
              type: number
              x-parser-schema-id: <anonymous-schema-117>
            volume_sol:
              type: integer
              format: int64
              description: '**Lamports**, despite the name. Divide by 1e9.'
              x-parser-schema-id: <anonymous-schema-118>
            volume_token:
              type: integer
              format: int64
              description: Token base units.
              x-parser-schema-id: <anonymous-schema-119>
            trade_count:
              type: integer
              x-parser-schema-id: <anonymous-schema-120>
            buy_count:
              type: integer
              x-parser-schema-id: <anonymous-schema-121>
            sell_count:
              type: integer
              x-parser-schema-id: <anonymous-schema-122>
            unique_traders:
              type: integer
              x-parser-schema-id: <anonymous-schema-123>
            is_closed:
              type: boolean
              description: >-
                `false` for mid-bar updates, `true` on the final tick for that
                bar. A `(pair, timeframe, timestamp)` triple emitted with
                `is_closed: true` is the authoritative bar — earlier `is_closed:
                false` frames for that same triple are interim.
              x-parser-schema-id: <anonymous-schema-124>
          example:
            type: ohlcv
            pair_address: HdqYz5GVuWgNXbE6fBkgCYXUSPGoyco5yYLBaL5ZzKAR
            token_address: BsZoKtYtP3V2xJnqonEaYXXaQdLv8kPM45aRbt23oPEJ
            timeframe: 1m
            timestamp: '2026-05-20T12:20:00+00:00'
            open: 7.55398e-7
            high: 7.61372e-7
            low: 7.55398e-7
            close: 7.61372e-7
            volume_sol: 2058053069
            volume_token: 2713479778451
            trade_count: 5
            buy_count: 5
            sell_count: 0
            unique_traders: 5
            is_closed: false
          x-parser-schema-id: WsOhlcvFrame
        title: Candle update
        description: >-
          One bar update for a subscribed (pool, timeframe). NOTE: `volume_sol`
          is in **lamports** despite the name (divide by 1e9 for SOL);
          `volume_token` is in base units (divide by 10**quote_decimals).
        example: |-
          {
            "const": "<string>",
            "pair_address": "<string>",
            "token_address": "<string>",
            "timeframe": "<string>",
            "timestamp": "<string>",
            "open": 123,
            "high": 123,
            "low": 123,
            "close": 123,
            "volume_sol": 123,
            "volume_token": 123,
            "trade_count": 123,
            "buy_count": 123,
            "sell_count": 123,
            "unique_traders": 123,
            "is_closed": true
          }
        bindings: []
        extensions:
          - id: x-parser-unique-object-id
            value: ohlcv
    bindings: []
    extensions: &ref_0
      - id: x-parser-unique-object-id
        value: ohlcv
  - &ref_4
    id: receiveOhlcvConnected
    title: Receive ohlcv connected
    description: Connect hello
    type: send
    messages:
      - &ref_9
        id: connected
        payload:
          - name: Connect hello
            description: >-
              Sent once on connect with a UUID `client_id` and the index
              inception timestamp.
            type: object
            properties:
              - name: client_id
                type: string
                required: false
              - name: data_inception
                type: string
                required: false
              - name: const
                type: string
                description: connected
                required: false
        headers: []
        jsonPayloadSchema:
          type: object
          properties:
            client_id:
              type: string
              format: uuid
              x-parser-schema-id: <anonymous-schema-102>
            data_inception:
              type: string
              format: date-time
              x-parser-schema-id: <anonymous-schema-103>
            status:
              const: connected
              x-parser-schema-id: <anonymous-schema-104>
          x-parser-schema-id: OhlcvConnectedBody
        title: Connect hello
        description: >-
          Sent once on connect with a UUID `client_id` and the index inception
          timestamp.
        example: |-
          {
            "client_id": "<string>",
            "data_inception": "<string>",
            "const": "<string>"
          }
        bindings: []
        extensions:
          - id: x-parser-unique-object-id
            value: connected
    bindings: []
    extensions: *ref_0
  - &ref_5
    id: receiveOhlcvSubscribed
    title: Receive ohlcv subscribed
    description: Subscribe / add / remove / unsubscribe ack
    type: send
    messages:
      - &ref_10
        id: subscribed
        payload:
          - name: Subscribe / add / remove ack
            description: >-
              Sent after each successful op. `status` and `op` echo the op
              (`subscribed`/`added`/`removed`/`unsubscribed`).
              `pairs`/`subjects` are the new LIVE TOTALS after the op (not a
              delta). A subject = one (pool, timeframe) pair.
            type: object
            properties:
              - name: status
                type: string
                description: >-
                  Echoes the op that was applied. `op` field carries the same
                  value.
                enumValues:
                  - subscribed
                  - added
                  - removed
                  - unsubscribed
                required: false
              - name: op
                type: string
                description: Same value as `status`; the op-tagged ack form.
                enumValues:
                  - subscribed
                  - added
                  - removed
                  - unsubscribed
                required: false
              - name: pairs
                type: integer
                description: >-
                  Distinct pool count in the live set AFTER this op (a total,
                  not a delta). `0` after `unsubscribe`.
                required: false
              - name: timeframes
                type: array
                description: Distinct timeframes in the live set after this op.
                required: false
                properties:
                  - name: item
                    type: string
                    enumValues:
                      - 1s
                      - 30s
                      - 1m
                      - 5m
                      - 15m
                      - 1h
                      - 4h
                      - 1d
                    required: false
              - name: subjects
                type: integer
                description: >-
                  Total live (pool x timeframe) subjects after this op. `0`
                  after `unsubscribe`.
                required: false
        headers: []
        jsonPayloadSchema:
          type: object
          properties:
            status:
              type: string
              enum:
                - subscribed
                - added
                - removed
                - unsubscribed
              description: >-
                Echoes the op that was applied. `op` field carries the same
                value.
              x-parser-schema-id: <anonymous-schema-105>
            op:
              type: string
              enum:
                - subscribed
                - added
                - removed
                - unsubscribed
              description: Same value as `status`; the op-tagged ack form.
              x-parser-schema-id: <anonymous-schema-106>
            pairs:
              type: integer
              description: >-
                Distinct pool count in the live set AFTER this op (a total, not
                a delta). `0` after `unsubscribe`.
              x-parser-schema-id: <anonymous-schema-107>
            timeframes:
              type: array
              items: *ref_1
              description: Distinct timeframes in the live set after this op.
              x-parser-schema-id: <anonymous-schema-108>
            subjects:
              type: integer
              description: >-
                Total live (pool x timeframe) subjects after this op. `0` after
                `unsubscribe`.
              x-parser-schema-id: <anonymous-schema-109>
          x-parser-schema-id: OhlcvSubscribedBody
        title: Subscribe / add / remove ack
        description: >-
          Sent after each successful op. `status` and `op` echo the op
          (`subscribed`/`added`/`removed`/`unsubscribed`). `pairs`/`subjects`
          are the new LIVE TOTALS after the op (not a delta). A subject = one
          (pool, timeframe) pair.
        example: |-
          {
            "status": "<string>",
            "op": "<string>",
            "pairs": 123,
            "timeframes": {
              "item": "<string>"
            },
            "subjects": 123
          }
        bindings: []
        extensions:
          - id: x-parser-unique-object-id
            value: subscribed
    bindings: []
    extensions: *ref_0
  - &ref_6
    id: receiveOhlcvError
    title: Receive ohlcv error
    description: Stream error
    type: send
    messages:
      - &ref_11
        id: error
        payload:
          - name: Stream error
            description: >-
              Validation failure, auth issue, or server-side error. Connection
              usually stays open after a recoverable error.
            type: object
            properties:
              - name: const
                type: string
                description: error
                required: false
              - name: message
                type: string
                required: false
              - name: error
                type: string
                description: >-
                  Some errors return just `{"error":"…"}` instead of
                  `{"type":"error","message":"…"}` — handle both.
                required: false
              - name: cap
                type: integer
                description: >-
                  Present only on the per-connection subject-cap rejection:
                  `{"error":"subject cap reached for this connection; cap
                  <N>","cap":<N>}`. Non-fatal — the prior subscription stays
                  intact.
                required: false
        headers: []
        jsonPayloadSchema:
          type: object
          properties:
            type:
              const: error
              x-parser-schema-id: <anonymous-schema-78>
            message:
              type: string
              example: 'invalid subscribe: timeframes empty'
              x-parser-schema-id: <anonymous-schema-79>
            error:
              type: string
              description: >-
                Some errors return just `{"error":"…"}` instead of
                `{"type":"error","message":"…"}` — handle both.
              example: 'invalid subscribe: unknown op: frobnicate'
              x-parser-schema-id: <anonymous-schema-80>
            cap:
              type: integer
              description: >-
                Present only on the per-connection subject-cap rejection:
                `{"error":"subject cap reached for this connection; cap
                <N>","cap":<N>}`. Non-fatal — the prior subscription stays
                intact.
              x-parser-schema-id: <anonymous-schema-81>
          x-parser-schema-id: StreamErrorBody
        title: Stream error
        description: >-
          Validation failure, auth issue, or server-side error. Connection
          usually stays open after a recoverable error.
        example: |-
          {
            "const": "<string>",
            "message": "<string>",
            "error": "<string>",
            "cap": 123
          }
        bindings: []
        extensions:
          - id: x-parser-unique-object-id
            value: error
    bindings: []
    extensions: *ref_0
  - &ref_2
    id: sendOhlcvSubscribe
    title: Send ohlcv subscribe
    description: Subscribe / add / remove / unsubscribe (pools x timeframes)
    type: receive
    messages:
      - &ref_7
        id: subscribe
        payload:
          - name: Subscribe / add / remove / unsubscribe
            description: >-
              Op-tagged frame that mutates the live (pool x timeframe) set. The
              FIRST frame subscribes; later frames on the same socket can
              `add`/`remove`/`unsubscribe` or `subscribe` (reset) without
              reconnecting. `op` absent = `subscribe` = REPLACE.
            type: object
            properties:
              - name: op
                type: string
                description: >-
                  Mutation against the live (pool x timeframe) set. Absent =
                  `subscribe`. `subscribe` REPLACES the whole set (reset); `add`
                  unions; `remove` drops; `unsubscribe` clears all. The server
                  reads `op` only — a legacy `type` field is ignored.
                enumValues:
                  - subscribe
                  - add
                  - remove
                  - unsubscribe
                required: true
              - name: const
                type: string
                description: subscribe
                required: false
              - name: description
                type: string
                description: >-
                  Legacy field, IGNORED by the server (it reads `op`). Retained
                  for backward compatibility with the original one-shot
                  subscribe frame.
                required: false
              - name: pair_addresses
                type: array
                description: >-
                  Pool / pair addresses. Required (>=1) for
                  `subscribe`/`add`/`remove`; optional and ignored for
                  `unsubscribe`.
                required: false
                properties:
                  - name: item
                    type: string
                    required: false
              - name: timeframes
                type: array
                description: >-
                  Required (>=1) for `subscribe`/`add`/`remove`; optional and
                  ignored for `unsubscribe`.
                required: false
                properties:
                  - name: item
                    type: string
                    enumValues:
                      - 1s
                      - 30s
                      - 1m
                      - 5m
                      - 15m
                      - 1h
                      - 4h
                      - 1d
                    required: false
        headers: []
        jsonPayloadSchema:
          type: object
          required:
            - op
          properties:
            op:
              type: string
              enum:
                - subscribe
                - add
                - remove
                - unsubscribe
              default: subscribe
              description: >-
                Mutation against the live (pool x timeframe) set. Absent =
                `subscribe`. `subscribe` REPLACES the whole set (reset); `add`
                unions; `remove` drops; `unsubscribe` clears all. The server
                reads `op` only — a legacy `type` field is ignored.
              x-parser-schema-id: <anonymous-schema-125>
            type:
              const: subscribe
              description: >-
                Legacy field, IGNORED by the server (it reads `op`). Retained
                for backward compatibility with the original one-shot subscribe
                frame.
              x-parser-schema-id: <anonymous-schema-126>
            pair_addresses:
              type: array
              items:
                type: string
                x-parser-schema-id: <anonymous-schema-128>
              description: >-
                Pool / pair addresses. Required (>=1) for
                `subscribe`/`add`/`remove`; optional and ignored for
                `unsubscribe`.
              x-parser-schema-id: <anonymous-schema-127>
            timeframes:
              type: array
              items: *ref_1
              description: >-
                Required (>=1) for `subscribe`/`add`/`remove`; optional and
                ignored for `unsubscribe`.
              x-parser-schema-id: <anonymous-schema-129>
          x-parser-schema-id: OhlcvSubscribeBody
        title: Subscribe / add / remove / unsubscribe
        description: >-
          Op-tagged frame that mutates the live (pool x timeframe) set. The
          FIRST frame subscribes; later frames on the same socket can
          `add`/`remove`/`unsubscribe` or `subscribe` (reset) without
          reconnecting. `op` absent = `subscribe` = REPLACE.
        example: |-
          {
            "op": "<string>",
            "const": "<string>",
            "description": "<string>",
            "pair_addresses": {
              "item": "<string>"
            },
            "timeframes": {
              "item": "<string>"
            }
          }
        bindings: []
        extensions:
          - id: x-parser-unique-object-id
            value: subscribe
    bindings: []
    extensions: *ref_0
sendOperations:
  - *ref_2
receiveOperations:
  - *ref_3
  - *ref_4
  - *ref_5
  - *ref_6
sendMessages:
  - *ref_7
receiveMessages:
  - *ref_8
  - *ref_9
  - *ref_10
  - *ref_11
extensions:
  - id: x-parser-unique-object-id
    value: ohlcv
securitySchemes:
  - id: ApiKeyQuery
    name: api_key
    type: httpApiKey
    description: >-
      `?api_key=ohlcv_live_sk_…` appended to the connection URL.
      Browser-friendly; the only option in environments where you can't set
      headers.
    in: query
    extensions: []

````