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 200 levels published per asset.
- Bybit V5 linear perpetuals. Public WebSocket,
orderbook.<tier>.<SYMBOL>. Tier 200 where the venue supports it (BTC, ETH, SOL today), tier 50 for the rest. Snapshot frame on subscribe, monotonicu-incremented deltas, resubscribe forces a fresh snapshot on sequence gaps. - OKX V5 SWAP. Public
bookschannel (400 levels, no auth), top 200 retained. 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 (raw / 4 / 3, plusnSigFigs=2for XRP and DOGE: four streams total on those, three on the rest). 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.
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.05 bnb: $0.10 xrp: $0.001 doge: $0.0001
Multi-bucket views
Each cycle the same raw depth is binned into two bucket sizes per asset in parallel: the primary tick (above) and a 5× coarser variant. The user picks which to display; the footprint re-wires to the right bucket without reconnecting.
For BTC that means a $1 view (fine-grained, dense near mid) and a $5 view. With 200 bucket bins per side at $1 the footprint reaches ±$200 from mid; the $5 view reaches ±$1 000, which is where round-number magnets ($79 k / $80 k / $81 k) live. The same logic scales per asset: ETH publishes $0.10 + $0.50, DOGE publishes $0.0001 + $0.001.
Aggregation: sum + per-exchange split
For each bucket, the aggregator sums quantities across the three 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):
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)[:200]
top_asks = sorted(ask_buckets, by price asc)[:200]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 which are tier-locked across all three venues for 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) 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. 200 bucket bins per side per bucket size: ±$200 reach on the $1 BTC view, ±$1 000 reach on the $5 BTC view. Round-number anchors at ±2 % from mid sit beyond what any of the three exchanges return on their public channels. That depth is 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. - 200 levels ≠ deep book. Each producer feeds 200 raw levels. In normal markets that spans roughly $30-50 from mid on BTC at native quote granularity; on calmer pairs it can reach further. 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 three-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). Material changes to the compute path bump the version and update the published date. Cosmetic edits do not.