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.SubscribeExternalFillsStream of trades observed on external DEX aggregators (Jupiter, OKX, DFlow, Titan), normalized onto one pair.Cross-venue trade tape, external flow analytics.
MarketDataService.SubscribeExternalQuotesStream of per-venue bid/ask for one pair, polled from each aggregator's quote API.Reference-price comparison, spread monitoring.
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
FeedLevel::L1common_pb2.FEED_LEVEL_L1FeedLevel.L1Best bid + best askTickers, mark prices, sanity checks.
FeedLevel::L2common_pb2.FEED_LEVEL_L2FeedLevel.L2Aggregated depth, snapshot + deltasDepth charts, mid calculation, taker sizing.
FeedLevel::L3common_pb2.FEED_LEVEL_L3FeedLevel.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 flint_api_client::api::proto::{
    FeedLevel, GetBookRequest, Pair, PairSubscription, SubscribeRequest,
};

let pair = Pair {
    base_id: Some(1),
    base: "SOL".into(),
    quote_id: Some(0),
    quote: "USDC".into(),
    label: "SOL/USDC".into(),
};

// 1. Cold-start snapshot.
let mut market = client.market_data();
let snapshot = market
    .get_book(GetBookRequest { pair: Some(pair.clone()), 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/flint-api-client";
import { FeedLevel } from "@superis-labs/flint-api-client/gen/flint/spot/v1/common_pb.js";

const transport = new GrpcWebTransport();
const market = createServiceClient(MarketDataService, transport);
const pair = { baseId: 1n, quoteId: 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();
python
from flint.gen.flint.spot.v1 import common_pb2
from flint.gen.flint.spot.v1.market_data import messages_pb2 as md

pair = common_pb2.Pair(base_id=1, quote_id=0)

# 1. Cold-start snapshot.
snapshot = await client.market_data.GetBook(
    md.GetBookRequest(pair=pair, level=common_pb2.FEED_LEVEL_L2)
)
book = build_local_book(snapshot)

# 2. Subscribe to deltas.
stream = client.market_data.Subscribe(
    md.SubscribeRequest(
        pairs=[
            common_pb2.PairSubscription(
                pair=pair,
                level=common_pb2.FEED_LEVEL_L2,
                snapshot_only=False,
            )
        ]
    )
)

async for ev in stream:
    apply_event(book, ev)

ResilientStream reconnects and replays the request; subscribe to its fan-out stream in each consumer.streamWithBackoff yields connected, item, and disconnected events; reconnect timing matches the backoffDuration schedule.The async for stream yields market events directly; keep reconnect policy in your loop. Holding the book in your store and calling GetBook on reconnect keeps the local copy consistent.

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 flint_api_client::api::proto::{Pair, SubscribeFillsRequest};

let mut market = client.market_data();
let mut fills = market
    .subscribe_fills(SubscribeFillsRequest {
        pairs: vec![Pair {
            base_id: Some(1),
            base: "SOL".into(),
            quote_id: Some(0),
            quote: "USDC".into(),
            label: "SOL/USDC".into(),
        }],
    })
    .await?
    .into_inner();

while let Some(fill) = fills.message().await? {
    println!("{} {} @ {}", fill.side, fill.size.unwrap().value, fill.price.unwrap().value);
}
python
from flint.gen.flint.spot.v1 import common_pb2
from flint.gen.flint.spot.v1.market_data import messages_pb2 as md

pair = common_pb2.Pair(base_id=1, quote_id=0)
fills = client.market_data.SubscribeFills(
    md.SubscribeFillsRequest(pairs=[pair])
)

async for fill in fills:
    print(f"{fill.side} {fill.size.value} @ {fill.price.value}")

External fills ​

Trades executed through external Solana DEX aggregators (Jupiter, OKX, DFlow, Titan), normalized onto a Flint pair. One pair per subscription — price is quote per base, size is in base units, and source names the aggregator the swap routed through. The server taps a shared upstream feed only while at least one subscriber is connected.

rust
use flint_api_client::api::proto::{Pair, SubscribeExternalFillsRequest};

let pair = Pair {
    base_id: Some(1),
    base: "SOL".into(),
    quote_id: Some(0),
    quote: "USDC".into(),
    label: "SOL/USDC".into(),
};

let fills = client.subscribe_market_data_external_fills(
    SubscribeExternalFillsRequest { pair: Some(pair) },
);
let mut rx = fills.subscribe().await;

while let Ok(fill) = rx.recv().await {
    println!(
        "[{}] {} {} @ {}",
        fill.source, fill.side, fill.size.unwrap().value, fill.price.unwrap().value,
    );
}
ts
const pair = { baseId: 1n, quoteId: 0n };

for await (const fill of market.subscribeExternalFills({ pair })) {
  console.log(`[${fill.source}] ${fill.side} ${fill.size?.value} @ ${fill.price?.value}`);
}
python
from flint.gen.flint.spot.v1 import common_pb2
from flint.gen.flint.spot.v1.market_data import messages_pb2 as md

pair = common_pb2.Pair(base_id=1, quote_id=0)
fills = client.market_data.SubscribeExternalFills(
    md.SubscribeExternalFillsRequest(pair=pair)
)

async for fill in fills:
    print(f"[{fill.source}] {fill.side} {fill.size.value} @ {fill.price.value}")

External quotes ​

Per-venue bid/ask for one pair, obtained by polling each aggregator's quote API at a fixed notional. Both sides are quoted at the same base size (bid.size == ask.size); ask_route/bid_route carry the underlying AMM labels each leg routes through. Only pairs quoted in the global (USD) numeraire are supported.

These quotes are estimates, not high-fidelity prices

Each value is a periodic poll of an external aggregator's quote API at a fixed notional — not a live, executable book. It can be stale between polls, is specific to that one notional size, and excludes slippage, gas, and price impact you'd actually incur. Treat it as a rough reference for comparison and monitoring, not as a firm price to trade or settle against.

rust
use flint_api_client::api::proto::{Pair, SubscribeExternalQuotesRequest};

let pair = Pair {
    base_id: Some(1),
    base: "SOL".into(),
    quote_id: Some(0),
    quote: "USDC".into(),
    label: "SOL/USDC".into(),
};

let quotes = client.subscribe_market_data_external_quotes(
    SubscribeExternalQuotesRequest { pair: Some(pair) },
);
let mut rx = quotes.subscribe().await;

while let Ok(q) = rx.recv().await {
    let (bid, ask) = (q.bid.unwrap(), q.ask.unwrap());
    println!("{}: {} / {}", q.venue, bid.price.unwrap().value, ask.price.unwrap().value);
}
ts
const pair = { baseId: 1n, quoteId: 0n };

for await (const q of market.subscribeExternalQuotes({ pair })) {
  console.log(`${q.venue}: ${q.bid?.price?.value} / ${q.ask?.price?.value}`);
}
python
from flint.gen.flint.spot.v1 import common_pb2
from flint.gen.flint.spot.v1.market_data import messages_pb2 as md

pair = common_pb2.Pair(base_id=1, quote_id=0)
quotes = client.market_data.SubscribeExternalQuotes(
    md.SubscribeExternalQuotesRequest(pair=pair)
)

async for q in quotes:
    print(f"{q.venue}: {q.bid.price.value} / {q.ask.price.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);
}
python
markets = await client.list_markets()
for m in markets.values():
    print(f"{m.name}: spot {m.base_spot_id} → {m.quote_spot_id}")

ConfigCache parses ListPairs into one cached structClient.list_markets() parses ListPairs into dict[PairKey, MarketInfo] 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
HealthState::Healthycommon_pb2.HEALTH_STATE_HEALTHYHealthState.HEALTHYBook is fresh.Quote and trade normally.
HealthState::Degradedcommon_pb2.HEALTH_STATE_DEGRADEDHealthState.DEGRADEDBook may be stale.Widen quotes; consider pausing taker flow.
HealthState::Haltedcommon_pb2.HEALTH_STATE_HALTEDHealthState.HALTEDDon't act on this data.Pause submissions until the state is healthy again.

The status can be global (no pair) or scoped to one pair.

Rate limits ​

MarketDataService is public, so it's rate limited per client IP. Its unary RPCs (GetBook, ListPairs) draw from a 50 requests/sec token bucket; over-budget calls return RESOURCE_EXHAUSTED (gRPC status 8). The bucket starts full — so a burst of up to 50 is fine — and refills continuously, so back off and retry rather than hammering.

The streaming RPCs (Subscribe, SubscribeFills, SubscribeExternalFills, SubscribeExternalQuotes, SubscribeMarketSnapshots) don't touch that bucket: each holds one of your 100 concurrent connection slots for its lifetime instead of billing per event. For high-throughput consumers, prefer a single long-lived stream over polling GetBook.

See API → Rate limits for the full limit table across services.

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.
SubscribeExternalFills / SubscribeExternalQuotesLong-lived, one pair each.

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

Built on Solana