# StreamL2BookDiff - Stream Ordered L2 Diffs

`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.

## Request Parameters

- `coins` (`repeated string, required`): One to 20 distinct, case-sensitive native coin identifiers; duplicates are ignored before applying the cap.
- `n_levels` (`optional uint32, optional`): Omitted means 20 levels per side; 1-100 selects bounded depth; explicit 0 requests full depth when the endpoint supports it.
- `n_sig_figs` (`optional uint32, optional`): Optional price aggregation using 2, 3, 4, or 5 significant figures.
- `mantissa` (`optional uint64, optional`): Optional aggregation mantissa; valid only with n_sig_figs 5 and value 2 or 5.

## 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"
    "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()))
        }
    }
}
```

## Response Fields

- `time` (`int64, required`): Coalesce-window block time in Unix milliseconds.
- `block_number` (`int64, required`): Sequential Hyperliquid block number at the end of the window.
- `diffs` (`repeated L2CoinDiff, required`): Per-coin snapshots or changed levels; unchanged coins are omitted.
- `diffs.coin` (`string, required`): Coin whose derived book changed.
- `diffs.seq` (`uint64, required`): Per-coin monotonic sequence, starting at 1 on the snapshot.
- `diffs.prev_seq` (`uint64, required`): Previous entry's seq for this coin, or 0 on the snapshot.
- `diffs.bids` (`repeated L2Level, required`): Full bid side on a snapshot, otherwise changed bid levels.
- `diffs.bids.px` (`string, required`): Aggregated price as a decimal string.
- `diffs.bids.sz` (`string, required`): Current size, or 0.0 to remove this level.
- `diffs.bids.n` (`uint32, required`): Resting order count, or 0 on a removed level.
- `diffs.asks` (`repeated L2Level, required`): Full ask side on a snapshot, otherwise changed ask levels.
- `diffs.asks.px` (`string, required`): Aggregated price as a decimal string.
- `diffs.asks.sz` (`string, required`): Current size, or 0.0 to remove this level.
- `diffs.asks.n` (`uint32, required`): Resting order count, or 0 on a removed level.
- `diffs.snapshot` (`bool, required`): True when bids and asks contain the complete book and local state must be rebuilt.

## Successful Response

```protobuf
time: 1785736814057
block_number: 1058646974
diffs: {
  coin: "BTC"
  seq: 1
  prev_seq: 0
  bids: { px: "114250.0" sz: "1.35" n: 4 }
  asks: { px: "114251.0" sz: "0.82" n: 3 }
  snapshot: true
}
```

## Error Responses

### Invalid request

- Code: `INVALID_ARGUMENT`
- Description: The coin list or an aggregation parameter is malformed, unsupported, or outside its allowed range.

### Endpoint capability unavailable

- Code: `FAILED_PRECONDITION`
- Description: This stream is unavailable on the endpoint, or the endpoint does not support a full-depth request.

### View unavailable

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

### Lost incremental frames

- Code: `ABORTED`
- Description: Frame loss or a stream state reset broke the diff chain; reconnect and rebuild from opening snapshots.

### 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.

## 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](https://www.dwellir.com/docs/hyperliquid/grpc#v3-market-streaming-service) for common request and error rules.

Related references: [StreamL2Book](https://www.dwellir.com/docs/hyperliquid/stream_l2_book) and [StreamRawBookDiffs](https://www.dwellir.com/docs/hyperliquid/stream_raw_book_diffs).
