Skip to main content

oxarchive

PyPI version License: MIT

Python client for 0xArchive market data in notebooks, research scripts, and data pipelines.

0xArchive is granular market data infrastructure for two venues: Hyperliquid and Lighter. Hyperliquid includes core perps, HIP-3 builder perps, HIP-4 outcome markets, and Hyperliquid Spot. HIP-3, HIP-4, and Spot live under the Hyperliquid namespace (/v1/hyperliquid/hip3, client.hyperliquid.hip3, etc.). Lighter has two deployments: mainnet (/v1/lighter, client.lighter) and Robinhood Chain (/v1/rh-lighter, client.rh_lighter). Account positions are available on Hyperliquid, HIP-3, and both Lighter deployments.

Use the Python SDK when the workflow already lives in Python and you want typed REST helpers, async support, WebSocket support, pagination, and reconstruction utilities before moving into a larger pipeline.

Installation

pip install oxarchive

For WebSocket support:

pip install oxarchive[websocket]

Quick Start

from datetime import datetime, timedelta, timezone

from oxarchive import Client

client = Client(api_key="0xa_your_api_key")

# First successful call: Hyperliquid BTC order book
hl_orderbook = client.hyperliquid.orderbook.get("BTC")
print(f"Hyperliquid BTC mid price: {hl_orderbook.mid_price}")

# Lighter uses its own venue client
lighter_orderbook = client.lighter.orderbook.get("BTC")
print(f"Lighter BTC mid price: {lighter_orderbook.mid_price}")

# Lighter on Robinhood Chain: the second Lighter deployment (USDG-quoted)
rh_orderbook = client.rh_lighter.orderbook.get("AAPL-USDG")
print(f"Robinhood Chain AAPL-USDG mid price: {rh_orderbook.mid_price}")

# Hyperliquid HIP-3 builder perps stay under client.hyperliquid.hip3.
# Coins are case-sensitive and carry the builder prefix.
hip3_instruments = client.hyperliquid.hip3.instruments.list()
hip3_orderbook = client.hyperliquid.hip3.orderbook.get("xyz:TSLA")
hip3_trades = client.hyperliquid.hip3.trades.recent("xyz:TSLA", limit=10)
hip3_funding = client.hyperliquid.hip3.funding.current("xyz:TSLA")
hip3_oi = client.hyperliquid.hip3.open_interest.current("xyz:TSLA")
print(f"xyz:TSLA mid: {hip3_orderbook.mid_price}, funding: {hip3_funding.funding_rate}")

# Hyperliquid spot pairs live under client.spot. Symbols are dashed canonical.
spot_pairs = client.spot.pairs.list()
spot_orderbook = client.spot.orderbook.get("HYPE-USDC")
print(f"HYPE-USDC mid: {spot_orderbook.mid_price}")

# Account positions: the largest open BTC position, then everything its wallet holds
largest = client.hyperliquid.positions.market("BTC", limit=1).data[0]
positions = client.hyperliquid.positions.get(largest.user_address)
for p in positions.data.positions:
    print(p.symbol, p.side, p.size, p.unrealized_pnl)

# Order book history for the last hour (every plan reads the most recent 30 days)
now = datetime.now(timezone.utc)
history = client.hyperliquid.orderbook.history(
    "ETH",
    start=now - timedelta(hours=1),
    end=now,
    limit=100,
)
print(f"{len(history.data)} ETH snapshots; more pages: {history.has_more}")

Choose Your Next Path

Need Link
First authenticated route Quick Start
SDK install and route docs SDK docs
Claude Code, ChatGPT Codex, and coding-agent workflows AI Clients
File-based historical pulls Data Catalog
Route contract and machine context OpenAPI, llms.txt
Plans and limits Pricing

Data Coverage

First served instant of each dataset, as client.capabilities() reports it (times are UTC):

Venue Coverage Notes
Hyperliquid Order book from 2023-04-15; trades from 2023-04-15 03:31; funding, open interest and price history from 2023-05-20 02:50; candles from 2025-03-22 12:00; liquidations from 2025-12-22; L4, order history and full-depth L2 from 2026-03-11 01:03 Core perpetuals.
Hyperliquid HIP-3 Price history from 2025-10-13 12:12; trades from 2025-10-13 12:24; candles and liquidations from 2025-12-22; order book from 2026-02-16 16:57; funding and OI from 2026-02-16 17:03; L4, order history and full-depth L2 from 2026-03-11 01:03 Builder perps; funding and OI update at roughly 10 seconds.
Hyperliquid HIP-4 L4 and order history from 2026-05-02 07:47; trades and candles from 2026-05-02 08:00; order book, outcome-side OI and price history from 2026-05-02 16:51 Outcome markets; OI updates at ~10s. No funding or liquidations.
Hyperliquid Spot Candles from 2025-03-22 10:50; trades from 2025-03-22 10:50:22; TWAP from 2026-05-05 13:05; order book from 2026-05-05 19:56; L4 from 2026-05-05 22:57 (PURR-USDC from 2026-03-11 01:03), with REST order history reaching back further Dashed canonical symbols (HYPE-USDC, PURR-USDC); client.spot.pairs.list() lists the pairs. Candle intervals are 1m/5m/15m/30m/1h/4h/1d/1w with a 1,000-row page cap and numeric timestamp-string cursors; pass each next_cursor back unchanged. No funding/OI/liquidations.
Lighter Trades from 2025-01-17 08:43; candles from 2025-08-01; funding, OI and price history from 2025-08-25 15:28; order book from 2026-01-29 02:13; L3 from 2026-03-05 03:33; liquidations from 2026-06-10 Exact starts vary by market. Maker/taker trade context; L3 caps at 250 orders per side; funding/OI update at ~10s.
Lighter on Robinhood Chain Trades and liquidations from 2026-06-26 20:10:26 UTC (venue launch); candles from 2026-06-26 20:10; order book, OI, funding and price history from 2026-08-22 18:43 The second Lighter deployment: USDG-quoted perps (BTC) and spot markets (AAPL-USDG); client.rh_lighter.instruments.list() lists them. No L3.
Account positions Hyperliquid change log from 2025-05-25, HIP-3 from 2025-10-13, hourly history from 2026-06-07; Lighter mainnet from 2025-01-17, Robinhood Chain from 2026-06-26 Live snapshots every 5 minutes (Hyperliquid, HIP-3) or 2 minutes (Lighter). See Account Positions.

client.capabilities() returns the first served instant of every datatype on every venue, and client.symbols.list() the coverage of each symbol, so a market listed later starts later. See Capabilities. Free plans read the most recent 30 days of these datasets.

Async Support

All methods have async versions prefixed with a:

import asyncio
from oxarchive import Client

async def main():
    client = Client(api_key="0xa_your_api_key")

    # Async get (Hyperliquid)
    orderbook = await client.hyperliquid.orderbook.aget("BTC")
    print(f"BTC mid price: {orderbook.mid_price}")

    # Async get (Lighter)
    lighter_ob = await client.lighter.orderbook.aget("BTC")

    # Don't forget to close the client
    await client.aclose()

asyncio.run(main())

Or use as async context manager:

async with Client(api_key="0xa_your_api_key") as client:
    orderbook = await client.hyperliquid.orderbook.aget("BTC")

Configuration

client = Client(
    api_key="0xa_your_api_key",           # Required (or via OXARCHIVE_API_KEY env var)
    base_url="https://api.0xarchive.io", # Optional
    timeout=30.0,                         # Optional, request timeout in seconds (default: 30.0)
)

If api_key is omitted, the SDK falls back to the OXARCHIVE_API_KEY environment variable.

# OXARCHIVE_API_KEY=0xa_your_api_key python script.py
client = Client()

API Version

Every request sends 0xArchive-Version: 2026-10-01 (oxarchive.API_VERSION), and WebSocket connections select the same version. The version fixes the response shapes the SDK parses: every route answers with the {success, data, meta} envelope, record times are RFC 3339 strings with an integer companion in fields ending _ms, errors carry a stable error_code, and Lighter replay messages have the live shapes. The API echoes the version in the 0xArchive-Version response header.

Pagination

Paged methods return a CursorResponse with data, has_more, next_cursor and meta. While has_more is True, pass next_cursor back unchanged as cursor, with the other arguments unchanged. Stop when has_more is False: a page can be short, or even empty, and still not be the last, so stop on has_more rather than on the page size.

from datetime import datetime, timedelta, timezone

end = datetime.now(timezone.utc)
start = end - timedelta(hours=1)

page = client.hyperliquid.trades.history("BTC", start=start, end=end, limit=1000)
trades = list(page.data)
while page.has_more:
    page = client.hyperliquid.trades.history(
        "BTC", start=start, end=end, limit=1000, cursor=page.next_cursor
    )
    trades.extend(page.data)

The iterators (cvd.iterate(), positions.iterate_history() and the other iterate_* helpers) follow the cursor for you and stop when has_more is False.

Response Metadata

CursorResponse.meta is the response's meta: request_id, count, has_more, and on per-symbol routes symbol (the canonical public symbol) and venue ("hyperliquid", "hip3", "hip4", "spot", "lighter" or "rh-lighter"). A record that a method returns on its own, such as an order book, a funding rate, an instrument or a market summary, carries the same meta as response_meta.

book = client.hyperliquid.hip3.orderbook.get("xyz:TSLA")
print(book.response_meta.venue, book.response_meta.symbol)  # hip3 xyz:TSLA

page = client.spot.trades.history("HYPE-USDC", start=start, end=end)
print(page.meta.venue, page.meta.symbol, page.meta.request_id)

Methods that return a raw dict (the full-depth L2, L3 and L4 snapshots) or a plain list do not carry it.

Capabilities

client.capabilities() lists what each venue serves, one Capability per venue and datatype: the REST routes and WebSocket channels, whether the datatype streams live (live) and replays (replay), the first served instant (available_from), its cadence, the largest page (page_limit) and the accepted intervals. The route needs no API key and costs no credits.

rows = client.capabilities()
for row in rows:
    if row.venue == "spot":
        print(row.datatype, row.live, row.replay, row.available_from, row.page_limit)

replayable = {channel for row in rows if row.replay for channel in row.ws_channels}

# Async version
rows = await client.acapabilities()

Per-symbol coverage is on client.symbols.list(). The WebSocket client checks subscriptions and replays against the same table (see Available Channels).

REST API Reference

All examples use client.hyperliquid.* but the same methods are available on client.lighter.* for Lighter data.

The examples read these recent windows, which every plan can reach (Free reads the most recent 30 days, at most 30 days per request):

from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc)
hour_ago = now - timedelta(hours=1)
day_ago = now - timedelta(days=1)
week_ago = now - timedelta(days=7)

Order Book

# Get current order book (Hyperliquid)
orderbook = client.hyperliquid.orderbook.get("BTC")

# Get current order book (Lighter)
orderbook = client.lighter.orderbook.get("BTC")

# Get order book at specific timestamp
historical = client.hyperliquid.orderbook.get("BTC", timestamp=day_ago)

# Get with limited depth
shallow = client.hyperliquid.orderbook.get("BTC", depth=10)

# Get historical snapshots (start and end are required)
history = client.hyperliquid.orderbook.history(
    "BTC",
    start=hour_ago,
    end=now,
    limit=1000,
    depth=20  # Price levels per side
)

# HIP-3 order book (case-sensitive coins)
hip3_ob = client.hyperliquid.hip3.orderbook.get("xyz:TSLA")
hip3_history = client.hyperliquid.hip3.orderbook.history("xyz:TSLA", start=hour_ago, end=now)

# Async versions
orderbook = await client.hyperliquid.orderbook.aget("BTC")
history = await client.hyperliquid.orderbook.ahistory("BTC", start=..., end=...)
hip3_ob = await client.hyperliquid.hip3.orderbook.aget("xyz:TSLA")

Orderbook Depth

The depth parameter is route-specific. Hyperliquid-family native L2 is capped at 20 levels per side. Lighter native L2 includes all served levels, and Lighter L3 is capped at 250 orders per side. depth applies to get() and to history() on every venue (Hyperliquid core, HIP-3, HIP-4, spot and Lighter), and to the full-depth l2_orderbook.get() and l2_orderbook.history().

Note: Hyperliquid native L2 source data contains 20 levels per side. Dedicated L2 routes derived from L4 return all served levels where supported. Lighter native L2 also includes all served levels; Lighter L3 returns up to 250 individual resting orders per side.

Lighter Orderbook Granularity

Lighter order book history (mainnet and Robinhood Chain) supports a granularity parameter for different data resolutions. history() returns snapshots at checkpoint, 30s, 10s and 1s; tick-level data (a checkpoint plus raw deltas) comes from history_tick() and the other methods under Orderbook Reconstruction.

Granularity Interval Credit Multiplier
checkpoint ~60s 1x
30s 30s 2x
10s 10s 3x
1s 1s 10x
tick tick-level 20x
# Get Lighter orderbook history with 10s resolution
history = client.lighter.orderbook.history(
    "BTC",
    start=hour_ago,
    end=now,
    granularity="10s"
)

# Get 1-second resolution
history = client.lighter.orderbook.history(
    "BTC",
    start=hour_ago,
    end=now,
    granularity="1s"
)

# Tick-level data: a checkpoint plus raw deltas
tick_data = client.lighter.orderbook.history_tick(
    "BTC",
    start=hour_ago,
    end=now,
)

Note: The granularity parameter is ignored for Hyperliquid orderbook history.

Orderbook Reconstruction

For tick-level data, the SDK provides client-side orderbook reconstruction. This efficiently reconstructs full orderbook state from a checkpoint and incremental deltas.

from datetime import datetime, timedelta, timezone
from oxarchive import OrderBookReconstructor

# Option 1: Get fully reconstructed snapshots (simplest)
snapshots = client.lighter.orderbook.history_reconstructed(
    "BTC",
    start=datetime.now(timezone.utc) - timedelta(hours=1),
    end=datetime.now(timezone.utc)
)

for ob in snapshots:
    print(f"{ob.timestamp}: bid={ob.bids[0].px} ask={ob.asks[0].px}")

# Option 2: Get raw tick data for custom reconstruction
tick_data = client.lighter.orderbook.history_tick(
    "BTC",
    start=datetime.now(timezone.utc) - timedelta(hours=1),
    end=datetime.now(timezone.utc)
)

print(f"Checkpoint: {len(tick_data.checkpoint.bids)} bids")
print(f"Deltas: {len(tick_data.deltas)} updates")

# Option 3: Auto-paginating iterator (recommended for large time ranges)
# Automatically handles pagination, fetching up to 1,000 deltas per request
for snapshot in client.lighter.orderbook.iterate_tick_history(
    "BTC",
    start=datetime.now(timezone.utc) - timedelta(days=1),  # 24 hours of data
    end=datetime.now(timezone.utc)
):
    print(snapshot.timestamp, "Mid:", snapshot.mid_price)
    if some_condition:
        break  # Early exit supported

# Option 4: Manual iteration (single page, for custom logic)
for snapshot in client.lighter.orderbook.iterate_reconstructed(
    "BTC", start=start, end=end
):
    # Process each snapshot without loading all into memory
    process(snapshot)
    if some_condition:
        break  # Early exit if needed

# Option 5: Get only final state (most efficient)
reconstructor = client.lighter.orderbook.create_reconstructor()
final = reconstructor.reconstruct_final(tick_data.checkpoint, tick_data.deltas)

# Check for sequence gaps
gaps = OrderBookReconstructor.detect_gaps(tick_data.deltas)
if gaps:
    print("Sequence gaps detected:", gaps)

# Async versions available
snapshots = await client.lighter.orderbook.ahistory_reconstructed("BTC", start=..., end=...)
tick_data = await client.lighter.orderbook.ahistory_tick("BTC", start=..., end=...)
# Async auto-paginating iterator
async for snapshot in client.lighter.orderbook.aiterate_tick_history("BTC", start=..., end=...):
    process(snapshot)

Methods:

Method Description
history_tick(coin, ...) Get raw checkpoint + deltas (single page, max 1,000 deltas)
history_reconstructed(coin, ...) Get fully reconstructed snapshots (single page)
iterate_tick_history(coin, ...) Auto-paginating iterator for large time ranges
aiterate_tick_history(coin, ...) Async auto-paginating iterator
iterate_reconstructed(coin, ...) Memory-efficient iterator (single page)
create_reconstructor() Create a reconstructor instance for manual control

Note: The API returns a maximum of 1,000 deltas per request. For time ranges with more deltas, use iterate_tick_history() / aiterate_tick_history() which handle pagination automatically.

Parameters:

Parameter Default Description
depth all Maximum price levels in output
emit_all True If False, only return final state

Trades

trades.history() pages the trade tape of a window with a cursor. trades.list() is the same method under its earlier name and keeps working.

# Get trade history with cursor-based pagination
result = client.hyperliquid.trades.history("ETH", start=hour_ago, end=now, limit=1000)
trades = list(result.data)

# Paginate through all results
while result.has_more:
    result = client.hyperliquid.trades.history(
        "ETH",
        start=hour_ago,
        end=now,
        cursor=result.next_cursor,
        limit=1000
    )
    trades.extend(result.data)

# One side of the tape: side="buy" (side == "B") or side="sell" (side == "A").
# The API filters before paging, so a full page holds `limit` matching trades.
buys = client.hyperliquid.trades.history("ETH", start=hour_ago, end=now, side="buy")

# Get recent trades (HIP-3, HIP-4, spot and Lighter), optionally one side
recent = client.lighter.trades.recent("BTC", limit=100)
recent_sells = client.spot.trades.recent("HYPE-USDC", limit=100, side="sell")

# HIP-3 recent trades (case-sensitive coins)
hip3_recent = client.hyperliquid.hip3.trades.recent("xyz:TSLA", limit=100)

# HIP-3 trade history
hip3_trades = client.hyperliquid.hip3.trades.history("xyz:TSLA", start=day_ago, end=now)

# Async versions
result = await client.hyperliquid.trades.ahistory("ETH", start=..., end=...)
recent = await client.lighter.trades.arecent("BTC", limit=100)
hip3_recent = await client.hyperliquid.hip3.trades.arecent("xyz:TSLA", limit=100)

Note: recent() is available on HIP-3, HIP-4, spot and both Lighter deployments. The API does not serve it for Hyperliquid core, so client.hyperliquid.trades.recent() raises OxArchiveError with error_code == "unsupported_for_venue" before sending; use history() with a time range there.

Instruments

# List all trading instruments (Hyperliquid)
instruments = client.hyperliquid.instruments.list()

# Get specific instrument details
btc = client.hyperliquid.instruments.get("BTC")
print(f"BTC size decimals: {btc.sz_decimals}")

# Async versions
instruments = await client.hyperliquid.instruments.alist()
btc = await client.hyperliquid.instruments.aget("BTC")

Lighter Instruments

Lighter instruments have a different schema with additional fields for fees, market IDs, and minimum order amounts:

# List Lighter instruments (returns LighterInstrument, not Instrument)
lighter_instruments = client.lighter.instruments.list()

# Get specific Lighter instrument
eth = client.lighter.instruments.get("ETH")
print(f"ETH taker fee: {eth.taker_fee}")
print(f"ETH maker fee: {eth.maker_fee}")
print(f"ETH market ID: {eth.market_id}")
print(f"ETH min base amount: {eth.min_base_amount}")

# Async versions
lighter_instruments = await client.lighter.instruments.alist()
eth = await client.lighter.instruments.aget("ETH")

Key differences:

Field Hyperliquid (Instrument) Lighter (LighterInstrument)
Symbol name symbol
Size decimals sz_decimals size_decimals
Fee info Not available taker_fee, maker_fee, liquidation_fee
Market ID Not available market_id
Min amounts Not available min_base_amount, min_quote_amount

HIP-3 Instruments

HIP-3 instruments are derived from live market data and include mark price, open interest, and mid price:

# List all HIP-3 instruments
hip3_instruments = client.hyperliquid.hip3.instruments.list()
for inst in hip3_instruments:
    print(f"{inst.coin} ({inst.namespace}:{inst.ticker}): mark={inst.mark_price}, OI={inst.open_interest}")

# Get specific HIP-3 instrument (case-sensitive)
tsla = client.hyperliquid.hip3.instruments.get("xyz:TSLA")
print(f"Mark price: {tsla.mark_price}")

# Async versions
hip3_instruments = await client.hyperliquid.hip3.instruments.alist()
tsla = await client.hyperliquid.hip3.instruments.aget("xyz:TSLA")

HIP-3 coins: builders list and delist markets over time, so this README does not pin a list. Call client.hyperliquid.hip3.instruments.list() for the current set. Coin names are case-sensitive and carry the builder prefix (xyz:TSLA, xyz:XYZ100).

HIP-3 Market Breadth

HIP-3 breadth reports the percentage of eligible instruments trading above their current UTC-session VWAP. The current snapshot is available from client.hyperliquid.hip3.breadth.current(), and history is cursor-paginated through client.hyperliquid.hip3.breadth.history():

current = client.hyperliquid.hip3.breadth.current()
print(f"{current.value_pct}% above VWAP ({current.counts.eligible} eligible)")

history = client.hyperliquid.hip3.breadth.history(
    start=day_ago,
    end=now,
    interval="5m",  # 1m, 5m, 15m, 30m, 1h, 4h, 1d
    limit=1000,
)
while history.has_more:
    history = client.hyperliquid.hip3.breadth.history(
        start=day_ago,
        end=now,
        interval="5m",
        cursor=history.next_cursor,
        limit=1000,
    )

The session resets at 00:00 UTC and compares the close of the most recently completed one-minute candle with session VWAP. Instruments without session volume or with a completed candle older than five minutes are excluded, so coverage_ratio varies with market hours. value_pct is None, not zero, when no instrument is eligible. History begins on 2026-08-28; the SDK does not imply synthetic pre-launch history. Interval downsampling uses the last snapshot in each bucket, never an average of percentages.

The same breadth is served for Hyperliquid core perpetuals on client.hyperliquid.breadth, with the same methods, intervals and cursor paging. Core history begins on 2026-08-24, and core responses carry empty namespaces maps.

current = client.hyperliquid.breadth.current()
history = client.hyperliquid.breadth.history(start=day_ago, interval="1h")

HIP-3 Oracle

The deployer-pushed external price of a HIP-3 market, and its instantaneous discovery bounds:

price = client.hyperliquid.hip3.oracle.external_price("xyz:TSLA")
print(price.external_price, price.mark_price, price.block_number)

bounds = client.hyperliquid.hip3.oracle.discovery_bounds("xyz:TSLA")
print(bounds.reference_source, bounds.lower_bound, bounds.upper_bound)

# Async versions
price = await client.hyperliquid.hip3.oracle.aexternal_price("xyz:TSLA")
bounds = await client.hyperliquid.hip3.oracle.adiscovery_bounds("xyz:TSLA")

external_price and mark_price are None when the market has none. The discovery bounds are reference_price times one minus and one plus bound_fraction, where the reference is the external price when available and the mark price otherwise (reference_source) and the fraction follows from the market's max leverage. The full ratcheted range can be wider when a deployer's reset configuration applies. timestamp is a UTC datetime, and timestamp_ms the same instant in Unix milliseconds.

HIP-4 Outcome Markets

HIP-4 binary-outcome markets resolve to Yes (side 0) or No (side 1) at expiry. Each outcome has two per-side coins (#N, where N = 10*outcome_id + side). The SDK accepts both the bare numeric ("0") and #-prefixed ("#0") forms. On REST paths it sends the bare form (the backend routes both to the same record). HIP-4 serves L4 and order history from 2026-05-02 07:47 UTC, trades and candles from 2026-05-02 08:00 UTC, and the order book, outcome-side OI and prices from 2026-05-02 16:51 UTC, with raw OI updates at ~10s. HIP-3 and Lighter candle pages accept up to 10,000 rows; HIP-4 candle pages are capped at 1,000 rows. HIP-4 has no funding and no liquidations. The mark_price field on HIP-4 OI/summary responses is an implied probability in [0, 1], not a USD price.

# Outcome-level metadata (one row per outcome_id; sides folded into side_specs).
result = client.hyperliquid.hip4.list_outcomes(is_settled=False, limit=50)
for o in result.data:
    print(f"#{o.outcome_id}: {o.underlying} {o.class_} expiry={o.expiry}")

# Outcomes settle and new ones list every day, so find one rather than pin it.
# Here: the Yes side of an open BTC price outcome, from the symbol universe.
yes = next(
    s for s in client.symbols.list()
    if s.exchange == "hip4" and not s.is_settled
    and (s.slug or "").startswith("btc-above-") and "-yes-" in s.slug
)
coin = yes.symbol                     # "#N"
outcome_id = int(coin[1:]) // 10      # N = 10 * outcome_id + side

# Single-outcome detail. Includes aggregated_oi (paired both-sides snapshot).
outcome = client.hyperliquid.hip4.get_outcome(outcome_id)
agg = outcome.aggregated_oi
print(f"Display OI: {agg.outcome_display_open_interest_contracts} {agg.currency}")
print(f"Side parity: {agg.side_supply_parity}")

# Look up by slug (per-outcome OR per-side). Returns aggregated_oi too.
outcome = client.hyperliquid.hip4.get_outcome_by_slug(outcome.slug)

# Filter the list endpoint by slug. Short-circuits to a one-item response.
result = client.hyperliquid.hip4.list_outcomes(slug=yes.slug)

# Questions group several binary outcomes under one multi-choice resolver:
# one named outcome per choice, plus a fallback outcome that resolves Yes when
# no named choice does. Page with next_cursor, passed back unchanged.
page = client.hyperliquid.hip4.list_questions(limit=100)
question = client.hyperliquid.hip4.get_question(page.data[0].question_id)
print(question.named_outcome_ids, question.fallback_outcome_id, question.settled_named_outcomes)
# Also: client.hyperliquid.hip4.questions.list() / .get(), and alist() / aget()

# Per-side instruments. Either bare or "#"-prefixed works.
yes_side = client.hyperliquid.hip4.instruments.get(coin[1:])                   # bare, recommended
no_side = client.hyperliquid.hip4.instruments.get(f"#{10 * outcome_id + 1}")   # also works

# Market data.
ob = client.hyperliquid.hip4.get_orderbook(coin)
trades = client.hyperliquid.hip4.get_trades_recent(coin, limit=50)
candles = client.hyperliquid.hip4.candles.history(coin, start=day_ago, end=now, interval="1h")
oi = client.hyperliquid.hip4.get_open_interest_current(coin)  # mark_price is in [0, 1]
summary = client.hyperliquid.hip4.get_summary(coin)           # mark_price is in [0, 1]

Hyperliquid Spot

Hyperliquid spot pairs live at /v1/hyperliquid/spot and are accessible via client.spot. Symbols use dashed canonical form (HYPE-USDC, PURR-USDC); the server resolves dashed to wire format (PURR/USDC or @107) internally. Spot has no funding, no open interest, or liquidations. Candle history is served at /v1/hyperliquid/spot/candles/{symbol} from 2025-03-22 10:50 UTC, supports 1m, 5m, 15m, 30m, 1h, 4h, 1d, and 1w, and accepts a maximum of 1,000 rows per page with numeric timestamp-string cursors; pass each next_cursor back unchanged.

Trades are served from 2025-03-22 10:50:22 UTC, TWAP statuses from 2026-05-05 13:05 UTC, the order book from 2026-05-05 19:56 UTC, and L4 from 2026-05-05 22:57 UTC (PURR-USDC from 2026-03-11 01:03 UTC); REST order history reaches back further. Pairs listed later start later; client.symbols.list() has each pair's starts.

# Pair discovery
pairs = client.spot.pairs.list()
hype = client.spot.pairs.get("HYPE-USDC")
print(
    f"{hype.symbol}: base={hype.base_token_name} "
    f"quote={hype.quote_token_name} pair_index={hype.pair_index}"
)

# Current orderbook
ob = client.spot.orderbook.get("HYPE-USDC")
print(f"HYPE-USDC mid: {ob.mid_price}, spread bps: {ob.spread_bps}")

# Orderbook history
history = client.spot.orderbook.history("HYPE-USDC", start=hour_ago, end=now)

# Trades by time window
trades = client.spot.trades.list("HYPE-USDC", start=hour_ago, end=now, limit=1000)

# Candle history (coverage starts at 2025-03-22 10:50 UTC)
spot_candles = client.spot.candles.history(
    "HYPE-USDC",
    start=day_ago,
    end=now,
    interval="1h",
    limit=1000,
)

# L4 endpoints (full reconstruction, raw diffs, and checkpoint history)
snapshot = client.spot.l4_orderbook.get("HYPE-USDC")
diffs = client.spot.l4_orderbook.diffs("HYPE-USDC", start=..., end=...)
checkpoints = client.spot.l4_orderbook.history("HYPE-USDC", start=..., end=...)

# L4 order lifecycle
orders = client.spot.orders.history("HYPE-USDC", start=..., end=...)

# TWAP statuses, by symbol (history() is the same method as by_symbol()) or by user wallet
twap_pair = client.spot.twap.history("HYPE-USDC", start=..., end=...)
twap_user = client.spot.twap.by_user("0xabc...", start=..., end=...)

# Per-dataset freshness lag: orderbook, trades, l4_diffs, l4_checkpoints,
# orders and twap, each with optional lag_ms and last_updated
fresh = client.spot.get_freshness("HYPE-USDC")
print(fresh.orderbook.lag_ms, fresh.trades.last_updated)
for dataset, lag in fresh.tables.items():
    print(f"{dataset}: lag={lag.lag_ms}ms last_updated={lag.last_updated}")

# Async versions are available on every method:
ob = await client.spot.orderbook.aget("HYPE-USDC")
pairs = await client.spot.pairs.alist()
twap = await client.spot.twap.aby_user("0xabc...", start=..., end=...)
fresh = await client.spot.aget_freshness("HYPE-USDC")

Lighter on Robinhood Chain

Lighter has two deployments: mainnet (client.lighter) and Robinhood Chain (client.rh_lighter, REST root /v1/rh-lighter). The Robinhood Chain deployment has the same resources as client.lighter except the L3 order book, which is not captured there, and the L1 account resolver. Markets are quoted in USDG. Perps use uppercase symbols (BTC); spot markets use dashed symbols (AAPL-USDG). Symbols are case-insensitive. Market symbols and ids belong to each deployment, so BTC on client.rh_lighter is a different market from BTC on client.lighter.

Coverage: trades and liquidations from the venue launch, 2026-06-26 20:10:26 UTC; order book, open interest, and funding from 2026-08-22 18:43 UTC; account positions from 2026-06-26. A request that starts before a data type's first date is refused with the API's coverage error. Liquidations from before live capture were backfilled from the venue's finalized export: those rows have source == "bucket" and an empty raw_json, while rows captured live have source == "ws" and the venue's raw JSON. Candles are served from 2026-06-26 20:10 UTC.

Trades follow the same finalization contract as mainnet Lighter. trades.list() returns canonical trades only: end is clamped to the finalization watermark, about a day behind, reported as result.meta.finalized_through, with meta.requested_end and meta.clamped_to set when the clamp applied. trades.recent() serves the preliminary tier.

rh = client.rh_lighter

# Markets
markets = rh.instruments.list()
aapl = rh.instruments.get("AAPL-USDG")

# Order book (from 2026-08-22 18:43 UTC)
book = rh.orderbook.get("AAPL-USDG")
books = rh.orderbook.history("BTC", start=hour_ago, end=now)

# Trades (from 2026-06-26 20:10:26 UTC). end is clamped to the finalization watermark.
page = rh.trades.list("BTC", start=day_ago, end=now, limit=1000)
print(page.meta.finalized_through, page.meta.clamped_to)
recent = rh.trades.recent("BTC")  # preliminary tier

# Open interest and funding (perps)
oi = rh.open_interest.current("BTC")
funding = rh.funding.history("BTC", start=day_ago, end=now)

# Liquidations and liquidation volume (from 2026-06-26 20:10:26 UTC)
liqs = rh.liquidations.history("BTC", start=week_ago, end=now)
volume = rh.liquidations.volume("BTC", start=week_ago, end=now, interval="1d")

# Freshness, summary, price history
fresh = rh.get_freshness("BTC")
summary = rh.get_summary("BTC")
prices = rh.get_price_history("BTC", start=day_ago, end=now, interval="1h")

# Account positions by Lighter account index (perp markets)
positions = rh.positions.get(4521)

# Async versions are available on every method
book = await rh.orderbook.aget("AAPL-USDG")

Funding Rates

# Get current funding rate
current = client.hyperliquid.funding.current("BTC")

# Get funding rate history (start is required)
history = client.hyperliquid.funding.history(
    "ETH",
    start=week_ago,
    end=now
)

# Get funding rate history with aggregation interval
history = client.hyperliquid.funding.history(
    "BTC",
    start=week_ago,
    end=now,
    interval="1h"
)

# HIP-3 funding (case-sensitive coins)
hip3_current = client.hyperliquid.hip3.funding.current("xyz:TSLA")
hip3_history = client.hyperliquid.hip3.funding.history("xyz:TSLA", start=week_ago, end=now)

# Async versions
current = await client.hyperliquid.funding.acurrent("BTC")
history = await client.hyperliquid.funding.ahistory("ETH", start=..., end=...)
hip3_current = await client.hyperliquid.hip3.funding.acurrent("xyz:TSLA")

Unit note: funding_rate is a fractional, non-annualized rate. For example, 0.0001 means 0.01% for the funding interval. This is a breaking normalization for Lighter consumers that previously compensated for percent units; do not apply a second percent conversion.

Funding History Parameters

Parameter Type Required Description
coin str Yes Coin symbol (e.g., 'BTC', 'ETH')
start Timestamp Yes Start timestamp
end Timestamp Yes End timestamp
cursor Timestamp No Cursor from previous response for pagination
limit int No Max results (default: 100, max: 1000)
interval str No Aggregation interval: '1m', '5m', '15m', '30m', '1h', '4h', '1d'. Omit for raw rows: Hyperliquid core funding is ~1 min; HIP-3 and Lighter funding are ~10s. HIP-4 has no funding.

Open Interest

# Get current open interest
current = client.hyperliquid.open_interest.current("BTC")

# Get open interest history (start is required)
history = client.hyperliquid.open_interest.history(
    "ETH",
    start=week_ago,
    end=now
)

# Get open interest history with aggregation interval
oi = client.hyperliquid.open_interest.history(
    "BTC",
    start=week_ago,
    end=now,
    interval="1h"
)

# HIP-3 open interest (case-sensitive coins)
hip3_current = client.hyperliquid.hip3.open_interest.current("xyz:TSLA")
hip3_history = client.hyperliquid.hip3.open_interest.history("xyz:TSLA", start=week_ago, end=now)

# Async versions
current = await client.hyperliquid.open_interest.acurrent("BTC")
history = await client.hyperliquid.open_interest.ahistory("ETH", start=..., end=...)
hip3_current = await client.hyperliquid.hip3.open_interest.acurrent("xyz:TSLA")

Open Interest History Parameters

Parameter Type Required Description
coin str Yes Coin symbol (e.g., 'BTC', 'ETH')
start Timestamp Yes Start timestamp
end Timestamp Yes End timestamp
cursor str No Numeric timestamp string returned as next_cursor; pass it back unchanged
limit int No Max results (default: 100, max: 1000)
interval str No Aggregation interval: '1m', '5m', '15m', '30m', '1h', '4h', '1d'. Omit for raw rows: HIP-3, HIP-4 outcome-side OI, and Lighter update at ~10s.

Liquidations

Get historical liquidation events. Available for Hyperliquid and HIP-3 (from 2025-12-22), Lighter (client.lighter.liquidations, from 2026-06-10) and Lighter on Robinhood Chain (client.rh_lighter.liquidations, from 2026-06-26 20:10:26 UTC). The projected forced-liquidation price-level endpoints refresh about every five minutes. This is a measured cadence, not an exact five-minute guarantee.

# Get liquidation history for a coin (Hyperliquid)
liquidations = client.hyperliquid.liquidations.history(
    "BTC",
    start=day_ago,
    end=now,
    limit=100
)

# Paginate through all results
all_liquidations = list(liquidations.data)
while liquidations.has_more:
    liquidations = client.hyperliquid.liquidations.history(
        "BTC",
        start=day_ago,
        end=now,
        cursor=liquidations.next_cursor,
        limit=1000
    )
    all_liquidations.extend(liquidations.data)

# Get liquidations for a specific user (Hyperliquid core; HIP-3 has no per-user route)
user_liquidations = client.hyperliquid.liquidations.by_user(
    "0x1234...",
    start=week_ago,
    end=now,
    symbol="BTC"  # optional filter
)

# HIP-3 liquidations (case-sensitive coins)
hip3_liquidations = client.hyperliquid.hip3.liquidations.history(
    "xyz:TSLA",
    start=week_ago,
    end=now,
    limit=100
)

# HIP-3 liquidation volume
hip3_volume = client.hyperliquid.hip3.liquidations.volume(
    "xyz:TSLA",
    start=week_ago,
    end=now,
    interval="1h"
)

# Lighter liquidations (mainnet and Robinhood Chain). A row keeps both sides'
# account fields; see LighterLiquidation. Pass next_cursor back unchanged.
lighter_liquidations = client.lighter.liquidations.history(
    "BTC",
    start=day_ago,
    end=now,
    limit=1000
)
for liq in lighter_liquidations.data:
    # timestamp is a UTC datetime; timestamp_ms is the same instant in Unix milliseconds
    print(liq.timestamp, liq.price, liq.size, liq.usd_amount, liq.ask_account, liq.bid_account)

# Robinhood Chain liquidations start at the venue launch, 2026-06-26 20:10:26 UTC.
# Rows from before live capture have source == "bucket" and an empty raw_json.
rh_liquidations = client.rh_lighter.liquidations.history("BTC", start=week_ago, end=now)

# Async versions
liquidations = await client.hyperliquid.liquidations.ahistory("BTC", start=..., end=...)
lighter_liquidations = await client.lighter.liquidations.ahistory("BTC", start=..., end=...)
user_liquidations = await client.hyperliquid.liquidations.aby_user("0x...", start=..., end=...)
hip3_liquidations = await client.hyperliquid.hip3.liquidations.ahistory("xyz:TSLA", start=..., end=...)
hip3_volume = await client.hyperliquid.hip3.liquidations.avolume("xyz:TSLA", start=..., end=...)

Liquidation Volume

Get pre-aggregated liquidation volume in time-bucketed intervals. Returns total, long, and short USD volumes per bucket -- 100-1000x less data than individual liquidation records. Available for Hyperliquid and HIP-3, and for both Lighter deployments, where each bucket carries total_usd and count only (no long/short split).

# Get hourly liquidation volume for the last week (Hyperliquid)
volume = client.hyperliquid.liquidations.volume(
    "BTC",
    start=week_ago,
    end=now,
    interval="1h"  # 1m, 5m, 15m, 30m, 1h, 4h, 1d
)

for bucket in volume.data:
    print(f"{bucket.timestamp}: total=${bucket.total_usd}, long=${bucket.long_usd}, short=${bucket.short_usd}")

# HIP-3 liquidation volume
hip3_volume = client.hyperliquid.hip3.liquidations.volume(
    "xyz:TSLA",
    start=week_ago,
    end=now,
    interval="1d"
)

# Lighter liquidation volume (mainnet and Robinhood Chain): total_usd and count per bucket
lighter_volume = client.lighter.liquidations.volume("BTC", start=..., end=..., interval="1h")
rh_volume = client.rh_lighter.liquidations.volume("BTC", start=..., end=..., interval="1d")

# Convenience method on HyperliquidClient (Hyperliquid only)
volume = client.hyperliquid.get_liquidation_volume("BTC", start=..., end=..., interval="1h")

# Async versions
volume = await client.hyperliquid.liquidations.avolume("BTC", start=..., end=..., interval="1h")
hip3_volume = await client.hyperliquid.hip3.liquidations.avolume("xyz:TSLA", start=..., end=..., interval="1d")

Freshness

Check when each data type was last updated for a specific coin. Useful for verifying data recency before pulling it.

# Hyperliquid
freshness = client.hyperliquid.get_freshness("BTC")
print(f"Orderbook last updated: {freshness.orderbook.last_updated}, lag: {freshness.orderbook.lag_ms}ms")
print(f"Trades last updated: {freshness.trades.last_updated}, lag: {freshness.trades.lag_ms}ms")
print(f"Funding last updated: {freshness.funding.last_updated}")
print(f"OI last updated: {freshness.open_interest.last_updated}")

# Lighter
lighter_freshness = client.lighter.get_freshness("BTC")

# Lighter on Robinhood Chain
rh_freshness = client.rh_lighter.get_freshness("BTC")

# HIP-3 (case-sensitive coins)
hip3_freshness = client.hyperliquid.hip3.get_freshness("xyz:TSLA")

# Async versions
freshness = await client.hyperliquid.aget_freshness("BTC")
lighter_freshness = await client.lighter.aget_freshness("BTC")
hip3_freshness = await client.hyperliquid.hip3.aget_freshness("xyz:TSLA")

Summary

Get a combined market snapshot in a single call -- mark/oracle price, funding rate, open interest, 24h volume, and 24h liquidation volumes.

# Hyperliquid (includes volume + liquidation data)
summary = client.hyperliquid.get_summary("BTC")
print(f"Mark price: {summary.mark_price}")
print(f"Oracle price: {summary.oracle_price}")
print(f"Funding rate: {summary.funding_rate}")
print(f"Open interest: {summary.open_interest}")
print(f"24h volume: {summary.volume_24h}")
print(f"24h liquidation volume: ${summary.liquidation_volume_24h}")
print(f"  Long: ${summary.long_liquidation_volume_24h}")
print(f"  Short: ${summary.short_liquidation_volume_24h}")

# Lighter (price, funding, OI; no volume/liquidation data)
lighter_summary = client.lighter.get_summary("BTC")

# Lighter on Robinhood Chain (same shape as mainnet Lighter)
rh_summary = client.rh_lighter.get_summary("BTC")

# HIP-3 (includes mid_price; case-sensitive coins)
hip3_summary = client.hyperliquid.hip3.get_summary("xyz:TSLA")
print(f"Mid price: {hip3_summary.mid_price}")

# Async versions
summary = await client.hyperliquid.aget_summary("BTC")
lighter_summary = await client.lighter.aget_summary("BTC")
hip3_summary = await client.hyperliquid.hip3.aget_summary("xyz:TSLA")

Price History

Get mark, oracle, and mid price history over time. Supports aggregation intervals. Data projected from open interest records.

# Hyperliquid: from 2023-05-20 02:50 UTC
prices = client.hyperliquid.get_price_history(
    "BTC",
    start=day_ago,
    end=now,
    interval="1h"  # 1m, 5m, 15m, 30m, 1h, 4h, 1d
)

for snapshot in prices.data:
    print(f"{snapshot.timestamp}: mark={snapshot.mark_price}, oracle={snapshot.oracle_price}, mid={snapshot.mid_price}")

# Lighter
lighter_prices = client.lighter.get_price_history("BTC", start=day_ago, end=now, interval="1h")

# Lighter on Robinhood Chain
rh_prices = client.rh_lighter.get_price_history("BTC", start=day_ago, end=now, interval="1h")

# HIP-3 (case-sensitive coins)
hip3_prices = client.hyperliquid.hip3.get_price_history("xyz:TSLA", start=week_ago, end=now, interval="1d")

# Paginate for larger ranges
result = client.hyperliquid.get_price_history("BTC", start=..., end=..., interval="4h", limit=1000)
while result.has_more:
    result = client.hyperliquid.get_price_history(
        "BTC", start=..., end=..., interval="4h",
        cursor=result.next_cursor, limit=1000
    )

# Async versions
prices = await client.hyperliquid.aget_price_history("BTC", start=..., end=..., interval="1h")
lighter_prices = await client.lighter.aget_price_history("BTC", start=..., end=..., interval="1h")
hip3_prices = await client.hyperliquid.hip3.aget_price_history("xyz:TSLA", start=..., end=..., interval="1d")

Candles (OHLCV)

Get historical OHLCV candle data aggregated from trades. Core Hyperliquid, HIP-3, and Lighter candle pages accept up to 10,000 rows; HIP-4 and Hyperliquid Spot candle pages accept up to 1,000 rows. Candles are served from 2025-03-22 12:00 UTC on Hyperliquid core, 2025-12-22 on HIP-3, 2026-05-02 08:00 UTC on HIP-4, 2025-03-22 10:50 UTC on Hyperliquid Spot, 2025-08-01 on Lighter and 2026-06-26 20:10 UTC on Robinhood Chain. Candle pagination cursors are numeric timestamp strings returned as next_cursor; pass each one back unchanged.

# Get candle history (start is required)
candles = client.hyperliquid.candles.history(
    "BTC",
    start=day_ago,
    end=now,
    interval="1h",  # 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
    limit=100
)

# Iterate through candles
for candle in candles.data:
    print(f"{candle.timestamp}: O={candle.open} H={candle.high} L={candle.low} C={candle.close} V={candle.volume}")

# Cursor-based pagination for large datasets
result = client.hyperliquid.candles.history("BTC", start=..., end=..., interval="1m", limit=1000)
while result.has_more:
    result = client.hyperliquid.candles.history(
        "BTC", start=..., end=..., interval="1m",
        cursor=result.next_cursor, limit=1000
    )

# Lighter candles
lighter_candles = client.lighter.candles.history(
    "BTC",
    start=day_ago,
    end=now,
    interval="15m"
)

# Lighter on Robinhood Chain candles (from 2026-06-26 20:10 UTC)
rh_candles = client.rh_lighter.candles.history("BTC", start=day_ago, end=now, interval="1h")

# HIP-3 candles (case-sensitive coins)
hip3_candles = client.hyperliquid.hip3.candles.history(
    "xyz:TSLA",
    start=day_ago,
    end=now,
    interval="1h"
)

# Hyperliquid Spot candles (dashed canonical symbols; max 1,000 rows)
spot_candles = client.spot.candles.history(
    "HYPE-USDC",
    start=day_ago,
    end=now,
    interval="1h",
    limit=1000,
)

# Async versions
candles = await client.hyperliquid.candles.ahistory("BTC", start=..., end=..., interval="1h")
hip3_candles = await client.hyperliquid.hip3.candles.ahistory("xyz:TSLA", start=..., end=..., interval="1h")
spot_candles = await client.spot.candles.ahistory("HYPE-USDC", start=day_ago, end=now, interval="1h")

Available Intervals

Interval Description
1m 1 minute
5m 5 minutes
15m 15 minutes
30m 30 minutes
1h 1 hour (default)
4h 4 hours
1d 1 day
1w 1 week

Cumulative Volume Delta

Taker buy and sell notional per bucket, their difference, and a running total, for Hyperliquid core and HIP-3. Intervals of 1h and longer roll up hourly totals; 1m, 5m, 15m and 30m are summed from taker fills.

# One page. Without start (and without a cursor) the response is the newest
# `limit` buckets of the 24 hours before `end`.
page = client.hyperliquid.cvd.history(
    "BTC",
    start=day_ago,
    end=now,
    interval="1m",  # 1m, 5m, 15m, 30m, 1h (default), 4h, 1d, 1w
    limit=500,      # buckets per page (default 500, max 10000)
)
for bucket in page.data:
    print(bucket.timestamp, bucket.buy_volume, bucket.sell_volume, bucket.delta)

# Follow next_cursor with the same start, end and interval while has_more is
# True. Stop on has_more, not on a short page: below 1h a page can be short
# and still not be the last.
while page.has_more:
    page = client.hyperliquid.cvd.history(
        "BTC",
        start=day_ago,
        end=now,
        interval="1m",
        cursor=page.next_cursor,
    )

# Or let the iterator follow the cursor
running = 0.0
for bucket in client.hyperliquid.cvd.iterate("BTC", start=day_ago, end=now, interval="1m"):
    running += bucket.delta

# HIP-3 (case-sensitive coins)
hip3 = client.hyperliquid.hip3.cvd.history("xyz:TSLA", start=day_ago, interval="1h")

# Async versions
page = await client.hyperliquid.cvd.ahistory("BTC", start=day_ago)
async for bucket in client.hyperliquid.hip3.cvd.aiterate("xyz:TSLA", start=day_ago):
    ...

Buckets are labelled by their open time (timestamp, a UTC datetime; timestamp_ms in Unix milliseconds) and omitted when they hold no trades. 4h, 1d and 1w buckets open on UTC epoch boundaries, so 1w buckets open on Thursdays, and at those widths the first and last bucket of a window can be partial. cumulative_delta restarts on every page: to join pages, rebuild the running total from delta. A response that is one page of several says so in meta.notice.

L4 Orderbook (Order-Level)

Get L4 order-level orderbook data with user attribution. Available for Hyperliquid core and HIP-3 (from 2026-03-11 01:03 UTC), HIP-4 (client.hyperliquid.hip4.l4_orderbook, from 2026-05-02 07:47 UTC) and spot (client.spot.l4_orderbook, from 2026-05-05 22:57 UTC; PURR-USDC from 2026-03-11 01:03 UTC).

In a snapshot from get(), each resting order's timestamp is the time it joined the queue, as an RFC 3339 string, with timestamp_ms the same instant in Unix milliseconds. When the queue time is unknown, timestamp is None and timestamp_ms is 0.

# Get current L4 orderbook snapshot (Hyperliquid)
snapshot = client.hyperliquid.l4_orderbook.get("BTC")
snapshot = client.hyperliquid.l4_orderbook.get("BTC", depth=10)

# Get L4 orderbook at a specific timestamp
historical = client.hyperliquid.l4_orderbook.get("BTC", timestamp=hour_ago)

# Get L4 orderbook diffs (order-level changes)
diffs = client.hyperliquid.l4_orderbook.diffs(
    "BTC",
    start=hour_ago,
    end=now,
    limit=1000
)

# Get L4 orderbook history (full checkpoints over time)
history = client.hyperliquid.l4_orderbook.history(
    "BTC",
    start=hour_ago,
    end=now,
    limit=10
)

# HIP-3 L4 orderbook (case-sensitive coins)
hip3_snapshot = client.hyperliquid.hip3.l4_orderbook.get("xyz:TSLA")
hip3_diffs = client.hyperliquid.hip3.l4_orderbook.diffs("xyz:TSLA", start=..., end=...)
hip3_history = client.hyperliquid.hip3.l4_orderbook.history("xyz:TSLA", start=..., end=...)

# Async versions
snapshot = await client.hyperliquid.l4_orderbook.aget("BTC")
diffs = await client.hyperliquid.l4_orderbook.adiffs("BTC", start=..., end=...)
history = await client.hyperliquid.l4_orderbook.ahistory("BTC", start=..., end=...)
hip3_snapshot = await client.hyperliquid.hip3.l4_orderbook.aget("xyz:TSLA")

Methods:

Method Description
get(symbol, *, timestamp, depth) Get L4 orderbook snapshot
diffs(symbol, *, start, end, cursor, limit) Get L4 orderbook diffs (order-level changes)
history(symbol, *, start, end, cursor, limit) Get L4 orderbook history (full checkpoints; depth applies to get() only)

History replays over the WebSocket too, on every product; see L4 and Full-Depth Replay.

L3 Orderbook (Lighter Only)

Get Lighter L3 individual order-level snapshots from 2026-03-05 03:33 UTC, capped at 250 orders per side.

# Get current L3 orderbook snapshot
snapshot = client.lighter.l3_orderbook.get("BTC")
snapshot = client.lighter.l3_orderbook.get("BTC", depth=20)

# Get L3 orderbook at a specific timestamp
historical = client.lighter.l3_orderbook.get("BTC", timestamp=hour_ago)

# Only one account's resting orders
mine = client.lighter.l3_orderbook.get("BTC", account=281474976710654)

# Get L3 orderbook history
history = client.lighter.l3_orderbook.history(
    "BTC",
    start=hour_ago,
    end=now,
    account=281474976710654,   # optional: one account's orders
    limit=100
)

# Paginate through results
while history.has_more:
    history = client.lighter.l3_orderbook.history(
        "BTC",
        start=hour_ago,
        end=now,
        cursor=history.next_cursor,
        limit=100
    )

# Async versions
snapshot = await client.lighter.l3_orderbook.aget("BTC")
history = await client.lighter.l3_orderbook.ahistory("BTC", start=..., end=...)

Methods:

Method Description
get(symbol, *, timestamp, depth, account) Get an L3 snapshot, up to 250 orders per side
history(symbol, *, start, end, cursor, limit, account) Get L3 history from 2026-03-05 03:33 UTC, up to 250 orders per side (no depth or granularity)

L2 Orderbook (Full-Depth)

Get L2 full-depth orderbook derived from L4 data. Available for Hyperliquid and HIP-3, from 2026-03-11 01:03 UTC.

# L2 full-depth orderbook
l2 = client.hyperliquid.l2_orderbook.get("BTC")
l2_historical = client.hyperliquid.l2_orderbook.get("BTC", timestamp=hour_ago)

# L2 orderbook history, optionally limited to the top levels of each snapshot
l2_history = client.hyperliquid.l2_orderbook.history("BTC", start=hour_ago, end=now)
l2_top = client.hyperliquid.l2_orderbook.history("BTC", start=hour_ago, end=now, depth=50)

# L2 tick-level diffs
l2_diffs = client.hyperliquid.l2_orderbook.diffs("BTC", start=hour_ago, end=now)

# HIP-3 L2 orderbook
hip3_l2 = client.hyperliquid.hip3.l2_orderbook.get("xyz:TSLA")

# Async versions
l2 = await client.hyperliquid.l2_orderbook.aget("BTC")
l2_history = await client.hyperliquid.l2_orderbook.ahistory("BTC", start=..., end=...)
l2_diffs = await client.hyperliquid.l2_orderbook.adiffs("BTC", start=..., end=...)

Methods:

Method Description
get(symbol, *, timestamp, depth) Get L2 full-depth orderbook snapshot
history(symbol, *, start, end, cursor, limit, depth) Get L2 orderbook history (every level, or the top depth levels per side)
diffs(symbol, *, start, end, cursor, limit) Get L2 tick-level diffs

Orders (L4 Order History)

Get L4 order history, order flow aggregation, and TP/SL data. Available for Hyperliquid core, HIP-3 and HIP-4; spot serves order history only.

# Get order history
result = client.hyperliquid.orders.history(
    "BTC",
    start=hour_ago,
    end=now,
    limit=1000
)

# Filter by user, status, or order type
result = client.hyperliquid.orders.history(
    "BTC",
    start=hour_ago,
    end=now,
    user="0x1234...",
    status="filled",
    order_type="limit"
)

# Only orders whose trigger fired (triggered=False excludes them)
fired = client.hyperliquid.orders.history("BTC", start=day_ago, end=now, triggered=True)

# Get order flow aggregation: one page of time buckets
flow = client.hyperliquid.orders.flow(
    "BTC",
    start=day_ago,
    end=now,
    interval="1m",  # 1m (default), 5m, 15m, 1h
)
buckets = list(flow.data)
# A page holds up to `limit` buckets (default 1000, max 10000); follow the
# cursor with the same start, end and interval while has_more is True
while flow.has_more:
    flow = client.hyperliquid.orders.flow(
        "BTC",
        start=day_ago,
        end=now,
        interval="1m",
        cursor=flow.next_cursor,
    )
    buckets.extend(flow.data)

# Get TP/SL history
tpsl = client.hyperliquid.orders.tpsl(
    "BTC",
    start=day_ago,
    end=now,
    user="0x1234...",       # optional
    triggered=True          # optional filter
)

# HIP-3 orders (case-sensitive coins)
hip3_orders = client.hyperliquid.hip3.orders.history("xyz:TSLA", start=..., end=...)
hip3_flow = client.hyperliquid.hip3.orders.flow("xyz:TSLA", start=..., end=..., interval="1h")
hip3_tpsl = client.hyperliquid.hip3.orders.tpsl("xyz:TSLA", start=..., end=...)

# Async versions
result = await client.hyperliquid.orders.ahistory("BTC", start=..., end=...)
flow = await client.hyperliquid.orders.aflow("BTC", start=..., end=...)
tpsl = await client.hyperliquid.orders.atpsl("BTC", start=..., end=...)
hip3_orders = await client.hyperliquid.hip3.orders.ahistory("xyz:TSLA", start=..., end=...)

Methods:

Method Description
history(symbol, *, start, end, user, status, order_type, triggered, cursor, limit) Get order history
flow(symbol, *, start, end, interval, cursor, limit) Get order flow aggregation, one page of buckets
tpsl(symbol, *, start, end, user, triggered, cursor, limit) Get TP/SL history

Hyperliquid core and HIP-3 also have trigger_levels() and trigger_levels_history(). HIP-4 serves history(), flow() and tpsl(). Spot serves history(symbol, *, start, end, cursor, limit) only, without user, status, order-type or triggered filters.

Account Positions

What an account holds, now or at any instant, plus hourly history, the change log of every fill that moved a position, account summaries, and market-wide listings. Available on four clients with the same method names:

Client Account key Change log from Hourly history from Live snapshot
client.hyperliquid.positions 0x wallet address 2025-05-25 2026-06-07 every 5 minutes
client.hyperliquid.hip3.positions 0x wallet address (optional dex) 2025-10-13 2026-06-07 every 5 minutes
client.lighter.positions integer Lighter account index 2025-01-17 2025-01-17 every 2 minutes
client.rh_lighter.positions integer Lighter account index 2026-06-26 2026-06-26 every 2 minutes

Lighter positions cover perp markets. On Lighter mainnet, client.lighter.accounts.by_l1(address) resolves an L1 address to the account indices it owns.

# Now: the latest live snapshot
current = client.hyperliquid.positions.get("0xabc...")
for p in current.data.positions:
    print(p.symbol, p.side, p.size, p.entry_price, p.unrealized_pnl, p.quality)
print(current.data.account)      # AccountSummary on the first page of a snapshot, or None
print(current.meta.as_of, current.meta.quality, current.meta.stale)

# As of an instant: the state after every event before it. An exact UTC hour
# serves the hourly snapshot; any other instant is reconstructed from the
# change log (meta.source == "reconstructed"; mark fields are at the instant).
then = client.hyperliquid.positions.get("0xabc...", timestamp=day_ago)
print(then.meta.source, then.meta.built_through, then.meta.clamped_to)

# Hourly history and the change log in [start, end), following cursors
for row in client.hyperliquid.positions.iterate_history("0xabc...", start=day_ago, end=now):
    print(row.snapshot_ts, row.symbol, row.size)
for leg in client.hyperliquid.positions.iterate_changes("0xabc...", start=day_ago, end=now, symbol="BTC"):
    print(leg.timestamp, leg.event_type, leg.start_position, "->", leg.end_position, leg.closed_pnl)

# Account summaries: the clearinghouse summary on Hyperliquid and HIP-3,
# position aggregates on Lighter and Robinhood Chain
summary = client.hyperliquid.positions.account("0xabc...")
hourly = client.hyperliquid.positions.account_history("0xabc...", start=day_ago, end=now)
lighter_summary = client.lighter.positions.account(4521)
lighter_hourly = client.rh_lighter.positions.account_history(4521, start=day_ago, end=now)

# HIP-3: dex narrows a wallet to one dex; symbols are case-sensitive
hip3 = client.hyperliquid.hip3.positions.get("0xabc...", dex="xyz")

# Every open position in a market, largest value first; totals on the first page
page = client.hyperliquid.positions.market("BTC", side="long", min_value=1_000_000)
print(page.meta.totals.long_count, page.meta.totals.top10_value_share)
last_hour = now.replace(minute=0, second=0, microsecond=0) - timedelta(hours=1)  # an exact UTC hour
at_hour = client.hyperliquid.positions.market("BTC", hour=last_hour)

# Long/short aggregates: now, or one per hour over [start, end)
now_summary = client.hyperliquid.positions.market_summary("BTC")
series = client.hyperliquid.positions.market_summary("BTC", start=day_ago, end=now)

# Bulk: every open position across markets at one hourly snapshot
for row in client.hyperliquid.positions.iterate_all(last_hour):
    print(row.user_address, row.symbol, row.size)

# Lighter (mainnet and Robinhood Chain): integer account indices
owned = client.lighter.accounts.by_l1("0xabc...")
for account in owned.data.accounts:
    lighter_now = client.lighter.positions.get(int(account.account_index))
    # Without a symbol filter, the first page carries position aggregates
    # (total_position_value, total_unrealized_pnl, long_value, short_value, n_positions).
    print(lighter_now.data.account)
changes = client.lighter.positions.changes(4521, start=day_ago, end=now)
print(changes.meta.finalized_through)  # legs before it are final
market = client.lighter.positions.market("BTC", include_system=True)  # include system accounts
rh_now = client.rh_lighter.positions.get(4521)

# Async versions of every method (aget, ahistory, achanges, amarket, ...)
# and of every iterator (aiterate_history, aiterate_changes, ...)
current = await client.hyperliquid.positions.aget("0xabc...")
async for leg in client.lighter.positions.aiterate_changes(4521, start=..., end=...):
    ...

Methods (on every positions client unless noted):

Method Returns
get(key, *, timestamp, symbol, dex, cursor, limit) CursorResponse[WalletPositions]: positions, account, account_seen
history(key, *, start, end, symbol, dex, cursor, limit) Hourly Position rows
changes(key, *, start, end, symbol, dex, cursor, limit) PositionChange legs
market(symbol, *, hour, side, min_value, include_system, cursor, limit) MarketPosition rows; meta.totals on the first page
market_summary(symbol, *, start, end, include_system, cursor, limit) MarketPositionsSummary (one now, or one per hour)
all(hour, *, include_system, cursor, limit) MarketPosition rows across every market at one hour
account(key, *, dex) AccountSummary rows at the latest live snapshot: the clearinghouse summary on Hyperliquid and HIP-3, position aggregates on Lighter
account_history(key, *, start, end, dex, cursor, limit) Hourly AccountSummary rows (on Lighter, one per hour, at most 744 per page)
iterate_history, iterate_changes, iterate_market, iterate_market_summary, iterate_all, iterate_account_history Iterators that follow next_cursor for you
client.lighter.accounts.by_l1(l1_address, *, cursor, limit) LighterL1Accounts: l1_address, total_accounts, accounts (Lighter mainnet only)

key is a 0x address on Hyperliquid and HIP-3 and an integer account index on Lighter. dex applies to HIP-3 and include_system to Lighter. Timestamps accept Unix milliseconds, ISO strings, or datetimes, and a time without a time zone is UTC; hour must be an exact UTC hour.

Things to know:

  • Numbers are decimal strings. A flat position is "0". A value is None when it is unknown, never a guess.
  • meta carries the context of every response: as_of (the instant the state describes), snapshot_ts, source (snapshot, reconstructed, or changes), quality, stale (the live snapshot is older than 12 minutes), built_through (reads are clamped to it; clamped_to and requested_end show when they were), finalized_through (everything before it is final), and totals on market listings.
  • Quality is per row. quality is complete, partial (for example no mark), or degraded; Lighter rows can also be preliminary, unreconciled, or incomplete.
  • Empty results are explained. When a wallet has no open positions, account_seen is flat, never_seen (no activity recorded in the covered history; see meta.notice and meta.coverage_from), or outside_coverage.
  • Cursors are bound to their request. Pass next_cursor back with every other argument unchanged. If the snapshot a market cursor was paging is replaced, the API answers 409 (OxArchiveError.code == 409); restart without a cursor.
  • Billing and limits. Position rows are billed like trades, 1,000 rows per credit; account(), account_history(), and by_l1() are billed at the per-request minimum. Wallet routes return up to 5,000 rows per page (default 500), market routes up to 2,000 (default 100), bulk all() up to 2,000 (default 1,000), and summary series up to 168 hours per page.

Wallet Classification

Precomputed daily behavior metrics for active wallets on Hyperliquid core and HIP-3: orders, fills, cancel and fill rates, maker ratio, order sizes, cancel speed, fees, realized PnL, TWAP, priority gas and builder usage. Filter and sort server side, and page with limit and offset.

page = client.hyperliquid.wallets.classify(
    min_orders=1000,
    min_volume_usd=1_000_000,
    sort="total_volume_usd",   # default total_orders
    order="desc",
    limit=100,                 # 1 to 1000 (default 100)
    offset=0,                  # at most 100000
    date=day_ago.date(),       # daily snapshot; default yesterday (UTC)
)
print(f"{page.total} wallets match on {page.date}")
for wallet in page.wallets:
    m = wallet.metrics
    print(wallet.address, m.total_volume_usd, m.maker_ratio, m.cancel_rate)

# HIP-3 wallets, filtered by behavior
hip3 = client.hyperliquid.hip3.wallets.classify(uses_twap=True, min_cancel_rate=0.5)

# Async versions
page = await client.hyperliquid.wallets.aclassify(sort="realized_pnl_usd")

date accepts a date, a YYYY-MM-DD string, or a datetime (a datetime without a time zone is UTC). Each request costs 10 credits, with row-based metering on the wallets returned where it applies. Every metric field is optional.

Symbol Universe

Every public market across the venue families (hyperliquid, hip3, hip4, spot, lighter, rh-lighter), with the data types served for each and coverage dates overall and per data type. Use it to discover symbols before choosing a venue-specific route. The route needs no API key.

symbols = client.symbols.list()
hip3 = [s.symbol for s in symbols if s.exchange == "hip3"]
btc = next(s for s in symbols if s.exchange == "hyperliquid" and s.symbol == "BTC")
print(btc.data_types, btc.coverage_from, btc.coverage_by_type.get("trades"))

# HIP-4 entries also carry the slug, the outcome pair and a display title
outcomes = [s for s in symbols if s.exchange == "hip4" and not s.is_settled]

# Async version
symbols = await client.symbols.alist()

The whole universe is returned in one response, so fetch it once and filter locally.

Data Quality Monitoring

Monitor data coverage, incidents, latency, and SLA compliance across venue APIs. Venue scopes are hyperliquid, hip3, hip4, spot (Hyperliquid spot), lighter (Lighter mainnet), and rh-lighter (Lighter on Robinhood Chain).

# Get overall system health status
status = client.data_quality.status()
print(f"System status: {status.status}")
for exchange, info in status.exchanges.items():
    print(f"  {exchange}: {info.status}")

# Get data coverage summary for venue APIs
coverage = client.data_quality.coverage()
for exchange in coverage.exchanges:
    print(f"{exchange.exchange}:")
    for dtype, info in exchange.data_types.items():
        print(f"  {dtype}: {info.total_records:,} records, {info.completeness}% complete")

# Get symbol-specific coverage with gap detection
btc = client.data_quality.symbol_coverage("hyperliquid", "BTC")
oi = btc.data_types["open_interest"]
print(f"BTC OI completeness: {oi.completeness}%")
print(f"Historical coverage: {oi.historical_coverage}%")  # Hour-level granularity
print(f"Gaps found: {len(oi.gaps)}")
for gap in oi.gaps[:5]:
    print(f"  {gap.duration_minutes} min gap: {gap.start} -> {gap.end}")

# Check empirical data cadence (when available)
ob = btc.data_types["orderbook"]
if ob.cadence:
    print(f"Orderbook cadence: ~{ob.cadence.median_interval_seconds}s median, p95={ob.cadence.p95_interval_seconds}s")

# Time-bounded gap detection (last 7 days)
from datetime import datetime, timedelta, timezone
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
btc_7d = client.data_quality.symbol_coverage("hyperliquid", "BTC", from_time=week_ago)

# List incidents with filtering
result = client.data_quality.list_incidents(status="open")
for incident in result.incidents:
    print(f"[{incident.severity}] {incident.title}")

# Get latency metrics
latency = client.data_quality.latency()
for exchange, metrics in latency.exchanges.items():
    print(f"{exchange}: OB lag {metrics.data_freshness.orderbook_lag_ms}ms")

# Account positions freshness, one row per venue
for venue in client.data_quality.positions_freshness():
    print(venue.venue, venue.product, venue.live_age_seconds, venue.stale, venue.finalized_through)

# Get SLA compliance metrics for a specific month (both default to the current one)
sla = client.data_quality.sla(year=now.year, month=now.month)
print(f"Period: {sla.period}")
print(f"Uptime: {sla.actual.uptime}% ({sla.actual.uptime_status})")
print(f"API P99: {sla.actual.api_latency_p99_ms}ms ({sla.actual.latency_status})")

# Async versions available for all methods
status = await client.data_quality.astatus()
coverage = await client.data_quality.acoverage()

Data Quality Endpoints

Method Description
status() Overall system health and per-exchange status
coverage() Data coverage summary for venue APIs
status_coverage() The same summary from the public /v1/status/coverage route
exchange_coverage(exchange) Coverage details for a venue scope (hyperliquid, hip3, hip4, spot, lighter, rh-lighter)
symbol_coverage(exchange, symbol, *, from_time, to_time) Coverage with gap detection, cadence, and historical coverage. The symbol is sent as given, URL-encoded (xyz:TSLA, HYPE-USDC, #0)
list_incidents(...) List incidents with filtering and pagination
get_incident(incident_id) Get specific incident details
latency() Current latency metrics (WebSocket, REST, data freshness)
sla(year, month) SLA compliance metrics for a specific month
positions_freshness() Account positions freshness per venue: latest live and hourly snapshots, staleness, built_through, finalized_through

Note: Data Quality endpoints (coverage(), exchange_coverage(), symbol_coverage()) perform complex aggregation queries and may take 30-60 seconds on first request (results are cached server-side for 5 minutes). If you encounter timeout errors, create a client with a longer timeout:

client = Client(
    api_key="0xa_your_api_key",
    timeout=60.0  # 60 seconds for data quality endpoints
)

Webhooks

Push delivery of market, account and platform events. Instead of polling a route on a timer, register an HTTPS URL and 0xArchive posts to it when something happens.

Three objects, in the order you create them:

  1. An endpoint is an HTTPS URL 0xArchive posts to. Creating one returns its signing secret, once.
  2. A subscription is a rule: one event type, on one endpoint, with an optional configuration of filters, parameters and conditions.
  3. A watched address is a wallet. Event types whose scope is addresses report only on wallets on this list.
# 1. Somewhere to deliver
endpoint = client.webhooks.create_endpoint(
    "https://example.com/hooks/0xarchive",
    description="Desk alerts",
)
store_secret(endpoint.secret)  # shown once, never returned again

# 2. Size the rule before turning it on
estimate = client.webhooks.estimate(
    "market.liquidation",
    {"venue": "hyperliquid", "min_notional_usd": 250_000},
    lookback_days=7,
)
print(f"median {estimate.per_day_p50:.0f} deliveries a day, busiest day {estimate.per_day_max}")

# 3. Turn it on
sub = client.webhooks.create_subscription(
    endpoint.id,
    "market.liquidation",
    {"venue": "hyperliquid", "min_notional_usd": 250_000},
)

# 4. Send a real signed test delivery to the receiver
client.webhooks.test_endpoint(endpoint.id)

Plan limits

Webhook delivery needs a paid plan. client.webhooks.limits() reports what the plan allows and what is in use: endpoints, subscriptions, watched wallets, today's delivery budget and when it resets, and how many subscriptions are paused.

Plan Endpoints Subscriptions Watched wallets Deliveries per day
Free 0 0 0 0
Build 1 8 2 5,000
Pro 4 40 15 50,000
Scale 12 200 50 500,000
Enterprise 100 2,000 250 No daily ceiling

estimate() and dry_run() are available on every plan, Free included, so a rule can be designed and sized before there is anywhere to deliver it. They are metered like the market data they return, and an account can run six of them a minute, shared between the two. An event type whose scope is addresses previews against your watched wallets, so it needs at least one on the list.

limits = client.webhooks.limits()
print(limits.plan, limits.included)
print(limits.subscriptions.used, "of", limits.subscriptions.limit)
print(limits.deliveries_per_day.remaining, "deliveries left until", limits.deliveries_per_day.resets_at)
print(limits.paused_subscriptions.count, "paused")

Paused subscriptions

When an account reaches its daily delivery budget, its subscriptions pause and say so: status becomes "auto_paused", pause_reason is "deliveries_per_day_cap" (or "plan_no_webhooks" when the plan has no delivery), and pause_message explains what clears it. Nothing is queued while a rule is paused, and a paused rule does not restart when the budget resets. Resume it, then recover the window it missed from the REST routes:

for sub in client.webhooks.list_subscriptions():
    if sub.status == "auto_paused":
        print(sub.id, sub.pause_message, sub.suppressed_count)

# One rule, or every paused rule on the account
result = client.webhooks.resume_subscription(sub.id)
result = client.webhooks.resume_all_subscriptions()
if result.gap:
    window = result.gap.replay_window
    print(f"re-read {window.start} to {window.end}: {result.gap.note}")

gap.suppressed_count is a lower bound on what matched while paused. It is None, with counted False, when the rules were address scoped, because their occurrences were never looked at. Resuming while the budget is still spent is refused (OxArchiveError with code 409) rather than granted and paused again. Your own enabled switch is never changed by a resume.

Event types

The catalog is the only place event types are declared. Read the filters, parameters, metrics and operators from it rather than hardcoding them.

for t in client.webhooks.event_types():
    if t.live:
        print(t.type, t.scope, t.latency_class, t.description)

liq = next(t for t in client.webhooks.event_types() if t.type == "market.liquidation")
print(liq.filters)                   # accepted config keys
print(liq.params)                    # tunable parameters, with type, bounds and default
print(liq.metrics)                   # what conditions can be written against
print(liq.operators["number"])       # operators for number metrics
print(liq.cost_floor)                # the smallest occurrence the type reports
Scope Reports on Needs
public Market-wide activity Nothing extra
addresses Your watched wallets At least one watched wallet
user Your own account and platform activity Nothing extra

A type whose live is False is published but does not yet accept subscriptions.

Configuration: filters, parameters and conditions

A subscription's configuration is checked against the event type's declaration before anything is stored, so an unknown key, an undeclared parameter, an out of range value or a condition on a metric the event does not carry is refused with the reason, never dropped. Declared parameters left out are stored at their defaults, so the stored configuration comes back more complete than what was sent. Pass the configuration as a dict or as a WebhookSubscriptionConfig; the API calls it filters on subscriptions and config on the previews, and the SDK takes it as config everywhere.

from oxarchive import WebhookSubscriptionConfig, WebhookSubscriptionCondition

# Address scoped rules can only name watched wallets: add the wallet first
client.webhooks.add_address("0x6b9e773128f453f5c2c60935ee2de2cbc5390a24", label="Desk 1")

sub = client.webhooks.create_subscription(
    endpoint.id,
    "account.fill",
    WebhookSubscriptionConfig(
        addresses=["0x6b9e773128f453f5c2c60935ee2de2cbc5390a24"],
        conditions=[
            WebhookSubscriptionCondition(metric="notional_usd", op=">=", value=25_000),
            WebhookSubscriptionCondition(metric="taker", op="equal", value=True),
        ],
    ),
)

# Edit in place. A config replaces the stored one, so send the whole object.
client.webhooks.update_subscription(sub.id, config={"addresses": [...], "conditions": [...]})

# Or switch the rule off without losing it
client.webhooks.update_subscription(sub.id, enabled=False)

Conditions accept symbol spellings (>=, <, !=) as well as canonical names (greater_than_or_equal, less_than, not_equal), and are stored canonically. between and not_between take a [low, high] pair, in and not_in a list, and is_empty and is_not_empty no value. A subscription holds at most 16 conditions, all of which must hold for a delivery.

Previewing a rule

dry_run() answers "which recent occurrences would this have delivered?". estimate() answers "how often would it have fired?". Both validate the configuration exactly as create_subscription() does, so a configuration that previews cleanly subscribes cleanly. Neither stores anything or sends a delivery. Not every event type can be previewed; a type that cannot is refused with the list of those that can.

# Which ones, over a recent window (60 s to 24 h, default 1 h)
preview = client.webhooks.dry_run(
    "market.liquidation",
    {"venue": "hyperliquid", "min_notional_usd": 500_000},
    lookback_s=86_400,
    limit=100,  # 1 to 200
)
print(f"{preview.matched} matched between {preview.window.from_} and {preview.window.to}")
for occurrence in preview.occurrences[:5]:
    print(occurrence.observed_at_estimate, occurrence.data)

# How often, over 1 to 30 days (default 7)
est = client.webhooks.estimate("market.liquidation_burst", lookback_days=14)
print(f"{est.total} over {est.days} days, median {est.per_day_p50:.0f} a day ({est.basis.mode})")

# And what a different threshold would have produced
for rung in est.ladder:
    print(f"  at {rung.value:,.0f}: {rung.per_day:.1f} a day")

window.from_ is named with a trailing underscore because from is a Python keyword. window can start later than requested when a scan reached its row cap; truncated says so.

Verifying deliveries

Every delivery is signed with HMAC-SHA256. Verify it, or anyone who learns the URL can post to it.

The one rule that matters: verify the raw request body, before any JSON parser touches it. The signature covers the exact bytes that were sent, and their key order and spacing need not match what any JSON library produces, so a re-serialised dict will not verify.

import os
from oxarchive import WebhookVerifier, WebhookSignatureError

verifier = WebhookVerifier(os.environ["OXARCHIVE_WEBHOOK_SECRET"])

# Flask
@app.post("/webhooks/0xarchive")
def receive():
    try:
        event = verifier.verify(request.get_data(), request.headers)
    except WebhookSignatureError as e:
        # Answer 4xx. A 5xx asks for a retry of the same unverifiable delivery.
        app.logger.warning("rejected webhook: %s", e.reason)
        return "", 400

    if already_seen(event.id):   # delivery is at-least-once
        return "", 200
    enqueue(event.payload)       # do the work out of band
    return "", 200
Framework Raw body
Flask request.get_data()
FastAPI / Starlette await request.body()
Django request.body

Not request.get_json(), not a parsed Pydantic model, not request.POST. A proxy or gateway in front of the receiver that re-encodes JSON also breaks verification: verify before that layer, or turn it off.

Each delivery carries these headers:

Header Meaning
0xa-signature t=<unix seconds>,v1=<hex>, and during a secret rotation a second v1=
0xa-event-id Event identifier. Deduplicate on this. Stable across retries and repeat deliveries
0xa-event-type The event type, for example account.fill
content-type application/json

The signed string is the header's timestamp, a full stop, and the raw body. The event id, the event type and the other headers are not signed, so the replay window plus deduplication on 0xa-event-id is the defence against replays. Serve the receiver over HTTPS.

WebhookVerifier accepts every v1= in the header, compares signatures in constant time, and enforces a replay window of 5 minutes by default. Every attempt, including retries and repeat deliveries, is signed with a fresh timestamp, so a legitimate delivery is never older than network and clock skew:

verifier = WebhookVerifier(secret, tolerance_seconds=120)  # tighten it if you like

A failed check raises WebhookSignatureError with a reason: missing_header, malformed_header, timestamp_out_of_tolerance, no_secrets or signature_mismatch. is_valid() returns a bool instead. For a one-off check without holding state:

from oxarchive import verify_webhook

event = verify_webhook(raw_body_bytes, headers, secret)

Rotating a secret

rotate_secret() issues a new secret and keeps the previous one verifying for 24 hours. During the overlap every delivery carries two v1= signatures, one under each secret, so a receiver holding either keeps verifying.

rotated = client.webhooks.rotate_secret(endpoint.id)

verifier.add_secret(rotated.secret)   # now accepts both
deploy()                              # ship the new secret to every replica
verifier.remove_secret(old_secret)    # before the 24 hours are up

Only one previous secret is kept: rotating twice inside the window retires the original at once. A verifier written by hand must read every v1= in the header, not just the first, or it fails for up to 24 hours after each rotation.

Retries and failures

Behaviour Detail
Success HTTP 200 to 299. Redirects are not followed, so a 301 or 302 is a failure
Timeout 10 seconds per attempt. Acknowledge at once and process asynchronously
Retry schedule 5 s, 30 s, 2 min, 10 min, 1 h, then hourly, for up to 24 hours
Retry-After Honoured on a failed attempt, between 1 second and 5 minutes. A 429 does not count against the endpoint's health
Auto-disable After 10 consecutive failures spanning at least 6 hours (status == "auto_disabled")
Recovery Fix the receiver, then client.webhooks.enable_endpoint(endpoint_id)
# What happened
for d in client.webhooks.list_deliveries(endpoint.id, limit=100):
    print(d.event_type, d.state, d.attempts, d.last_status_code, d.last_error)

# Send one again after fixing the receiver. The event id is unchanged, so a
# receiver that already processed it can deduplicate it.
client.webhooks.redeliver(delivery_id)

A test delivery and a repeat delivery are real signed deliveries: they count against today's budget. A repeat to a switched-off endpoint is refused (code 409), since nothing would be sent; re-enable the endpoint first.

Watched addresses

client.webhooks.add_address("0x6b9e773128f453f5c2c60935ee2de2cbc5390a24", label="Desk 1")
watched = client.webhooks.list_addresses()
client.webhooks.delete_address(watched[0].id)

Adding a wallet that is already watched changes nothing and does not count against the cap again. Addresses are stored lowercase. Hyperliquid bridge system addresses are refused: such an address is a counterparty to every bridge movement of its token rather than an account. Removing a wallet leaves subscriptions that named it unchanged, so edit those rules too.

Every webhook method

Method Route
event_types() GET /v1/webhooks/event-types
limits() GET /v1/webhooks/limits
list_endpoints() GET /v1/webhooks/endpoints
create_endpoint(url, description="") POST /v1/webhooks/endpoints
delete_endpoint(endpoint_id) DELETE /v1/webhooks/endpoints/{id}
enable_endpoint(endpoint_id) POST /v1/webhooks/endpoints/{id}/enable
rotate_secret(endpoint_id) POST /v1/webhooks/endpoints/{id}/rotate
test_endpoint(endpoint_id) POST /v1/webhooks/endpoints/{id}/test
list_deliveries(endpoint_id, limit=None) GET /v1/webhooks/endpoints/{id}/deliveries
redeliver(delivery_id) POST /v1/webhooks/deliveries/{id}/redeliver
list_subscriptions() GET /v1/webhooks/subscriptions
create_subscription(endpoint_id, event_type, config=None) POST /v1/webhooks/subscriptions
update_subscription(subscription_id, config=None, enabled=None) PATCH /v1/webhooks/subscriptions/{id}
delete_subscription(subscription_id) DELETE /v1/webhooks/subscriptions/{id}
resume_subscription(subscription_id) POST /v1/webhooks/subscriptions/{id}/resume
resume_all_subscriptions() POST /v1/webhooks/subscriptions/resume
dry_run(event_type, config=None, lookback_s=None, limit=None) POST /v1/webhooks/subscriptions/dry-run
estimate(event_type, config=None, lookback_days=None) POST /v1/webhooks/subscriptions/estimate
list_addresses() GET /v1/webhooks/addresses
add_address(address, label="") POST /v1/webhooks/addresses
delete_address(address_id) DELETE /v1/webhooks/addresses/{id}

Every method has an async version prefixed with a: await client.webhooks.aestimate(...), await client.webhooks.acreate_endpoint(...), and so on. Management calls cost no credits; the two previews are metered like market data.

Web3 Authentication

Get API keys programmatically using an Ethereum wallet. No browser or email required.

Free includes every market, route, schema, and served depth, with history limited to the most recent rolling 30 days and a maximum 30-day span per request or replay. Build and above keep the full retained archive. Plans gate capacity and Free's 30-day history window, not route families, schemas, or served depth. The one exception is the live mempool WebSocket channel, included with the Pro, Scale and Enterprise plans. See Pricing for plan capacity.

Free Tier (SIWE)

# pip install eth-account
from eth_account import Account
from eth_account.messages import encode_defunct

acct = Account.from_key("0xYOUR_PRIVATE_KEY")

# 1. Get SIWE challenge
challenge = client.web3.challenge(acct.address)

# 2. Sign with personal_sign (EIP-191)
signable = encode_defunct(text=challenge.message)
signed = acct.sign_message(signable)
signature = signed.signature.hex()
if not signature.startswith("0x"):
    signature = "0x" + signature

# 3. Submit → receive API key
result = client.web3.signup(message=challenge.message, signature=signature)
print(result.api_key)  # "0xa_..."

Paid Tier (x402 USDC on Base)

# pip install eth-account
import json
import time
import base64
import secrets
from eth_account import Account
from eth_account.messages import encode_typed_data

acct = Account.from_key("0xYOUR_PRIVATE_KEY")

USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"

# 1. Get pricing
quote = client.web3.subscribe_quote("build")
# quote.amount = "49000000" ($49 USDC), quote.pay_to = "0x..."

# 2. Build & sign EIP-3009 transferWithAuthorization
nonce_bytes = secrets.token_bytes(32)
valid_after = 0
valid_before = int(time.time()) + 3600

domain = {
    "name": "USD Coin",
    "version": "2",
    "chainId": 8453,
    "verifyingContract": USDC_ADDRESS,
}
types = {
    "TransferWithAuthorization": [
        {"name": "from", "type": "address"},
        {"name": "to", "type": "address"},
        {"name": "value", "type": "uint256"},
        {"name": "validAfter", "type": "uint256"},
        {"name": "validBefore", "type": "uint256"},
        {"name": "nonce", "type": "bytes32"},
    ],
}
message = {
    "from": acct.address,
    "to": quote.pay_to,
    "value": int(quote.amount),
    "validAfter": valid_after,
    "validBefore": valid_before,
    "nonce": "0x" + nonce_bytes.hex(),
}

signable = encode_typed_data(domain, types, message)
signed = acct.sign_message(signable)
signature = signed.signature.hex()
if not signature.startswith("0x"):
    signature = "0x" + signature

# 3. Build x402 payment envelope and base64-encode
payment_payload = base64.b64encode(json.dumps({
    "x402Version": 2,
    "payload": {
        "signature": signature,
        "authorization": {
            "from": acct.address,
            "to": quote.pay_to,
            "value": quote.amount,
            "validAfter": str(valid_after),
            "validBefore": str(valid_before),
            "nonce": "0x" + nonce_bytes.hex(),
        },
    },
}).encode()).decode()

# 4. Submit payment → receive API key + subscription
sub = client.web3.subscribe("build", payment_signature=payment_payload)
print(sub.api_key, sub.tier, sub.expires_at)

Key Management

# List and revoke keys (requires a fresh SIWE signature)
keys = client.web3.list_keys(message=challenge.message, signature=signature)
client.web3.revoke_key(message=challenge.message, signature=signature, key_id=keys.keys[0].id)

Legacy API (Deprecated)

The following legacy methods are deprecated and will be removed in v2.0. They default to Hyperliquid data:

# Deprecated - use client.hyperliquid.orderbook.get() instead
orderbook = client.orderbook.get("BTC")

# Deprecated - use client.hyperliquid.trades.list() instead
trades = client.trades.list("BTC", start=..., end=...)

WebSocket Client

The WebSocket client supports live subscriptions for supported Hyperliquid and Lighter channels (mainnet and Robinhood Chain) and historical replay. For file-based historical exports, use the Data Catalog.

WebSocket bulk streaming has been discontinued. For large dataset downloads, use the S3 Parquet bulk export in the Data Catalog. The stream(), multi_stream(), and stream_stop() methods remain for compatibility but are deprecated: each call emits a DeprecationWarning, and the server answers with an error message (a WsError on on_message) instead of data. The on_batch, on_stream_start, on_stream_progress, and on_stream_complete handler setters are deprecated too: setting one emits a DeprecationWarning, and the handler is never called.

Lighter supports live subscriptions on lighter_orderbook, lighter_trades, lighter_open_interest, and lighter_funding at wss://api.0xarchive.io/ws (the client default). lighter_candles and lighter_l3_orderbook remain replay-only. All six Lighter channels support historical replay, and replay messages have the same shapes as the live ones.

Lighter on Robinhood Chain supports live subscriptions on rh_lighter_orderbook, rh_lighter_trades, rh_lighter_open_interest, and rh_lighter_funding at wss://api.0xarchive.io/ws only, with the same message shapes as the mainnet Lighter channels. rh_lighter_candles is replay-only. All five channels support historical replay.

The mempool channel (pending Hyperliquid transactions) is served only at wss://stream.0xarchive.io/ws and is included with the Pro, Scale and Enterprise plans. See Pending Transactions (Mempool).

Every connection selects API version 2026-10-01 (the version connection parameter). Server errors arrive on on_message as WsError messages with a stable error_code (see Error Handling): for example unsupported_for_venue when a channel does not offer the requested mode, slow_consumer when the connection fell behind and messages were dropped, and endpoint_unsupported when the endpoint does not serve the channel.

import asyncio
from oxarchive import OxArchiveWs, WsOptions

ws = OxArchiveWs(WsOptions(api_key="0xa_your_api_key"))

Real-time Streaming

Subscribe to live market data from Hyperliquid.

import asyncio
from oxarchive import OxArchiveWs, WsOptions

async def main():
    ws = OxArchiveWs(WsOptions(api_key="0xa_your_api_key"))

    # Set up handlers
    ws.on_open(lambda: print("Connected"))
    ws.on_close(lambda code, reason: print(f"Disconnected: {code}"))
    ws.on_error(lambda e: print(f"Error: {e}"))

    # Connect
    await ws.connect()

    # Subscribe to channels
    ws.subscribe_orderbook("BTC")
    ws.subscribe_orderbook("ETH")
    ws.subscribe_trades("BTC")
    ws.subscribe_all_tickers()

    # Handle real-time data
    ws.on_orderbook(lambda coin, data: print(f"{coin}: {data.mid_price}"))
    ws.on_trades(lambda coin, trades: print(f"{coin}: {len(trades)} trades"))

    # Keep running
    await asyncio.sleep(60)

    # Unsubscribe and disconnect
    ws.unsubscribe_orderbook("ETH")
    await ws.disconnect()

asyncio.run(main())

Hyperliquid open_interest and funding also stream live, in addition to historical replay. Subscribe with ws.subscribe("open_interest", "BTC") or ws.subscribe("funding", "BTC"). Each update's data holds coin and a ctx object with fields such as openInterest, funding, markPx, and oraclePx. These channels have no typed callback, so their updates arrive on on_message as WsData messages.

Full-Depth L2 Book

orderbook_full (Hyperliquid core) and hip3_orderbook_full (HIP-3) stream every price level of the book, not just the top 20, aggregated from the order-level book. A subscription opens with an l4_snapshot of the whole book, followed by l4_batch messages of changed levels. Receive them with on_l4_snapshot and on_l4_batch; the channel argument tells them apart from the L4 channels. Stored history replays over the same messages (see L4 and Full-Depth Replay) and is also served over REST by l2_orderbook.history() and l2_orderbook.diffs().

book = {"B": {}, "A": {}}

def on_snapshot(channel, coin, message):
    data = message["data"]
    book["B"] = {level["px"]: level for level in data["bids"]}
    book["A"] = {level["px"]: level for level in data["asks"]}
    print(coin, data["bid_count"], data["ask_count"], data["mid_price"])

def on_batch(channel, coin, levels):
    for level in levels:  # apply in the order received
        side = book[level["side"]]
        if level["sz"] == 0:
            side.pop(level["px"], None)  # the level was removed
        else:
            side[level["px"]] = level

ws.on_l4_snapshot(on_snapshot)
ws.on_l4_batch(on_batch)
ws.subscribe_orderbook_full("BTC")
ws.subscribe_hip3_orderbook_full("xyz:TSLA")  # case-sensitive

Each level is {"px", "sz", "n"} with numeric values; a changed level also carries side ("B" or "A") and bn, the block it was applied in. The snapshot's data also holds bid_count, ask_count, total_bid_size, total_ask_size, mid_price, spread, spread_bps and is_crossed, and last_block_number on the message is the block the snapshot reflects.

Live Lighter Data

Live Lighter messages use the same data envelope as Hyperliquid live data. Symbols are the ones returned by client.lighter.instruments.list(); they are case-insensitive on subscribe and echoed uppercase. Live Lighter data is served at wss://api.0xarchive.io/ws, the client default; wss://stream.0xarchive.io/ws does not serve Lighter channels and answers a Lighter subscribe with an error pointing to wss://api.0xarchive.io/ws. It is available on every plan and metered per message like Hyperliquid live data, with the same per-plan subscription and connection limits and the limit of 10 subscribe operations per second.

import asyncio
from oxarchive import OxArchiveWs, WsError, WsOptions

def on_book(coin, book):
    print(f"Lighter {coin} mid {book.mid_price} ({len(book.bids)} bids, {len(book.asks)} asks)")

def on_trades(coin, legs):
    # Two legs per trade: count distinct trade ids, not legs
    print(f"Lighter {coin}: {len({leg.trade_id for leg in legs})} trades")

def on_context(channel, coin, ctx):
    print(f"Lighter {coin} OI {ctx.open_interest} funding {ctx.funding_rate} mark {ctx.mark_price}")

def on_message(msg):
    if isinstance(msg, WsError):  # lag notices carry error_code "slow_consumer"
        print(f"Server: {msg.error_code}: {msg.message}")

async def main():
    ws = OxArchiveWs(WsOptions(api_key="0xa_your_api_key"))

    # Dedicated Lighter handlers keep Lighter BTC apart from Hyperliquid BTC
    ws.on_lighter_orderbook(on_book)
    ws.on_lighter_trades(on_trades)
    ws.on_lighter_market_context(on_context)
    ws.on_message(on_message)

    await ws.connect()

    ws.subscribe_lighter_orderbook("BTC")                   # one book per second (default)
    ws.subscribe_lighter_orderbook("ETH", interval_ms=250)  # at most one book every 250 ms
    ws.subscribe_lighter_trades("BTC")
    ws.subscribe_lighter_funding("BTC")                     # same message as lighter_open_interest

    await asyncio.sleep(60)
    await ws.disconnect()

asyncio.run(main())

The generic form works too: ws.subscribe("lighter_orderbook", "BTC", interval_ms=500).

lighter_orderbook: every message is a full top-20 book per side, not a diff. Bids and asks are ordered best first, px and sz are decimal strings exactly as Lighter publishes them, and n is always 1 because Lighter does not publish per-level order counts. The server sends the newest book at most once per interval: once per second by default, or at the interval_ms you pass (100 to 5000, lighter_orderbook only; the SDK raises ValueError otherwise). Each book sent is one metered message. On subscribe, the current book is sent right away when one is available; illiquid markets can go minutes without a change. Books arrive on on_lighter_orderbook when it is set, otherwise on on_orderbook, as OrderBook records.

lighter_trades: each trade arrives as two legs, one per side, sharing trade_id. side is "A" for the ask side and "B" for the bid side, crossed=True marks the taker leg, account_index is that side's Lighter account index (a string), order_id is that side's order id, and start_position is that account's signed position before the trade. fee, fee_token, closed_pnl, and direction are always None in live messages. Count trades by distinct trade_id, not by list length, and compute volume from one leg per trade_id. Trades arrive on on_lighter_trades when it is set, otherwise on on_trades, as Trade records; the raw legs are modelled by LighterLiveTrade. Live trades are preliminary. The finalized record, with fields the live stream does not carry (such as fees), is served by client.lighter.trades.list(), which returns reconciled trades only; client.lighter.trades.recent() serves the preliminary tier.

lighter_open_interest and lighter_funding: both channels carry the same message, updated as Lighter publishes it (about once per second per market), with the latest values sent on subscribe when available. on_lighter_market_context receives (channel, coin, LighterMarketContext):

Field Wire key Meaning
open_interest openInterest Lighter's reported open interest (same value as client.lighter.open_interest.current())
funding_rate funding Current funding rate as a fraction (Lighter publishes percent; divided by 100, same as REST funding_rate)
premium premium Premium as a fraction
mark_price markPx Mark price
oracle_price oraclePx Lighter's index price
mid_price midPx Mid price
day_ntl_volume dayNtlVlm 24h quote volume
day_base_volume dayBaseVlm 24h base volume
prev_day_price prevDayPx Derived from the last trade price and Lighter's 24h percent change
impact_prices impactPxs Always None (Lighter has no impact prices)

Falling behind: if your connection falls behind lighter_trades, lighter_open_interest, or lighter_funding, the server sends an error notice with error_code == "slow_consumer" (for example Dropped ~N live lighter_trades messages for BTC: ...) and continues. If the lag persists, it stops that subscription with Stopped the lighter_trades stream for BTC: your connection is too slow to keep up. Re-subscribe to resume.; call the subscribe method again to resume. lighter_orderbook always sends the newest book; a book skipped between intervals loses nothing because each book is a full state.

Live Lighter on Robinhood Chain Data

The Robinhood Chain deployment of Lighter streams live on its own channels: rh_lighter_orderbook, rh_lighter_trades, rh_lighter_open_interest, and rh_lighter_funding. Each message has exactly the shape of its mainnet counterpart described above, rh_lighter_orderbook accepts interval_ms (100 to 5000, default one book per second), and the metering, limits, and lag notices are the same. Symbols are those returned by client.rh_lighter.instruments.list(): uppercase perps (BTC) and dashed spot markets (AAPL-USDG), case-insensitive on subscribe. These channels are served at wss://api.0xarchive.io/ws, the client default, and not at wss://stream.0xarchive.io/ws.

Robinhood Chain symbols collide with mainnet ones, so these messages have their own handlers: on_rh_lighter_orderbook, on_rh_lighter_trades, and on_rh_lighter_market_context. They never reach the mainnet on_lighter_* handlers. Without a dedicated handler, books and trades fall back to on_orderbook and on_trades; WsData.channel on on_message always names the channel.

ws.on_rh_lighter_orderbook(lambda coin, book: print(f"Robinhood Chain {coin} mid {book.mid_price}"))
ws.on_rh_lighter_trades(lambda coin, legs: print(f"Robinhood Chain {coin}: {len({l.trade_id for l in legs})} trades"))
ws.on_rh_lighter_market_context(lambda channel, coin, ctx: print(f"{coin} OI {ctx.open_interest}"))

ws.subscribe_rh_lighter_orderbook("AAPL-USDG")                 # one book per second (default)
ws.subscribe_rh_lighter_orderbook("BTC", interval_ms=500)
ws.subscribe_rh_lighter_trades("BTC")
ws.subscribe_rh_lighter_funding("BTC")                         # same message as rh_lighter_open_interest

# Generic form
ws.subscribe("rh_lighter_orderbook", "BTC", interval_ms=250)

Live trades are preliminary; client.rh_lighter.trades.list() serves the finalized record.

Pending Transactions (Mempool)

The mempool channel streams signed Hyperliquid transactions (orders, cancels, modifies, TWAPs, leverage changes, transfers and every other action type) as our Hyperliquid node receives them from its peers, before they are included in a block. It covers every Hyperliquid product: perps, HIP-3, HIP-4 and spot.

  • Live only. Pending transactions are never stored. There is no replay, history, REST route or export; replay() raises ValueError for it before sending.
  • One endpoint. It is served only at wss://stream.0xarchive.io/ws (oxarchive.websocket.STREAM_WS_URL), with the same API key and protocol. At wss://api.0xarchive.io/ws, the client default, a subscribe is answered with a WsError whose error_code is endpoint_unsupported. Use a separate client for the channels served at the default endpoint.
  • Plans. It is included with the Pro, Scale and Enterprise plans; on other plans a subscribe is answered with error_code == "forbidden". Every other channel stays on every plan, Free included. Each data message is metered like any other WebSocket message.
import asyncio
from oxarchive import OxArchiveWs, WsError, WsOptions
from oxarchive.websocket import STREAM_WS_URL

async def main():
    ws = OxArchiveWs(WsOptions(api_key="0xa_your_api_key", ws_url=STREAM_WS_URL))

    def on_mempool(symbol, items):
        for item in items:
            print(symbol, item.received_at, item.action["type"], item.symbols)

    def on_message(msg):
        if isinstance(msg, WsError):
            print(msg.error_code, msg.message)

    ws.on_mempool(on_mempool)
    ws.on_message(on_message)
    await ws.connect()

    ws.subscribe_mempool("BTC")       # actions that reference BTC
    ws.subscribe_mempool("xyz:TSLA")  # HIP-3; spot "HYPE-USDC", HIP-4 "#49720"
    # ws.subscribe_mempool()          # pending transactions our node receives (unfiltered)

    await asyncio.sleep(60)
    await ws.disconnect()

asyncio.run(main())

symbol is optional on this channel only. Without it you receive every pending transaction our Hyperliquid node receives; with it, every action whose asset ids include that market, whole (an order batch that touches BTC and ETH reaches both subscriptions). Symbols are spelled as everywhere else: perps BTC, HIP-3 xyz:TSLA, spot HYPE-USDC (HYPE/USDC is also accepted) and HIP-4 #49720. An unknown symbol is answered with invalid_symbol. The subscribed acknowledgement carries the canonical symbol, or None for the unfiltered stream.

The server sends one message per batch of transactions as it arrives. on_mempool receives the subscription's symbol (None when unfiltered) and one MempoolItem per signed action; on_message receives the whole message as WsMempoolData.

Field Description
received_at When our node received the transaction: an RFC 3339 UTC string with nanosecond precision. Not a block time.
received_at_ms The same time in Unix milliseconds
symbols Markets the action's asset ids reference, in first-seen order without repeats. Empty for actions with no market, such as transfers, noop, scheduleCancel and validator actions
action The action exactly as signed, in Hyperliquid's exchange-action format: asset ids (a or asset) rather than symbols, prices and sizes as strings, keys in the order sent
nonce The action's nonce
vault_address The vault or subaccount the action acts for, or None
expires_after_ms The action's expiresAfter in Unix milliseconds, or None
signature MempoolSignature with r, s and v. The signer's address is not included

action["type"] names the action, for example order, cancel, cancelByCloid, modify, batchModify, scheduleCancel, twapOrder, twapCancel, updateLeverage, updateIsolatedMargin, noop, evmRawTx, or a transfer such as usdSend, spotSend, usdClassTransfer or sendAsset. Hyperliquid adds action types, so handle types you do not recognise.

A pending transaction is not an executed one: it can still be rejected, expire or never land in a block. The same signed action can occasionally arrive twice; deduplicate on signature if that matters to you.

Volume and limits: the unfiltered stream is several megabytes per second before compression. The client negotiates permessage-deflate compression by default; subscribe with a symbol where you can. Unfiltered subscriptions are limited server-wide, and when they are at capacity a subscribe without a symbol is answered with rate_limited; symbol subscriptions are not capped this way. A connection that reads too slowly is disconnected, as on any channel, and the usual per-connection limits apply (subscriptions per plan, 10 subscribe operations per second). If the feed is temporarily unavailable, a subscribe is answered with upstream_unavailable.

Historical Replay

Replay historical data with timing preserved. Perfect for backtesting.

Important: Replay data is delivered via on_historical_data(), NOT on_trades() or on_orderbook(). The real-time callbacks only receive live market data from subscriptions.

import asyncio
import time
from oxarchive import OxArchiveWs, WsOptions

async def main():
    ws = OxArchiveWs(WsOptions(api_key="ox_..."))

    # Handle replay data - this is where historical records arrive
    ws.on_historical_data(lambda coin, ts, data:
        print(f"{ts}: {data['mid_price']}")
    )

    # Replay lifecycle events
    ws.on_replay_start(lambda ch, coin, start, end, speed:
        print(f"Starting replay: {ch}/{coin} at {speed}x")
    )

    ws.on_replay_complete(lambda ch, coin, sent:
        print(f"Replay complete: {sent} records")
    )

    await ws.connect()

    # Start replay at 10x speed
    await ws.replay(
        "orderbook", "BTC",
        start=int(time.time() * 1000) - 86400000,  # 24 hours ago
        end=int(time.time() * 1000),                # End of bounded replay window
        speed=10                                     # Optional, defaults to 1x
    )

    # Lighter replay with granularity. Replay messages have the same shapes
    # as the live Lighter messages.
    await ws.replay(
        "lighter_orderbook", "BTC",
        start=int(time.time() * 1000) - 86400000,
        end=int(time.time() * 1000),
        speed=10,
        granularity="10s"  # Options: 'checkpoint', '30s', '10s', '1s', 'tick'
    )

    # Handle tick-level data (granularity='tick')
    ws.on_historical_tick_data(lambda coin, checkpoint, deltas:
        print(f"Checkpoint: {len(checkpoint['bids'])} bids, Deltas: {len(deltas)}")
    )

    # Control playback
    await ws.replay_pause()
    await ws.replay_resume()
    await ws.replay_seek(int(time.time() * 1000) - 43_200_000)  # Jump to 12 hours ago
    await ws.replay_stop()

asyncio.run(main())

on_replay_data receives every replayed record together with its channel: (channel, coin, timestamp, record). Lighter and Robinhood Chain records are decoded with the same parsers as live messages: an OrderBook on lighter_orderbook, a list[Trade] (one leg per message) on lighter_trades and a LighterMarketContextUpdate on lighter_open_interest and lighter_funding, and likewise for the rh_lighter_* channels. Other channels' records are passed through as sent. decode_lighter_payload(channel, coin, data) applies the same decoding to a replay_snapshot.

def on_record(channel, coin, timestamp, record):
    if channel == "lighter_trades":
        for leg in record:  # list[Trade]
            print(timestamp, leg.side, leg.price, leg.size, leg.account_index)
    elif channel == "lighter_orderbook":
        print(timestamp, record.mid_price)  # OrderBook

ws.on_replay_data(on_record)
await ws.replay("lighter_trades", "BTC", start=start_ms, end=end_ms, speed=10)

Replay only runs on channels that replay (see Available Channels): replay() and multi_replay() raise ValueError before sending for a live-only channel such as ticker, spot_orderbook or spot_trades.

L4 and Full-Depth Replay

Every L4 channel replays: l4_diffs and l4_orders (Hyperliquid core), hip3_l4_diffs and hip3_l4_orders, hip4_l4_diffs and hip4_l4_orders, and spot_l4_diffs and spot_l4_orders. So do the full-depth L2 channels orderbook_full and hip3_orderbook_full. Replay is available from each product's first L4 checkpoint (available_from in client.capabilities(); a coin listed later starts at its own first checkpoint).

These replays are bulk. A replay begins with one typed WsL4Snapshot from the nearest checkpoint at or before start and continues with ordered WsL4Batch messages until end, as fast as the connection takes them: speed is ignored, replay_seek() is refused (restart the replay at the new start instead), and each replays on its own, never inside multi_replay(). Apply each batch in the order received; use the snapshot's last_block_number with each event's block/sequence fields as the checkpoint boundary. Full-depth batches carry changed price levels {"side", "px", "sz", "n", "bn"}, exactly like the live channel. The dedicated on_l4_snapshot and on_l4_batch callbacks keep their raw payload shapes; on_message receives the typed envelopes.

from oxarchive import WsL4Batch, WsL4Snapshot


def on_message(message):
    if isinstance(message, WsL4Snapshot):
        rebuild_book_from_snapshot(message.data)
    elif isinstance(message, WsL4Batch):
        for event in message.data:
            apply_l4_event(event)


ws.on_message(on_message)
await ws.replay("l4_diffs", "BTC", start=..., end=...)
await ws.replay("hip3_l4_orders", "xyz:TSLA", start=..., end=...)
await ws.replay("spot_l4_diffs", "PURR-USDC", start=..., end=...)
await ws.replay("orderbook_full", "ETH", start=..., end=...)

Gap Detection

During historical replay, the server automatically detects gaps in the data and notifies the client. This helps identify periods where data may be missing.

import asyncio
from oxarchive import OxArchiveWs, WsOptions

async def main():
    ws = OxArchiveWs(WsOptions(api_key="ox_..."))

    # Handle gap notifications during replay
    def handle_gap(channel, coin, gap_start, gap_end, duration_minutes):
        print(f"Gap detected in {channel}/{coin}:")
        print(f"  From: {gap_start}")
        print(f"  To: {gap_end}")
        print(f"  Duration: {duration_minutes} minutes")

    ws.on_gap(handle_gap)

    await ws.connect()

    # Start replay - gaps will be reported via on_gap callback
    await ws.replay(
        "orderbook", "BTC",
        start=int(time.time() * 1000) - 86400000,
        end=int(time.time() * 1000),
        speed=10
    )

asyncio.run(main())

Gap thresholds vary by channel:

  • orderbook, candles, liquidations: 2 minutes
  • trades: 60 minutes (trades can naturally have longer gaps during low activity periods)

WebSocket Configuration

ws = OxArchiveWs(WsOptions(
    api_key="0xa_your_api_key",
    ws_url="wss://api.0xarchive.io/ws",  # Optional; STREAM_WS_URL for mempool
    auto_reconnect=True,                  # Auto-reconnect on disconnect (default: True)
    reconnect_delay=1.0,                  # Initial reconnect delay in seconds (default: 1.0)
    max_reconnect_attempts=10,            # Max reconnect attempts (default: 10)
    ping_interval=30.0,                   # Keep-alive ping interval in seconds (default: 30.0)
))

Available Channels

The tables below mirror client.capabilities(). The same table ships in the SDK as oxarchive.websocket.WS_CHANNELS: subscribe() raises ValueError before sending for a channel without live data, replay() for a channel without replay, and multi_replay() also for the bulk L4 and full-depth channels, which replay on their own.

from oxarchive.websocket import WS_CHANNELS

spec = WS_CHANNELS["spot_l4_diffs"]
print(spec.venue, spec.live, spec.replay, spec.bulk_replay)  # spot True True True

spec = WS_CHANNELS["mempool"]
print(spec.ws_endpoint, spec.plans)  # wss://stream.0xarchive.io/ws ('pro', 'scale', 'enterprise')

Hyperliquid Channels

Channel Description Requires Coin Live Subscription Historical Replay
orderbook L2 order book updates Yes Yes Yes
trades Trade/fill updates Yes Yes Yes
candles OHLCV candle data Yes No Yes
liquidations Liquidation events (2025-12-22+) Yes Yes Yes
open_interest Open interest snapshots Yes Yes Yes
funding Funding rate records Yes Yes Yes
ticker Price and 24h volume Yes Yes No
all_tickers All market tickers No Yes No
l4_diffs L4 orderbook diffs with user attribution Yes Yes Yes (bulk)
l4_orders Order lifecycle events with user attribution Yes Yes Yes (bulk)
orderbook_full Full-depth L2 order book: every price level, then changed levels Yes Yes Yes (bulk)
mempool Pending transactions on every Hyperliquid product, before they are in a block (Pro, Scale and Enterprise plans; served only at wss://stream.0xarchive.io/ws) No (optional filter) Yes No

Bulk replays open with an l4_snapshot and continue with ordered l4_batch messages; see L4 and Full-Depth Replay.

Note: liquidations and hip3_liquidations now stream live. Each item shares the trades wire shape (a fill row with is_liquidation: true). The SDK exposes a typed on_liquidations callback that decodes them into :class:Liquidation records.

HIP-3 Builder Perps Channels

Channel Description Requires Coin Live Subscription Historical Replay
hip3_orderbook HIP-3 L2 order book snapshots Yes Yes Yes
hip3_trades HIP-3 trade/fill updates Yes Yes Yes
hip3_candles HIP-3 OHLCV candle data Yes No Yes
hip3_open_interest HIP-3 open interest snapshots Yes Yes Yes
hip3_funding HIP-3 funding rate records Yes Yes Yes
hip3_liquidations HIP-3 liquidation events (2025-12-22+) Yes Yes Yes
hip3_l4_diffs HIP-3 L4 orderbook diffs Yes Yes Yes (bulk)
hip3_l4_orders HIP-3 order lifecycle events Yes Yes Yes (bulk)
hip3_orderbook_full HIP-3 full-depth L2 order book: every price level, then changed levels Yes Yes Yes (bulk)

Note: Stored full-depth history is also served over REST by l2_orderbook.history() and l2_orderbook.diffs().

Note: HIP-3 coins are case-sensitive (e.g., xyz:TSLA, xyz:XYZ100). Do not uppercase them.

HIP-4 Outcome Market Channels

Channel Description Requires Coin Live Subscription Historical Replay
hip4_orderbook HIP-4 L2 order book snapshots Yes No Yes
hip4_trades HIP-4 trade/fill updates Yes Yes Yes
hip4_open_interest HIP-4 per-side OI ticks Yes No Yes
hip4_l4_diffs HIP-4 L4 orderbook diffs Yes Yes Yes (bulk)
hip4_l4_orders HIP-4 order lifecycle events Yes Yes Yes (bulk)

hip4_orderbook and hip4_open_interest replay stored history but do not stream live, so subscribe() raises ValueError for them before sending. For the live book, subscribe to hip4_l4_diffs; for the current book and open interest, use client.hyperliquid.hip4.orderbook and client.hyperliquid.hip4.open_interest over REST.

HIP-4 has no funding or liquidation channels, and no candle channel: HIP-4 candles are served over REST. Subscribe with the raw #N coin form (N = 10 * outcome_id + side, where side 0 is Yes and 1 is No); the SDK passes it through unmodified in the JSON body. When a market settles, the server pushes a single outcome_settled frame and proactively unsubscribes the client from every hip4_* channel for that coin. Use OxArchiveWs.on_outcome_settled() to handle the event:

# The Yes side of an open BTC price outcome
coin = next(
    s.symbol for s in client.symbols.list()
    if s.exchange == "hip4" and not s.is_settled
    and (s.slug or "").startswith("btc-above-") and "-yes-" in s.slug
)

def on_settled(msg):
    print(f"Outcome {msg.outcome_id} side {msg.side} settled at {msg.settlement_value}")
    # Server has already auto-unsubscribed; the SDK mirrors that locally.

ws.on_outcome_settled(on_settled)
ws.subscribe_hip4_trades(coin)
ws.subscribe_hip4_l4_diffs(coin)

Hyperliquid Spot Channels

Channel Description Requires Coin Live Subscription Historical Replay
spot_orderbook Spot L2 order book snapshots Yes Yes No
spot_trades Spot trade/fill updates Yes Yes No
spot_twap Spot TWAP status updates (served over REST only: client.spot.twap) Yes No No
spot_l4_diffs Spot L4 orderbook diffs Yes Yes Yes (bulk)
spot_l4_orders Spot L4 order lifecycle events Yes Yes Yes (bulk)

Note: Spot symbols are dashed canonical (HYPE-USDC, PURR-USDC); the server resolves dashed to wire format internally. The existing on_orderbook and on_trades typed callbacks fire for spot_orderbook and spot_trades. Spot TWAP statuses do not stream: subscribe_spot_twap() raises ValueError before sending; read them with client.spot.twap.history() or client.spot.twap.by_user().

ws = OxArchiveWs(WsOptions(api_key="ox_..."))
await ws.connect()
ws.on_orderbook(lambda coin, ob: print(f"{coin} mid: {ob.mid_price}"))
ws.subscribe_spot_orderbook("HYPE-USDC")
ws.subscribe_spot_trades("HYPE-USDC")

Lighter Channels

Channel Description Requires Coin Live Subscription Historical Replay
lighter_orderbook Lighter L2 order book (live: full top-20 book per side, 1 per second by default, interval_ms 100 to 5000) Yes Yes Yes
lighter_trades Lighter trade/fill updates (live: two legs per trade) Yes Yes Yes
lighter_candles Lighter OHLCV candle data Yes No Yes
lighter_open_interest Lighter open interest (live: market context, same message as lighter_funding) Yes Yes Yes
lighter_funding Lighter funding rates (live: market context, same message as lighter_open_interest) Yes Yes Yes
lighter_l3_orderbook Lighter L3 order-level orderbook Yes No Yes

Live Lighter subscriptions are served at wss://api.0xarchive.io/ws. Replay messages of lighter_orderbook, lighter_trades, lighter_open_interest and lighter_funding have the shapes of the live messages described under Live Lighter Data above (a replayed trade message holds one leg); on_replay_data decodes them. Current Lighter data is also available through the REST resources, and historical Lighter data through REST, WebSocket replay, or exports.

Lighter on Robinhood Chain Channels

Channel Description Requires Coin Live Subscription Historical Replay
rh_lighter_orderbook L2 order book (live: full top-20 book per side, 1 per second by default, interval_ms 100 to 5000) Yes Yes Yes (from 2026-08-22)
rh_lighter_trades Trade/fill updates (live: two legs per trade) Yes Yes Yes (from 2026-06-26)
rh_lighter_candles OHLCV candle data Yes No Yes (from 2026-06-26)
rh_lighter_open_interest Open interest (live: market context, same message as rh_lighter_funding) Yes Yes Yes (from 2026-08-22)
rh_lighter_funding Funding rates (live: market context, same message as rh_lighter_open_interest) Yes Yes Yes (from 2026-08-22)

There is no L3 channel for this deployment. Live Robinhood Chain subscriptions are served at wss://api.0xarchive.io/ws only. Replay messages have the live shapes, like mainnet Lighter replay, and a multi-channel replay must stay within the rh_lighter_* family.

Candle Replay

# Replay candles at 10x speed
await ws.replay(
    "candles", "BTC",
    start=int(time.time() * 1000) - 86400000,
    end=int(time.time() * 1000),
    speed=10,
    interval="15m"  # 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
)

# Lighter candles
await ws.replay(
    "lighter_candles", "BTC",
    start=int(time.time() * 1000) - 86400000,
    end=int(time.time() * 1000),
    speed=10,
    interval="5m"
)

HIP-3 Replay

# Replay HIP-3 orderbook at 50x speed
await ws.replay(
    "hip3_orderbook", "xyz:TSLA",
    start=int(time.time() * 1000) - 3600000,
    end=int(time.time() * 1000),
    speed=50,
)

# HIP-3 candles
await ws.replay(
    "hip3_candles", "xyz:TSLA",
    start=int(time.time() * 1000) - 86400000,
    end=int(time.time() * 1000),
    speed=100,
    interval="1h"
)

Open Interest / Funding Replay

The Hyperliquid open_interest and funding channels support both replay and live subscriptions (see Real-time Streaming above), as do hip3_open_interest and hip3_funding. lighter_open_interest and lighter_funding support both replay and live subscriptions (see Live Lighter Data above), as do rh_lighter_open_interest and rh_lighter_funding.

# Replay open interest at 50x speed
await ws.replay(
    "open_interest", "BTC",
    start=int(time.time() * 1000) - 86400000,
    end=int(time.time() * 1000),
    speed=50,
)

# Replay funding rates
await ws.replay(
    "funding", "ETH",
    start=int(time.time() * 1000) - 86400000,
    end=int(time.time() * 1000),
    speed=50,
)

# HIP-3 funding replay
await ws.replay(
    "hip3_funding", "xyz:TSLA",
    start=int(time.time() * 1000) - 86400000,
    end=int(time.time() * 1000),
    speed=100,
)

Multi-Channel Replay

Replay multiple channels in a single synchronized timeline. All data is interleaved by timestamp, preserving the original timing relationships between orderbook updates, trades, funding rates, and open interest. Before the timeline begins, replay_snapshot messages provide the initial state for each channel. All channels must belong to one venue, and the bulk L4 and full-depth channels replay on their own, so multi_replay() raises ValueError for them before sending.

import asyncio
import time
from oxarchive import OxArchiveWs, WsOptions

async def main():
    ws = OxArchiveWs(WsOptions(api_key="ox_..."))

    # Handle initial state snapshots (sent before timeline starts)
    def on_snapshot(channel, coin, timestamp, data):
        print(f"Initial {channel} state at {timestamp}:")
        if channel == "orderbook":
            print(f"  Mid price: {data.get('mid_price')}")
        elif channel == "funding":
            print(f"  Rate: {data.get('funding_rate')}")
        elif channel == "open_interest":
            print(f"  OI: {data.get('open_interest')}")

    # Handle interleaved timeline data, with the channel each record belongs to
    def on_record(channel, coin, timestamp, record):
        print(f"[{channel}] {coin} @ {timestamp}: {record}")

    ws.on_replay_snapshot(on_snapshot)
    ws.on_replay_data(on_record)

    ws.on_replay_start(lambda ch, coin, start, end, speed:
        print(f"Multi-channel replay started at {speed}x")
    )
    ws.on_replay_complete(lambda ch, coin, sent:
        print(f"Replay complete: {sent} total records")
    )

    await ws.connect()

    # Replay orderbook + trades + funding together at 10x speed
    await ws.multi_replay(
        ["orderbook", "trades", "funding"],
        "BTC",
        start=int(time.time() * 1000) - 86400000,
        end=int(time.time() * 1000),
        speed=10,
    )

    await asyncio.sleep(60)
    await ws.disconnect()

asyncio.run(main())

Multi-channel replay examples by exchange:

# Hyperliquid: orderbook + trades + OI + funding
await ws.multi_replay(
    ["orderbook", "trades", "open_interest", "funding"],
    "BTC",
    start=start_ms,
    end=end_ms,
    speed=10,
)

# Lighter: orderbook + trades + OI + funding
await ws.multi_replay(
    ["lighter_orderbook", "lighter_trades", "lighter_open_interest", "lighter_funding"],
    "BTC",
    start=start_ms,
    end=end_ms,
    speed=10,
)

# Lighter on Robinhood Chain: orderbook + trades + OI + funding
await ws.multi_replay(
    ["rh_lighter_orderbook", "rh_lighter_trades", "rh_lighter_open_interest", "rh_lighter_funding"],
    "BTC",
    start=start_ms,
    end=end_ms,
    speed=10,
)

# HIP-3: orderbook + trades + OI + funding
await ws.multi_replay(
    ["hip3_orderbook", "hip3_trades", "hip3_open_interest", "hip3_funding"],
    "xyz:TSLA",
    start=start_ms,
    end=end_ms,
    speed=10,
)

Timestamp Formats

The SDK accepts timestamps in multiple formats and sends them as Unix milliseconds. A time without a time zone is UTC: a naive datetime, an ISO string without an offset ("2026-10-01T12:00:00"), and a date alone ("2026-10-01", midnight UTC) mean the same instant on every machine, whatever its local time zone. For the current time, use an aware datetime such as datetime.now(timezone.utc); a naive datetime.now() is local wall-clock time and would be read as UTC.

import time
from datetime import datetime, timedelta, timezone

# Unix milliseconds (int): one hour ago
client.hyperliquid.orderbook.get("BTC", timestamp=int(time.time() * 1000) - 3_600_000)

# ISO string: a date alone is midnight UTC; a time without an offset is UTC
day = (datetime.now(timezone.utc) - timedelta(days=2)).date().isoformat()  # for example "2026-10-01"
client.hyperliquid.orderbook.history("BTC", start=day, end=f"{day}T01:00:00")

# datetime object: naive datetimes are UTC, aware ones keep their offset
start = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=2)  # naive, read as UTC
client.hyperliquid.orderbook.history(
    "BTC",
    start=start,
    end=datetime.now(timezone.utc),  # aware
)

Error Handling

Every error the API returns raises OxArchiveError. Branch on error_code, a stable machine-readable code, rather than on the message:

from datetime import datetime, timedelta, timezone

from oxarchive import Client, OxArchiveError

client = Client(api_key="0xa_your_api_key")
now = datetime.now(timezone.utc)

try:
    trades = client.hyperliquid.trades.history("BTC", start=now - timedelta(hours=1), end=now, side="both")
except OxArchiveError as e:
    print(e.error_code)    # "invalid_parameter"
    print(e.status)        # 400 (also e.code)
    print(e.param)         # "side"
    print(e.valid_values)  # ["buy", "sell"]
    print(e.message)       # "Invalid side 'both'. Use buy or sell."
    print(e.request_id)    # quote it when contacting support
    print(e.details)       # the whole error body, including route-specific fields
error_code Meaning
invalid_parameter A parameter failed to parse or validate; param and valid_values say which and what it accepts
invalid_symbol The symbol is not listed on this venue
invalid_interval The interval is not one the route accepts
invalid_cursor The cursor is malformed, stale, or was issued for other filters
invalid_time_range start is after end, or a time could not be parsed
range_before_coverage The whole range ends before the dataset's first served instant
historical_range_exceeded The span is longer than the plan allows per request
historical_depth_exceeded The request reaches further back than the plan's history window
unsupported_for_venue The datatype, or the WebSocket mode, is not offered on this venue; details["available_on"] lists where it is
route_not_found No route matches the path
not_found The route exists but the resource id does not
unauthorized, forbidden Missing or invalid API key; not allowed on this plan
insufficient_credits, rate_limited Monthly credits spent; too many requests
conflict The request conflicts with current state
upstream_unavailable, internal_error Retry later; quote request_id if it persists
endpoint_unsupported, slow_consumer WebSocket only (see below)
positions_unavailable, api_key_limit_reached, oauth_not_permitted Route-specific

The set is exported as oxarchive.ERROR_CODES (and the ErrorCode type). A server can add a code before an SDK release lists it, so treat an unknown value like a generic error. A request the SDK refuses before sending, such as client.hyperliquid.trades.recent(), raises OxArchiveError with the code the API would have returned; a network failure raises it with status 0 and no error_code.

WebSocket errors arrive on on_message as WsError messages with the same error_code and a message. Two codes are WebSocket only: slow_consumer (the connection fell behind a stream and messages were dropped; re-subscribe or restart the replay to resync) and endpoint_unsupported (the endpoint does not serve the channel or operation; the message names the one that does).

from oxarchive import WsError

def on_message(msg):
    if isinstance(msg, WsError):
        if msg.error_code == "slow_consumer":
            ws.subscribe_trades("BTC")  # resync
        else:
            print(msg.error_code, msg.message)

ws.on_message(on_message)

Type Hints

Full type hint support with Pydantic models:

from oxarchive import Client, LighterGranularity
from oxarchive.types import (
    OrderBook, Trade, Instrument, LighterInstrument, FundingRate, OpenInterest, Candle, Liquidation,
    LiquidationVolume, CoinFreshness, CoinSummary, PriceSnapshot,
    WsReplaySnapshot, LighterLiveTrade, LighterMarketContext, LighterMarketContextUpdate,
    LighterLiquidation, LighterLiquidationVolume, ResponseMeta,
    Position, PositionChange, MarketPosition, MarketPositionsSummary, AccountSummary,
    WalletPositions, LighterL1Accounts,
    CvdBucket, Hip3OracleExternalPrice, Hip4Question, WalletClassification, SymbolEntry,
    WebhookSubscription, WebhookEstimate, WebhookLimits,
    Capability, ErrorCode,
)
from oxarchive.resources.trades import CursorResponse

# Orderbook reconstruction types
from oxarchive import (
    OrderBookReconstructor,
    OrderbookDelta,
    TickData,
    ReconstructedOrderBook,
    ReconstructOptions,
)

client = Client(api_key="0xa_your_api_key")

orderbook: OrderBook = client.hyperliquid.orderbook.get("BTC")
result: CursorResponse[list[Trade]] = client.hyperliquid.trades.history("BTC", start=..., end=...)
more: bool = result.has_more
print(result.meta.venue, result.meta.symbol)  # hyperliquid BTC
capabilities: list[Capability] = client.capabilities()

# Lighter current data is available, so recent() is available
recent: list[Trade] = client.lighter.trades.recent("BTC")

# Account positions: typed rows plus the response meta
current: CursorResponse[WalletPositions] = client.hyperliquid.positions.get("0xabc...")
meta: ResponseMeta = current.meta
legs: CursorResponse[list[PositionChange]] = client.rh_lighter.positions.changes(4521, start=..., end=...)

# Cumulative volume delta, wallet classification and webhooks
cvd: CursorResponse[list[CvdBucket]] = client.hyperliquid.cvd.history("BTC", start=...)
wallets: WalletClassification = client.hyperliquid.wallets.classify(limit=10)
rules: list[WebhookSubscription] = client.webhooks.list_subscriptions()

# Lighter granularity type hint
granularity: LighterGranularity = "10s"

# Orderbook reconstruction
tick_data: TickData = client.lighter.orderbook.history_tick("BTC", start=..., end=...)
snapshots: list[ReconstructedOrderBook] = client.lighter.orderbook.history_reconstructed("BTC", start=..., end=...)

Data Catalog

For large-scale data exports (route-specific order books, fill-level trade history, and other retained datasets), use the Data Catalog. It lets you choose markets, datasets, and date ranges, see a live quote, and export zstd-compressed Parquet.

Requirements

  • Python 3.9+
  • httpx
  • pydantic

License

MIT

Metadata

Release files for oxarchive 1.13.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for oxarchive 1.13.0
File Size Uploaded
oxarchive-1.13.0.tar.gz 214.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for oxarchive 1.13.0
File Interpreter ABI Platform
oxarchive-1.13.0-py3-none-any.whl Python 3 none any Details

Total release size: 401.9 kB

Release files / oxarchive-1.13.0.tar.gz

Download URL oxarchive-1.13.0.tar.gz
Size 214.8 kB
Tags Source
SHA-256 checksum
How to use checksums
9fe031884ee781f354e613ee5fab76697fe0a60d077d6fbaf53f398b709db33d
BLAKE2b-256 checksum
How to use checksums
07a084250e1da8f6ac78decdd5103febd2563be6793d5313fa642f7d438ff4ec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / oxarchive-1.13.0-py3-none-any.whl

Download URL oxarchive-1.13.0-py3-none-any.whl
Size 187.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
36e04b0356dda2beea92f22dd6f04884156d42c0bb0d56ce6dd0a16e2a10e7bc
BLAKE2b-256 checksum
How to use checksums
13e1811dd13537a04ec6759dbcaace64a09e74c80a9f09510f08ba6fdf0f4228
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

1.13.0 This release

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.5

2 release files

0.6.4

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.4

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.11

2 release files

0.3.10

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page