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

# Crypto price feeds

> crypto_price_tick — the live underlying-asset price per instrument — and crypto_window_opened, the frozen open price of an Up/Down window.

Two pushes serve [crypto markets](/concepts/markets/crypto), both on
the public `/ws/market` gateway:

| Channel             | `ids`                                  | Push                   | Answers                                      |
| ------------------- | -------------------------------------- | ---------------------- | -------------------------------------------- |
| `crypto_price_tick` | one or more `instrumentId` (`BTC-USD`) | `crypto_price_tick`    | "what is the coin worth right now?"          |
| `market_lifecycle`  | one or more market `symbol`s           | `crypto_window_opened` | "what is the price to beat for this window?" |

Both are **display feeds**: they are unsigned, ephemeral, and always
re-confirmed by the read model on the next REST fetch. Never treat a
frame as settlement evidence — the authoritative frozen open is
`cryptoWindow.openPrice` on the market payload.

## `crypto_price_tick`

* Gateway: `/ws/market`
* `ids`: one or more crypto **instrument ids** — `BTC-USD`, `ETH-USD`,
  … (the value on `crypto.instrumentId` /
  `event.cryptoSeries.instrumentId`). Case-insensitive; the ack echoes
  them lower-cased.
* Required scope: none. The market gateway also accepts anonymous
  handshakes, so a signed-out browser can subscribe.
* Pushes: `crypto_price_tick`

```json theme={null}
{ "channel": "crypto_price_tick", "ids": ["BTC-USD", "ETH-USD"] }
```

Keyed by **instrument, not market symbol** — every strike, ladder rung
and window over the same asset shares one feed, so a page showing ten
BTC markets needs exactly one subscription.

```json theme={null}
{
  "type":    "crypto_price_tick",
  "sid":     1,
  "channel": "crypto_price_tick",
  "id":      "btc-usd",
  "serverTs": 1785346775266,
  "data": {
    "instrumentId": "BTC-USD",
    "price":        "63755779816750000000000",
    "decimals":     18,
    "at":           "2026-07-29T17:39:33.000Z",
    "src":          "datastreams"
  }
}
```

| Field          | Meaning                                                                                                                                                     |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `instrumentId` | Catalog id in its canonical casing (`BTC-USD`). The envelope `id` is the lower-cased topic key.                                                             |
| `price`        | **Unscaled integer as a decimal string.** Render as `price / 10 ** decimals`.                                                                               |
| `decimals`     | Exponent for `price` (18 on the Data Streams feed).                                                                                                         |
| `at`           | When the source observed the price, ISO-8601 UTC. **This, not `serverTs`, is the point's x-coordinate.**                                                    |
| `src`          | `"datastreams"` (live spot, \~1/s) or `"candlestick"` (one-minute candle close, \~1/min). Advisory — an unknown producer tag is omitted rather than echoed. |

### How to consume it

1. Fetch the curve once —
   [`GET /api/markets/{symbol}/crypto-price-history?range=10m`](/concepts/markets/crypto#underlying-price-curve).
2. Subscribe to `crypto_price_tick` for that market's `instrumentId`.
3. Append each tick at its own `at`, dropping any tick not newer than
   the last point you hold.

There is **no backfill on subscribe** — the consumer binds to new
messages only, so a reconnecting client is never replayed a stale
backlog. Re-fetch the REST curve after a reconnect instead.

Ticks are keyed by `(instrumentId, at)` and duplicates are possible
(multiple producer replicas relaying the same candle). De-duplicating by
`at` makes them harmless.

<Warning>
  **Silence is not zero.** Ticks flow only while a producer is active for
  that instrument: the live feed runs while an Up/Down window is open on
  the asset, and the candle feed while a long-dated rule's observation
  window is open. An idle instrument simply emits nothing — don't render
  "no data" as a price change, and don't infer a market state from tick
  silence.
</Warning>

The gateway drops ticks that arrive too late to be useful (about 3
minutes for the live feed, 6 for the candle feed) rather than drawing a
stale point on a live chart. The next tick covers the gap.

## `crypto_window_opened`

Fires **once per Up/Down window**, when the oracle freezes the window's
open price — the price the window's close is measured against. The pin
is write-once, so the value in this frame is final.

It rides the per-market `market_lifecycle` channel, so a window's page
needs one subscription for the open pin, the status transitions and the
resolution:

* Gateway: `/ws/market`
* `ids`: one or more market `symbol`s (lower-cased; the ack echoes them
  lower-cased)
* Required scope: none
* Pushes: `crypto_window_opened`, `status_changed`, `market_disputed`,
  `market_resolved`

```json theme={null}
{ "channel": "market_lifecycle", "ids": ["btc15m-20260729t1730"] }
```

```json theme={null}
{
  "type":    "crypto_window_opened",
  "sid":     2,
  "channel": "market_lifecycle",
  "id":      "btc15m-20260729t1745",
  "serverTs": 1785347145030,
  "data": {
    "marketSymbol": "BTC15M-20260729T1745",
    "questionId":   "0xbbf9afc68bb99bf73f7cc5cc333a08de1d14d13fc48c34aecbc27429e2dd8af0",
    "price":        "63879447997060525000000",
    "decimals":     18,
    "observedAt":   "2026-07-29T17:45:00.000Z",
    "frozenAt":     "2026-07-29T17:45:45.000Z",
    "tsMs":         1785347145000
  }
}
```

| Field        | Meaning                                                                                                                                                                                 |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `price`      | Frozen open, unscaled integer as a decimal string — same scaling rule as above. Mirrored on the read model as `cryptoWindow.openPrice`.                                                 |
| `decimals`   | Exponent for `price` (equals `cryptoSeries.streamDecimals`).                                                                                                                            |
| `observedAt` | When the source observed the report the pin was taken from — the window start instant.                                                                                                  |
| `frozenAt`   | The freeze cutoff actually applied (the window start plus the series' collection grace — \~45 s on the shipped 15-minute series, so expect this frame slightly after the window opens). |

A frame missing `price` or `decimals` is dropped by the gateway rather
than forwarded — a blank open would be indistinguishable from "not
frozen yet", which is a different state.

The other frames on this channel are market-scoped lifecycle events:

| `type`            | `data`                                                                                            |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| `status_changed`  | `{ marketSymbol, fromStatus, toStatus, action?, txHash?, tsMs }`                                  |
| `market_disputed` | `{ marketSymbol, questionId, challengedBy?, challengedAt?, proposedOutcomeCode?, txHash?, tsMs }` |
| `market_resolved` | `{ marketSymbol, questionId, outcomeCode?, winnerIndex?, negRiskMarketId?, txHash?, tsMs }`       |

## Worked example

```javascript theme={null}
import WebSocket from 'ws';

const ws = new WebSocket('wss://ws-gateway.dev.predictstreet.sde.adifoundation.ai/ws/market');

ws.on('open', () => {
  ws.send(JSON.stringify({
    id: 1,
    cmd: 'subscribe',
    params: {
      subscriptions: [
        { channel: 'crypto_price_tick', ids: ['BTC-USD'] },
        { channel: 'market_lifecycle',  ids: ['btc15m-20260729t1730'] },
      ],
    },
  }));
});

const scale = (price, decimals) =>
  Number(BigInt(price) / 10n ** BigInt(Math.max(0, decimals - 6))) / 1e6;

ws.on('message', (raw) => {
  const ev = JSON.parse(raw.toString('utf8'));
  switch (ev.type) {
    case 'crypto_price_tick':
      onSpot(ev.data.instrumentId, scale(ev.data.price, ev.data.decimals), ev.data.at);
      break;
    case 'crypto_window_opened':
      onPriceToBeat(ev.data.marketSymbol, scale(ev.data.price, ev.data.decimals));
      break;
    case 'market_resolved':
      refetchMarket(ev.data.marketSymbol);
      break;
  }
});
```

Scale through `BigInt` (or a decimal library) — an 18-decimal integer
does not fit a JS number.
