GetFills
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#
rpc GetFills(Position) returns (BlockFills) {}
Request Message#
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#
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#
{
"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#
| Field | Type | Description |
|---|---|---|
local_time | string | ISO-8601 timestamp when node processed the fill |
block_time | string | ISO-8601 timestamp from block consensus |
block_number | number | Block height containing these fills |
events | array | Array of [address, fill_data] pairs |
Fill Event Fields#
| Field | Type | Description |
|---|---|---|
coin | string | Trading pair symbol (e.g., "BTC", "ETH") |
px | string | Fill price |
sz | string | Fill size |
side | string | "B" for buy, "A" for sell |
time | number | Fill timestamp in milliseconds |
startPosition | string | Position size before this fill |
dir | string | Direction: "Open Long", "Open Short", "Close Long", "Close Short" |
closedPnl | string | Realized PnL if closing position |
hash | string | Transaction hash |
oid | number | Order ID |
crossed | boolean | Whether order crossed the spread |
fee | string | Trading fee (negative = rebate) |
tid | number | Unique trade identifier |
cloid | string | Client order ID (optional) |
feeToken | string | Token used for fee |
builder | string | Builder address (optional) |
builderFee | string | Builder fee amount (optional) |
For the complete fill specification, see the StreamFills documentation.
Implementation Examples#
- Go
- Python
- Node.js
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()
}
}
}
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)
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#
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#
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#
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#
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#
- Position Selection: Use block height for precise queries; timestamps may return different fills across requests
- Data Validation: Always validate JSON structure and handle optional fields
- Deduplication: Use
tid(trade ID) as the unique identifier for deduplication - Error Recovery: Implement retry logic with exponential backoff
- 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#
- GitHub: gRPC Code Examples - Complete working examples
- StreamFills Documentation - Full fill specification and streaming examples
- Copy Trading Bot - Production-ready example using fill data
Need help? Contact our support team or check the Hyperliquid gRPC documentation.