Skip to content

Market data

Reading the book, fills, and pair catalog. Public — no session token required.

What's available

RPCReturnsUse it for
MarketDataService.ListPairsCatalog of pairs + per-spot metadata.Boot — discover what's tradeable.
MarketDataService.GetBookOne-shot L2 or L3 snapshot.Cold-start reconciliation, periodic resync.
MarketDataService.SubscribeStream of L1/L2/L3 book updates + status events for one or more pairs.Continuous order-book, depth charts, status alerts.
MarketDataService.SubscribeFillsStream of executed trades. Optional per-pair filter.Trade tape, volume metrics.
MarketDataService.SubscribeMarketSnapshotsConsolidated 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:

LevelPayloadUse for
FEED_LEVEL_L1Best bid + best askTickers, mark prices, sanity checks.
FEED_LEVEL_L2Aggregated depth, snapshot + deltasDepth charts, mid calculation, taker sizing.
FEED_LEVEL_L3Per-maker order listMaker analytics. Does not include oracle-offset orders (those are virtual).

Most integrations want L2.

Snapshot then stream

The canonical pattern for a UI:

rust
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);
}
ts
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.

rust
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

rust
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:

StateWhat it meansWhat to do
HEALTH_STATE_HEALTHYBook is fresh.Quote and trade normally.
HEALTH_STATE_DEGRADEDBook may be stale.Widen quotes; consider pausing taker flow.
HEALTH_STATE_HALTEDDon'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

OperationSuggested cadence
ListPairsOnce 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.
SubscribeFillsLong-lived.

Polling GetBook >1 Hz means you should be on Subscribe instead.

Build on Solana