Docs

GetFills - Get Fill Data

Retrieve fills at a specific position from Hyperliquid L1 Gateway via gRPC. Get order execution data for analysis and reconciliation.

Retrieve fills at a specific position from the Hyperliquid L1 Gateway.

Full Code Examples

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

When to Use This Method

GetFills is essential for:

  • Trade Reconciliation - Verify fills at a specific point in time
  • Historical Analysis - Retrieve fill data for backtesting or reporting
  • Auditing - Verify trade executions at specific blocks
  • Debugging - Investigate specific fills during development

Method Signature

protobuf
rpc GetFills(Position) returns (BlockFills) {}

Request Message

protobuf
message Position {
  // Leave all fields unset or zero to target the latest data.
  oneof position {
    int64 timestamp_ms = 1; // ms since Unix epoch, inclusive
    int64 block_height = 2; // block height, inclusive
  }
}

The Position message allows flexible fill targeting:

  • timestamp_ms: Get fills at or after a specific time (milliseconds since Unix epoch)
  • block_height: Get fills at a specific block height
  • Empty/zero: Get the latest fills

Response Message

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 the following structure:

Data Structure

JSON
{
  "local_time": "2025-07-27T08:50:10.334741319",
  "block_time": "2025-07-27T08:50:10.273720809",
  "block_number": 676607012,
  "events": [
    [
      "0x7839e2f2c375dd2935193f2736167514efff9916",
      {
        "coin": "BTC",
        "px": "118136.0",
        "sz": "0.00009",
        "side": "B",
        "time": 1753606210273,
        "startPosition": "-1.41864",
        "dir": "Close Short",
        "closedPnl": "-0.003753",
        "hash": "0xe7822040155eaa2e737e042854342401120052bbf063906ce8c8f3babe853a79",
        "oid": 121670079265,
        "crossed": false,
        "fee": "-0.000212",
        "tid": 161270588369408,
        "cloid": "0x09367b9f8541c581f95b02aaf05f1508",
        "feeToken": "USDC",
        "builder": "0x49ae63056b3a0be0b166813ee687309ab653c07c",
        "builderFee": "0.005528"
      }
    ]
  ]
}

Field Descriptions

FieldTypeDescription
local_timestringISO-8601 timestamp when node processed the fill
block_timestringISO-8601 timestamp from block consensus
block_numbernumberBlock height containing these fills
eventsarrayArray of [address, fill_data] pairs

Fill Event Fields

FieldTypeDescription
coinstringTrading pair symbol (e.g., "BTC", "ETH")
pxstringFill price
szstringFill size
sidestring"B" for buy, "A" for sell
timenumberFill timestamp in milliseconds
startPositionstringPosition size before this fill
dirstringDirection: "Open Long", "Open Short", "Close Long", "Close Short"
closedPnlstringRealized PnL if closing position
hashstringTransaction hash
oidnumberOrder ID
crossedbooleanWhether order crossed the spread
feestringTrading fee (negative = rebate)
tidnumberUnique trade identifier
cloidstringClient order ID (optional)
feeTokenstringToken used for fee
builderstringBuilder address (optional)
builderFeestringBuilder fee amount (optional)

For the complete fill specification, see the StreamFills documentation.

Implementation Examples

Go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"

    pb "hyperliquid-grpc-client/api/v2"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/metadata"
)

type FillEvent struct {
    Coin          string  `json:"coin"`
    Px            string  `json:"px"`
    Sz            string  `json:"sz"`
    Side          string  `json:"side"`
    Time          int64   `json:"time"`
    StartPosition string  `json:"startPosition"`
    Dir           string  `json:"dir"`
    ClosedPnl     string  `json:"closedPnl"`
    Hash          string  `json:"hash"`
    Oid           int64   `json:"oid"`
    Crossed       bool    `json:"crossed"`
    Fee           string  `json:"fee"`
    Tid           int64   `json:"tid"`
    Cloid         string  `json:"cloid,omitempty"`
    FeeToken      string  `json:"feeToken"`
}

type FillsData struct {
    LocalTime   string          `json:"local_time"`
    BlockTime   string          `json:"block_time"`
    BlockNumber int64           `json:"block_number"`
    Events      [][]interface{} `json:"events"`
}

func getFills(ctx context.Context, client pb.HyperliquidL1GatewayClient, position *pb.Position) (*FillsData, error) {
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    response, err := client.GetFills(ctx, position)
    if err != nil {
        return nil, fmt.Errorf("RPC call failed: %v", err)
    }

    fmt.Printf("Response size: %d bytes\n\n", len(response.Data))

    var fills FillsData
    if err := json.Unmarshal(response.Data, &fills); err != nil {
        return nil, fmt.Errorf("failed to parse JSON: %v", err)
    }

    return &fills, nil
}

func main() {
    endpoint := os.Getenv("HYPERLIQUID_ENDPOINT")
    apiKey := os.Getenv("API_KEY")

    if endpoint == "" {
        log.Fatal("Error: HYPERLIQUID_ENDPOINT environment variable is required.")
    }

    if apiKey == "" {
        log.Fatal("Error: API_KEY environment variable is required.")
    }

    fmt.Println("Hyperliquid Go gRPC Client - Get Fills")
    fmt.Println("======================================")
    fmt.Printf("Endpoint: %s\n\n", endpoint)

    creds := credentials.NewTLS(nil)

    opts := []grpc.DialOption{
        grpc.WithTransportCredentials(creds),
        grpc.WithDefaultCallOptions(
            grpc.MaxCallRecvMsgSize(150 * 1024 * 1024),
        ),
    }

    fmt.Println("Connecting to gRPC server...")
    conn, err := grpc.NewClient(endpoint, opts...)
    if err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewHyperliquidL1GatewayClient(conn)
    fmt.Println("Connected successfully!\n")

    ctx := context.Background()
    ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", apiKey)

    // Request latest fills (empty position)
    request := &pb.Position{}

    // Or request by block height:
    // request := &pb.Position{Position: &pb.Position_BlockHeight{BlockHeight: 676607012}}

    fmt.Println("Fetching fills...\n")

    fills, err := getFills(ctx, client, request)
    if err != nil {
        log.Fatalf("Failed to get fills: %v", err)
    }

    // Display fills information
    fmt.Printf("Block Number: %d\n", fills.BlockNumber)
    fmt.Printf("Block Time: %s\n", fills.BlockTime)
    fmt.Printf("Local Time: %s\n", fills.LocalTime)
    fmt.Printf("Fill Events: %d\n\n", len(fills.Events))

    // Process each fill event
    for i, event := range fills.Events {
        if len(event) >= 2 {
            address := event[0].(string)
            fillData, _ := json.Marshal(event[1])
            var fill FillEvent
            json.Unmarshal(fillData, &fill)

            fmt.Printf("Fill #%d:\n", i+1)
            fmt.Printf("  Address: %s\n", address)
            fmt.Printf("  Coin: %s\n", fill.Coin)
            fmt.Printf("  Side: %s\n", fill.Side)
            fmt.Printf("  Price: %s\n", fill.Px)
            fmt.Printf("  Size: %s\n", fill.Sz)
            fmt.Printf("  Direction: %s\n", fill.Dir)
            fmt.Printf("  Fee: %s %s\n", fill.Fee, fill.FeeToken)
            fmt.Println()
        }
    }
}
Python
import grpc
import json
import sys
import os
from datetime import datetime
from dotenv import load_dotenv
import hyperliquid_pb2
import hyperliquid_pb2_grpc

load_dotenv()

def get_fills(block_height=None, timestamp_ms=None):
    endpoint = os.getenv('HYPERLIQUID_ENDPOINT')
    api_key = os.getenv('API_KEY')

    if not endpoint:
        print("Error: HYPERLIQUID_ENDPOINT environment variable is required.")
        sys.exit(1)

    if not api_key:
        print("Error: API_KEY environment variable is required.")
        sys.exit(1)

    print('Hyperliquid Python gRPC Client - Get Fills')
    print('==========================================')
    print(f'Endpoint: {endpoint}\n')

    credentials = grpc.ssl_channel_credentials()

    options = [
        ('grpc.max_receive_message_length', 150 * 1024 * 1024),
    ]

    metadata = [('x-api-key', api_key)]

    print('Connecting to gRPC server...')
    with grpc.secure_channel(endpoint, credentials, options=options) as channel:
        client = hyperliquid_pb2_grpc.HyperliquidL1GatewayStub(channel)
        print('Connected successfully!\n')

        # Create request based on parameters
        if block_height is not None:
            request = hyperliquid_pb2.Position(block_height=block_height)
            print(f'Requesting fills at block height: {block_height}')
        elif timestamp_ms is not None:
            request = hyperliquid_pb2.Position(timestamp_ms=timestamp_ms)
            print(f'Requesting fills at timestamp: {timestamp_ms}')
        else:
            request = hyperliquid_pb2.Position()
            print('Requesting latest fills')

        print('\nFetching fills...\n')

        try:
            response = client.GetFills(request, metadata=metadata)
            print(f'Response size: {len(response.data)} bytes\n')

            process_fills(response.data)

        except grpc.RpcError as e:
            print(f'RPC error: {e}')
        except Exception as e:
            print(f'Error: {e}')

def process_fills(data):
    try:
        fills = json.loads(data.decode('utf-8'))

        print('FILLS DATA')
        print('==========')

        print(f'Block Number: {fills.get("block_number")}')
        print(f'Block Time: {fills.get("block_time")}')
        print(f'Local Time: {fills.get("local_time")}')

        events = fills.get('events', [])
        print(f'Fill Events: {len(events)}\n')

        # Aggregate statistics
        total_volume = 0
        total_pnl = 0
        coins_traded = set()
        buy_count = 0
        sell_count = 0

        for i, event in enumerate(events):
            if len(event) >= 2:
                address = event[0]
                fill = event[1]

                coins_traded.add(fill.get('coin', ''))

                size = float(fill.get('sz', 0))
                price = float(fill.get('px', 0))
                total_volume += size * price

                pnl = float(fill.get('closedPnl', 0))
                total_pnl += pnl

                if fill.get('side') == 'B':
                    buy_count += 1
                else:
                    sell_count += 1

                # Display first 5 fills in detail
                if i < 5:
                    print(f'Fill #{i + 1}:')
                    print(f'  Address: {address}')
                    print(f'  Coin: {fill.get("coin")}')
                    print(f'  Side: {"Buy" if fill.get("side") == "B" else "Sell"}')
                    print(f'  Price: {fill.get("px")}')
                    print(f'  Size: {fill.get("sz")}')
                    print(f'  Direction: {fill.get("dir")}')
                    print(f'  Closed PnL: {fill.get("closedPnl")}')
                    print(f'  Fee: {fill.get("fee")} {fill.get("feeToken")}')
                    print()

        if len(events) > 5:
            print(f'... and {len(events) - 5} more fills\n')

        # Summary
        print('SUMMARY')
        print('=======')
        print(f'Total Fills: {len(events)}')
        print(f'Buys: {buy_count}, Sells: {sell_count}')
        print(f'Coins Traded: {", ".join(coins_traded)}')
        print(f'Total Volume: ${total_volume:,.2f}')
        print(f'Total Realized PnL: ${total_pnl:,.4f}')

    except json.JSONDecodeError as e:
        print(f'Failed to parse JSON: {e}')
    except Exception as e:
        print(f'Error processing fills: {e}')

if __name__ == '__main__':
    # Get latest fills
    get_fills()

    # Or get by block height:
    # get_fills(block_height=676607012)

    # Or get by timestamp:
    # get_fills(timestamp_ms=1753606210273)
JavaScript
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
require('dotenv').config();

const PROTO_PATH = path.join(__dirname, 'v2.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
});

const proto = grpc.loadPackageDefinition(packageDefinition);

async function getFills(options = {}) {
    const endpoint = process.env.HYPERLIQUID_ENDPOINT;
    const apiKey = process.env.API_KEY;

    if (!endpoint) {
        console.error('Error: HYPERLIQUID_ENDPOINT environment variable is required.');
        process.exit(1);
    }

    if (!apiKey) {
        console.error('Error: API_KEY environment variable is required.');
        process.exit(1);
    }

    console.log('Hyperliquid Node.js gRPC Client - Get Fills');
    console.log('===========================================');
    console.log(`Endpoint: ${endpoint}\n`);

    const metadata = new grpc.Metadata();
    metadata.add('x-api-key', apiKey);

    const client = new proto.hyperliquid_l1_gateway.v2.HyperliquidL1Gateway(
        endpoint,
        grpc.credentials.createSsl(),
        {
            'grpc.max_receive_message_length': 150 * 1024 * 1024
        }
    );

    // Build request
    const request = {};
    if (options.blockHeight) {
        request.block_height = options.blockHeight;
        console.log(`Requesting fills at block height: ${options.blockHeight}`);
    } else if (options.timestampMs) {
        request.timestamp_ms = options.timestampMs;
        console.log(`Requesting fills at timestamp: ${options.timestampMs}`);
    } else {
        console.log('Requesting latest fills');
    }

    console.log('\nFetching fills...\n');

    client.GetFills(request, metadata, (error, response) => {
        if (error) {
            console.error('gRPC error:', error.message);
            return;
        }

        try {
            const fills = JSON.parse(response.data);
            console.log(`Response size: ${response.data.length} bytes\n`);

            processFills(fills);

        } catch (error) {
            console.error('Failed to parse response:', error.message);
        }
    });
}

function processFills(fills) {
    console.log('FILLS DATA');
    console.log('==========');

    console.log(`Block Number: ${fills.block_number}`);
    console.log(`Block Time: ${fills.block_time}`);
    console.log(`Local Time: ${fills.local_time}`);

    const events = fills.events || [];
    console.log(`Fill Events: ${events.length}\n`);

    // Aggregate statistics
    let totalVolume = 0;
    let totalPnl = 0;
    const coinsTraded = new Set();
    let buyCount = 0;
    let sellCount = 0;

    for (let i = 0; i < events.length; i++) {
        const event = events[i];
        if (event.length >= 2) {
            const address = event[0];
            const fill = event[1];

            coinsTraded.add(fill.coin || '');

            const size = parseFloat(fill.sz || 0);
            const price = parseFloat(fill.px || 0);
            totalVolume += size * price;

            const pnl = parseFloat(fill.closedPnl || 0);
            totalPnl += pnl;

            if (fill.side === 'B') {
                buyCount++;
            } else {
                sellCount++;
            }

            // Display first 5 fills in detail
            if (i < 5) {
                console.log(`Fill #${i + 1}:`);
                console.log(`  Address: ${address}`);
                console.log(`  Coin: ${fill.coin}`);
                console.log(`  Side: ${fill.side === 'B' ? 'Buy' : 'Sell'}`);
                console.log(`  Price: ${fill.px}`);
                console.log(`  Size: ${fill.sz}`);
                console.log(`  Direction: ${fill.dir}`);
                console.log(`  Closed PnL: ${fill.closedPnl}`);
                console.log(`  Fee: ${fill.fee} ${fill.feeToken}`);
                console.log();
            }
        }
    }

    if (events.length > 5) {
        console.log(`... and ${events.length - 5} more fills\n`);
    }

    // Summary
    console.log('SUMMARY');
    console.log('=======');
    console.log(`Total Fills: ${events.length}`);
    console.log(`Buys: ${buyCount}, Sells: ${sellCount}`);
    console.log(`Coins Traded: ${[...coinsTraded].join(', ')}`);
    console.log(`Total Volume: $${totalVolume.toLocaleString(undefined, { minimumFractionDigits: 2 })}`);
    console.log(`Total Realized PnL: $${totalPnl.toFixed(4)}`);
}

// Get latest fills
getFills();

// Or get by block height:
// getFills({ blockHeight: 676607012 });

// Or get by timestamp:
// getFills({ timestampMs: 1753606210273 });

Common Use Cases

1. PnL Reconciliation

Python
def reconcile_pnl(client, metadata, start_block, end_block):
    """Calculate total PnL between two blocks"""
    pnl_by_address = {}

    for block_height in range(start_block, end_block + 1):
        request = hyperliquid_pb2.Position(block_height=block_height)
        response = client.GetFills(request, metadata=metadata)
        fills = json.loads(response.data)

        for event in fills.get('events', []):
            if len(event) >= 2:
                address = event[0]
                fill = event[1]
                pnl = float(fill.get('closedPnl', 0))

                if address not in pnl_by_address:
                    pnl_by_address[address] = 0
                pnl_by_address[address] += pnl

    return pnl_by_address

2. Fill Verification

Go
func verifyFill(client pb.HyperliquidL1GatewayClient, ctx context.Context, blockHeight int64, traderId string) []map[string]interface{} {
    request := &pb.Position{Position: &pb.Position_BlockHeight{BlockHeight: blockHeight}}
    response, _ := client.GetFills(ctx, request)

    var fills map[string]interface{}
    json.Unmarshal(response.Data, &fills)

    var traderFills []map[string]interface{}
    events := fills["events"].([]interface{})

    for _, event := range events {
        eventArr := event.([]interface{})
        if len(eventArr) >= 2 {
            address := eventArr[0].(string)
            if address == traderId {
                fillData := eventArr[1].(map[string]interface{})
                traderFills = append(traderFills, fillData)
            }
        }
    }

    return traderFills
}

3. Volume Analysis

JavaScript
function analyzeVolume(fills) {
    const volumeByCoin = {};
    const events = fills.events || [];

    for (const event of events) {
        if (event.length >= 2) {
            const fill = event[1];
            const coin = fill.coin;
            const volume = parseFloat(fill.sz) * parseFloat(fill.px);

            if (!volumeByCoin[coin]) {
                volumeByCoin[coin] = {
                    totalVolume: 0,
                    buyVolume: 0,
                    sellVolume: 0,
                    fillCount: 0
                };
            }

            volumeByCoin[coin].totalVolume += volume;
            volumeByCoin[coin].fillCount++;

            if (fill.side === 'B') {
                volumeByCoin[coin].buyVolume += volume;
            } else {
                volumeByCoin[coin].sellVolume += volume;
            }
        }
    }

    return volumeByCoin;
}

Error Handling

JavaScript
async function robustGetFills(client, metadata, position, retries = 3) {
    for (let attempt = 0; attempt < retries; attempt++) {
        try {
            return await new Promise((resolve, reject) => {
                client.GetFills(position, metadata, (error, response) => {
                    if (error) reject(error);
                    else resolve(response);
                });
            });
        } catch (error) {
            console.warn(`Attempt ${attempt + 1} failed:`, error.message);

            if (attempt === retries - 1) {
                throw error;
            }

            // Exponential backoff
            await new Promise(resolve =>
                setTimeout(resolve, Math.pow(2, attempt) * 1000)
            );
        }
    }
}

Best Practices

  1. Position Selection: Use block height for precise queries; timestamps may return different fills across requests
  2. Data Validation: Always validate JSON structure and handle optional fields
  3. Deduplication: Use tid (trade ID) as the unique identifier for deduplication
  4. Error Recovery: Implement retry logic with exponential backoff
  5. Resource Management: Close gRPC connections properly to avoid resource leaks

Current Limitations

  • Data Retention: Historical fill data is limited to a 24-hour rolling window
  • Rate Limits: Be mindful of request frequency to avoid overwhelming the service

Resources

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