Guides
Backfill the Full Hyperliquid Candle Archive cover

Backfill the Full Hyperliquid Candle Archive

Pull the full Hyperliquid candle archive for one market and one interval from Dwellir's index endpoint with a resumable Python exporter.

LanguagePython
FormatCSV
ProtocolREST

If you need the full candle archive for one Hyperliquid market, the contract is straightforward: request one candle at a time and iterate the bucket-open timestamps yourself.

That is the intended archival path on api-hyperliquid-index.n.dwellir.com today. There is no separate bulk history route, and range-style query params do not change the response shape. In this guide, we show you how to build a resumable Python exporter that discovers the endpoint with the Dwellir CLI, steps through the archive one bucket at a time, skips empty gaps, and writes the results to CSV.

Copy-Paste Prompt for Your Coding Tool

Paste this into Claude Code, Codex, Cursor, Windsurf, or another coding agent:

Text
Build me a resumable Python script that backfills the full Hyperliquid candle archive from Dwellir for one market and one interval at a time.

Requirements:
- First check whether the `dwellir` CLI is installed.
- If it is not installed, recommend installing it with one of these options:
  - `brew tap dwellir-public/homebrew-tap && brew install dwellir`
  - `curl -fsSL https://raw.githubusercontent.com/dwellir-public/cli/main/scripts/install.sh | sh`
- After installation, ask me to authenticate it by running `dwellir auth login`.
- Check authentication with `dwellir auth status`.
- Get an enabled API key with `dwellir keys list --toon`. If there are multiple good candidates, ask me which key to use.
- Discover the Hyperliquid Index endpoint with `dwellir endpoints search hyperliquid --ecosystem hyperliquid --network mainnet --key <name> --toon`.
- Use the Dwellir Hyperliquid REST candles endpoint at `api-hyperliquid-index.n.dwellir.com`.
- Export one market and one interval at a time.
- Treat the data as sparse: if a bucket returns 404, skip it rather than fabricating a candle.
- The OHLCV archive starts at `2025-07-27T08:00:00Z`.
- Iterate bucket-open timestamps between a start and end time.
- Support `1s`, `1m`, and `5m`.
- Write a CSV with columns: market, interval, bucket_start, open, high, low, close, volume, trades_count, vwap.
- Persist resume state to a sidecar file so the export can continue after an interruption.
- Make the resume logic safe if the process stops between writing a CSV row and updating the state file.
- Write the state file atomically so a partial write does not break the next run.
- Show me how to run a short validation window first, then how to run a larger backfill for BTC `1m`.

What You Will Learn

  • discover the Hyperliquid Index endpoint and your API key name with the Dwellir CLI
  • confirm the single-candle REST contract before starting a long backfill
  • export one market and one interval into a resumable CSV dataset
  • handle sparse 404 buckets without fabricating candles
  • resume a large archive download after an interruption

Prerequisites

  • Python 3.10+
  • the dwellir CLI installed and authenticated
  • the requests package
  • a Dwellir API key name you can use with dwellir endpoints search ... --key <name>

Check your setup:

Bash
python --version
dwellir auth status
dwellir keys list --toon
pip install requests

If you do not have the CLI yet, install it with one of these:

Bash
brew tap dwellir-public/homebrew-tap
brew install dwellir
Bash
curl -fsSL https://raw.githubusercontent.com/dwellir-public/cli/main/scripts/install.sh | sh

Discover the Endpoint and Verify the Contract

Start by listing your available keys, then ask the CLI to inject the key you want into the Hyperliquid Index endpoint URL:

Bash
dwellir keys list --toon
dwellir endpoints search hyperliquid --ecosystem hyperliquid --network mainnet --key YOUR_KEY_NAME --toon

You should see a Hyperliquid HyperCore Index entry with an HTTPS URL like:

Text
https://api-hyperliquid-index.n.dwellir.com/YOUR_API_KEY

Now verify the REST contract with a single known candle:

Bash
curl "https://api-hyperliquid-index.n.dwellir.com/YOUR_API_KEY/v1/candles?market=BTC&interval=1m&time=2026-03-30T23:00:00Z"

You should get one candle back:

JSON
{
  "market": "BTC",
  "interval": "1m",
  "bucket_start": "2026-03-30T23:00:00Z",
  "open": "66745",
  "high": "66746",
  "low": "66687",
  "close": "66687",
  "volume": "4.78586",
  "trades_count": 256,
  "vwap": "66733.634306059987321311"
}

Important: the current route is still one candle per response even if you add range-like params such as limit, bars, after, before, start, or end. For archival retrieval, iterate bucket-open timestamps and issue one request per bucket.

Choose the Time Window You Want to Backfill

The exporter in this guide works on one market and one interval at a time.

Use these rules when you pick your window:

  • the intended archive floor is 2025-07-27T08:00:00Z
  • 1s data must be aligned to exact seconds
  • 1m data must be aligned to exact minutes
  • 5m data must be aligned to 00, 05, 10, and so on
  • candles are sparse, so some buckets will return 404 and should be skipped

For rough planning, 30 days of one market is approximately:

IntervalTheoretical buckets in 30 days
1s2,592,000
1m43,200
5m8,640

Build a Resumable Backfill Script

Save this as backfill_hyperliquid_ohlcv.py:

Python
import argparse
import csv
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path

import requests


STEP_BY_INTERVAL = {
    "1s": timedelta(seconds=1),
    "1m": timedelta(minutes=1),
    "5m": timedelta(minutes=5),
}


def parse_utc(value: str) -> datetime:
    return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)


def format_utc(value: datetime) -> str:
    return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


def load_state(state_path: Path):
    if not state_path.exists():
        return None
    try:
        return json.loads(state_path.read_text())
    except json.JSONDecodeError:
        print(f"Ignoring malformed state file at {state_path}")
        return None


def save_state(state_path: Path, payload):
    temp_path = state_path.with_name(state_path.name + ".tmp")
    temp_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
    temp_path.replace(state_path)


def fetch_candle(session: requests.Session, api_key: str, market: str, interval: str, bucket_start: datetime):
    response = session.get(
        f"https://api-hyperliquid-index.n.dwellir.com/{api_key}/v1/candles",
        params={
            "market": market,
            "interval": interval,
            "time": format_utc(bucket_start),
        },
        timeout=30,
    )

    if response.status_code == 404:
        return None

    response.raise_for_status()
    return response.json()


def ensure_csv_header(output_path: Path):
    if output_path.exists() and output_path.stat().st_size > 0:
        return

    with output_path.open("w", newline="") as handle:
        writer = csv.DictWriter(
            handle,
            fieldnames=[
                "market",
                "interval",
                "bucket_start",
                "open",
                "high",
                "low",
                "close",
                "volume",
                "trades_count",
                "vwap",
            ],
        )
        writer.writeheader()


def read_last_bucket_start(output_path: Path):
    if not output_path.exists() or output_path.stat().st_size == 0:
        return None

    with output_path.open(newline="") as handle:
        rows = csv.DictReader(handle)
        last_row = None
        for row in rows:
            last_row = row

    if last_row is None:
        return None

    return last_row["bucket_start"]


def append_row(output_path: Path, candle):
    with output_path.open("a", newline="") as handle:
        writer = csv.DictWriter(
            handle,
            fieldnames=[
                "market",
                "interval",
                "bucket_start",
                "open",
                "high",
                "low",
                "close",
                "volume",
                "trades_count",
                "vwap",
            ],
        )
        writer.writerow(candle)


def backfill(api_key: str, market: str, interval: str, start: str, end: str, output_path: Path):
    if interval not in STEP_BY_INTERVAL:
        raise ValueError(f"unsupported interval: {interval}")

    start_dt = parse_utc(start)
    end_dt = parse_utc(end)
    step = STEP_BY_INTERVAL[interval]
    state_path = output_path.with_suffix(output_path.suffix + ".state.json")
    state = load_state(state_path)

    current = start_dt
    fetched = 0
    skipped = 0

    ensure_csv_header(output_path)
    last_bucket_start = read_last_bucket_start(output_path)

    if state:
        current = parse_utc(state["next_time"])
        fetched = state["fetched"]
        skipped = state["skipped"]
        print(f"Resuming from {format_utc(current)}")

    if last_bucket_start:
        last_written_time = parse_utc(last_bucket_start) + step
        if last_written_time > current:
            current = last_written_time
            print(f"Continuing after last written candle at {last_bucket_start}")

    with requests.Session() as session:
        while current <= end_dt:
            candle = fetch_candle(session, api_key, market, interval, current)

            if candle is None:
                skipped += 1
            else:
                append_row(output_path, candle)
                fetched += 1

            next_time = current + step
            save_state(
                state_path,
                {
                    "market": market,
                    "interval": interval,
                    "next_time": format_utc(next_time),
                    "fetched": fetched,
                    "skipped": skipped,
                    "output_path": str(output_path),
                },
            )

            processed = fetched + skipped
            if processed % 100 == 0 or current == end_dt:
                print(
                    f"processed={processed} fetched={fetched} skipped={skipped} current={format_utc(current)}"
                )

            current = next_time

    save_state(
        state_path,
        {
            "market": market,
            "interval": interval,
            "next_time": format_utc(current),
            "fetched": fetched,
            "skipped": skipped,
            "output_path": str(output_path),
            "completed": True,
        },
    )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("api_key")
    parser.add_argument("market")
    parser.add_argument("interval", choices=sorted(STEP_BY_INTERVAL))
    parser.add_argument("start")
    parser.add_argument("end")
    parser.add_argument("output_path")
    args = parser.parse_args()

    backfill(
        api_key=args.api_key,
        market=args.market,
        interval=args.interval,
        start=args.start,
        end=args.end,
        output_path=Path(args.output_path),
    )


if __name__ == "__main__":
    main()

This script does four things that matter for archive retrieval:

  • writes each returned candle directly to CSV
  • treats 404 as an empty sparse bucket
  • keeps a sidecar state file so you can resume long runs without replaying the last written candle
  • writes the state file atomically and ignores a malformed checkpoint if a previous write was interrupted
  • logs progress every 100 processed buckets and at the end of the run

Run a Short Validation Window First

Before you launch a long archive job, validate the script on a bounded window:

Bash
python backfill_hyperliquid_ohlcv.py \
  YOUR_API_KEY \
  BTC \
  1m \
  2026-03-30T22:56:00Z \
  2026-03-30T23:00:00Z \
  btc-1m-validation.csv

You should see output like:

Text
processed=5 fetched=5 skipped=0 current=2026-03-30T23:00:00Z

The resulting CSV should start like this:

csv
market,interval,bucket_start,open,high,low,close,volume,trades_count,vwap
BTC,1m,2026-03-30T22:56:00Z,66737,66738,66731,66735,0.95317,103,66736.472748827628011999
BTC,1m,2026-03-30T22:57:00Z,66735,66748,66724,66747,2.80108,130,66728.220450683464188383
BTC,1m,2026-03-30T22:58:00Z,66748,66755,66747,66754,16.60528,174,66748.61734761472406371

Backfill a Larger Archive

Once the validation window looks correct, extend the time range.

Example: one month of BTC 1m candles:

Bash
python backfill_hyperliquid_ohlcv.py \
  YOUR_API_KEY \
  BTC \
  1m \
  2026-03-01T00:00:00Z \
  2026-03-30T23:59:00Z \
  btc-1m-march-2026.csv

Example: one hour of BTC 1s candles:

Bash
python backfill_hyperliquid_ohlcv.py \
  YOUR_API_KEY \
  BTC \
  1s \
  2026-03-30T23:00:00Z \
  2026-03-30T23:59:59Z \
  btc-1s-2026-03-30T23.csv

In a sampled live check against the endpoint, a 1s minute window returned 50 candles and 10 sparse 404 gaps. That is normal. The archive is sparse by design.

Putting It All Together

The full archival workflow is:

  1. discover your API key name with dwellir keys list --toon
  2. confirm the index endpoint with dwellir endpoints search hyperliquid --ecosystem hyperliquid --network mainnet --key YOUR_KEY_NAME --toon
  3. validate one known candle with curl
  4. run a short backfill window
  5. scale the same script to the full date range you want
  6. resume from the .state.json file if the job stops midway

When the run is complete, you will have:

  • a CSV containing only materialized candles
  • a state file recording the last attempted bucket
  • a repeatable pull process for any supported market slug and interval

Going to Production

For long or repeated archive pulls, tighten the workflow before you automate it:

  • keep the API key in an environment variable or secret manager instead of shell history
  • split very large jobs into date partitions such as daily or weekly windows
  • write outputs to a durable location before merging them downstream
  • treat the CSV as sparse source data and densify later only if your analytics pipeline needs it
  • add retry and backoff logic around transient 5xx or network errors

If you need a typed analytics format after the backfill is complete, convert the CSV to Parquet as a second step instead of changing the retrieval pattern.

Next Steps

This guide used Dwellir's Hyperliquid Index endpoint. To run it against your own markets, get a key from dashboard.dwellir.com.