Market data
Reading the book, fills, and pair catalog. Public — no session token required.
What's available
| RPC | Returns | Use it for |
|---|---|---|
MarketDataService.ListPairs | Catalog of pairs + per-spot metadata. | Boot — discover what's tradeable. |
MarketDataService.GetBook | One-shot L2 or L3 snapshot. | Cold-start reconciliation, periodic resync. |
MarketDataService.Subscribe | Stream of L1/L2/L3 book updates + status events for one or more pairs. | Continuous order-book, depth charts, status alerts. |
MarketDataService.SubscribeFills | Stream of executed trades. Optional per-pair filter. | Trade tape, volume metrics. |
MarketDataService.SubscribeMarketSnapshots | Consolidated cross-market snapshot stream — top-of-book per pair in a single feed. | Dashboards, watchlists, anything that wants every pair at once without N independent Subscribe streams. |
A single Subscribe call returns a multiplexed stream — every pair you subscribed to plus cross-cutting StatusEvents arrive on the same channel.
Pick your level
Subscriptions take a FeedLevel:
| Level | Payload | Use for |
|---|---|---|
FEED_LEVEL_L1 | Best bid + best ask | Tickers, mark prices, sanity checks. |
FEED_LEVEL_L2 | Aggregated depth, snapshot + deltas | Depth charts, mid calculation, taker sizing. |
FEED_LEVEL_L3 | Per-maker order list | Maker analytics. Does not include oracle-offset orders (those are virtual). |
Most integrations want L2.
Snapshot then stream
The canonical pattern for a UI:
use sweetspot_api_client::api::proto::{
FeedLevel, GetBookRequest, Pair, PairSubscription, SpotId, SubscribeRequest,
};
let pair = Pair { base: Some(SpotId { id: 1 }), quote: Some(SpotId { id: 0 }) };
// 1. Cold-start snapshot.
let mut market = client.market_data();
let snapshot = market
.get_book(GetBookRequest { pair: Some(pair), level: FeedLevel::L2.into() })
.await?
.into_inner();
let book = build_local_book(snapshot);
// 2. Subscribe to deltas. `subscribe_market_data` wraps the stream
// in a `ResilientStream` that reconnects and replays the request
// automatically.
let book_stream = client.subscribe_market_data(SubscribeRequest {
pairs: vec![PairSubscription {
pair: Some(pair),
level: FeedLevel::L2.into(),
snapshot_only: false,
}],
});
let mut rx = book_stream.subscribe().await;
while let Ok(ev) = rx.recv().await {
apply_event(&mut book, ev);
}import {
GrpcWebTransport,
MarketDataService,
createServiceClient,
streamWithBackoff,
} from "@superis-labs/sweetspot-client";
import { FeedLevel } from "@superis-labs/sweetspot-client/gen/sweetspot/api/v1/common_pb.js";
const transport = new GrpcWebTransport({ baseUrl: "https://api.example.com" });
const market = createServiceClient(MarketDataService, transport);
const pair = { base: { id: 1n }, quote: { id: 0n } };
// Cold-start snapshot.
const snapshot = await market.getBook({ pair, level: FeedLevel.L2 });
const book = buildLocalBook(snapshot);
// Subscribe to deltas with auto-reconnect — pure async generator,
// no class lifecycle, no internal subscribers. Compose inside your
// state layer (Zustand, React Query, plain effect).
const ctrl = new AbortController();
(async () => {
for await (const ev of streamWithBackoff(
(signal) =>
market.subscribe(
{ pairs: [{ pair, level: FeedLevel.L2, snapshotOnly: false }] },
{ signal },
),
ctrl.signal,
)) {
if (ev.kind === "connected") {
// On every (re)connect, GetBook to resync — the server restarts
// the L2 stream from a fresh snapshot and you may have missed
// deltas in flight.
const resync = await market.getBook({ pair, level: FeedLevel.L2 });
rebuildLocalBook(book, resync);
} else if (ev.kind === "item") {
applyEvent(book, ev.value);
}
}
})();
// later: ctrl.abort();streamWithBackoff yields connected, item, and disconnected events; reconnect timing matches the backoffDuration schedule. Holding the book in your store and calling GetBook on each connected event keeps the local copy consistent after reconnects.
Fills
Separate stream so book consumers don't pay to deserialize fills. Filter by pair (empty = all pairs). Fills are public trade-tape events; they do not include maker_id. Use MakerService.SubscribeBalance (or MakerService.SubscribeFills for maker-attributed fills) when you need inventory changes scoped to the authenticated maker.
use sweetspot_api_client::api::proto::{Pair, SpotId, SubscribeFillsRequest};
let mut market = client.market_data();
let mut fills = market
.subscribe_fills(SubscribeFillsRequest {
pairs: vec![Pair { base: Some(SpotId { id: 1 }), quote: Some(SpotId { id: 0 }) }],
})
.await?
.into_inner();
while let Some(fill) = fills.message().await? {
println!("{} {} @ {}", fill.side, fill.size.unwrap().value, fill.price.unwrap().value);
}Discovering pairs at boot
let cfg = client.refresh_config(None).await?;
for p in &cfg.pairs {
println!("{}/{}: spot {} → {}", p.base_name, p.quote_name, p.base_spot_id, p.quote_spot_id);
}ConfigCache parses ListPairs into one cached struct you can pass into the quoting layer.
Health events
Subscribe multiplexes StatusEvents onto the same stream. Treat them as advisory — the SDK doesn't gate calls on them. Common values:
| State | What it means | What to do |
|---|---|---|
HEALTH_STATE_HEALTHY | Book is fresh. | Quote and trade normally. |
HEALTH_STATE_DEGRADED | Book may be stale. | Widen quotes; consider pausing taker flow. |
HEALTH_STATE_HALTED | Don't act on this data. | Pause submissions until you see HEALTHY again. |
The status can be global (no pair) or scoped to one pair.
Rate limits
MarketDataService is per-IP rate limited. Bursting will return RESOURCE_EXHAUSTED; the bucket replenishes. For high-throughput consumers, prefer the streaming RPCs over polling GetBook — streams don't bill against the bucket per event.
Backoff
| Operation | Suggested cadence |
|---|---|
ListPairs | Once at boot, then on schema changes. |
GetBook (single pair) | Once at boot, then on stream reconnect. |
Subscribe (any level) | Long-lived. Don't tear down + reopen on every event. |
SubscribeFills | Long-lived. |
Polling GetBook >1 Hz means you should be on Subscribe instead.
