Docs

StreamTwapStatuses - Real-time TWAP Status Streaming

Stream TWAP order lifecycle events from Hyperliquid L1 Gateway via gRPC. Track TWAP status changes and execution progress per block in real time.

Stream TWAP order lifecycle events starting from a position, providing real-time access to TWAP state updates across Hyperliquid.

Full Code Examples

Clone our gRPC Code Examples Repository for complete, runnable implementations in Go, Python, and Node.js.

When to Use This Method

StreamTwapStatuses is essential for:

  • TWAP Lifecycle Tracking - Follow TWAP orders as their state changes
  • Execution Monitoring - Track executed size and notional over time
  • Strategy Analytics - Reconstruct TWAP execution progress across blocks
  • Compliance and Auditing - Record TWAP status transitions with block alignment

Pairs with TWAP Actions

StreamTwapStatuses reports lifecycle changes after the order exists on HyperCore. To decode the signed request that created or canceled a TWAP, pair this stream with twapOrder and twapCancel actions from StreamBlocks.

Method Signature

protobuf
rpc StreamTwapStatuses(Position) returns (stream BlockTwapStatuses) {}

Response Stream

protobuf
message BlockTwapStatuses {
  // JSON-encoded block envelope from "node_twap_statuses_by_block".
  bytes data = 1;
}

The data field contains a JSON-encoded block envelope with:

  • Block reference (local_time, block_time, block_number)
  • An events array of TWAP status records
  • A TWAP state object behind each lifecycle update

Per-Block Delivery

StreamTwapStatuses emits one message per on-disk block-envelope record, including records with an empty events array. Most blocks do not contain a TWAP lifecycle update, so empty envelopes are normal. They still let consumers advance their cursor and confirm exactly which block was last processed.

Messages arrive in strictly increasing block_number order with no gaps and no duplicates. The stream is stateless: reconnect by passing the next block_height (or a timestamp_ms) in the Position and resume exactly where you left off. Both are inclusive; an empty or zero Position targets the latest data.

Full TWAP Status Spec

StreamTwapStatuses emits a JSON payload matching Hyperliquid's node_twap_statuses_by_block format, passed through byte-for-byte from the node with no normalization. The envelope is the same block-envelope shape as fills, but events holds TWAP lifecycle objects rather than fill tuples.

Top-level keys (always present):

JSON
{
  "local_time": "2026-07-03T06:00:14.508835456",
  "block_time": "2026-07-03T06:00:14.057132297",
  "block_number": 1058646974,
  "events": [
    {
      "time": "2026-07-03T06:00:14.057132297",
      "twap_id": 1999381,
      "state": {
        "coin": "kPEPE",
        "user": "0x2ee1f7d5650bb9bc08e101dc5ab2300b4f5c1be9",
        "side": "B",
        "sz": "555555.0",
        "executedSz": "555555.0",
        "executedNtl": "1371.51378",
        "minutes": 5,
        "reduceOnly": false,
        "randomize": false,
        "timestamp": 1783058112775
      },
      "status": "finished"
    }
  ]
}

Status event fields (events[i]):

FieldTypeDescription
timestringEvent timestamp in the no-Z layout. A string, not an integer millisecond value
twap_idnumberTWAP order identifier
stateobjectTWAP state at the lifecycle update. See TWAP State Object
statusstring or objectLifecycle status. See Status Values

Every event is global, not user-scoped to the API key. Filter by state.user, state.coin, or twap_id in your application when you only need a subset.

TWAP State Object

FieldTypeDescription
coinstringCoin identifier
userstringUser address
sidestring"B" for buy, "A" for sell
szstringTarget TWAP size as a numeric string
executedSzstringExecuted size so far as a numeric string
executedNtlstringExecuted notional so far as a numeric string
minutesnumberTWAP duration in minutes
reduceOnlybooleanWhether the TWAP is reduce-only
randomizebooleanWhether the TWAP randomizes slice timing or size
timestampnumberTWAP creation time in Unix milliseconds

The state object is a snapshot of the TWAP at the lifecycle event, not a fill-by-fill execution log. Use executedSz and executedNtl to measure aggregate progress for that TWAP at the event boundary. For individual trade executions, consume StreamFills and join on user, coin, and time windows according to your own strategy model.

Status Values

Observed string status values include:

StatusMeaning
activatedTWAP order became active
finishedTWAP execution completed or otherwise reached its terminal state
terminatedTWAP order was terminated before normal completion

status can also be an object when the lifecycle update carries an error:

JSON
{
  "status": {
    "error": "Reduce only order would increase position."
  }
}

Treat the status set as non-exhaustive. Hyperliquid can add new lifecycle states and error payloads over time.

Lifecycle Examples

An activated event starts a TWAP lifecycle. At activation, executedSz and executedNtl are typically zero:

JSON
{
  "time": "2026-07-03T06:00:30.373151284",
  "twap_id": 1999392,
  "state": {
    "coin": "xyz:NBIS",
    "user": "0x6f97b7de6be7b7771e975e46bae96c35e332e172",
    "side": "B",
    "sz": "226.72",
    "executedSz": "0.0",
    "executedNtl": "0.0",
    "minutes": 7,
    "reduceOnly": false,
    "randomize": false,
    "timestamp": 1783058430373
  },
  "status": "activated"
}

A terminated event records the final observed state when a TWAP stops before normal completion. Partial execution is visible through executedSz and executedNtl:

JSON
{
  "time": "2026-07-03T06:02:08.835250901",
  "twap_id": 1999394,
  "state": {
    "coin": "xyz:NBIS",
    "user": "0x6f97b7de6be7b7771e975e46bae96c35e332e172",
    "side": "B",
    "sz": "226.7",
    "executedSz": "60.45",
    "executedNtl": "13377.625",
    "minutes": 7,
    "reduceOnly": false,
    "randomize": false,
    "timestamp": 1783058434767
  },
  "status": "terminated"
}

Guarantees and alignment:

  • events contains TWAP status records from the block across all users.
  • Messages arrive in strictly increasing block_number, one per on-disk block-envelope record.
  • block_number corresponds to abci_block.round in StreamBlocks; block_time aligns with abci_block.time.

Developer tips:

  • Use twap_id as the primary key for TWAP lifecycle state.
  • Parse sz, executedSz, and executedNtl as decimals to avoid precision loss.
  • Treat time as a formatted string, distinct from the integer-millisecond timestamp inside state.
  • Handle empty events arrays; quiet blocks are normal.
  • Store the raw block envelope if you need reproducible audits; the gateway does not normalize or reshape the node payload.

Common Use Cases

1. TWAP Lifecycle Index

Python
from decimal import Decimal

def apply_twap_status(block, index):
    for event in block.get('events', []):
        state = event['state']
        target = Decimal(state['sz'])
        executed = Decimal(state['executedSz'])

        status = event['status']
        status_key = status if isinstance(status, str) else 'error'

        index[event['twap_id']] = {
            'status': status,
            'status_key': status_key,
            'user': state['user'],
            'coin': state['coin'],
            'side': state['side'],
            'target_size': target,
            'executed_size': executed,
            'progress': executed / target if target else Decimal('0'),
            'block_number': block['block_number'],
            'block_time': block['block_time'],
        }

2. TWAP Alert Stream

Python
def alert_on_terminal_twaps(block):
    for event in block.get('events', []):
        status = event['status']
        if status not in ('finished', 'terminated') and not isinstance(status, dict):
            continue

        state = event['state']
        status_label = status.get('error', 'error') if isinstance(status, dict) else status
        print(
            f"{status_label} twap={event['twap_id']} "
            f"user={state['user']} coin={state['coin']} "
            f"executed={state['executedSz']}/{state['sz']}"
        )

3. Streaming TWAP Statuses in Python

Python
import grpc
import json
import os
from decimal import Decimal
import hyperliquid_pb2
import hyperliquid_pb2_grpc

def apply_twap_status(block, index):
    for event in block.get('events', []):
        state = event['state']
        target = Decimal(state['sz'])
        executed = Decimal(state['executedSz'])

        status = event['status']
        status_key = status if isinstance(status, str) else 'error'

        index[event['twap_id']] = {
            'status': status,
            'status_key': status_key,
            'user': state['user'],
            'coin': state['coin'],
            'side': state['side'],
            'target_size': target,
            'executed_size': executed,
            'progress': executed / target if target else Decimal('0'),
            'block_number': block['block_number'],
        }

def stream_twap_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()

        twaps = {}

        for response in client.StreamTwapStatuses(request, metadata=metadata):
            block = json.loads(response.data)
            apply_twap_status(block, twaps)

Error Handling and Reconnection

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.StreamTwapStatuses(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: Track the last processed block_number so you can resume from the next block
  3. Stable Keys: Index TWAP lifecycle state on twap_id
  4. Defensive Parsing: Handle empty events arrays, unknown future string statuses, and object status payloads
  5. Numeric Precision: Parse numeric strings with decimal-safe types
  6. Action Correlation: Join with StreamBlocks when you need the original signed twapOrder or twapCancel action
  7. Selective Storage: Store terminal events and the last active state if you do not need every empty envelope

Current Limitations

  • Data Retention: Historical availability follows the gateway's retained on-disk feed; pruned positions return NotFound
  • Schema Coverage: The upstream lifecycle set can grow; treat documented statuses as examples, not an exhaustive list
  • Backpressure: High-volume periods may require careful handling to avoid overwhelming downstream systems

Resources

Need help? Contact our support team or check the Hyperliquid gRPC documentation.