# GET /v1/hl/levels

> Hyperliquid take-profit, stop-loss and liquidation level runs of one coin over a time range.

Returns the measured take-profit, stop-loss and liquidation levels of one Hyperliquid coin as runs: how much USD notional sat in each price bucket, from when to when. Flowscope draws these as the [Hyperliquid heatmaps](/docs/app/liquidations#hyperliquid-maps).

## Request

```text
GET /v1/hl/levels/<COIN>?from=<ms>&to=<ms>
HEAD /v1/hl/levels/<COIN>?from=<ms>&to=<ms>
```

| Parameter | In | Type | Description |
|---|---|---|---|
| `COIN` | Path | string | A coin from [`/v1/hl/coins`](/docs/api/hl-coins), case-sensitive (`BTC`, `kPEPE`) |
| `from` | Query | integer, ms | Start of the range, inclusive |
| `to` | Query | integer, ms | End of the range, exclusive |

Rules for the range:

- `to` must be greater than `from`.
- The range may span at most 60 days.
- A missing or unparsable `to` defaults to a far-future sentinel, and a missing `from` to one day before `to`. Always pass both: `from` alone makes the range longer than 60 days and is rejected.

## Response

| Header | Value |
|---|---|
| `Content-Type` | `application/octet-stream` |
| `Cache-Control` | `no-cache` |

The body is one **levels payload** in the [positions codec](/docs/api/positions-codec#levels-payload): a header with the coin, the bucket size, the newest sample time and the range answered, then the runs, then a checksum.

What the payload contains:

- Every run that **overlaps** the range: `start < to` and `start + duration > from`.
- Runs sorted by their end time.
- Runs still open are included, ending at the newest sample (`latest`).
- The payload's `from` is the requested `from`, or later when nothing is recorded before that point. Use it to tell "no levels" from "not recorded yet".

Each run has a kind:

| Kind | Value | Meaning |
|---|---|---|
| Long TP | 0 | Take-profit of a long: a sell above the price |
| Short TP | 1 | Take-profit of a short: a buy below the price |
| Long SL | 2 | Stop of a long: a sell below the price |
| Short SL | 3 | Stop of a short: a buy above the price |
| Long liquidation | 4 | Liquidation of a long, below the price |
| Short liquidation | 5 | Liquidation of a short, above the price |

A run's bucket covers prices `[bucket × bucket_px, (bucket + 1) × bucket_px)`, and its `notional` is the total USD of the orders or positions in it.

## Status codes

| Status | Body | When |
|---|---|---|
| `200` | Levels payload | The coin has samples |
| `400` | `bad range` | `to <= from`, or the range is longer than 60 days |
| `404` | `coin not recorded` | The coin is not in `--hl-coins` |
| `404` | `hyperliquid positions not recorded` | Hyperliquid recording is off |
| `503` | `no sample yet` | The coin is recorded but has not been sampled yet |

## Example

The last hour of BTC levels:

```bash
TO=$(( $(date +%s) * 1000 ))
FROM=$(( TO - 3600000 ))
curl -fsS -o btc.levels "http://127.0.0.1:8787/v1/hl/levels/BTC?from=$FROM&to=$TO"
ls -l btc.levels
```

An hour is typically about 12 kB.

The start of the file, annotated:

```text
46 53 50 4c                 "FSPL"
01 00                       version 1
00 00                       reserved
00 00 00 00 00 00 34 40     bucket_px = 20.0
...                         latest, from, to (i64 each)
03 42 54 43                 coin: length 3, "BTC"
...                         run count (u32), runs (21 bytes each), checksum
```

### Decoding in Rust

The `flowscope-engine` crate has the decoder:

```rust
use flowscope_engine::positions::codec::decode_levels;

let bytes = std::fs::read("btc.levels")?;
let p = decode_levels(&bytes)?;
println!("{} bucket {} from {} to {} latest {}", p.coin, p.bucket_px, p.from, p.to, p.latest);
for r in &p.runs {
    let lo = f64::from(r.bucket) * p.bucket_px;
    println!("{:?} {lo}..{} ${:.0} from {} for {} ms", r.kind, lo + p.bucket_px, r.notional, r.start, r.dur);
}
```

In any other language, follow the byte layout in [Positions codec](/docs/api/positions-codec#levels-payload).

## How answers are built

Runs that ended before what memory holds completely come from the coin's `.levels` day files; the rest come from memory. Payloads in the day files with a different bucket size are skipped, so every answer uses one grid.

Levels are sampled every 10 seconds. A Flowscope client loads the last 3 hours, then asks for the tail every 10 seconds and for older history six hours at a time.

## Related

- [`/v1/hl/footprint`](/docs/api/hl-footprint) returns the positioning footprints of the same coin.
- [Recorder: Hyperliquid](/docs/recorder/hyperliquid#levels) describes how levels are sampled.
