gRPC
Overview
Section titled “Overview”The gRPC bridge provides low-latency streaming access to Hyperliquid market data via Protocol Buffers over HTTP/2 with TLS. All connections use channel-level encryption and require API key authentication via gRPC metadata.
Every response carries microsecond-precision timestamps at both the ingest and publish stages of the pipeline. Given ingest timestamp and publish timestamp , the internal processing latency is:
This value is typically on the order of single-digit microseconds for cached responses and tens of microseconds for live computation, enabling clients to distinguish bridge overhead from upstream and network latency.
Endpoints
Section titled “Endpoints”| Region | Endpoint | Notes |
|---|---|---|
| US (East US 2) | hl.grpc.aleatoric.systems:443 | Primary |
| JP (Japan East) | hl-jp.grpc.aleatoric.systems:443 | Live production edge |
Both production endpoints terminate TLS at the edge. No plaintext connections are accepted.
Latency Characteristics
Section titled “Latency Characteristics”| Percentile | Target | Typical |
|---|---|---|
| p50 | < 10 ms | 2 — 5 ms (same-region) |
| p95 | < 50 ms | 15 — 30 ms |
| p99 | < 150 ms | 40 — 80 ms |
Latency is measured end-to-end from the moment the bridge receives an upstream tick to the moment the serialized protobuf frame is written to the client socket. Cross-region connections (e.g., EU client to US endpoint) will observe additional network round-trip time on top of these figures.
Service Definition
Section titled “Service Definition”syntax = "proto3";
package aleatoric.hl.v1;
// Primary market data service for Hyperliquid.service PriceService { // Unary RPCs rpc GetMidPrice(MidPriceRequest) returns (MidPriceResponse); rpc GetBlockNumber(BlockNumberRequest) returns (BlockNumberResponse);
// Server-streaming RPCs rpc StreamMids(StreamMidsRequest) returns (stream MidPriceResponse); rpc StreamTrades(StreamTradesRequest) returns (stream TradeEvent); rpc StreamL2Book(StreamL2BookRequest) returns (stream L2BookSnapshot); rpc StreamLiquidations(StreamLiquidationsRequest) returns (stream LiquidationEvent);}
// ── Request messages ──────────────────────────────────────────────
message MidPriceRequest { string coin = 1; // Asset symbol, e.g. "BTC", "ETH"}
message StreamMidsRequest { // Empty — subscribes to all coins.}
message StreamTradesRequest { string coin = 1; // Required. Asset symbol to subscribe to.}
message StreamL2BookRequest { string coin = 1; // Required. Asset symbol. int32 depth = 2; // Max levels per side (default 20, max 200).}
message StreamLiquidationsRequest { // Empty — subscribes to all liquidation events.}
message BlockNumberRequest { // Empty.}
// ── Response messages ─────────────────────────────────────────────
message MidPriceResponse { string coin = 1; double price = 2; // Mid price in USD double best_bid = 3; // Best bid price double best_ask = 4; // Best ask price int64 ts_ms = 5; // Upstream timestamp (ms since epoch) int64 upstream_ts_ms = 6; // Source exchange timestamp (ms) int64 ingest_ts_us = 7; // Bridge ingest time (μs since epoch) int64 publish_ts_us = 8; // Bridge publish time (μs since epoch) bool cached = 9; // True if served from cache}
message TradeEvent { string coin = 1; double price = 2; // Execution price in USD double size = 3; // Fill size in base asset string side = 4; // "buy" or "sell" (taker side) int64 ts_ms = 5; int64 ingest_ts_us = 6; int64 publish_ts_us = 7;}
message L2BookSnapshot { string coin = 1; repeated PriceLevel bids = 2; repeated PriceLevel asks = 3; int64 ts_ms = 4; int64 ingest_ts_us = 5; int64 publish_ts_us = 6;}
message PriceLevel { double price = 1; // Limit price in USD double size = 2; // Aggregate size at this level}
message LiquidationEvent { string coin = 1; string side = 2; // "long" or "short" double size = 3; // Liquidated position size double mark_price = 4; // Mark price at liquidation int64 ts_ms = 5; int64 ingest_ts_us = 6; int64 publish_ts_us = 7;}
message BlockNumberResponse { int64 block_number = 1; int64 ts_ms = 2;}Response Fields Reference
Section titled “Response Fields Reference”| Field | Type | Unit | Description |
|---|---|---|---|
coin | string | — | Asset symbol (e.g., "BTC", "ETH", "SOL") |
price | double | USD | Mid price, defined as |
best_bid | double | USD | Top-of-book bid price |
best_ask | double | USD | Top-of-book ask price |
size | double | Base asset | Fill or level size in native units |
side | string | — | Taker direction ("buy" / "sell") or position direction ("long" / "short") |
mark_price | double | USD | Oracle mark price at the time of the event |
ts_ms | int64 | ms | Upstream exchange timestamp (milliseconds since Unix epoch) |
upstream_ts_ms | int64 | ms | Original source timestamp before any bridge processing |
ingest_ts_us | int64 | Timestamp when the message entered the bridge ingest pipeline | |
publish_ts_us | int64 | Timestamp when the serialized response was dispatched to the client | |
cached | bool | — | true if the response was served from the in-memory cache rather than a live upstream tick |
Timestamp Precision
Section titled “Timestamp Precision”The ingest_ts_us and publish_ts_us fields use microsecond resolution ( s). Together they allow clients to compute the bridge processing latency per message:
For cached responses, is typically . For live ticks flowing through the full normalization pipeline, expect depending on message complexity.
Authentication
Section titled “Authentication”All RPCs require a valid API key passed via gRPC metadata. The key must be sent with every call (unary) or at stream initiation (server-streaming).
| Metadata Key | Value |
|---|---|
x-api-key | Your API key (e.g., ak_live_...) |
Python Example
Section titled “Python Example”import grpc
ENDPOINT = "hl.grpc.aleatoric.systems:443"API_KEY = "ak_live_your_key_here"
# Create a secure channel with TLSchannel = grpc.secure_channel(ENDPOINT, grpc.ssl_channel_credentials())stub = PriceServiceStub(channel)
metadata = [("x-api-key", API_KEY)]
# Unary callresponse = stub.GetMidPrice( MidPriceRequest(coin="BTC"), metadata=metadata,)print(f"BTC mid: {response.price}")grpcurl Example
Section titled “grpcurl Example”grpcurl \ -H "x-api-key: ak_live_your_key_here" \ -d '{"coin": "BTC"}' \ hl.grpc.aleatoric.systems:443 \ aleatoric.hl.v1.PriceService/GetMidPriceStreaming Subscriptions
Section titled “Streaming Subscriptions”Stream All Mid Prices (Python)
Section titled “Stream All Mid Prices (Python)”import grpcfrom aleatoric.hl.v1 import price_service_pb2 as pbfrom aleatoric.hl.v1 import price_service_pb2_grpc as rpc
ENDPOINT = "hl.grpc.aleatoric.systems:443"API_KEY = "ak_live_your_key_here"
channel = grpc.secure_channel(ENDPOINT, grpc.ssl_channel_credentials())stub = rpc.PriceServiceStub(channel)metadata = [("x-api-key", API_KEY)]
# Server-streaming call — returns an iteratorstream = stub.StreamMids(pb.StreamMidsRequest(), metadata=metadata)
for tick in stream: latency_us = tick.publish_ts_us - tick.ingest_ts_us print(f"{tick.coin} mid={tick.price:.2f} " f"bid={tick.best_bid:.2f} ask={tick.best_ask:.2f} " f"bridge_lat={latency_us} μs")Stream Trades (Python)
Section titled “Stream Trades (Python)”stream = stub.StreamTrades( pb.StreamTradesRequest(coin="ETH"), metadata=metadata,)
for trade in stream: print(f"ETH trade: {trade.side} {trade.size:.4f} @ {trade.price:.2f}")Stream L2 Book (grpcurl)
Section titled “Stream L2 Book (grpcurl)”grpcurl \ -H "x-api-key: ak_live_your_key_here" \ -d '{"coin": "BTC", "depth": 5}' \ hl.grpc.aleatoric.systems:443 \ aleatoric.hl.v1.PriceService/StreamL2BookStream Liquidations (grpcurl)
Section titled “Stream Liquidations (grpcurl)”grpcurl \ -H "x-api-key: ak_live_your_key_here" \ -d '{}' \ hl.grpc.aleatoric.systems:443 \ aleatoric.hl.v1.PriceService/StreamLiquidationsError Codes
Section titled “Error Codes”The bridge uses standard gRPC status codes. Clients should handle these in retry and alerting logic.
| gRPC Code | Numeric | Cause | Recommended Action |
|---|---|---|---|
UNAUTHENTICATED | 16 | Missing or invalid x-api-key metadata | Verify key is active and correctly formatted |
PERMISSION_DENIED | 7 | Key valid but tier insufficient for this RPC | Upgrade plan or contact sales |
RESOURCE_EXHAUSTED | 8 | Rate limit exceeded for your tier | Back off exponentially; see Rate Limits |
UNAVAILABLE | 14 | Bridge temporarily unreachable (deploy, upstream outage) | Reconnect with exponential backoff + jitter |
INVALID_ARGUMENT | 3 | Malformed request (e.g., unknown coin symbol) | Check request parameters |
DEADLINE_EXCEEDED | 4 | Client-set deadline elapsed before response | Increase deadline or check network path |
INTERNAL | 13 | Unexpected server-side error | Retry once; if persistent, contact support |
Connection Management
Section titled “Connection Management”Keepalive Settings
Section titled “Keepalive Settings”gRPC channels should be configured with keepalive pings to detect dead connections before the TCP stack does. Recommended settings:
options = [ ("grpc.keepalive_time_ms", 30_000), # Send ping every 30 s ("grpc.keepalive_timeout_ms", 10_000), # Wait 10 s for pong ("grpc.keepalive_permit_without_calls", 1), # Ping even when idle ("grpc.http2.max_pings_without_data", 0), # No limit ("grpc.max_receive_message_length", 16 * 1024 * 1024), # 16 MB]
channel = grpc.secure_channel(ENDPOINT, grpc.ssl_channel_credentials(), options)Reconnection Strategy
Section titled “Reconnection Strategy”For server-streaming RPCs, the server will close the stream on deploy or upstream disconnect. Clients must handle UNAVAILABLE and re-subscribe:
import timeimport random
def stream_with_reconnect(stub, metadata, max_retries=None): """Exponential backoff reconnection loop.""" attempt = 0 while max_retries is None or attempt < max_retries: try: stream = stub.StreamMids( pb.StreamMidsRequest(), metadata=metadata, ) attempt = 0 # Reset on successful connection for tick in stream: yield tick except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAUTHENTICATED: raise # Do not retry auth failures attempt += 1 delay = min(2 ** attempt, 60) + random.uniform(0, 1) print(f"Stream disconnected ({e.code().name}), " f"reconnecting in {delay:.1f}s...") time.sleep(delay)Best Practices
Section titled “Best Practices”- One channel per endpoint. gRPC multiplexes RPCs over a single HTTP/2 connection. Creating multiple channels to the same endpoint wastes resources.
- Reuse stubs. Stubs are lightweight wrappers around the channel and safe to share across threads.
- Set deadlines on unary calls. Streaming RPCs are long-lived by design, but unary calls like
GetMidPriceshould carry a deadline (e.g., 5 s) to avoid hanging on network partitions. - Monitor
RESOURCE_EXHAUSTED. If you receive this status, reduce call frequency rather than retrying immediately. - Choose the region your plan entitles you to use.
hl.grpc.aleatoric.systemsremains the default US path, whilehl-jp.grpc.aleatoric.systemsis the Japan production hostname for JP-enabled clients.