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

# Real-Time Token OHLCV Candle Streaming

> Stream sealed token candles over SSE at 1s, 5s or 60s, resume a dropped connection with Last-Event-ID, and backfill the last 15 minutes. All 35 chains. Pro plan.

One token, one interval, one candle pushed the moment its bucket closes. Open, high, low, close, average, USD volume and a swap count arrive already aggregated, so a chart stays current without you keeping a tick buffer of your own.

This feed also remembers. A dropped connection resumes where it left off, and a fresh one can start with the last fifteen minutes already on screen.

This page is the working reference: how a candle is built, how to subscribe, what resume and backfill really do, what it costs, and the four behaviours that will surprise you. Parameters and schemas are at [GET /sse/ohlcv](/streaming/stream-real-time-ohlcv-for-a-single-asset).

<Warning>
  **This feed is Pro only, and a free key does not open it.** Every other stream either runs keyless or opens with a free key. This one answers `403` with `{"message":"this endpoint requires a Pro plan"}` to anything less, on a connection you expected to stay open.

  Pro also means a different host: `streaming-pro.dexpaprika.com`, never `streaming.`. Plans are on [pricing](https://dexpaprika.com/api/pricing), the migration is in [upgrading to Pro](/api-pro/upgrading), and your key lives in [console.dexpaprika.com](https://console.dexpaprika.com).
</Warning>

## How a candle is built

Worth reading before the cost section, because these rules are the cost.

A bucket opens when the first swap in that interval touches the token, and it collects every swap until the interval ends. Shortly after it ends the bucket is sealed and sent.

| Interval | Bucket    | Sealed after | Accepts late swaps for |
| -------- | --------- | ------------ | ---------------------- |
| `1s`     | 1 second  | 1 second     | 2 minutes              |
| `5s`     | 5 seconds | 2 seconds    | 5 minutes              |
| `60s`    | 1 minute  | 3 seconds    | 10 minutes             |

Three consequences follow, and each one shows up in real consumers.

**A quiet interval produces nothing at all.** No swap means no bucket, and no bucket means no event. A token that trades twice a minute sends two candles a minute on the `1s` stream, not sixty. You are never charged for silence.

**A candle can be sent more than once.** When a swap arrives after its bucket was already sealed, within the late window above, the bucket is recomputed and republished under **the same `timestamp`** with updated values. This is correct behaviour on chains that deliver blocks out of order, and it means your store must be keyed on `timestamp` and overwrite. A consumer that appends will draw the same minute twice.

**`interval` is a cost lever before it is a resolution choice.** The same token on `1s` and on `60s` delivers the same trades; only the bucketing differs. See [what it costs](#what-it-costs).

All **35 chains we index stream candles**, with no separate coverage list to check. The set matches [GET /networks](/api-reference/networks/get-a-list-of-available-blockchain-networks) exactly.

## Subscribe

`Authorization` carries the key as the **entire** header value, with no scheme word in front of it.

<CodeGroup>
  ```bash Live candles theme={null}
  curl -N -H "Authorization: $DEXPAPRIKA_API_KEY" \
    "https://streaming-pro.dexpaprika.com/sse/ohlcv?method=token_ohlcv&chain=ethereum&address=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&interval=60s"
  ```

  ```bash With the last 5 minutes first theme={null}
  curl -N -H "Authorization: $DEXPAPRIKA_API_KEY" \
    "https://streaming-pro.dexpaprika.com/sse/ohlcv?method=token_ohlcv&chain=ethereum&address=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&interval=60s&since=$(( $(date +%s) - 300 ))"
  ```

  ```bash Sample 20 events and stop theme={null}
  curl -N -H "Authorization: $DEXPAPRIKA_API_KEY" \
    "https://streaming-pro.dexpaprika.com/sse/ohlcv?method=token_ohlcv&chain=solana&address=So11111111111111111111111111111111111111112&interval=1s&limit=20"
  ```
</CodeGroup>

`method` is `token_ohlcv` and `address` is a token address. `interval` defaults to `1s`, which is the most expensive choice, so set it deliberately. `request_id` is echoed on every event if you send one.

One subscription per connection here, unlike prices, reserves and transactions, which take up to 25 in a POST body. Watching ten tokens means ten connections, and **10 concurrent streams per IP** is the ceiling.

## What an event looks like

```
event: token_ohlcv
id: 1757930400
data: {"chain":"ethereum","token_id":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","interval":"60s","timestamp":"2026-09-15T08:40:00Z","open":0.9998,"high":1.0003,"low":0.9996,"close":1.0001,"avg":1.0000,"volume_usd":15230.50,"txns":12}
```

| Field           | Type    | Meaning                                                                        |
| --------------- | ------- | ------------------------------------------------------------------------------ |
| `chain`         | string  | Network id                                                                     |
| `token_id`      | string  | Token address, as you passed it                                                |
| `interval`      | string  | `1s`, `5s` or `60s`, echoing your subscription                                 |
| `timestamp`     | string  | ISO 8601, UTC, the **start** of the bucket. This is the identity of the candle |
| `open`, `close` | number  | First and last sample price in USD, ordered by block time rather than arrival  |
| `high`, `low`   | number  | Extremes in USD over the bucket                                                |
| `avg`           | number  | Mean of the price samples. **Not volume weighted**, see below                  |
| `volume_usd`    | number  | Total USD volume in the bucket                                                 |
| `txns`          | integer | Number of swaps in the bucket                                                  |

The `id:` line is the candle's `timestamp` as unix seconds. Browsers track it automatically and replay it as `Last-Event-ID` on reconnect, which is what makes resume work with no code on your side.

<Note>
  **`avg` is an unweighted mean.** It averages the price samples in the bucket, so a hundred-dollar swap and a hundred-thousand-dollar swap move it by the same amount. When you want a volume weighted figure, derive it from `volume_usd` and the underlying trades on [the transactions feed](/streaming/transactions-streaming) instead of reading `avg`.
</Note>

## Resume and backfill

Two parameters put history in front of the live tail, and they behave differently on the edge cases.

|                       | `since` (query)               | `Last-Event-ID` (header)                      |
| --------------------- | ----------------------------- | --------------------------------------------- |
| Value                 | Unix seconds                  | Unix seconds, normally the last `id:` you saw |
| Older than 15 minutes | `400`, the request is refused | **Clamped silently** to 15 minutes ago        |
| In the future         | `400`                         | Ignored, you get live updates only            |
| Unparseable           | `400`                         | Ignored, you get live updates only            |
| Precedence            | Wins                          | Used only when `since` is absent              |

The two-column split is deliberate. `since` is something you wrote, so a mistake is worth an error. `Last-Event-ID` is sent by a browser after an outage of unknown length, and an outage longer than the window should still reconnect rather than fail.

Three things to plan around:

**The boundary is inclusive.** Resuming at the last `id:` you saw redelivers that candle. Harmless if you overwrite by `timestamp`, which the republish rule already requires; pass `since` one second later if you would rather not pay for it.

**Backfill counts toward `limit`.** `limit` caps total events, history included, so `since=<5 minutes ago>&limit=10` can close the connection on backfill alone and never reach a live candle.

**Fifteen minutes is the whole memory.** Anything older is a REST query, not a stream parameter. Sizing a longer history belongs in [plan your credit usage](/knowledge-base/credit-usage).

## What it costs

[Each delivered event costs one credit](/knowledge-base/credit-usage). Heartbeats, warnings and error frames are free, so an idle connection costs nothing.

Because a candle needs at least one swap, your bill is bounded twice over:

```
candles per hour  <=  min( 3600 / interval_seconds , swaps per hour )
```

The left term is the ceiling you are buying into:

| Interval | Ceiling per minute | Ceiling per 30 days |
| -------- | ------------------ | ------------------- |
| `1s`     | 60                 | 2,592,000           |
| `5s`     | 12                 | 518,400             |
| `60s`    | 1                  | 43,200              |

A continuously trading major asset approaches the left term, so **one `1s` subscription is the single most expensive thing in the streaming API**, sixty times a `60s` subscription on the same token for the same trades. A long-tail token never gets near it, and the right term governs: forty swaps an hour is forty candles an hour on any interval.

Republished candles are billed again, so a chain with frequent late blocks costs slightly more than the ceiling arithmetic suggests.

<Tip>
  Measure before you commit. `&limit=100` on your real token and interval, timed, gives you the rate; the rate gives you the month. It takes a minute and beats any estimate on this page. Your included credits and the overage rate are on [pricing](https://dexpaprika.com/api/pricing), and usage is in [console.dexpaprika.com](https://console.dexpaprika.com).
</Tip>

## Error codes

These arrive as an HTTP status with a JSON body, before the stream opens.

| Status | Body                                                                                 | Cause                                                                                | Fix                                                                                                 |
| ------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `403`  | `{"message":"this endpoint requires a Pro plan"}`                                    | Keyless or a free key                                                                | [Upgrade](https://dexpaprika.com/api/pricing), then move to `streaming-pro.`                        |
| `403`  | Cloudflare HTML block page                                                           | On a `-pro` host, no `Authorization` header, or a value that is not a DexPaprika key | Send the key itself. A missing or malformed header is stopped at the edge and never reaches the API |
| `401`  | `{"message":"api key verification has failed"}`                                      | Key present and rejected                                                             | Check for a truncated paste                                                                         |
| `400`  | `{"message":"invalid query parameters: interval (must be one of 1s, 5s, 60s)"}`      | Unsupported interval                                                                 | Only those three exist                                                                              |
| `400`  | `{"message":"invalid query parameters: method (is required)"}`                       | Missing `method`                                                                     | Send `method=token_ohlcv`                                                                           |
| `400`  | `{"message":"unsupported chain: X (is not supported)"}`                              | Unknown `chain`                                                                      | Use an id from [GET /networks](/api-reference/networks/get-a-list-of-available-blockchain-networks) |
| `400`  | `{"message":"invalid query parameters: since (must be within the last 15 minutes)"}` | Backfill too far back                                                                | Clamp your own value, or drop it and use `Last-Event-ID`                                            |
| `404`  | `{"message":"token not found: ethereum/0x..."}`                                      | Address not indexed on that chain                                                    | Check the address, and that it matches the chain                                                    |
| `429`  | `{"message":"ip stream limit exceeded"}`                                             | More than 10 concurrent streams from one IP                                          | Close idle connections                                                                              |

<Note>
  **`403` twice means two different things.** A JSON body is our API telling you the plan is wrong. An unbranded Cloudflare page means the request never reached us: on a `-pro` host the edge only forwards requests whose `Authorization` value is a DexPaprika key, so the usual cause is an unset environment variable leaving the header empty. Recovery is immediate on the next request with a correct header.
</Note>

## In-stream events

Once the stream is open, HTTP status no longer applies. Branch on the event name.

| Event         | Billed | Meaning                                         |
| ------------- | ------ | ----------------------------------------------- |
| `token_ohlcv` | yes    | A sealed candle, live or backfilled             |
| `ping`        | no     | Heartbeat, `{"time":<unix>}`, every 15 seconds  |
| `warning`     | no     | Non-fatal notice, the connection stays open     |
| `error`       | no     | Terminal failure, sent immediately before close |

A handler that parses every `data:` line will try to read `{"time":1757930400}` as a candle.

## Good practices

**Key your store on `timestamp` and overwrite.** This is the one rule that separates a correct consumer from a plausible one, because republished candles and an inclusive resume boundary both deliver a timestamp you already hold.

**Trust the heartbeat, not the socket.** A dead TCP connection looks exactly like a quiet token, and on this feed a quiet token is the normal state. Pings arrive every 15 seconds; treat 45 seconds of silence as a disconnect.

**Let the `id:` line do the resuming.** With `EventSource` you get it for free. Writing your own client, record the last `id:` and send it back as `Last-Event-ID`, rather than computing a `since` and having to handle the stricter validation yourself.

**Reconnect with backoff and jitter.** A retry storm turns one rate limit into a persistent one. Start at a second, double to a minute.

**Pick the widest interval the product can live with.** Sixty seconds of resolution for one sixtieth of the spend is the largest saving available on this endpoint, and most charts cannot render faster than the eye anyway.

**Treat the feed as a live tail.** Fifteen minutes is all it remembers. Anything longer lives in your own store, written as candles arrive.

## A working consumer

Handles resume, the republish rule, the heartbeat and backoff.

```js theme={null}
// candle-tail.mjs: Node 18+, no dependencies. Needs DEXPAPRIKA_API_KEY.
const KEY = process.env.DEXPAPRIKA_API_KEY;
const CHAIN = "ethereum";
const TOKEN = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const INTERVAL = "60s"; // 1s costs 60x this for the same trades

// Candles are keyed by timestamp and OVERWRITTEN, never appended: a late swap
// republishes a sealed candle under the same timestamp with new values, and
// resuming redelivers the candle at the resume point.
const candles = new Map();

let lastEventId = null;
let lastBeat = Date.now();

async function tail() {
  const url = new URL("https://streaming-pro.dexpaprika.com/sse/ohlcv");
  url.searchParams.set("method", "token_ohlcv");
  url.searchParams.set("chain", CHAIN);
  url.searchParams.set("address", TOKEN);
  url.searchParams.set("interval", INTERVAL);

  const headers = {
    // The key is the whole Authorization value, with no scheme word in front of it.
    authorization: KEY,
    accept: "text/event-stream",
  };
  // Older than 15 minutes is clamped rather than refused, so this is always safe
  // to send. The same value as `since` would be a 400.
  if (lastEventId) headers["last-event-id"] = lastEventId;

  const res = await fetch(url, { headers });
  if (!res.ok) {
    // 403 = plan or missing header, 401 = key rejected, 400 = bad parameters.
    // None of them get better by retrying unchanged.
    throw new Error(`${res.status} ${await res.text()}`);
  }

  const decoder = new TextDecoder();
  let buffer = "";

  for await (const chunk of res.body) {
    buffer += decoder.decode(chunk, { stream: true });
    const frames = buffer.split("\n\n");
    buffer = frames.pop() ?? "";

    for (const frame of frames) {
      const name = frame.match(/^event: (.+)$/m)?.[1];
      const data = frame.match(/^data: (.+)$/m)?.[1];
      const id = frame.match(/^id: (.+)$/m)?.[1];
      if (!name || !data) continue;

      if (name === "ping") { lastBeat = Date.now(); continue; }
      if (name === "warning") { console.warn("[warning]", data); continue; }
      if (name === "error") { throw new Error(`stream error: ${data}`); }
      if (name !== "token_ohlcv") continue;

      const c = JSON.parse(data);
      const known = candles.has(c.timestamp);
      candles.set(c.timestamp, c); // overwrite, do not append
      if (id) lastEventId = id;

      console.log(
        `${known ? "revised" : "sealed "} ${c.timestamp}  ` +
          `o ${c.open}  h ${c.high}  l ${c.low}  c ${c.close}  ` +
          `$${c.volume_usd.toFixed(2)} in ${c.txns} swaps`,
      );
    }
  }
}

// Pings every 15s, so 45s of silence means the socket is gone. A token with no
// trades is silent too, which is exactly why the heartbeat is the liveness test.
setInterval(() => {
  if (Date.now() - lastBeat > 45_000) {
    console.error("no heartbeat, exiting for the supervisor to restart");
    process.exit(1);
  }
}, 5_000);

let delay = 1_000;
for (;;) {
  try {
    await tail();
    delay = 1_000;
  } catch (err) {
    console.error(String(err));
    if (/^4(0[0134])/.test(String(err))) process.exit(1); // fix the request, do not retry
    await new Promise((r) => setTimeout(r, delay + Math.random() * 500));
    delay = Math.min(delay * 2, 60_000);
  }
}
```

## FAQs

<AccordionGroup>
  <Accordion title="Can I stream candles without a Pro plan?">
    No. `/sse/ohlcv` answers `403` to keyless callers and to free keys alike, and it is the only stream where registering a free key does not help. Token prices, pool reserves and swap transactions all have keyless or free-key paths; see [streaming overview](/streaming/introduction).
  </Accordion>

  <Accordion title="Why did I receive the same candle twice?">
    Two reasons, both expected. A swap that lands after its bucket was sealed causes the candle to be recomputed and republished under the same `timestamp`. Separately, resuming with `Last-Event-ID` or `since` is inclusive of that second, so the candle at the resume point is redelivered. Key your store on `timestamp` and overwrite.
  </Accordion>

  <Accordion title="How far back can the stream backfill?">
    Fifteen minutes. `since` older than that is refused with a `400`; a `Last-Event-ID` older than that is clamped to the window rather than refused, so a client returning from a long outage still reconnects. Longer history is a REST query.
  </Accordion>

  <Accordion title="Does a 1s stream really cost sixty times a 60s stream?">
    Up to. Both carry the same trades, so the multiplier only reaches sixty on a token that trades in every single second. On a quieter token the candle count is bounded by the number of swaps instead, and the two intervals converge. [Plan your credit usage](/knowledge-base/credit-usage) works through the arithmetic.
  </Accordion>

  <Accordion title="Which chains stream candles?">
    All 35 that DexPaprika indexes. The set matches [GET /networks](/api-reference/networks/get-a-list-of-available-blockchain-networks) exactly, so there is no separate coverage list to check.
  </Accordion>
</AccordionGroup>

## Where to go next

<CardGroup cols={2}>
  <Card title="Endpoint reference" icon="code" href="/streaming/stream-real-time-ohlcv-for-a-single-asset">
    Every parameter, schema and event type.
  </Card>

  <Card title="Plan your credit usage" icon="calculator" href="/knowledge-base/credit-usage">
    What a continuous stream costs, with worked models.
  </Card>

  <Card title="Upgrading to Pro" icon="arrow-up-right" href="/api-pro/upgrading">
    Moving a client to the paid hosts, and the one check that confirms it worked.
  </Card>

  <Card title="Swap transactions" icon="arrow-right-arrow-left" href="/streaming/transactions-streaming">
    The individual trades these candles are built from.
  </Card>
</CardGroup>
