> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xarchive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Mempool channel

> Stream signed Hyperliquid transactions before they are in a block, match orders and cancels to their outcomes, and recover who signed them.

Watch Hyperliquid transactions before they are in a block. The `mempool` channel streams each signed action our Hyperliquid node receives from its peers, such as orders, cancels, modifies, TWAPs, leverage changes and transfers, as soon as it arrives. It covers every Hyperliquid product: perps, HIP-3, HIP-4 and Spot.

<CardGroup cols={3}>
  <Card title="See pending actions" icon="eye" href="#subscribe">
    Orders, cancels and modifies as they reach our node, before they are in a block.
  </Card>

  <Card title="Join to outcomes" icon="link" href="#from-pending-to-outcome">
    Match a pending order to its `l4_orders` event by client order id, and a cancel by order id.
  </Card>

  <Card title="Know the sender" icon="fingerprint" href="#who-sent-it">
    Recover the signing wallet from the signature, then resolve it to the trading account.
  </Card>
</CardGroup>

<Warning>
  Pending is not executed. A transaction on this channel is not yet in a block, and it can still be rejected, expire or never land. On liquid perps most post-only orders are rejected, because they would cross the book; [From pending to outcome](#from-pending-to-outcome) shows where rejections appear.
</Warning>

The channel is live only. It has no replay, no history, no REST route and no export, and it is never stored: a message you miss while not subscribed cannot be fetched later.

## Connect

Connect to `wss://stream.0xarchive.io/ws`, the only endpoint that serves `mempool`. It takes the same API key and the same protocol as `wss://api.0xarchive.io/ws`: send the key in the opening handshake as `Authorization: Bearer $OXARCHIVE_API_KEY`, as [WebSocket connection](/websocket/connection) describes. The stream endpoint also serves `l4_orders`, so one connection carries both sides of [the join below](#from-pending-to-outcome).

The `mempool` channel is included with the Pro, Scale and Enterprise plans. It is the one channel that is not on every plan: every other channel and route is on every plan, Free included.

<AccordionGroup>
  <Accordion title="On Free and Build: forbidden">
    ```json theme={"theme":"github-dark"}
    {
      "type": "error",
      "message": "The mempool channel is included with the Pro, Scale and Enterprise plans. Upgrade at https://0xarchive.io/pricing.",
      "error_code": "forbidden"
    }
    ```
  </Accordion>

  <Accordion title="On wss://api.0xarchive.io/ws: endpoint_unsupported">
    ```json theme={"theme":"github-dark"}
    {
      "type": "error",
      "message": "The 'mempool' channel is live only and served on wss://stream.0xarchive.io/ws. Subscribe to it there.",
      "error_code": "endpoint_unsupported"
    }
    ```

    Most other channels are served only on `wss://api.0xarchive.io/ws`, so a client that also needs them keeps a second connection there.
  </Accordion>
</AccordionGroup>

## Subscribe

`symbol` is optional on this channel and on no other. Leave it out to receive every pending transaction our Hyperliquid node receives, or name one market to receive only the actions that reference it. Spell it as on the other channels: `BTC` for perps, `xyz:XYZ100` for HIP-3, `HYPE-USDC` for Spot (`HYPE/USDC` is accepted too), and the outcome side coin, such as `#49720`, for HIP-4.

<Tabs>
  <Tab title="WebSocket">
    Every pending action our node receives:

    ```json theme={"theme":"github-dark"}
    { "op": "subscribe", "channel": "mempool" }
    ```

    The server confirms it with `null` in `coin` and `symbol`:

    ```json theme={"theme":"github-dark"}
    { "type": "subscribed", "channel": "mempool", "coin": null, "symbol": null }
    ```

    Only the actions that reference BTC, sent as a separate message:

    ```json theme={"theme":"github-dark"}
    { "op": "subscribe", "channel": "mempool", "symbol": "BTC" }
    ```

    The confirmation carries the canonical spelling, such as `HYPE-USDC` for a subscribe sent as `HYPE/USDC`:

    ```json theme={"theme":"github-dark"}
    { "type": "subscribed", "channel": "mempool", "coin": "BTC", "symbol": "BTC" }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":"github-dark"}
    import asyncio, os
    from oxarchive.websocket import OxArchiveWs, WsOptions, STREAM_WS_URL

    async def main():
        ws = OxArchiveWs(WsOptions(api_key=os.environ["OXARCHIVE_API_KEY"], ws_url=STREAM_WS_URL))
        ws.on_mempool(lambda symbol, items: print(symbol, len(items), items[0].action["type"]))
        await ws.connect()
        ws.subscribe_mempool("BTC")  # subscribe_mempool() for every pending action
        await asyncio.sleep(10)
        await ws.disconnect()

    asyncio.run(main())
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={"theme":"github-dark"}
    import { OxArchiveWs, STREAM_WS_URL } from "@0xarchive/sdk";

    const ws = new OxArchiveWs({ apiKey: process.env.OXARCHIVE_API_KEY!, wsUrl: STREAM_WS_URL });
    ws.onMempool((symbol, items) => console.log(symbol, items.length, items[0]?.action.type));
    await ws.connect();
    ws.subscribeMempool("BTC"); // subscribeMempool() for every pending action
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={"theme":"github-dark"}
    oxa stream subscribe mempool BTC   # leave out BTC for every pending action
    ```

    The CLI connects to `wss://stream.0xarchive.io/ws` for this channel on its own and prints each `data` message.
  </Tab>
</Tabs>

A symbol subscription receives every action whose asset ids include that market. An action that references several markets, such as an order batch on BTC and ETH, is sent whole to the subscribers of each. An unknown symbol returns `error_code` `invalid_symbol`. To stop, send `unsubscribe` with the same fields: without `symbol` it stops the unfiltered stream, with `symbol` that symbol's subscription.

## What arrives

Each `data` message carries one batch of transactions, sent as soon as the node receives it from a peer. `coin` and `symbol` are the subscription's symbol, or `null` on the unfiltered stream, and `data` holds one item per signed action:

```json theme={"theme":"github-dark"}
{
  "type": "data",
  "channel": "mempool",
  "coin": "BTC",
  "symbol": "BTC",
  "data": [
    {
      "received_at": "2026-10-08T01:57:23.548737209Z",
      "received_at_ms": 1791424643548,
      "symbols": ["BTC"],
      "action": {
        "type": "order",
        "orders": [
          {
            "a": 0,
            "b": true,
            "p": "83276",
            "s": "0.40011",
            "r": false,
            "t": { "limit": { "tif": "Alo" } },
            "c": "0x7849acc2c6c2f6f0fe4bc80ef13d1504"
          }
        ],
        "grouping": "na"
      },
      "nonce": 1791424643400,
      "vault_address": null,
      "expires_after_ms": null,
      "signature": {
        "r": "0x5afc29701358e4082d744d4332915079664d27d0244565b94b7966adfd2776d9",
        "s": "0x57e2d718fa45c0c191dc0f0c1ad6e6e0fc3e026cdfc4148c3ae8b3b32fd02c8d",
        "v": 28
      }
    }
  ]
}
```

| Field | Meaning |
| - | - |
| `received_at` | When our Hyperliquid node received the transaction, as an RFC 3339 UTC string with nanosecond precision. It is not a block time. |
| `received_at_ms` | The same time in Unix milliseconds. |
| `symbols` | The markets the action's asset ids reference, in the canonical spelling, in first-seen order and without repeats. `[]` for an action with no market, such as a transfer, `noop`, `scheduleCancel` or a validator action. |
| `action` | The action exactly as it was signed, in Hyperliquid's exchange-action format: markets are asset ids, not symbols, and prices and sizes are strings. |
| `nonce` | The action's nonce, an integer. |
| `vault_address` | The vault or subaccount the action acts for, or `null`. |
| `expires_after_ms` | The action's `expiresAfter` time in Unix milliseconds, or `null`. |
| `signature` | The action's signature, as `r`, `s` and `v`. |

`action.type` names the action, and its fields follow Hyperliquid's exchange API. The ones you join on:

| `action.type` | Where the orders and ids are |
| - | - |
| `order` | `orders[]`: `a` asset id, `b` buy, `p` price, `s` size, `r` reduce-only, `t` order type and time in force, `c` client order id (optional) |
| `cancel` | `cancels[]`: `a` asset id, `o` order id |
| `cancelByCloid` | `cancels[]`: `asset` asset id, `cloid` client order id |
| `modify` | `oid` of the order it replaces, and the new `order` in the shape above |
| `batchModify` | `modifies[]`, each an `oid` and an `order` |

Other common types are `scheduleCancel`, `twapOrder`, `twapCancel`, `updateLeverage`, `updateIsolatedMargin`, `noop`, `evmRawTx`, and transfers such as `usdSend`, `spotSend`, `usdClassTransfer` and `sendAsset`. Hyperliquid adds types over time, so pass over a type you do not recognize instead of failing on it.

The same signed action can occasionally arrive twice. Deduplicate on `signature` when you count or store actions.

## From pending to outcome

Subscribe to `l4_orders` for the same market on the same connection. Its events show what the exchange did with pending orders and cancels:

<Steps>
  <Step title="Orders: join on the client order id">
    An `l4_orders` event carries `cloid` when the order was placed with one, the same value as `c` on the pending order, whether it came in `orders[]` or as the new order of a `modify` or `batchModify`. Its first event for that `cloid` is the order's outcome: `open` (resting), `filled`, `canceled` (for example an immediate-or-cancel order that did not fill), or on Spot and HIP-4 a rejection. An order placed without a client order id can only be matched approximately, on market, side, price and size.
  </Step>

  <Step title="Cancels: join on the order id">
    A `cancel` names the order by `o`. If the cancel succeeds, a `canceled` event for that `oid` follows; if the order already filled or was canceled, none does. A `cancelByCloid` names the order by `cloid`.
  </Step>

  <Step title="Rejections: know where they show">
    On Spot and HIP-4, a rejected order appears on `l4_orders` with the reason as its status, such as `badAloPxRejected`. On perps and HIP-3, `l4_orders` carries accepted orders only: a pending order whose `cloid` never arrives there within a few seconds was not accepted: the exchange refused it, or it expired.
  </Step>
</Steps>

Not every order passes through the mempool on its way to a block. Most orders reach our node this way, but some reach a block without being gossiped to it first, so some `l4_orders` events have no pending action before them.

This Python script follows BTC orders and counts how many the exchange accepted. It uses the `websockets` package (`pip install websockets`).

```python theme={"theme":"github-dark"}
import asyncio, json, os, time
import websockets

URL = "wss://stream.0xarchive.io/ws"
HEADERS = {"Authorization": f"Bearer {os.environ['OXARCHIVE_API_KEY']}"}

async def main():
    pending = {}  # cloid -> received_at_ms of the pending order
    accepted = not_accepted = 0
    last_sweep = time.time()
    async with websockets.connect(URL, additional_headers=HEADERS, max_size=None) as ws:
        for channel in ("mempool", "l4_orders"):
            await ws.send(json.dumps({"op": "subscribe", "channel": channel, "symbol": "BTC"}))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("type") == "error":
                raise RuntimeError(f"{msg['error_code']}: {msg['message']}")
            if msg.get("channel") == "mempool" and msg["type"] == "data":
                for item in msg["data"]:
                    if "BTC" not in item["symbols"]:
                        continue
                    action = item["action"]
                    orders = action.get("orders", []) + [m["order"] for m in action.get("modifies", [])]
                    if action["type"] == "modify":
                        orders.append(action["order"])
                    for order in orders:
                        if order.get("a") == 0 and order.get("c"):  # 0 is BTC's asset id
                            pending[order["c"]] = item["received_at_ms"]
            elif msg.get("channel") == "l4_orders" and msg["type"] == "l4_batch":
                for event in msg["data"]:
                    if pending.pop(event.get("cloid"), None) is not None:
                        accepted += 1  # first event: open, filled or canceled
            if time.time() - last_sweep >= 1:
                last_sweep = time.time()
                cutoff = last_sweep * 1000 - 5000
                for cloid in [c for c, seen in pending.items() if seen < cutoff]:
                    del pending[cloid]
                    not_accepted += 1  # never reached l4_orders: refused or expired
                print(f"accepted {accepted}  not accepted {not_accepted}  pending {len(pending)}")

asyncio.run(main())
```

## Who sent it

The signer's address is not included, but every item carries what it takes to recover it: the action exactly as signed, the nonce, the vault address, the expiry and the signature. Keep `action` with its keys in the order received; JSON parsers in Python and JavaScript do this by default.

<Steps>
  <Step title="Recover the signer">
    Orders, cancels, modifies and leverage changes are exchange actions, signed as Hyperliquid's `Agent` typed data over a hash of the action. This function recovers the address that signed one, with `msgpack` and `eth-account` (`pip install msgpack eth-account`):

    ```python theme={"theme":"github-dark"}
    import msgpack
    from eth_account import Account
    from eth_account.messages import encode_typed_data
    from eth_utils import keccak

    def recover_signer(item):
        """The address that signed an exchange action (orders, cancels, modifies, leverage)."""
        data = msgpack.packb(item["action"])  # keep the action's keys in the order received
        data += item["nonce"].to_bytes(8, "big")
        vault = item["vault_address"]
        data += b"\x00" if vault is None else b"\x01" + bytes.fromhex(vault[2:])
        if item["expires_after_ms"] is not None:
            data += b"\x00" + item["expires_after_ms"].to_bytes(8, "big")
        typed = {
            "domain": {"name": "Exchange", "version": "1", "chainId": 1337,
                       "verifyingContract": "0x0000000000000000000000000000000000000000"},
            "types": {
                "EIP712Domain": [{"name": "name", "type": "string"}, {"name": "version", "type": "string"},
                                 {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"}],
                "Agent": [{"name": "source", "type": "string"}, {"name": "connectionId", "type": "bytes32"}],
            },
            "primaryType": "Agent",
            "message": {"source": "a", "connectionId": keccak(data)},
        }
        sig = item["signature"]
        return Account.recover_message(encode_typed_data(full_message=typed),
                                       vrs=(sig["v"], int(sig["r"], 16), int(sig["s"], 16)))
    ```

    Transfers such as `usdSend` and `spotSend` are user-signed actions with their own typed data, so this function does not apply to them.
  </Step>

  <Step title="Resolve the account">
    Market makers and bots usually sign with an API wallet that acts for their account. Resolve the account in this order: the `vault_address` when the action has one; otherwise the account an API wallet acts for, from Hyperliquid's `userRole` info request; otherwise the signer itself.

    ```python theme={"theme":"github-dark"}
    import json, urllib.request

    def account_for(item, signer):
        """The trading account behind a signed action."""
        if item["vault_address"]:
            return item["vault_address"]  # the vault or subaccount it acts for
        body = json.dumps({"type": "userRole", "user": signer}).encode()
        req = urllib.request.Request("https://api.hyperliquid.xyz/info", data=body,
                                     headers={"Content-Type": "application/json"})
        role = json.load(urllib.request.urlopen(req))
        if role.get("role") == "agent":
            return role["data"]["user"]  # an API wallet acting for this account
        return signer  # the account signed for itself
    ```

    Cache the answer per signer and refresh it from time to time. Where the action has a matching `l4_orders` event, its `user` confirms the account.
  </Step>
</Steps>

## Volume and limits

The unfiltered stream carries every pending transaction our Hyperliquid node receives, several megabytes per second before compression. Most of it is market makers' post-only quotes. Use a client that negotiates permessage-deflate compression, as most WebSocket libraries do by default, and subscribe with a symbol where the job allows it.

* **Unfiltered capacity:** unfiltered subscriptions are limited across the service. When they are full, a subscribe without `symbol` returns `error_code` `rate_limited` with `The unfiltered mempool stream is at capacity. Subscribe with a symbol, or try again later.` Symbol subscriptions are not limited this way.
* **Slow consumers:** as on any channel, a connection that reads too slowly is closed after an `error` with `error_code` `slow_consumer`. Read each message off the socket promptly and do heavy work elsewhere.
* **Plan limits:** the unfiltered stream and each symbol count as one subscription toward your plan's limit per connection, and a connection accepts at most 10 subscribe or unsubscribe operations per second. See [WebSocket limits](/websocket/limits).
* **Credits:** each `data` message is billed like any other WebSocket message; see [Credits](/core-concepts/credits). The unfiltered stream sends many messages, so check its usage in the dashboard after a short run.

## Errors

Errors arrive as `error` messages with a stable `error_code`. The connection stays open after each of these except `slow_consumer`.

| `error_code` | Cause and next step |
| - | - |
| `forbidden` | Your plan does not include `mempool`. Move to Pro, Scale or Enterprise on the [pricing page](https://0xarchive.io/pricing). |
| `endpoint_unsupported` | You subscribed on `wss://api.0xarchive.io/ws`. Connect to `wss://stream.0xarchive.io/ws` and subscribe there. |
| `invalid_symbol` | The symbol is not a Hyperliquid market. Check its spelling for the product family. |
| `rate_limited` | The unfiltered stream is at capacity, or the connection reached its subscription or operation limit. Subscribe with a symbol, unsubscribe from something, or slow down. |
| `upstream_unavailable` | `The mempool channel is temporarily unavailable. Please try again shortly.` Retry with capped backoff. |
| `slow_consumer` | The connection fell behind and is closed. Reconnect, subscribe to less, and read faster. |

The channel has no replay: a `replay` command for `mempool` returns an `error`. [Errors](/errors#websocket-errors) lists every code.

## Next step

<Card title="L4 order book" icon="layer-group" href="/websocket/l4-orderbook">
  Follow order events and book changes with the `l4_orders` and `l4_diffs` channels.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.