Appearance
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.SubscribeExternalFills | Stream of trades observed on external DEX aggregators (Jupiter, OKX, DFlow, Titan), normalized onto one pair. | Cross-venue trade tape, external flow analytics. |
MarketDataService.SubscribeExternalQuotes | Stream of per-venue bid/ask for one pair, polled from each aggregator's quote API. | Reference-price comparison, spread monitoring. |
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 |
|---|---|---|
FeedLevel::L1common_pb2.FEED_LEVEL_L1FeedLevel.L1 | Best bid + best ask | Tickers, mark prices, sanity checks. |
FeedLevel::L2common_pb2.FEED_LEVEL_L2FeedLevel.L2 | Aggregated depth, snapshot + deltas | Depth charts, mid calculation, taker sizing. |
FeedLevel::L3common_pb2.FEED_LEVEL_L3FeedLevel.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:
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:
| State | What it means | What to do |
|---|---|---|
HealthState::Healthycommon_pb2.HEALTH_STATE_HEALTHYHealthState.HEALTHY | Book is fresh. | Quote and trade normally. |
HealthState::Degradedcommon_pb2.HEALTH_STATE_DEGRADEDHealthState.DEGRADED | Book may be stale. | Widen quotes; consider pausing taker flow. |
HealthState::Haltedcommon_pb2.HEALTH_STATE_HALTEDHealthState.HALTED | Don'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
| 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. |
SubscribeExternalFills / SubscribeExternalQuotes | Long-lived, one pair each. |
Polling GetBook >1 Hz means you should be on Subscribe instead.
