StreamL2BookDiff - Stream Ordered L2 Diffs
Reconstruct up to 20 Hyperliquid L2 books from typed opening snapshots and sequenced level diffs over V3 gRPC.
Hyperliquid RPC endpoint: endpoint URLs, plans and pricing.
StreamL2BookDiff carries aggregated L2 level changes for multiple coins. It is the bandwidth-efficient state-reconstruction counterpart to full StreamL2Book snapshots.
Request and opening behavior
coins is required and accepts 1 through 20 distinct, case-sensitive identifiers. The depth, significant-figure, and mantissa rules match StreamL2Book; there is no strict field because unchanged windows emit no diff.
The stream opens with one snapshot: true full-book entry per coin, each with seq: 1 and prev_seq: 0. Later entries contain changed levels only. A level whose sz is "0.0" removes that price.
package main
import (
"context"
"crypto/tls"
"errors"
"io"
"log"
"os"
"os/signal"
"syscall"
pb "example.com/hyperliquid-v3-client/gen/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)
func main() {
endpoint := os.Getenv("HYPERLIQUID_GRPC_ENDPOINT")
apiKey := os.Getenv("DWELLIR_API_KEY")
if endpoint == "" || apiKey == "" {
log.Fatal("set HYPERLIQUID_GRPC_ENDPOINT and DWELLIR_API_KEY")
}
conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", apiKey)
stream, err := pb.NewMarketStreamingClient(conn).StreamL2BookDiff(ctx, &pb.L2BookDiffRequest{
Coins: []string{"BTC", "ETH"},
NLevels: proto.Uint32(20),
NSigFigs: proto.Uint32(5),
Mantissa: proto.Uint64(5),
})
if err != nil {
log.Fatal(err)
}
last := make(map[string]uint64)
for {
update, err := stream.Recv()
if errors.Is(err, io.EOF) {
return
}
if err != nil {
log.Fatal(err)
}
for _, diff := range update.GetDiffs() {
if previous, ok := last[diff.GetCoin()]; ok && diff.GetPrevSeq() != previous {
log.Fatalf("gap for %s: prev_seq=%d expected=%d", diff.GetCoin(), diff.GetPrevSeq(), previous)
}
if !diff.GetSnapshot() && diff.GetSeq() != diff.GetPrevSeq()+1 {
log.Fatalf("non-contiguous sequence for %s: seq=%d prev_seq=%d", diff.GetCoin(), diff.GetSeq(), diff.GetPrevSeq())
}
last[diff.GetCoin()] = diff.GetSeq()
log.Printf("coin=%s snapshot=%t seq=%d bids=%d asks=%d", diff.GetCoin(), diff.GetSnapshot(), diff.GetSeq(), len(diff.GetBids()), len(diff.GetAsks()))
}
}
}Sequencing and recovery
Track seq independently per coin and require every prev_seq to equal the last accepted seq. A mismatch is loss. Subscriber lag closes the stream with ABORTED; reconnect and rebuild every coin from the new opening snapshots. Quiet coins do not advance and unchanged windows produce no frame. See the shared market-stream contract for common request and error rules.
Related references: StreamL2Book and StreamRawBookDiffs.