# StreamOrderStatuses - Real-time Order Status Streaming

Stream order status events starting from a position, providing real-time access to the lifecycle of every order on Hyperliquid: opens, fills, cancellations, and rejections.

Clone our [gRPC Code Examples Repository](https://github.com/dwellir-public/gRPC-code-examples) for complete, runnable implementations in Go, Python, and Node.js.

## When to Use This Method

`StreamOrderStatuses` is essential for:

- **Order Lifecycle Tracking** - Follow an order from open through fill, cancel, or rejection
- **Rejection Analysis** - Identify why orders are rejected using the `status` field
- **Resting-Order Context** - Read the full order object behind each status change
- **Compliance and Auditing** - Record every status transition with block alignment

Method signature

```protobuf
rpc StreamOrderStatuses(StreamRequest) returns (stream Record) {}
```

V3 accepts an inclusive `position.block_number` or Unix-millisecond `position.timestamp`. Explicit zero is invalid. A positioned call replays inclusively and then tails live; an empty request tails newly written status records. Set `progress` to opt in to [progress frames](#progress-frames).

```protobuf
filter: {
  fields: {
    field: "users"
    op: FILTER_OP_IN
    values: "0x1111111111111111111111111111111111111111"
  }
}
```

Order statuses accept `users`, `coins` and `dexes` filters. User matching is case-insensitive, coin matching is case-sensitive and dex matching is exact on the HIP-3 dex prefix; values within a field use OR semantics and separate fields use AND semantics. Records with zero matching events are skipped. Each `Record` contains optional `block_number` and `timestamp` cursors plus the filtered envelope in `data`. A pruned position returns `NOT_FOUND`; malformed filters return `INVALID_ARGUMENT`.

## V3 reference

### Request Parameters

- `position.timestamp` (`int64, optional`): Inclusive V3 Unix-millisecond cursor. Omit the position to tail newly written records.
- `position.block_number` (`int64, optional`): Inclusive V3 sequential block cursor. Explicit zero is invalid.
- `filter.fields` (`FieldFilter[], optional`): V3 users/coins constraints. Fields AND together; values within one field OR together.
- `filter.fields.field` (`string, required`): Feed-advertised filter field name, either users or coins.
- `filter.fields.op` (`FilterOp, optional`): FILTER_OP_UNSPECIFIED or FILTER_OP_IN.
- `filter.fields.values` (`string[], required`): One or more accepted values; entries within the list use OR semantics.
- `progress.interval_ms` (`uint32, optional`): Set progress to opt in to progress frames; leave it unset for none. Longest silence in milliseconds before a progress frame. 0 selects the feed default, currently 20000 within bounds of 10000 to 60000; ListFeeds reports the bounds in FeedInfo.progress.

### Request Example

```bash
grpcurl \
  -import-path content/docs/hyperliquid/grpc \
  -proto v3.proto \
  -H "x-api-key: ${DWELLIR_API_KEY}" \
  -d '{"position":{"block_number":"1058646974"},"filter":{"fields":[{"field":"coins","op":"FILTER_OP_IN","values":["BTC"]}]}}' \
  "${HYPERLIQUID_GRPC_ENDPOINT}" \
  hyperliquid_l1_gateway.v3.HyperliquidL1Gateway/StreamOrderStatuses
```

### Response Fields

- `block_number` (`optional int64, optional`): Sequential Hyperliquid block cursor; never the HyperBFT consensus round.
- `timestamp` (`optional int64, optional`): Record time in Unix milliseconds.
- `data` (`bytes, required`): Bytes from the node_order_statuses_by_block, with non-matching events removed when a filter is applied.
- `progress` (`StreamProgress, optional`): Set only on progress frames, which leave data, block_number, and timestamp unset. Check it before decoding data.
- `progress.scanned_through.block_number` (`int64, optional`): Inclusive cursor of the newest block the stream examined, sent or removed by the filter. A valid resume position; unset until the stream has examined a record.
- `progress.timestamp` (`optional int64, optional`): Source time at scanned_through in Unix milliseconds.

### Successful Response

```protobuf
block_number: 1058646974
timestamp: 1785736814057
data: "{\"block_number\":1058646974,\"events\":[]}"
```

### Error Responses

#### Invalid argument

- Code: `INVALID_ARGUMENT`
- Description: A cursor is zero or malformed, or a filter has an unsupported field, duplicate field, empty name, or empty value list.

#### Invalid progress request

- Code: `INVALID_ARGUMENT`
- Description: A nonzero progress.interval_ms is outside the bounds advertised in FeedInfo.progress.

#### Not found

- Code: `NOT_FOUND`
- Description: Available replay history varies by feed and endpoint. A cursor outside retained history returns NOT_FOUND.

#### Replay capacity exhausted

- Code: `RESOURCE_EXHAUSTED`
- Description: Replay capacity is temporarily unavailable; retry with backoff.

#### Filter operator not implemented

- Code: `UNIMPLEMENTED`
- Description: The requested filter operator is well formed but is not implemented by this gateway version.

#### Stream unavailable

- Code: `UNAVAILABLE`
- Description: No new records arrived before the stream timeout, or the stream failed mid-stream. Progress frames do not reset the timeout. Reconnect from the last cursor received.

#### Records lost

- Code: `DATA_LOSS`
- Description: The stream lost records, for example because it fell behind the live feed. It ends before sending any cursor past them; reconnect from the last cursor received to re-read the gap.

#### Slow consumer

- Code: `DEADLINE_EXCEEDED`
- Description: The stream could not deliver records in time because the client read too slowly or stopped reading; reconnect from the last processed cursor.

#### Unauthenticated

- Code: `UNAUTHENTICATED`
- Description: The request did not include usable authentication metadata.

#### Permission denied

- Code: `PERMISSION_DENIED`
- Description: The supplied credential is not permitted to call this service.

## V2 reference

### Request Parameters

- `timestamp_ms` (`int64, optional`): Inclusive Unix-millisecond cursor. Omit it to start from the latest data and continue streaming.
- `block_height` (`int64, optional`): Inclusive block-height cursor. Omit it to start from the latest data and continue streaming.

### Request Example

```bash
grpcurl \
  -import-path content/docs/hyperliquid/grpc \
  -proto v2.proto \
  -H "x-api-key: ${DWELLIR_API_KEY}" \
  -d '{"block_height":"1058646974"}' \
  "${HYPERLIQUID_GRPC_ENDPOINT}" \
  hyperliquid_l1_gateway.v2.HyperliquidL1Gateway/StreamOrderStatuses
```

### Response Fields

- `data` (`bytes, required`): JSON-encoded payload returned in the V2 BlockOrderStatuses wrapper.

### Successful Response

```protobuf
message BlockOrderStatuses {
  bytes data = 1;
}
```

```protobuf
rpc StreamOrderStatuses(Position) returns (stream BlockOrderStatuses) {}
```

V2 accepts the inclusive cursor fields `block_height` and `timestamp_ms`; an empty `Position` tails new status records. It returns the source JSON in `BlockOrderStatuses.data`.

## Response Stream

V3 carries the JSON-encoded `node_order_statuses_by_block` envelope in `Record.data`.

V2 carries the JSON-encoded `node_order_statuses_by_block` envelope in `BlockOrderStatuses.data`.

Decode the `data` field to access:

- Block reference (`local_time`, `block_time`, `block_number`)
- An `events` array of status records
- A full order object behind each status change

## Full Order Status Spec

`StreamOrderStatuses` emits a JSON payload matching Hyperliquid's `node_order_statuses_by_block` format. The envelope is the same block-envelope shape as fills, but `events` holds status-record objects rather than `[address, fill]` tuples.

Top-level keys (always present):

```json
{
  "local_time": "2026-04-20T00:00:00.040479928",   // ISO-8601 timestamp when node processed the block (nanosecond precision)
  "block_time": "2026-04-19T23:59:59.576040791",    // ISO-8601 timestamp from block consensus
  "block_number": 12345,                            // Block height containing these events
  "events": [                                       // Array of status-record objects
    {
      "time": "2026-04-19T23:59:59.576040791",      // Event timestamp (string, not integer ms)
      "user": "0x1234567890abcdef1234567890abcdef12345678",  // User address
      "hash": "0x00...",                            // Transaction hash (nullable)
      "builder": { "b": "0x00...", "f": 50 },       // Builder address and fee (nullable)
      "status": "open",                             // Status value (see table below)
      "order": { }                                  // Full order object (see Order Object)
    }
  ]
}
```

Status event fields (`events[i]`):

| Field     | Type           | Description                                                                                        |
| --------- | -------------- | -------------------------------------------------------------------------------------------------- |
| `time`    | string         | Event timestamp in the no-`Z` layout. Note: this is a string, **not** an integer millisecond value |
| `user`    | string         | User address                                                                                       |
| `hash`    | string or null | Transaction hash. `null` for some statuses (for example `reduceOnlyCanceled`)                      |
| `builder` | object or null | `{ "b": <address>, "f": <fee> }`, the builder address and fee                                      |
| `status`  | string         | Status value. See [Status Values](#status-values)                                                  |
| `order`   | object         | The resting-order view. See [Order Object](#order-object)                                          |

### Status Values

Observed `status` values (the list is not exhaustive):

| Status                            | Meaning                                     |
| --------------------------------- | ------------------------------------------- |
| `open`                            | Order accepted and resting on the book      |
| `filled`                          | Order filled                                |
| `canceled`                        | Order canceled                              |
| `reduceOnlyCanceled`              | Reduce-only order canceled                  |
| `badAloPxRejected`                | Add-liquidity-only order rejected on price  |
| `insufficientSpotBalanceRejected` | Rejected for insufficient spot balance      |
| `iocCancelRejected`               | Immediate-or-cancel order rejected          |
| `minTradeNtlRejected`             | Rejected for failing minimum trade notional |
| `perpMarginRejected`              | Rejected for insufficient perp margin       |
| `reduceOnlyRejected`              | Reduce-only order rejected                  |
| `selfTradeCanceled`               | Canceled to prevent a self-trade            |

### Order Object

The `order` field is the shared resting-order schema, the same object returned by [GetOrderBookSnapshot](https://www.dwellir.com/docs/hyperliquid/get_order_book_snapshot).

| Field              | Type           | Description                                                                                                              |
| ------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `coin`             | string         | Coin identifier                                                                                                          |
| `side`             | string         | `"B"` or `"A"`                                                                                                           |
| `limitPx`          | string         | Limit price (numeric string)                                                                                             |
| `sz`               | string         | Current size (numeric string)                                                                                            |
| `oid`              | number         | Order identifier. Not reliably unique across coins. See [Order Identifiers](#order-identifiers)                          |
| `timestamp`        | number         | Order-creation time in Unix milliseconds                                                                                 |
| `triggerCondition` | string         | `"N/A"` for non-trigger orders                                                                                           |
| `isTrigger`        | boolean        | Whether the order is a trigger order                                                                                     |
| `triggerPx`        | string         | `"0.0"` when `isTrigger` is false                                                                                        |
| `children`         | array          | Nested trigger-order schedule. Trigger orders (TP/SL) live here, not as top-level entries. Empty for non-trigger parents |
| `isPositionTpsl`   | boolean        | Whether this is a position-level take-profit / stop-loss                                                                 |
| `reduceOnly`       | boolean        | Whether the order is reduce-only                                                                                         |
| `orderType`        | string         | `"Limit"`, `"Trigger"`, etc.                                                                                             |
| `origSz`           | string         | Original size                                                                                                            |
| `tif`              | string         | Time-in-force, for example `"Alo"`, `"Gtc"`, `"Ioc"`                                                                     |
| `cloid`            | string or null | Client order ID                                                                                                          |

Guarantees and alignment:

- `events` contains all status records from the block, across all coins and users.
- Messages arrive in strictly increasing `block_number`, one per source block (including blocks with an empty `events` array).
- `block_number` is sequential and distinct from the potentially skipping `abci_block.round` consensus value in StreamBlocks; `block_time` aligns with `abci_block.time`.

Developer tips:

- Branch on `status` to track lifecycle transitions and isolate rejections.
- Handle `hash` and `builder` defensively; both can be `null`.
- Treat `time` as a formatted string, distinct from the integer-millisecond `time` on fill events.

## Order Identifiers

`oid` is **not reliably unique across coins**. Consumers that index or join per-order state must key on the `(coin, oid)` tuple, never on `oid` alone.

Outcome markets (HIP-4) demonstrably reuse oids across the two sides of a single market (for example `#30` and `#31`) within one block. Keying on `(coin, oid)` is safe whether or not oids turn out to be globally unique elsewhere.

## Common Use Cases

### 1. Rejection Monitor

```python
REJECTIONS = {
    'badAloPxRejected', 'insufficientSpotBalanceRejected', 'iocCancelRejected',
    'minTradeNtlRejected', 'perpMarginRejected', 'reduceOnlyRejected',
}

def count_rejections(block):
    counts = {}
    for event in block.get('events', []):
        status = event['status']
        if status in REJECTIONS:
            counts[status] = counts.get(status, 0) + 1
    return counts
```

### 2. Streaming Statuses in Python

Open V3 with an empty `StreamRequest` to tail newly written records, or set `StreamRequest.position.block_number` for inclusive replay. Decode each `Record.data` envelope before passing it to the rejection monitor above.

```python
import grpc
import json
import os
import hyperliquid_pb2
import hyperliquid_pb2_grpc

def stream_order_statuses():
    endpoint = os.getenv('HYPERLIQUID_ENDPOINT')
    api_key = os.getenv('API_KEY')

    credentials = grpc.ssl_channel_credentials()
    options = [('grpc.max_receive_message_length', 150 * 1024 * 1024)]  # 150MB max
    metadata = [('x-api-key', api_key)]

    with grpc.secure_channel(endpoint, credentials, options=options) as channel:
        client = hyperliquid_pb2_grpc.HyperliquidL1GatewayStub(channel)

        # Empty Position means latest; set block_height or timestamp_ms to backfill
        request = hyperliquid_pb2.Position()

        for response in client.StreamOrderStatuses(request, metadata=metadata):
            block = json.loads(response.data)
            for event in block.get('events', []):
                print(f"{event['status']:>24}  {event['order']['coin']}  oid={event['order']['oid']}")
```

## Progress Frames

Progress frames are an application-level heartbeat. Set `StreamRequest.progress` to receive one after each interval of silence. A `users` or `coins` filter can leave the stream silent for minutes. They keep it open through idle-limited proxies, which HTTP/2 keepalive pings do not, and advance your resume cursor through blocks the filter removed.

A progress frame sets `Record.progress.scanned_through.block_number` and leaves the data fields unset, so check `progress` before decoding `data`. Persist `scanned_through` as an inclusive resume cursor. See [progress frames](https://www.dwellir.com/docs/hyperliquid/grpc#progress-frames) for the full contract.

V2 streams do not send progress frames. Use the V3 method to opt in.

## Error Handling and Reconnection

Reconnect with `StreamRequest.position.block_number` set to the last committed V3 cursor, from a data record or a progress frame's `scanned_through`. Replay is inclusive, so deduplicate that record before advancing.

Reconnect by replaying from the last processed block. Pass the next `block_height` in the `Position` so no events are skipped or duplicated.

```python
def stream_with_resume(client, metadata, start_block):
    next_block = start_block
    while True:
        request = hyperliquid_pb2.Position(block_height=next_block)
        try:
            for response in client.StreamOrderStatuses(request, metadata=metadata):
                block = json.loads(response.data)
                process(block)
                next_block = block['block_number'] + 1
        except grpc.RpcError as e:
            print(f"stream error, resuming from {next_block}: {e}")
```

## Best Practices

1. **Connection Management**: Implement robust reconnection logic with exponential backoff
2. **Resumable State**: Follow the version-specific recovery guidance above and persist the cursor required by your selected API version
3. **Stable Keys**: Index per-order state on `(coin, oid)` to stay correct across coins
4. **Defensive Parsing**: Handle nullable `hash` and `builder` and the non-exhaustive `status` set
5. **Resource Cleanup**: Properly close streams and connections on shutdown

## Current Limitations

- **Replay History**: Available replay history varies by feed and endpoint. A cursor outside retained history returns `NOT_FOUND`.
- **Backpressure**: High-volume periods may require careful handling to avoid overwhelming downstream systems

## Resources

- **[GitHub: gRPC Code Examples](https://github.com/dwellir-public/gRPC-code-examples)** - Complete working examples
- **[GetOrderStatuses](https://www.dwellir.com/docs/hyperliquid/get_order_statuses)** - Point-in-time order status retrieval
- **[StreamRawBookDiffs](https://www.dwellir.com/docs/hyperliquid/stream_raw_book_diffs)** - The sibling block-envelope stream for resting-book changes
- **[Historical Data](https://www.dwellir.com/docs/hyperliquid/historical-data)** - Historical `node_order_statuses_by_block` downloads

*Need help? Contact our [support team](mailto:support@dwellir.com) or check the [Hyperliquid gRPC documentation](https://www.dwellir.com/docs/hyperliquid/grpc).*
