
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.
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:
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
404buckets without fabricating candles - resume a large archive download after an interruption
Prerequisites
- Python
3.10+ - the
dwellirCLI installed and authenticated - the
requestspackage - a Dwellir API key name you can use with
dwellir endpoints search ... --key <name>
Check your setup:
python --version
dwellir auth status
dwellir keys list --toon
pip install requestsIf you do not have the CLI yet, install it with one of these:
brew tap dwellir-public/homebrew-tap
brew install dwellircurl -fsSL https://raw.githubusercontent.com/dwellir-public/cli/main/scripts/install.sh | shDiscover 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:
dwellir keys list --toon
dwellir endpoints search hyperliquid --ecosystem hyperliquid --network mainnet --key YOUR_KEY_NAME --toonYou should see a Hyperliquid HyperCore Index entry with an HTTPS URL like:
https://api-hyperliquid-index.n.dwellir.com/YOUR_API_KEYNow verify the REST contract with a single known candle:
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:
{
"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 1sdata must be aligned to exact seconds1mdata must be aligned to exact minutes5mdata must be aligned to00,05,10, and so on- candles are sparse, so some buckets will return
404and should be skipped
For rough planning, 30 days of one market is approximately:
| Interval | Theoretical buckets in 30 days |
|---|---|
1s | 2,592,000 |
1m | 43,200 |
5m | 8,640 |
Build a Resumable Backfill Script
Save this as backfill_hyperliquid_ohlcv.py:
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
404as 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
100processed 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:
python backfill_hyperliquid_ohlcv.py \
YOUR_API_KEY \
BTC \
1m \
2026-03-30T22:56:00Z \
2026-03-30T23:00:00Z \
btc-1m-validation.csvYou should see output like:
processed=5 fetched=5 skipped=0 current=2026-03-30T23:00:00ZThe resulting CSV should start like this:
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.61734761472406371Backfill a Larger Archive
Once the validation window looks correct, extend the time range.
Example: one month of BTC 1m candles:
python backfill_hyperliquid_ohlcv.py \
YOUR_API_KEY \
BTC \
1m \
2026-03-01T00:00:00Z \
2026-03-30T23:59:00Z \
btc-1m-march-2026.csvExample: one hour of BTC 1s candles:
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.csvIn 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:
- discover your API key name with
dwellir keys list --toon - confirm the index endpoint with
dwellir endpoints search hyperliquid --ecosystem hyperliquid --network mainnet --key YOUR_KEY_NAME --toon - validate one known candle with
curl - run a short backfill window
- scale the same script to the full date range you want
- resume from the
.state.jsonfile 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
5xxor 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
- For a simpler flat-file walkthrough, use Export Hyperliquid OHLCV Candles to CSV.
- For a columnar export path, use Export Hyperliquid OHLCV Candles to Parquet.
- For the transport contract itself, see Hyperliquid OHLCV REST API.
This guide used Dwellir's Hyperliquid Index endpoint. To run it against your own markets, get a key from dashboard.dwellir.com.