Docs

StreamFills - Real-time Fill Streaming

Stream continuous fill data from Hyperliquid L1 Gateway via gRPC. Monitor order executions in real-time.

Stream continuous fill data starting from a position, providing real-time access to order executions on Hyperliquid.

Full Code Examples

Clone our gRPC Code Examples Repository for complete, runnable implementations. See the copy-trading-bot for a production-ready example using StreamFills.

When to Use This Method

StreamFills is essential for:

  • Trade Monitoring - Track order executions in real-time
  • Position Management - Monitor fills for active trading strategies
  • Settlement Tracking - Verify order completions and partial fills
  • Analytics & Reporting - Collect comprehensive trade execution data

Method Signature

protobuf
rpc StreamFills(Position) returns (stream BlockFills) {}

Response Stream

protobuf
message BlockFills {
  // JSON-encoded object from "node_fills" or "node_fills_by_block".
  bytes data = 1;
}

The data field contains a JSON-encoded fills object with:

  • Fill execution details (price, size, side)
  • Order identifiers (order ID, client order ID)
  • Counterparty information
  • Timestamp and block reference

Full Fill Spec

StreamFills emits a JSON payload matching Hyperliquid's node_fills format. Below is the exact structure.

Top-level keys (always present):

JSON
{
  "local_time": "2025-07-27T08:50:10.334741319",   // ISO-8601 timestamp when node processed fill (nanosecond precision)
  "block_time": "2025-07-27T08:50:10.273720809",   // ISO-8601 timestamp from block consensus
  "block_number": 676607012,                        // Block height containing this fill
  "events": [                                       // Array of [address, fill_data] pairs
    [
      "0x7839e2f2c375dd2935193f2736167514efff9916",  // User address (40 hex chars, lowercase)
      {
        "coin": "BTC",                              // Trading pair symbol
        "px": "118136.0",                           // Fill price (string)
        "sz": "0.00009",                            // Fill size (string)
        "side": "B",                                // "B" (buy) or "A" (sell/ask)
        "time": 1753606210273,                      // Fill timestamp (ms since Unix epoch)
        "startPosition": "-1.41864",                // Position size before fill (string)
        "dir": "Close Short",                       // Direction: "Open Long" | "Open Short" | "Close Long" | "Close Short"
        "closedPnl": "-0.003753",                   // Realized PnL from closing position (string, can be negative)
        "hash": "0xe7822040155eaa2e737e042854342401120052bbf063906ce8c8f3babe853a79",  // Transaction hash (64 hex)
        "oid": 121670079265,                        // Order ID (numeric)
        "crossed": false,                           // Whether order crossed the spread
        "fee": "-0.000212",                         // Trading fee (string, negative = rebate)
        "tid": 161270588369408,                     // Trade ID (unique identifier)
        "cloid": "0x09367b9f8541c581f95b02aaf05f1508",  // Client order ID (optional, 32 hex)
        "feeToken": "USDC",                         // Token used for fee payment
        "twapId": null,                             // TWAP order ID (optional, null if not a TWAP fill)
        "liquidation": null,                        // Liquidation details (optional, null for normal fills)
        "builder": "0x49ae63056b3a0be0b166813ee687309ab653c07c",  // Builder address (optional)
        "builderFee": "0.005528"                    // Builder fee amount (optional, string)
      }
    ]
    // ... more [address, fill_data] pairs
  ]
}

Fill event entry (events[i]):

jsonc
[
  "0x...",  // user_address - the trader's address
  {
    // Fill details object
  }
]

Fill details fields:

FieldTypeDescription
coinstringTrading pair symbol (e.g., "BTC", "ETH", "SOL")
pxstringFill price
szstringFill size
sidestring"B" for buy, "A" for sell
timenumberFill timestamp in milliseconds since Unix epoch
startPositionstringPosition size before this fill
dirstringPosition direction: "Open Long", "Open Short", "Close Long", "Close Short"
closedPnlstringRealized PnL if closing a position (can be negative)
hashstringTransaction hash (64 hex characters)
oidnumberOrder ID assigned by the system
crossedbooleanMaker/taker indicator. true = taker (crossed the spread), false = maker (resting order). See Maker vs Taker
feestringTrading fee (negative values indicate rebates)
tidnumberUnique trade identifier
cloidstringClient order ID if provided (optional)
feeTokenstringToken used for fee payment
twapIdnumber/nullTWAP order ID if the fill is part of a TWAP execution (optional)
liquidationobject/nullLiquidation details, present only when the fill is part of a forced position closure. See Liquidation Data
builderstringBuilder address if order was routed through a builder (optional)
builderFeestringFee paid to builder (optional)

Guarantees and alignment:

  • events array contains all fills from the block for all users.
  • Each event pairs a user address with their fill details.
  • block_number corresponds to abci_block.round in StreamBlocks.
  • block_time aligns with abci_block.time in StreamBlocks.
  • Multiple fills per block are delivered in a single message.

Developer tips:

  • Use tid as the unique identifier for deduplication.
  • Track startPosition and dir to reconstruct position changes.
  • Sum closedPnl across fills to calculate realized PnL.
  • Normalize px, sz, fee, and closedPnl (strings) to numeric types as appropriate.
  • Handle optional fields (cloid, twapId, liquidation, builder, builderFee) defensively.
  • Check liquidation to detect forced position closures. See Liquidation Data.
  • Use crossed to determine maker/taker status. See Maker vs Taker.

Liquidation Data

Liquidation events are embedded within the fills stream. When a fill is part of a forced position closure, the liquidation field is present on the fill object.

A single liquidation event produces two fills per match:

  1. The liquidated user's closing fill — the forced position close
  2. The counterparty's fill — the trader whose resting order filled the liquidation

Both fills share the same tid and hash. To identify the actual liquidated user (not the counterparty), check that:

  • liquidation is present on the fill
  • dir contains "Close" (e.g., "Close Long", "Close Short")
  • The user address matches liquidation.liquidatedUser

Liquidation Object Fields

FieldTypeDescription
liquidatedUserstringAddress of the user being liquidated
markPxstringMark price at the time of liquidation
methodstringLiquidation method: "market" or "backstop"

Liquidation Methods

MethodDescription
marketStandard liquidation filled against resting orders on the book
backstopBackstop liquidation used when the order book cannot absorb the position

Example: Liquidation Fill

JSON
{
  "block_number": 859864054,
  "block_time": "2026-04-16T14:22:31.456000Z",
  "events": [
    [
      "0x7a3b1c9d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
      {
        "coin": "BTC",
        "px": "103850.0",
        "sz": "0.0234",
        "side": "A",
        "time": 1744813351456,
        "startPosition": "0.0234",
        "dir": "Close Long",
        "closedPnl": "-31.599",
        "hash": "0xab12cd34ef56ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12",
        "oid": 199445678901,
        "crossed": true,
        "fee": "0.073107",
        "tid": 884916789012345,
        "cloid": null,
        "feeToken": "USDC",
        "twapId": null,
        "liquidation": {
          "liquidatedUser": "0x7a3b1c9d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
          "markPx": "103842.5",
          "method": "market"
        }
      }
    ],
    [
      "0x1234abcd5678ef901234abcd5678ef901234abcd",
      {
        "coin": "BTC",
        "px": "103850.0",
        "sz": "0.0234",
        "side": "B",
        "time": 1744813351456,
        "startPosition": "-0.125",
        "dir": "Close Short",
        "closedPnl": "2.457",
        "hash": "0xab12cd34ef56ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12",
        "oid": 199445678902,
        "crossed": false,
        "fee": "-0.007311",
        "tid": 884916789012345,
        "cloid": "0x00000000000003849201784629103847",
        "feeToken": "USDC",
        "twapId": null,
        "liquidation": {
          "liquidatedUser": "0x7a3b1c9d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
          "markPx": "103842.5",
          "method": "market"
        }
      }
    ]
  ]
}

In this example:

  • The first fill (0x7a3b...) is the liquidated userdir is "Close Long" and their address matches liquidatedUser
  • The second fill (0x1234...) is the counterparty — same liquidatedUser but a different address
  • The liquidated user has negative closedPnl (loss) and positive fee (paid fee as taker)
  • The counterparty has positive closedPnl (profit) and negative fee (received rebate as maker)

Maker vs Taker

The crossed field on every fill directly indicates maker or taker status:

crossedRoleMeaning
trueTakerThe order crossed the spread — it matched immediately against a resting order
falseMakerThe order was resting on the book — it provided liquidity and was filled by an incoming order

Fee as a Secondary Signal

The fee field corroborates the maker/taker role:

Fee signRoleExample
Positive (e.g., "0.073107")Taker — paid a fee"crossed": true, "fee": "0.073107"
Negative (e.g., "-0.007311")Maker — received a rebate"crossed": false, "fee": "-0.007311"

Matching Both Sides of a Trade

Each trade produces two fills (buyer and seller). Both sides share the same tid (trade ID) and hash (transaction hash). You can reconstruct the full trade by matching on either field.

Liquidation Fills and Maker/Taker

In liquidation fills, the liquidated user is always the taker (crossed: true) because the liquidation engine sends a market order to close their position. The counterparty whose resting order gets filled is the maker (crossed: false) and receives a fee rebate.

PartycrossedfeeRole
Liquidated usertruePositive (pays fee)Taker — forced market order
CounterpartyfalseNegative (receives rebate)Maker — resting order filled

Common Use Cases

1. Trade Execution Tracker

JavaScript
class TradeExecutionTracker {
    constructor(streamManager) {
        this.streamManager = streamManager;
        this.executedTrades = new Map();

        streamManager.on('fill', (fillData) => {
            this.processFill(fillData);
        });
    }

    processFill(fillData) {
        // Track each fill by order ID
        const orderId = fillData.oid;

        if (!this.executedTrades.has(orderId)) {
            this.executedTrades.set(orderId, {
                orderId,
                fills: [],
                totalFilled: 0,
                avgPrice: 0
            });
        }

        const trade = this.executedTrades.get(orderId);
        trade.fills.push(fillData);
        trade.totalFilled += parseFloat(fillData.sz);

        // Recalculate average price
        const totalValue = trade.fills.reduce(
            (sum, f) => sum + parseFloat(f.px) * parseFloat(f.sz), 0
        );
        trade.avgPrice = totalValue / trade.totalFilled;

        console.log(`Order ${orderId}: Filled ${trade.totalFilled} @ avg ${trade.avgPrice}`);
    }
}

2. Fill Rate Monitor

Python
class FillRateMonitor:
    def __init__(self):
        self.fills_per_minute = []
        self.current_minute_fills = 0
        self.last_minute = None

    def record_fill(self, fill_data):
        """Record a fill and track rates"""
        from datetime import datetime

        current_minute = datetime.now().replace(second=0, microsecond=0)

        if self.last_minute != current_minute:
            if self.last_minute is not None:
                self.fills_per_minute.append({
                    'minute': self.last_minute,
                    'count': self.current_minute_fills
                })
            self.last_minute = current_minute
            self.current_minute_fills = 0

        self.current_minute_fills += 1

    def get_average_rate(self, minutes=5):
        """Get average fills per minute over last N minutes"""
        recent = self.fills_per_minute[-minutes:]
        if not recent:
            return 0
        return sum(r['count'] for r in recent) / len(recent)

3. Position Reconciliation

Go
type PositionReconciler struct {
    client pb.HyperliquidL1GatewayClient
    positions map[string]float64
}

func (pr *PositionReconciler) ReconcileFills(ctx context.Context) {
    stream, err := pr.client.StreamFills(ctx, &pb.Position{})
    if err != nil {
        log.Fatal(err)
    }

    for {
        fill, err := stream.Recv()
        if err != nil {
            log.Printf("Stream error: %v", err)
            return
        }

        pr.updatePosition(fill.Data)
    }
}

func (pr *PositionReconciler) updatePosition(data []byte) {
    var fillData map[string]interface{}
    json.Unmarshal(data, &fillData)

    // Extract fill details and update position
    if coin, ok := fillData["coin"].(string); ok {
        size, _ := fillData["sz"].(float64)
        side, _ := fillData["side"].(string)

        if side == "B" {
            pr.positions[coin] += size
        } else {
            pr.positions[coin] -= size
        }

        log.Printf("Position %s: %f", coin, pr.positions[coin])
    }
}

Error Handling and Reconnection

JavaScript
class RobustFillStreamer {
    constructor(endpoint) {
        this.endpoint = endpoint;
        this.maxRetries = 5;
        this.retryDelay = 1000;
        this.currentRetries = 0;
    }

    async startStreamWithRetry() {
        while (this.currentRetries < this.maxRetries) {
            try {
                await this.startStream();
                this.currentRetries = 0;
                this.retryDelay = 1000;
            } catch (error) {
                this.currentRetries++;
                console.error(`Stream attempt ${this.currentRetries} failed:`, error.message);

                if (this.currentRetries >= this.maxRetries) {
                    throw new Error('Max retry attempts exceeded');
                }

                // Exponential backoff
                await this.sleep(this.retryDelay);
                this.retryDelay *= 2;
            }
        }
    }

    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

Best Practices

  1. Connection Management: Implement robust reconnection logic with exponential backoff
  2. Memory Management: Use bounded collections for storing recent fills to prevent memory leaks
  3. Performance: Process fills asynchronously to avoid blocking the stream
  4. Monitoring: Track stream health and fill rates
  5. Error Recovery: Handle various error types (network, parsing, processing) gracefully
  6. Resource Cleanup: Properly close streams and connections on shutdown

Current Limitations

  • Historical Data: Cannot stream from historical timestamps; only real-time streaming available
  • Data Retention: Node maintains only 24 hours of historical fill data
  • 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.