# StreamBbo - Stream Best Bid and Offer

`StreamBbo` sends the best bid and ask for each requested coin. It opens with the current top of book for every coin, then emits only when that coin's top changes.

## Request and opening behavior

`coins` is required and accepts `1` through `20` distinct, case-sensitive native coin identifiers. Duplicates are removed before enforcing the cap. Empty lists, blank entries, wildcard entries, and unknown or disabled coins return `INVALID_ARGUMENT`.

Each coin has an independent stream order. Cross-coin ordering is unspecified. `bid` or `ask` is unset when that side of the book is empty.

## Request Parameters

- `coins` (`repeated string, required`): One to 20 distinct, case-sensitive native coin identifiers; duplicates are ignored before applying the cap.

## Request Example

```go
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"
)

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).StreamBbo(ctx, &pb.BboRequest{Coins: []string{"BTC", "ETH"}})
    if err != nil {
        log.Fatal(err)
    }
    for {
        update, err := stream.Recv()
        if errors.Is(err, io.EOF) {
            return
        }
        if err != nil {
            log.Fatal(err)
        }
        log.Printf("coin=%s block=%d bid=%v ask=%v", update.GetCoin(), update.GetBlockNumber(), update.GetBid(), update.GetAsk())
    }
}
```

## Response Fields

- `coin` (`string, required`): Coin whose top of book changed or whose opening value is being sent.
- `time` (`int64, required`): Frame block time in Unix milliseconds.
- `block_number` (`int64, required`): Sequential Hyperliquid block number.
- `bid` (`L2Level, optional`): Best bid; unset when the bid side is empty.
- `bid.px` (`string, required`): Best-bid price as a decimal string.
- `bid.sz` (`string, required`): Aggregated size at the best bid.
- `bid.n` (`uint32, required`): Number of resting orders at the best bid.
- `ask` (`L2Level, optional`): Best ask; unset when the ask side is empty.
- `ask.px` (`string, required`): Best-ask price as a decimal string.
- `ask.sz` (`string, required`): Aggregated size at the best ask.
- `ask.n` (`uint32, required`): Number of resting orders at the best ask.

## Successful Response

```protobuf
coin: "BTC"
time: 1785736814057
block_number: 1058646974
bid: { px: "114250.0" sz: "1.35" n: 4 }
ask: { px: "114251.0" sz: "0.82" n: 3 }
```

## Error Responses

### Invalid coin list

- Code: `INVALID_ARGUMENT`
- Description: The list is empty, exceeds 20 distinct coins, contains a blank or wildcard, or names an unavailable coin.

### Feature unavailable

- Code: `FAILED_PRECONDITION`
- Description: This stream is unavailable on the selected endpoint.

### View unavailable

- Code: `UNAVAILABLE`
- Description: The requested BBO view is not ready.

### Stream state reset

- Code: `ABORTED`
- Description: The requested view or stream state reset; reconnect for fresh opening values.

### Slow consumer

- Code: `DEADLINE_EXCEEDED`
- Description: The consumer was too slow; reconnect.

### Unauthenticated

- Code: `UNAUTHENTICATED`
- Description: Usable x-api-key metadata was not supplied.

### Permission denied

- Code: `PERMISSION_DENIED`
- Description: The supplied credential cannot call this service.

## Delivery and recovery

The stream recovers from subscriber lag by invalidating its dedup state and re-sending the current top of book on a later window. That recovery frame may repeat a value already observed. `ABORTED` means the stream state reset; reconnect to receive fresh opening values. See the [shared market-stream contract](https://www.dwellir.com/docs/hyperliquid/grpc#v3-market-streaming-service) for common errors and delivery rules.

Related references: [StreamL2Book](https://www.dwellir.com/docs/hyperliquid/stream_l2_book) and the [Order Book WebSocket BBO channel](https://www.dwellir.com/docs/hyperliquid/bbo).
