Cross-exchange footprint
How the footprint chart combines Binance, Bybit, OKX and Hyperliquid perpetual futures depth + trade flow into per-minute bid × ask cells: sources, normalization, temporal alignment, wall classification, edge cases, limits.
See the live widget at /perpetuals/footprint.
Data sources
Four exchange feeds, public depth streams only. No API keys required.
- Binance USDT-M perpetuals. Diff-stream WebSocket plus a REST snapshot at cold start, both on the futures endpoint. Local sequence-validated L2 book, top 1,000 levels published per asset.
- Bybit V5 linear perpetuals. Public WebSocket,
orderbook.<tier>.<SYMBOL>. Tier 200 for every asset, the deepest tier the linear stream accepts. Snapshot frame on subscribe, monotonicu-incremented deltas, resubscribe forces a fresh snapshot on sequence gaps. - OKX V5 SWAP. Public
bookschannel (400 levels, no auth), all 400 published. Snapshot plus update frames chained via per-entryseqId/prevSeqId. We considered the tick-by-tick variants but they require a VIP fee tier we don't hold; see Limits. - Hyperliquid perpetuals. Public WebSocket
l2Booksubscription. The exchange caps each frame at 20 raw levels, so the daemon opens parallel subscriptions per asset at progressively coarsernSigFigsaggregations. BTC, ETH, SOL, BNB, HYPE and ZEC run three stripes (raw / 4 / 3); XRP adds annSigFigs=2outer stripe for four; DOGE and ENA run five, adding that outer stripe plus annSigFigs=5/mantissa=2stripe that carries its own book. The aggregator merges all stripes into one Hyperliquid contribution at publish time; the UI surfaces it as a single venue. - Tracked assets. BTC, ETH, SOL, BNB, XRP, DOGE, HYPE, ZEC, ENA.
Normalization: prices and quantities
Cross-exchange aggregation only works once every source speaks the same units. Two adjustments happen at ingest, before anything reaches the aggregator.
Quantity to base coin. Binance and Bybit return quantities in the base currency directly (BTC for BTC-USDT perp, etc.). OKX returns size in contracts, where one contract is ctVal units of the base coin. For BTC-USDT-SWAP ctVal = 0.01; for DOGE-USDT-SWAP it's 1000. The OKX feed resolves ctVal from the public instruments endpoint at startup and applies the multiplier on every level before publishing. Without this step, DOGE depth would read 1000× too deep on the cross-exchange view.
Price into per-asset buckets. Each exchange uses a slightly different tick size; the aggregator rounds prices down to the nearest bucket boundary so adjacent ticks across exchanges merge into the same wall. Per-asset primary buckets:
btc: $1.0 eth: $0.10 sol: $0.01 bnb: $0.02 xrp: $0.0002 doge: $0.00002 hype: $0.005 zec: $0.1 ena: $0.00002
One bucket size per asset
Each cycle the raw depth is binned at one bucket size per asset, the primary bucket in the table above. Footprint cells are keyed to the same size, so a wall and the trades printed against it share a row.
For BTC that means a $1 bucket, fine-grained and dense near mid. The same logic scales per asset: ETH bins at $0.10, DOGE at $0.00002.
Aggregation: sum + per-exchange split
For each bucket, the aggregator sums quantities across the four sources and keeps the per-exchange contribution alongside the total. That lets the UI render a single wall row plus a stacked breakdown on hover, without a second round-trip.
for src in (binance, bybit, okx, hyperliquid):
for (price, qty_base) in src.depth.bids:
bucket = floor(price / BUCKET_SIZE) × BUCKET_SIZE
bid_buckets[bucket].total += qty_base
bid_buckets[bucket].by[src] += qty_base
# asks: same, then sort by ascending price
top_bids = sorted(bid_buckets, by price desc)[:600]
top_asks = sorted(ask_buckets, by price asc)[:600]The output payload includes the source list and per-exchange best bid / best ask separately. We do not expose a single consolidated best bid and ask; see Limits.
Cadence and spoof resolution
The aggregator publishes at 10 Hz (one snapshot every ~100 ms). Producer feeds write at up to ~20 Hz, but the binding constraint is exchange push rate (~50-100 ms native).
At 100 ms cell width on the footprint, walls and spoofs land like:
- Spoof ≥ 100 ms → 1 visible cell (boundary detection).
- Spoof ≥ 200 ms → 2 visible cells (reliable).
- Spoof ≥ 500 ms → 5 cells, reads as a clear vertical speck.
- Wall lasting seconds → horizontal smear across many cells.
Sub-100 ms spoofs are not detected. That would require tick-by-tick (TBT) channels, and none of the four venues gives us one at our access level.
Temporal alignment
Each producer extracts the exchange event timestamp from its native depth stream (Binance's E field, Bybit's top-level ts, OKX's per-entry ts, Hyperliquid's time, which is also the field its trade frames stamp) and propagates it alongside the producer's own publish time.
At the aggregator each snapshot exposes the per-source event timestamp and the cross-exchange skew (max minus min of event times across the OK sources). The footprint snapshot tsstays the aggregator's polling moment so the timeline is uniformly spaced; the skew is surfaced in the tooltip when ≥ 100 ms (red ≥ 300 ms) so "consensus" in a single cell is honestly bounded.
Typical observed skew: < 100 ms in steady-state; spikes to 300-500 ms on basis-stress moments where one venue's feed briefly lags. Without this attribution a wall placed on Binance at T+0 and Bybit at T+200 ms would read as a single instantaneous consensus, which it isn't.
Stale handling
Each producer writes its source key with a 60-second TTL plus a timestamp. The aggregator computes age per source on every cycle and excludes any source older than 60 seconds from the union view; the excluded source still appears in the sources array with a stale status so the FE renders a degraded indicator.
On the FE: the source pills in the toolbar show colour + status, and the footprint continues with the remaining sources. A persistent stale state usually means the producer hit a watchdog reset or the WebSocket dropped without a clean reconnect, and the page degrades visibly rather than silently.
Limits and known gotchas
- No consolidated best bid and ask. Cross-exchange basis is normal: BTC perp on Binance often trades $5-15 below or above the same instrument on Bybit and OKX. A naive union top-of-book inverts during these periods (best aggregated bid greater than best aggregated ask). The widget shows aggregated walls plus per-exchange best quotes separately to avoid that pitfall, and surfaces a purple inversion band only when the gap exceeds 0.10 % of mid (configurable via
?inv_bps=Nin the URL). - Depth coverage. The aggregator publishes up to 600 bins per side and none further than 5 % from mid, and the history streams behind wall classification keep only the 60 bins per side nearest mid. Round-number anchors at ±2 % from mid sit well beyond those 60 bins, so walls there are structurally invisible to this methodology.
- Sub-100 ms latency.Aggregator polls at 10 Hz; exchange push rates cap at ~50-100 ms on free public channels. OKX's tick-by-tick variants (
books-l2-tbt,books50-l2-tbt) need a higher trading-fee tier we don't hold; subscription returnscode 64003. Bybit and Binance don't publish a comparable free tick-by-tick channel for futures. - 1,000 levels ≠ deep book.The deepest producer, Binance, feeds up to 1,000 raw levels per side (OKX 400, Bybit 200). On BTC at its $0.10 tick that spans roughly $100 from mid, about 0.15 %, and past that the merged book is Hyperliquid's coarse stripes alone; on calmer pairs the exchange books reach further. Exchange walls deeper in the book are simply not in the feed regardless of bucket size.
- Cross-exchange skew is real. The footprint renders cells at uniform 100 ms intervals, but the underlying four-venue snapshot for each cell can span up to
cross_exchange_skew_msin event time, typically < 100 ms, occasionally 200-500 ms. The tooltip surfaces this when meaningful; treat it as the floor on temporal granularity.
Versioning
Initial release v1.0.0 on 2026-05-08 (4 Hz aggregation, 50 levels). v2.0.0 on 2026-05-09: bumped to 10 Hz, 200 levels, multi-bucket views ($1 + $5 per asset), per-source event-timestamp alignment with cross-exchange skew exposure. v2.1.0 on 2026-05-21: added Hyperliquid as a fourth source via multi-resolution l2Book subscriptions (raw 20-level cap is too short on its own; coarser nSigFigs strips extend reach while keeping native granularity near mid). v3.0.0 on 2026-05-23: rebranded from cross-exchange order-book heatmap methodology to footprint methodology after the M2 heatmap UI was retired. Substrate (sources, normalization, alignment) is unchanged — the footprint init aggregator reads the same `prod:agg:<asset>:history` keys. Multi-bucket variants (`prod:agg:<asset>:b<size>:*`) were dropped at the same time (the heatmap was their only consumer). v3.1.0 on 2026-09-02: venue counts left stale by the Hyperliquid addition corrected (depth coverage, tick-by-tick availability and the per-cell snapshot span all read four venues now, not three), Hyperliquid's trade and book event timestamp named in temporal alignment, and the Hyperliquid stripe table updated with HYPE plus DOGE's own nSigFigs=5 / mantissa=2 stripe. Material changes to the compute path bump the version and update the published date. Cosmetic edits do not.