Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ tests/*
!tests/test_websocket_capabilities.py
!tests/test_positions.py
!tests/test_rh_lighter.py
!tests/test_mempool.py
!tests/_mock_api.py
!tests/test_timestamps.py
!tests/test_spec_fixes.py
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to the `oxarchive` Python SDK are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.13.0] - 2026-10-08

### Added
- The `mempool` WebSocket channel: signed Hyperliquid transactions as our
Hyperliquid node receives them from its peers, before they are included in
a block, for every Hyperliquid product. It is live only (`replay()` raises
`ValueError` for it before sending), served only at
`wss://stream.0xarchive.io/ws`, and included with the Pro, Scale and
Enterprise plans. Every other channel stays on every plan.
- `subscribe_mempool(symbol=None)` and `unsubscribe_mempool(symbol=None)`.
Without a symbol they cover every pending transaction our Hyperliquid node
receives; with one, only the actions that reference that market.
`mempool` is the only channel whose symbol is optional.
- `on_mempool()` handler, receiving `(symbol, [MempoolItem, ...])`, with
`symbol` set to `None` on the unfiltered stream. Mempool messages reach
`on_message()` as `WsMempoolData`.
- Typed items: `MempoolItem` (`received_at`, `received_at_ms`, `symbols`,
`action`, `nonce`, `vault_address`, `expires_after_ms`, `signature`) and
`MempoolSignature`. `action` is kept exactly as signed, as a dict in the
order sent.
- `oxarchive.websocket.STREAM_WS_URL` (`wss://stream.0xarchive.io/ws`). Pass
it as `WsOptions(ws_url=...)` for a client that subscribes to `mempool`.
- `ws_endpoint` and `plans` on `WsChannelSpec` and `Capability`, mirroring
the fields `/v1/capabilities` sets on the `mempool` row only. For
`ws_endpoint`, `None` means the default endpoint, `wss://api.0xarchive.io/ws`.
For `plans`, `None` means every plan.

## [1.12.0] - 2026-10-05

Builds on 1.11.0. Two venues, Hyperliquid and Lighter; Lighter now has two
Expand Down
69 changes: 67 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1852,7 +1852,7 @@ Every method has an async version prefixed with `a`: `await client.webhooks.aest

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. See [Pricing](https://www.0xarchive.io/pricing) for plan capacity.
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](https://www.0xarchive.io/pricing) for plan capacity.

#### Free Tier (SIWE)

Expand Down Expand Up @@ -1984,6 +1984,8 @@ The WebSocket client supports live subscriptions for supported Hyperliquid and L

> 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)](#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](#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.

```python
Expand Down Expand Up @@ -2152,6 +2154,65 @@ 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.

```python
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.
Expand Down Expand Up @@ -2298,7 +2359,7 @@ Gap thresholds vary by channel:
```python
ws = OxArchiveWs(WsOptions(
api_key="0xa_your_api_key",
ws_url="wss://api.0xarchive.io/ws", # Optional
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)
Expand All @@ -2315,6 +2376,9 @@ 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
Expand All @@ -2332,6 +2396,7 @@ print(spec.venue, spec.live, spec.replay, spec.bulk_replay) # spot True True Tr
| `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](#l4-and-full-depth-replay).

Expand Down
10 changes: 9 additions & 1 deletion oxarchive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@
WsData,
WsL4Snapshot,
WsL4Batch,
# Mempool (pending transactions) types
MempoolItem,
MempoolSignature,
WsMempoolData,
# Live Lighter payload types
LighterLiveTrade,
LighterMarketContext,
Expand Down Expand Up @@ -244,7 +248,7 @@
WS_CHANNELS = None # type: ignore
decode_lighter_payload = None # type: ignore

__version__ = "1.12.0"
__version__ = "1.13.0"

__all__ = [
# Client
Expand Down Expand Up @@ -408,6 +412,10 @@
"WsData",
"WsL4Snapshot",
"WsL4Batch",
# Mempool (Pending Transactions) Types
"MempoolItem",
"MempoolSignature",
"WsMempoolData",
# Live Lighter Payload Types
"LighterLiveTrade",
"LighterMarketContext",
Expand Down
121 changes: 117 additions & 4 deletions oxarchive/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,7 @@ class Candle(BaseModel):
"hip3_l4_diffs", "hip3_l4_orders",
"orderbook_full", "hip3_orderbook_full",
"spot_orderbook", "spot_trades", "spot_l4_diffs", "spot_l4_orders", "spot_twap",
"mempool",
]
"""Available WebSocket channels.

Expand All @@ -1288,8 +1289,8 @@ class Candle(BaseModel):
- Replay only: ``candles``, ``hip3_candles``, ``hip4_orderbook``,
``hip4_open_interest``, ``lighter_candles``, ``lighter_l3_orderbook`` and
``rh_lighter_candles``.
- Live only: ``ticker``, ``all_tickers``, ``spot_orderbook`` and
``spot_trades``.
- Live only: ``ticker``, ``all_tickers``, ``spot_orderbook``,
``spot_trades`` and ``mempool``.
- Neither: ``spot_twap``. Spot TWAP statuses are served over REST only
(``client.spot.twap``); subscribing raises ``ValueError``.

Expand All @@ -1304,6 +1305,11 @@ class Candle(BaseModel):
messages share one shape per channel (see :class:`LighterLiveTrade` and
:class:`LighterMarketContext`). HIP-4 has no funding or liquidation channels,
and spot has no funding, open interest or liquidation channels.

``mempool`` carries pending Hyperliquid transactions (see
:class:`MempoolItem`). It is the one channel served only at
``wss://stream.0xarchive.io/ws`` and only with the Pro, Scale and Enterprise
plans, and the one channel whose symbol is optional.
"""

WsConnectionState = Literal["connecting", "connected", "disconnected", "reconnecting"]
Expand Down Expand Up @@ -1345,8 +1351,10 @@ class WsError(BaseModel):
for example ``"invalid_parameter"``, ``"unsupported_for_venue"`` (the
channel does not offer that mode), ``"slow_consumer"`` (the connection
fell behind and messages were dropped: re-subscribe or restart the replay
to resync) or ``"endpoint_unsupported"`` (this endpoint does not serve the
channel; the message names the one that does).
to resync), ``"endpoint_unsupported"`` (this endpoint does not serve the
channel; the message names the one that does) or ``"forbidden"`` (the
channel is not included with your plan, as for ``mempool`` on plans other
than Pro, Scale and Enterprise).
"""

model_config = ConfigDict(extra="allow")
Expand Down Expand Up @@ -1407,6 +1415,101 @@ class WsL4Batch(BaseModel):
"""Events in server order; each event carries its own block/sequence data."""


# =============================================================================
# WebSocket Mempool Types
# =============================================================================
#
# ``mempool`` streams signed Hyperliquid transactions as our Hyperliquid node
# receives them from its peers, before they are included in a block, for every
# Hyperliquid product (perps, HIP-3, HIP-4 and spot). It is live only: there is
# no replay, history or REST route. It is served only at
# ``wss://stream.0xarchive.io/ws``, with the Pro, Scale and Enterprise plans.


class MempoolSignature(BaseModel):
"""The ``{r, s, v}`` signature of a pending action, as signed."""

model_config = ConfigDict(extra="allow")

r: str
"""``r`` as a hex string."""

s: str
"""``s`` as a hex string."""

v: int
"""Recovery id."""


class MempoolItem(BaseModel):
"""One signed action from a ``mempool`` message.

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 :attr:`signature` if that matters to you.
"""

model_config = ConfigDict(extra="allow")

received_at: Optional[str] = None
"""When our node received the transaction, as an RFC 3339 UTC string with
the node's nanosecond precision (``"2026-10-08T01:57:23.548737209Z"``). It
is not a block time. Kept as a string because ``datetime`` holds
microseconds only; :attr:`received_at_ms` has it as Unix milliseconds."""

received_at_ms: Optional[int] = None
"""When our node received the transaction, in Unix milliseconds."""

symbols: list[str] = Field(default_factory=list)
"""The markets the action's asset ids reference, spelled as everywhere else
(``BTC``, ``xyz:TSLA``, ``HYPE-USDC``, ``#49720``), in first-seen order
without repeats. Empty for actions with no market, such as transfers,
``noop``, ``scheduleCancel`` and validator actions."""

action: dict[str, Any]
"""The action exactly as signed, in Hyperliquid's exchange-action format:
asset ids (``a`` or ``asset``) rather than symbols, and prices and sizes as
strings, with keys in the order sent. ``action["type"]`` names it, for
example ``order``, ``cancel``, ``cancelByCloid``, ``modify``,
``batchModify``, ``scheduleCancel``, ``twapOrder``, ``twapCancel``,
``updateLeverage``, ``updateIsolatedMargin``, ``noop``, ``evmRawTx`` or a
transfer (``usdSend``, ``spotSend``, ``usdClassTransfer``, ``sendAsset``).
Hyperliquid adds action types, so handle types you do not recognise."""

nonce: Optional[int] = None
"""The action's nonce."""

vault_address: Optional[str] = None
"""The vault or subaccount the action acts for, or ``None``."""

expires_after_ms: Optional[int] = None
"""The action's ``expiresAfter`` in Unix milliseconds, or ``None``."""

signature: Optional[MempoolSignature] = None
"""The signature over the action. The signer's address is not included;
it can be recovered from the signature and the action."""


class WsMempoolData(BaseModel):
"""A ``mempool`` data message: one batch of pending transactions.

The server sends one message per batch our node receives from a peer, as
soon as it arrives. ``coin`` and ``symbol`` are the subscription's symbol,
or ``None`` on the unfiltered stream. A symbol subscription receives every
action that references that market, whole: an order batch that touches
``BTC`` and ``ETH`` reaches both subscriptions.
"""

type: Literal["data"]
channel: Literal["mempool"]
coin: Optional[str] = None
"""The subscription's symbol, or ``None`` on the unfiltered stream."""
symbol: Optional[str] = None
"""The subscription's symbol, or ``None`` on the unfiltered stream."""
data: list[MempoolItem]
"""One item per signed action in the batch."""


# =============================================================================
# WebSocket Live Lighter Payload Types
# =============================================================================
Expand Down Expand Up @@ -2853,6 +2956,16 @@ class Capability(BaseModel):
notes: Optional[str] = None
"""Anything else worth knowing, such as how its replay behaves."""

ws_endpoint: Optional[str] = None
"""The WebSocket endpoint that serves its channels, when it is not the
default endpoint (``"wss://stream.0xarchive.io/ws"`` for ``mempool``).
``None`` means the default endpoint, ``wss://api.0xarchive.io/ws``."""

plans: Optional[list[str]] = None
"""The plans that include it, when that is not every plan (``["pro",
"scale", "enterprise"]`` for ``mempool``). ``None`` means every plan, Free
included."""


# =============================================================================
# Data Quality Types
Expand Down
Loading
Loading