Skip to main content

Making Hyperliquid gRPC Requests with Go

Updated on
Feb 13, 2026

Overview

Go is a statically-typed, compiled language known for its simplicity, efficiency, and strong concurrency support. Follow the official installation guide to install Go. Verify the installation:

go version

Authentication for Go Requests

To securely access Hyperliquid gRPC, authentication is required. Quicknode endpoints consist of two components: endpoint name and token. You must use these components to configure a gRPC client with authentication credentials.

There are two authentication methods:

  1. Basic Authentication
  2. x-token Authentication

This document provides implementation details for both methods, though x-token authentication is the recommended approach.


Basic Authentication

The getClientWithBasicAuth function demonstrates how to handle authentication using Basic Authentication. It encodes the credentials in base64 and adds them to the authorization header.

Implementation

import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"log"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

func getClientWithBasicAuth(endpoint, token string) (*grpc.ClientConn, error) {
target := endpoint + ":10000"
conn, err := grpc.Dial(target,
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
grpc.WithPerRPCCredentials(basicAuth{
username: endpoint,
password: token,
}),
)
if err != nil {
return nil, fmt.Errorf("unable to dial endpoint: %w", err)
}
return conn, nil
}

type basicAuth struct {
username string
password string
}

func (b basicAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
auth := b.username + ":" + b.password
encoded := base64.StdEncoding.EncodeToString([]byte(auth))
return map[string]string{"authorization": "Basic " + encoded}, nil
}

func (basicAuth) RequireTransportSecurity() bool {
return true
}

Usage

conn, err := getClientWithBasicAuth("ENDPOINT_NAME", "TOKEN")
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()

x-token Authentication

The getClientWithXToken function demonstrates how to authenticate using an x-token. This method attaches the token to the x-token header of each request.

Implementation

import (
"context"
"crypto/tls"
"fmt"
"log"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

func getClientWithXToken(endpoint, token string) (*grpc.ClientConn, error) {
target := endpoint + ":10000"
conn, err := grpc.Dial(target,
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
grpc.WithPerRPCCredentials(xTokenAuth{
token: token,
}),
)
if err != nil {
return nil, fmt.Errorf("unable to dial endpoint: %w", err)
}
return conn, nil
}

type xTokenAuth struct {
token string
}

func (x xTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{"x-token": x.token}, nil
}

func (xTokenAuth) RequireTransportSecurity() bool {
return true
}

Usage

conn, err := getClientWithXToken("ENDPOINT_NAME", "TOKEN")
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()


Initiating the Go Project for Hyperliquid gRPC

Step 1: Initialize Go Project

Create a dedicated directory for your Hyperliquid gRPC project and navigate into it:

mkdir hyperliquid-grpc-go
cd hyperliquid-grpc-go
go mod init hyperliquid-grpc-go

Step 2: Install Dependencies

Install the necessary Go dependencies for gRPC and Protocol Buffers:

# Install gRPC and Protocol Buffers packages
go get google.golang.org/grpc
go get google.golang.org/protobuf/cmd/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc

# Make sure the protoc plugins are in your PATH
export PATH="$PATH:$(go env GOPATH)/bin"

Step 3: Create Proto Files

Create the proto directory and add the Hyperliquid streaming protocol definition:

# Create proto directory
mkdir -p proto

Create a new file named streaming.proto in the proto/ directory and add the following content:

syntax = "proto3";
package hyperliquid;

service Streaming {
// Bi-directional streaming
rpc StreamData (stream SubscribeRequest) returns (stream SubscribeUpdate);
rpc Ping (PingRequest) returns (PingResponse);
}

service BlockStreaming {
// Stream replica_cmds (raw blocks)
rpc StreamBlocks (Timestamp) returns (stream Block);
}

// --- Requests ---
message SubscribeRequest {
oneof request {
StreamSubscribe subscribe = 1;
Ping ping = 3;
}
reserved 2;
}

message StreamSubscribe {
StreamType stream_type = 1;

// Generic filters - field name to allowed values
// Recursively searches each event for matching field/value pairs
// Example: {"coin": ["ETH", "BTC"], "user": ["0x123..."], "type": ["deposit"]}
map<string, FilterValues> filters = 3;

// Optional name for this filter
// Allows multiple independent filters per stream (OR logic)
string filter_name = 4;
}

// Container for filter values
message FilterValues {
repeated string values = 1;
}

message Ping { int64 timestamp = 1; }

// --- Responses ---
message SubscribeUpdate {
oneof update {
StreamResponse data = 1;
Pong pong = 2;
}
}

message StreamResponse {
uint64 block_number = 1;
uint64 timestamp = 2; // Server ingress timestamp

// Raw JSON data from the file (Exact replica of source)
string data = 3;
}

// --- Data Types ---
enum StreamType {
UNKNOWN = 0;
TRADES = 1;
ORDERS = 2;
BOOK_UPDATES = 3;
TWAP = 4;
EVENTS = 5;
BLOCKS = 6;
WRITER_ACTIONS = 7;
}

message Block {
string data_json = 1;
}

message Pong { int64 timestamp = 1; }
message Timestamp { int64 timestamp = 1; }
message PingRequest { int32 count = 1; }
message PingResponse { int32 count = 1; }

Step 4: Generate Go Code from Proto Files

Generate Go client code from the protocol buffer definition:

# Create pb directory for generated files
mkdir -p pb

# Generate Go code from proto files
protoc --go_out=pb --go_opt=paths=source_relative \
--go-grpc_out=pb --go-grpc_opt=paths=source_relative \
proto/*.proto

After running these commands, your project structure should look like this:

hyperliquid-grpc-go/
├── go.mod
├── go.sum
├── pb/
│ ├── streaming.pb.go
│ └── streaming_grpc.pb.go
├── proto/
│ └── streaming.proto
└── main.go

The pb/ directory contains the auto-generated Go code from your proto files. You'll import these generated files in your application code.

Step 5: Create Main Application

Create your main application file to test the connection:

package main

import (
"context"
"crypto/tls"
"fmt"
"log"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"

pb "hyperliquid-grpc-go/pb/proto"
)

const (
grpcEndpoint = "your-endpoint.hype-mainnet.quiknode.pro:10000"
authToken = "your-auth-token"
)

// Create gRPC connection with TLS
func createConnection() (*grpc.ClientConn, error) {
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
}
creds := credentials.NewTLS(tlsConfig)

conn, err := grpc.Dial(
grpcEndpoint,
grpc.WithTransportCredentials(creds),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(100*1024*1024), // 100MB
),
)

return conn, err
}

// Create context with auth metadata
func createContext() context.Context {
md := metadata.Pairs("x-token", authToken)
return metadata.NewOutgoingContext(context.Background(), md)
}

// Test connectivity
func pingTest(client pb.StreamingClient) error {
ctx := createContext()

resp, err := client.Ping(ctx, &pb.PingRequest{Count: 1})
if err != nil {
return fmt.Errorf("ping failed: %v", err)
}

log.Printf("✅ Ping successful: %+v", resp)
return nil
}

func main() {
// Create connection
conn, err := createConnection()
if err != nil {
log.Fatalf("❌ Failed to connect: %v", err)
}
defer conn.Close()

// Create client
client := pb.NewStreamingClient(conn)

// Test connectivity
fmt.Printf("Testing connection to: %s\n", grpcEndpoint)
if err := pingTest(client); err != nil {
log.Fatalf("❌ Ping test failed: %v", err)
}

fmt.Println("✅ Connection test completed successfully!")
}

Step 6: Run the Application

Build and execute your application to test the gRPC connection:


# Download and update dependencies
go mod tidy

# Build and run
go build -o hyperliquid-grpc main.go
./hyperliquid-grpc

# Or run directly
go run main.go

Step 7: Verify Setup

Confirm that your setup is working correctly by checking the output, it should be something like below:

Testing connection to: docs-demo.hype-mainnet.quiknode.pro:10000
2025/12/30 18:10:52Ping successful: count:1
Connection test completed successfully!

If everything is set up correctly, you should see:

  • Connection established to the gRPC endpoint
  • Successful ping response
  • No compilation or runtime errors

Complete Example: Streaming Trades with Filtering

Below is a complete working example that demonstrates streaming trade data with coin filtering. This example shows how to:

  • Authenticate using x-token
  • Stream real-time trades for specific coins (BTC, ETH, SOL, HYPE)
  • Parse and display trade events with formatted output
  • Handle connection keep-alives with periodic pings
Click to view complete trades.go example
package main

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/joho/godotenv"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
pb "hyperliquid-grpc-go/pb"
)

// xTokenAuth implements credentials.PerRPCCredentials for x-token authentication
type xTokenAuth struct {
token string
}

func (x xTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{"x-token": x.token}, nil
}

func (xTokenAuth) RequireTransportSecurity() bool {
return true
}

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

// Trade data structure
type TradeData struct {
BlockNumber int64 `json:"block_number"`
BlockTime string `json:"block_time"`
Events [][]interface{} `json:"events"`
LocalTime string `json:"local_time"`
}

// Create gRPC connection with x-token authentication
func createConnection(endpoint, token string) (*grpc.ClientConn, error) {
target := endpoint + ":10000"
conn, err := grpc.Dial(
target,
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
grpc.WithPerRPCCredentials(xTokenAuth{token: token}),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(100*1024*1024), // 100MB
),
)
return conn, err
}

// Test connectivity
func pingTest(client pb.StreamingClient) error {
resp, err := client.Ping(context.Background(), &pb.PingRequest{Count: 1})
if err != nil {
return fmt.Errorf("ping failed: %v", err)
}
log.Printf("✅ Ping successful: %+v", resp)
return nil
}

// Parse and display trade events in a readable format
func displayTrade(data *TradeData) {
blockTime, err := time.Parse(time.RFC3339Nano, data.BlockTime)
if err != nil {
blockTime = time.Now()
}

fmt.Printf("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
fmt.Printf("📦 Block %d | %s\n", data.BlockNumber, blockTime.Format("15:04:05.000"))
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")

for _, event := range data.Events {
if len(event) < 2 {
continue
}

wallet, ok := event[0].(string)
if !ok {
continue
}

tradeDataBytes, err := json.Marshal(event[1])
if err != nil {
continue
}

var trade TradeEvent
if err := json.Unmarshal(tradeDataBytes, &trade); err != nil {
continue
}

tradeTime := time.Unix(trade.Time/1000, (trade.Time%1000)*1000000)

sideEmoji := "🟢"
sideName := "BUY "
if trade.Side == "A" {
sideEmoji = "🔴"
sideName = "SELL"
}

dirEmoji := "📈"
if trade.Dir == "Open Short" || trade.Dir == "Close Long" {
dirEmoji = "📉"
}

fmt.Printf("\n%s %s %s %-12s | %s @ $%s | Size: %s\n",
sideEmoji,
sideName,
dirEmoji,
trade.Dir,
trade.Coin,
trade.Px,
trade.Sz,
)
fmt.Printf(" 💳 Wallet: %s\n", wallet)
fmt.Printf(" 📊 Position: %s | Fee: %s %s | Crossed: %v",
trade.StartPosition,
trade.Fee,
trade.FeeToken,
trade.Crossed,
)

if trade.TwapId != nil {
fmt.Printf(" | TWAP")
}

fmt.Printf(" | Time: %s\n", tradeTime.Format("15:04:05"))
}
}

// Stream trades with improved output
func streamTrades(client pb.StreamingClient, coins []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

stream, err := client.StreamData(ctx)
if err != nil {
return fmt.Errorf("failed to create stream: %v", err)
}

coinFilter := &pb.FilterValues{
Values: coins,
}

filters := map[string]*pb.FilterValues{
"coin": coinFilter,
}

subscribeReq := &pb.SubscribeRequest{
Request: &pb.SubscribeRequest_Subscribe{
Subscribe: &pb.StreamSubscribe{
StreamType: pb.StreamType_TRADES,
StartBlock: 0,
Filters: filters,
FilterName: "coin-filter",
},
},
}

if err := stream.Send(subscribeReq); err != nil {
return fmt.Errorf("failed to send subscription: %v", err)
}

log.Printf("🔥 Subscribed to TRADES stream")
log.Printf("📊 Monitoring coins: %v", coins)
log.Println("\n⏳ Waiting for trade data...")

go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
pingReq := &pb.SubscribeRequest{
Request: &pb.SubscribeRequest_Ping{
Ping: &pb.Ping{
Timestamp: time.Now().Unix(),
},
},
}

if err := stream.Send(pingReq); err != nil {
return
}
}
}
}()

messageCount := 0

for {
resp, err := stream.Recv()
if err == io.EOF {
log.Println("Stream closed by server")
break
}

if err != nil {
return fmt.Errorf("failed to receive: %v", err)
}

switch update := resp.Update.(type) {
case *pb.SubscribeUpdate_Data:
messageCount++

var tradeData TradeData
if err := json.Unmarshal([]byte(update.Data.Data), &tradeData); err == nil {
displayTrade(&tradeData)
} else {
log.Printf("Error parsing trade data: %v", err)
}

case *pb.SubscribeUpdate_Pong:
log.Printf("\n🏓 Connection alive (pong: %d)\n", update.Pong.Timestamp)
}
}

return nil
}

func main() {
if err := godotenv.Load("../.env"); err != nil {
log.Fatalf("❌ Error loading ../.env file: %v", err)
}

grpcEndpoint := os.Getenv("qn_hyper_mainnet_grpc")
authToken := os.Getenv("grpc_token")

if grpcEndpoint == "" || authToken == "" {
log.Fatal("❌ Missing required environment variables")
}

fmt.Printf("🔗 Connecting to: %s:10000\n", grpcEndpoint)

conn, err := createConnection(grpcEndpoint, authToken)
if err != nil {
log.Fatalf("❌ Failed to connect: %v", err)
}
defer conn.Close()

client := pb.NewStreamingClient(conn)

if err := pingTest(client); err != nil {
log.Fatalf("❌ Ping test failed: %v", err)
}

fmt.Println("✅ Connection test completed successfully!")

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

coins := []string{"BTC", "ETH", "SOL", "HYPE"}

go func() {
if err := streamTrades(client, coins); err != nil {
log.Printf("❌ Stream error: %v", err)
}
}()

<-sigChan
fmt.Println("\n\n🛑 Shutting down gracefully...")
}

This example requires the godotenv package for loading environment variables:

go get github.com/joho/godotenv

Create a .env file with your credentials:

qn_hyper_mainnet_grpc=your-grpc-endpoint
grpc_token=your-token

When you run this example, you'll see formatted output like:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 Block 123456 | 14:30:45.123
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🟢 BUY 📈 Open Long | BTC @ $45000.50 | Size: 0.1
💳 Wallet: 0x123...
📊 Position: 0 | Fee: 0.02 USDC | Crossed: true | Time: 14:30:45

We ❤️ Feedback!

If you have any feedback or questions about this documentation, let us know. We'd love to hear from you!

Share this doc