--- # overview/onboarding # Onboarding Getting setup to quote on Flint ## Audience This page assumes you're either: - **Building a market-maker bot.** You'll quote on at least one pair and need a `maker_id` registered against your pubkey. - **Building a read-only integration** (analytics, charts, alerts). Public market data, aggregate stats, and historical backfills need no auth. ## 1. Get an endpoint The server binds **two** gRPC listeners so operators can front each with a different ingress policy (public CDN/cache vs. mTLS / authenticated edge): | Listener | Default port | Services | Auth | | --------- | ------------ | -------------------------------------------------- | ------------------------------ | | Public | `50051` | `MarketDataService`, `StatsService`, `HistoricalService`, `AuthService` | None — public. | | Authed | `50052` | `AuthService`, `MakerService`, `TxService` | Bearer session token for all except `AuthService`. | `AuthService` is available on both listeners. The public listener is enough for login flows; makers also configure the authed listener because the tokens it mints are used by `MakerService` and `TxService`. | Environment | Public | Authed | | ----------- | ---------------------------------------------- | --------------------------------------------------- | | Mainnet | `https://api.superis.exchange:443` | `https://auth.api.superis.exchange:443` | | Testnet | `https://testnet.api.superis.exchange:443` | `https://auth.testnet.api.superis.exchange:443` | | Localhost | `http://localhost:50051` | `http://localhost:50052` | (`SUPERIS_INSECURE=1` tells the SDKs to skip TLS for local hosts.) Market-data, stats, historical, and browser login integrations only need the public endpoint. Makers need the authed endpoint too. ## 2. Solana wallet You need an Solana keypair. The same key: 1. Authenticates with the gRPC server (signs the AuthService challenge). 2. Signs on-chain quoting transactions (`UpdateOracleFair`, `UpdateQuotingParams`). The server only accepts authenticated calls from pubkeys that are registered as **quoting authorities** for some `maker_id`. ## 3. Register your pubkey as a quoting authority If you don't already have a `maker_id`, the on-chain admin needs to create one for you. Maker creation is a one-time `CreateMaker` instruction signed by the admin; ask the operator that runs your deployment. If you already have a `maker_id` and want to delegate quoting to a hot wallet, send a `ManageMaker { AddQuoter }` instruction to the Flint program from the maker admin wallet, naming the hot wallet as the new quoter. ::: tip Hot vs. cold wallets Use a cold wallet to mint your `maker_id` and set `max_balance` / risk params (the admin path). Use a delegated hot wallet for the high-frequency `UpdateOracleFair` / `UpdateQuotingParams` flow. ::: ## 4. Fund the maker {#fund-the-maker} Flint allocates liquidity **dynamically at swap time** — not when you publish a quote. When a taker hits your book, the matcher reads your current balance and caps the fill against: - on the **sell side of the fill** (the token going out of your account): `available_to_sell` = your **deposited** balance of that token. - on the **buy side of the fill** (the token coming in): `available_to_buy` = `soft_max_balance − balance` — the headroom under your cap. Because the check happens at swap time, two properties fall out that surprise people coming from a CEX: 1. **You can publish levels larger than what you currently hold.** Quotes aren't rejected on size. The matcher fills up to whatever budget exists at the moment of the swap. A 10 SOL ask with 5 SOL deposited fills 5 SOL. 2. **Fills feed adjacent quotes.** A SOL ask that pays out SOL brings USDC in. That USDC immediately becomes `available_to_sell` USDC backing any SOL bid you also posted — the bid can then fill on the very next swap. A maker can run a two-sided book on a one-sided initial deposit; inventory cycles through the quotes. Worked examples (which balance gates which side of each fill): | Your quote | Sell-side budget — deposited | Buy-side budget — cap with headroom | | --------------------------- | --------------------------------------------| -------------------------------------------- | | Sell SOL on SOL/USDC | SOL deposited in the SOL micro-book | USDC `soft_max_balance` on the global market | | Buy SOL on SOL/USDC | USDC deposited in the global market | SOL `soft_max_balance` on the SOL micro-book | | Sell SOL on SOL/ETH | SOL deposited in the SOL micro-book | ETH `soft_max_balance` on the ETH micro-book | | Buy SOL on SOL/ETH | ETH deposited in the ETH micro-book | SOL `soft_max_balance` on the SOL micro-book | The same rule explains both modes — SOL/USDC just keeps the USDC side in the singleton `GlobalMarket` account instead of a per-spot micro-book. Each fill is a single two-token settlement; nothing is multihop. Two on-chain accounts hold these balances: | Account | Set by | Purpose | | --- | --- | --- | | `SpotMakerMicroBook.balance` (one per spot) | `DepositWithdraw` + `UpdateQuotingParams.max_balance` | Deposited base + per-spot `soft_max_balance` cap. | | `GlobalMarket.balances[maker_id]` (singleton) | `DepositWithdrawQuote` + `set_quote_max_balance` | Deposited USDC + global `soft_max_balance` cap. | ::: warning The silent filter Quotes are filtered only when a side's budget reads **literally zero** at swap time — no deposit on the sell side, or no `soft_max_balance` headroom on the buy side. A fresh maker has `soft_max_balance = 0` everywhere, which makes every buy-side budget 0. The classic cold-start mistake: a maker quoting SOL/USDC with deposited SOL but no `soft_max_balance` set on the USDC (global) side — sells go up, never fill because the buy-side USDC budget is 0. ::: ## 5. Install an SDK ::: code-group ```sh [Rust] cargo add sweetspot-api-client tokio --features tokio/macros,tokio/rt-multi-thread ``` ```sh [TypeScript] npm install @superis-labs/sweetspot-client @bufbuild/protobuf ``` ```sh [Python] pip install flint-sdk # from a checkout: pip install -e './python[dev]' make py-gen ``` ::: ## 6. First authenticated call ```rust use std::sync::Arc; use sweetspot_api_client::api::client::Client; let client = Client::builder() .public_endpoint("https://api.superis.exchange:443") .auth_endpoint("https://auth.api.superis.exchange:443") .wallet(Arc::new(my_keypair)) .build() .await?; let session = client.authenticate().await?; println!("authenticated as maker_id={}", session.maker_id); ``` If `Client::authenticate()` returns `UNAUTHENTICATED: pubkey not registered as a quoting authority`, your pubkey hasn't been added yet — go back to step 3. ## 7. Verify the program id Defense in depth. The RPC check confirms the advertised account exists, is executable, and is owned by a Solana BPF loader. To prove it is the Flint program you intended to trade against, compare it with a program id from trusted configuration as well. ```rust use sweetspot_api_client::api::config::verify_program_id; let cfg = client.refresh_config(None).await?; verify_program_id("https://api.mainnet-beta.solana.com", &cfg.program_id).await?; ``` Run this once at boot; if either the executable-account check or your trusted program-id comparison fails, refuse to quote until it passes. ## 8. Pick your path | Goal | Next page | | --- | --- | | Stream books and fills | [Market data](/sdks/market-data) | | Quote and submit fills | [Quoting](/sdks/quoting) | | Run or fork a Python maker bot | [`sweetspot-maker-example`](https://github.com/superis-labs/sweetspot-maker-example) | | Pull historical trades / candles | [Historical queries](/sdks/historical) | | Sign-in flow detail | [Auth flow](/sdks/auth) | --- # overview/exchange # How Flint works Flint is a Solana on-chain DEX that aggregates multiple private market makers into a unified virtual orderbook. Think of it as a multi-entity proprietary AMM: each maker independently quotes prices using pluggable quoting strategies, and when a trader wants to swap, the protocol builds a virtual book on the fly by merging all quoting intents and filling makers pro-rata at each price level. This design delivers two things that are usually at odds: - Efficient pricing updates — makers update a fair price plus offsets instead of rewriting full order state, so quote refreshes stay cheap. - Flexibility — each maker chooses their own quoting strategy, risk parameters, and cross‑market pairs, instead of being locked into one AMM curve or a single operator. ## Markets Flint has one on-chain `SpotMarket` account per listed token and one `GlobalMarket` for the quote currency (USDC). There are no per-pair accounts. Pairs are discovered from those spot markets and then joined virtually at match time. Two swap modes exist: - **Global swap** — base token vs. the quote currency (e.g. SOL ↔ USDC). The matcher loads one spot account; quote balances live in the singleton `GlobalMarket`. - **Cross swap** — base vs. base (e.g. SOL ↔ ETH). The matcher loads both spot accounts and bridges them through each maker's per-spot micro-book. The SDK's `ListPairs` RPC returns the catalog of discovered pairs along with their per-token metadata (decimals, atoms-per-lot, mints). ## Quoting strategies Pick one per market. Three are supported on-chain; the SDK exposes each as a fluent block on the shared `QuoteBuilder`. | Strategy | When to use | Rust builder entry | | --- | --- | --- | | **OrderList** | Explicit order-by-order control (place / cancel by price + size). The CEX-shaped option. | `QuoteBuilder::new().order_list("SOL", ...)` | | **OracleOffset** | You have an off-book fair price (oracle, internal mid). Quote a fair anchor + per-side delta vectors. | `QuoteBuilder::new().oracle_offset("SOL", ...)` | | **LinearDistribution** | Server interpolates linearly between buy/sell price ranges. Cheapest message size. | `QuoteBuilder::new().linear("SOL", ...)` | Strategies are documented under [Quoting](/sdks/quoting). ## Unit system You as a quoter work in **human units** (price `155.14`, size `10.0` SOL). The SDK converts to the on-chain integer forms at the boundary. You never write atoms or lots in normal flow. The three on-chain units exist: | Unit | Meaning | | --- | --- | | Atoms | Smallest token integer. 1 SOL = 10⁹ atoms. | | Lots | `atoms / atoms_per_contract_lots`. The matcher works in lot-space. | | Oracle units | Common reference unit for price comparisons across markets. | The SDK's `pricing` helpers (`human_to_oracle`, `human_size_to_lots`, `oracle_to_human`) do every conversion. See [Prices, sizes, units](/overview/units). ## Sequence numbers Two monotonic counters per spot market are checked by the matcher. Both must strictly increase across **all** transactions you ever submit, including across process restarts. | Counter | Purpose | | --- | --- | | `oracle_sequence_number` | Anti-replay on `OracleFair` updates. Bump on every fair-price flush. | | `order_sequence` | Anti-replay on `UpdateQuotingParams`. Bump on every params/order flush. | | `client_order_id` | Per-maker monotonic id for `OrderList` placements. | The SDK seeds all three counters from microseconds since Unix epoch on startup, so a process restart should not collide with prior on-chain state. Don't override the seed unless you know what you're doing, and don't run two quoting processes for the same `maker_id`. ## Cross-market spread For any **cross-swap** market (e.g. SOL ↔ ETH), you must explicitly allow crossing into the counterparty market's spot id by including a `cross_spread` entry. The matcher looks up `book.get_cross_params(SpotId::OF_COUNTERPARTY)` and silently filters your maker if absent. For a global swap (e.g. SOL ↔ USDC), the counterparty is `SpotId::GLOBAL`, encoded as `0`. You still need a `cross_spread` entry for `0` even though there is no second spot market. `Some(0)` is enough to pass the gate without widening. ```rust QuoteBuilder::new() .order_list("SOL", |b| b .init() .with_params(ParamsUpdate::new() .enable(true) .max_balance(100.0) .with_cross(0_u16, Some(0.0)))) // counterparty + spread cap .commit(&mut core).await?; ``` ## Budgets per leg {#budgets-per-leg} Liquidity is allocated **dynamically at swap time**. When a taker swap touches a maker's quote, the matcher reads the maker's current balance and caps the fill against: | Side of the fill | Budget read | What's actually needed | | ----------------------------------------- | --------------------------------------------| ---------------------- | | **Sell side** (token going out) | `available_to_sell` = `balance` | Deposited inventory. | | **Buy side** (token coming in) | `available_to_buy` = `soft_max_balance − balance` (sat.) | A non-zero `soft_max_balance` with headroom under it. | Because the check happens at swap time, makers can publish levels that exceed their current inventory — the matcher just fills up to the available budget at the moment of the swap. Quotes are silently filtered only when a side's budget is **literally zero** (no deposit on the sell side, or no `soft_max_balance` headroom on the buy side). A useful consequence: fills recycle. A SOL ask that takes SOL out brings USDC in, which immediately becomes `available_to_sell` USDC backing any SOL bid the maker also posted. A maker can run a two-sided book on a one-sided initial deposit; inventory cycles through the quotes. For a **global swap** (e.g. SOL ↔ USDC) the USDC side lives in the singleton `GlobalMarket` account (set via `DepositWithdrawQuote` + `set_quote_max_balance`). For a **cross swap** (e.g. SOL ↔ ETH) both sides live in per-spot micro-books. The mechanism is identical in both cases — only the account holding the balance differs. A common cold-start trap: a fresh maker has `soft_max_balance = 0` everywhere, so the buy-side budget reads zero on every direction. A sell-only SOL/USDC maker needs a non-zero USDC `soft_max_balance` on the global market — without it, the buy side of every sell fill is 0 and asks silently don't match. (Importantly, that's the **only** cap a sell-only maker needs; no cap is required on the SOL side they're never buying into.) See [Onboarding step 4](/overview/onboarding#fund-the-maker) for the boot sequence. ## OracleFair staleness Each oracle-offset order carries a `max_slot_staleness` byte. The matcher computes `slot_delay = current_slot - last_oracle_slot` and **rejects** any order where `slot_delay > max_slot_staleness`. --- # overview/units # Prices, sizes, and units Every numeric field that represents a human price, size, balance, or volume is a **decimal string** — books, fills, candles, trades, and maker balances all share the same `Decimal { value }` wrapper. This is the load-bearing contract: get it right and everything downstream works. ## Why strings Floating-point arithmetic loses precision for large notionals. Decimal strings preserve the exact representation. Parse with whatever fixed-precision type your language offers: | Language | Library | | --- | --- | | Rust | `rust_decimal::Decimal` | | Python | `decimal.Decimal` | | TypeScript | `bignumber.js` or `decimal.js` | ## Conventions on the wire | Field | Example | Meaning | | --- | --- | --- | | `price` | `"155.14"` | Quote per base, human-readable | | `size` | `"10.0"` | Base token units (e.g. SOL) | | `volume` | `"42.137"` | Candle base-asset volume | | `open` / `high` / `low` / `close` | `"154.97"` | Candle OHLC | | `balance` | `"1.5"` | Maker balance in token units (server scales by `SpotMetadata.decimals`) | | `best_bid` / `best_ask` / `mid` | strings | Optional, omitted on empty side | You will **never** see on the wire: - Lot multiples. - Oracle units. `MakerService` reports balances in human token units — the server applies `SpotMetadata.decimals` for you, so a 1.5-SOL balance arrives as `Decimal("1.5")`, not `1_500_000_000` atoms. The SDK's quoting layer converts the human values you pass to the on-chain integer forms exactly once, at the point of building the instruction. The integer forms are documented in [How Flint works](/overview/exchange#unit-system) for completeness — most integrators never have to think about them. ## Pricing helpers For when you need the conversion outside a `QuoteBuilder` call: ```rust use sweetspot_api_client::pricing::{human_to_oracle, oracle_to_human, human_size_to_lots}; let market = config.pairs.iter().find(|p| p.base_name == "SOL").unwrap(); let oracle = human_to_oracle(155.14, market); // → 155_140 let back = oracle_to_human(oracle, market); // → 155.14 let lots = human_size_to_lots(10.0, market); // → 10_000 ``` Edge cases (saturate, NaN, negative) are documented in the Rust function-level docs. Python exposes the same conversion helpers from `flint.onchain`, but human money inputs must be `decimal.Decimal`, decimal strings, or integers. Floats, NaN/Infinity, negatives, overflows, fractional atoms, and non-lot-aligned sizes raise instead of being rounded into quote bytes. ```python from decimal import Decimal from flint.onchain import human_size_to_lots, human_to_oracle, oracle_to_human oracle = human_to_oracle(Decimal("155.14"), market.units) back = oracle_to_human(oracle, market.units) lots = human_size_to_lots("10.0", market.units) ``` ## Timestamps - `Timestamp { micros }` — Unix microseconds since epoch. Used on trades, candles, stream events, and range bounds. - `slot` — Solana slot number. Useful for cross-referencing on-chain data. - Historical `start` is inclusive. Historical `end` is exclusive and defaults to server time when unset or `micros = 0`. ## Enums on the wire Enum values use their proto variant names in examples and generated code: | Field | Examples | | --- | --- | | `interval` | `"CANDLE_INTERVAL_5M"`, `"CANDLE_INTERVAL_1H"`, `"CANDLE_INTERVAL_1D"` | | `side` | `"SIDE_BUY"`, `"SIDE_SELL"` | | `state` | `"HEALTH_STATE_HEALTHY"`, `"HEALTH_STATE_DEGRADED"` | The SDKs all expose typed enums, so you don't have to hardcode the raw variant names or numbers. --- # overview/errors # Errors Every authenticated RPC can fail. Most failures fall into one of five codes; the SDKs surface the gRPC status verbatim plus an optional domain-specific reason string. ## Codes you'll see | gRPC code | When | Retry? | | --- | --- | --- | | `UNAUTHENTICATED` | Session token missing / expired / revoked. Pubkey not registered as a quoting authority. | Re-authenticate (`AuthFlow.refresh()`) and retry once. | | `INVALID_ARGUMENT` | Bad pair name, malformed pubkey, out-of-range limit, unsupported interval. | No — fix the input. | | `RESOURCE_EXHAUSTED` | Per-IP rate-limit bucket empty. | Yes, with exponential backoff. The bucket replenishes. | | `FAILED_PRECONDITION` | Historical query when the historical archive is disabled on this deployment. | No — feature isn't enabled. | | `NOT_FOUND` | Pair not in catalog, maker_id has no balances. | No — fix the input. | | `UNAVAILABLE` | Transport drop. The server is restarting or unreachable. | Yes, with backoff. `streamWithBackoff` (TS) / `ResilientStream` (Rust) auto-retries server streams; for unary calls you decide the policy. | | `INTERNAL` | A bug. Report it. | Capped retry (1–3 attempts) before surfacing. | The SDKs don't retry by default. Pick your policy explicitly. ## Reading errors ::: code-group ```rust [Rust] use tonic::Code; match client.get_balance(req).await { Ok(res) => /* ... */, Err(status) => match status.code() { Code::Unauthenticated => auth.refresh().await?, Code::ResourceExhausted => sleep_then_retry().await, _ => return Err(status.into()), }, } ``` ```ts [TypeScript] import { GrpcError, GrpcCode } from "@superis-labs/sweetspot-client"; try { await client.listPairs({}); } catch (err) { if (err instanceof GrpcError) { if (err.code === GrpcCode.ResourceExhausted) await sleep(backoff); if (err.code === GrpcCode.Unavailable) await sleep(backoff); } } ``` ::: ## Auth-flow specific failures `AuthService.Authenticate` returns `UNAUTHENTICATED` for any of: - The pubkey has no outstanding nonce (you didn't call `Challenge` first). - The nonce expired (TTL exceeded between `Challenge` and `Authenticate`). - The signature doesn't cover `b"SWEETSPOT-AUTH-V1:" || nonce`. - The pubkey isn't registered as a quoting authority for any maker. The SDKs' `AuthFlow.refresh()` always runs `Challenge → sign → Authenticate` from scratch, so you don't need to think about nonce expiry yourself. ## On-chain failures (quoting) When you submit a tx via the quoting layer, the on-chain program may revert. The SDK surfaces this through `Receipt`: ```rust match receipt.confirmed().await { Ok(()) => /* landed */, Err(CommitError::OnChain { reason }) => { // Reason is whatever the on-chain program returned. // Common: "OracleFairSequenceNotMonotonic", "InsufficientBalance". } Err(CommitError::Timeout) => /* didn't land in time */, Err(CommitError::Disconnected) => /* status stream dropped */, } ``` The most common on-chain reverts and what to do about them: | Reason | Cause | Fix | | --- | --- | --- | | `OracleFairSequenceNotMonotonic` | Two clients sharing a maker_id, or a clock-skew restart. | Don't share maker_ids; the SDK's microsecond seed handles restarts. | | `OrderSequenceNotMonotonic` | Same as above for `UpdateQuotingParams`. | Same fix. | | `OracleFairTooStale` | `current_slot - last_oracle_slot > order.staleness`. | Increase `staleness` or flush more often. | | `InsufficientBalance` | One of the legs the matcher budgets against — your micro-book or the counterparty — has zero deposited inventory or zero `soft_max_balance` headroom. | See [Budgets per leg](/overview/exchange#budgets-per-leg). | --- # api/index # API Six gRPC services in `sweetspot.api.v1`. `MarketDataService`, `StatsService`, `HistoricalService`, and `AuthService` are public. `MakerService` and `TxService` require credentials. | Service | Auth | What you use it for | | --- | --- | --- | | `AuthService` | none | Mint or revoke bearer session tokens. | | `MarketDataService` | none | Stream books, fills, market snapshots; list pairs; snapshot a single book. | | `StatsService` | none | Public aggregate stats — summary, volume breakdown/series, asset TVLs. | | `HistoricalService` | none | ClickHouse-backed historical trades + candles. | | `MakerService` | bearer session or org API key | Per-maker balances, fills, market snapshots, volume, and activity stats. | | `TxService` | keypair bearer session | Submit signed transactions, stream blockhash + tx status. | Each is fully wire-compatible with vanilla gRPC and gRPC-Web on the same URL. Pick whichever transport fits your stack — the SDKs hide the difference. ## Schema - api.proto — bundled protobuf source covering every service in the package. Use the checked-in `proto/` tree when you want a per-service slice. - openapi.yaml — OpenAPI 3.1 schema for the same service URLs used by gRPC-Web clients, suitable for Postman, Insomnia, Stoplight, or client-generation tools. CI fails the build if these files diverge from the upstream proto. ## URL convention ``` POST https:///sweetspot.api.v1./ Content-Type: application/grpc (gRPC binary, hot path) application/grpc-web+proto (browser-native) ``` No URL params, no path-based versioning beyond the package name. ## Authentication `MarketDataService`, `StatsService`, `HistoricalService`, and `AuthService` are open. `MakerService` and `TxService` expect: ``` authorization: Bearer ``` Mint a keypair token via `AuthService.Challenge → sign → Authenticate`. `MakerService` also accepts passwordless organization sessions and organization API keys. See the [Auth flow recipe](/sdks/auth) for the per-SDK helpers. --- # api/reference # Protocol Documentation ## Table of Contents - [sweetspot/api/v1/auth/messages.proto](#sweetspot_api_v1_auth_messages-proto) - [AuthenticateRequest](#sweetspot-api-v1-AuthenticateRequest) - [AuthenticateResponse](#sweetspot-api-v1-AuthenticateResponse) - [ChallengeRequest](#sweetspot-api-v1-ChallengeRequest) - [ChallengeResponse](#sweetspot-api-v1-ChallengeResponse) - [RequestLoginCodeRequest](#sweetspot-api-v1-RequestLoginCodeRequest) - [RequestLoginCodeResponse](#sweetspot-api-v1-RequestLoginCodeResponse) - [RevokeRequest](#sweetspot-api-v1-RevokeRequest) - [RevokeResponse](#sweetspot-api-v1-RevokeResponse) - [VerifyLoginCodeRequest](#sweetspot-api-v1-VerifyLoginCodeRequest) - [VerifyLoginCodeResponse](#sweetspot-api-v1-VerifyLoginCodeResponse) - [sweetspot/api/v1/auth/service.proto](#sweetspot_api_v1_auth_service-proto) - [AuthService](#sweetspot-api-v1-AuthService) - [sweetspot/api/v1/market_data/events.proto](#sweetspot_api_v1_market_data_events-proto) - [FillEvent](#sweetspot-api-v1-FillEvent) - [L1Event](#sweetspot-api-v1-L1Event) - [L2SnapshotEvent](#sweetspot-api-v1-L2SnapshotEvent) - [L2UpdateEvent](#sweetspot-api-v1-L2UpdateEvent) - [L3SnapshotEvent](#sweetspot-api-v1-L3SnapshotEvent) - [L3UpdateEvent](#sweetspot-api-v1-L3UpdateEvent) - [MarketDataEvent](#sweetspot-api-v1-MarketDataEvent) - [MarketSnapshot](#sweetspot-api-v1-MarketSnapshot) - [MarketsSnapshotEvent](#sweetspot-api-v1-MarketsSnapshotEvent) - [sweetspot/api/v1/market_data/messages.proto](#sweetspot_api_v1_market_data_messages-proto) - [GetBookRequest](#sweetspot-api-v1-GetBookRequest) - [GetBookResponse](#sweetspot-api-v1-GetBookResponse) - [ListPairsRequest](#sweetspot-api-v1-ListPairsRequest) - [ListPairsResponse](#sweetspot-api-v1-ListPairsResponse) - [ListedPair](#sweetspot-api-v1-ListedPair) - [SubscribeFillsRequest](#sweetspot-api-v1-SubscribeFillsRequest) - [SubscribeMarketSnapshotsRequest](#sweetspot-api-v1-SubscribeMarketSnapshotsRequest) - [SubscribeRequest](#sweetspot-api-v1-SubscribeRequest) - [sweetspot/api/v1/market_data/service.proto](#sweetspot_api_v1_market_data_service-proto) - [MarketDataService](#sweetspot-api-v1-MarketDataService) - [sweetspot/api/v1/tx/events.proto](#sweetspot_api_v1_tx_events-proto) - [BlockhashEvent](#sweetspot-api-v1-BlockhashEvent) - [SlotEvent](#sweetspot-api-v1-SlotEvent) - [TxAckEvent](#sweetspot-api-v1-TxAckEvent) - [TxConfirmedEvent](#sweetspot-api-v1-TxConfirmedEvent) - [TxFailedEvent](#sweetspot-api-v1-TxFailedEvent) - [TxStatusEvent](#sweetspot-api-v1-TxStatusEvent) - [sweetspot/api/v1/tx/messages.proto](#sweetspot_api_v1_tx_messages-proto) - [GetSponsoredPayersRequest](#sweetspot-api-v1-GetSponsoredPayersRequest) - [GetSponsoredPayersResponse](#sweetspot-api-v1-GetSponsoredPayersResponse) - [SubmitTxRequest](#sweetspot-api-v1-SubmitTxRequest) - [SubmitTxResponse](#sweetspot-api-v1-SubmitTxResponse) - [SubscribeBlockhashRequest](#sweetspot-api-v1-SubscribeBlockhashRequest) - [SubscribeSlotsRequest](#sweetspot-api-v1-SubscribeSlotsRequest) - [SubscribeTxStatusRequest](#sweetspot-api-v1-SubscribeTxStatusRequest) - [sweetspot/api/v1/tx/service.proto](#sweetspot_api_v1_tx_service-proto) - [TxService](#sweetspot-api-v1-TxService) - [sweetspot/api/v1/historical/messages.proto](#sweetspot_api_v1_historical_messages-proto) - [Candle](#sweetspot-api-v1-Candle) - [GetCandlesRequest](#sweetspot-api-v1-GetCandlesRequest) - [GetCandlesResponse](#sweetspot-api-v1-GetCandlesResponse) - [GetTradesRequest](#sweetspot-api-v1-GetTradesRequest) - [GetTradesResponse](#sweetspot-api-v1-GetTradesResponse) - [Trade](#sweetspot-api-v1-Trade) - [CandleInterval](#sweetspot-api-v1-CandleInterval) - [sweetspot/api/v1/historical/service.proto](#sweetspot_api_v1_historical_service-proto) - [HistoricalService](#sweetspot-api-v1-HistoricalService) - [sweetspot/api/v1/maker/events.proto](#sweetspot_api_v1_maker_events-proto) - [MakerBalanceEvent](#sweetspot-api-v1-MakerBalanceEvent) - [MakerFillEvent](#sweetspot-api-v1-MakerFillEvent) - [MakerMarketSnapshot](#sweetspot-api-v1-MakerMarketSnapshot) - [MakerMarketsSnapshotEvent](#sweetspot-api-v1-MakerMarketsSnapshotEvent) - [MakerQuote](#sweetspot-api-v1-MakerQuote) - [sweetspot/api/v1/maker/messages.proto](#sweetspot_api_v1_maker_messages-proto) - [GetBalanceRequest](#sweetspot-api-v1-GetBalanceRequest) - [GetBalanceResponse](#sweetspot-api-v1-GetBalanceResponse) - [GetMakerStatsRequest](#sweetspot-api-v1-GetMakerStatsRequest) - [GetMakerStatsResponse](#sweetspot-api-v1-GetMakerStatsResponse) - [GetMakerVolumeBreakdownRequest](#sweetspot-api-v1-GetMakerVolumeBreakdownRequest) - [GetMakerVolumeBreakdownResponse](#sweetspot-api-v1-GetMakerVolumeBreakdownResponse) - [GetMakerVolumeSeriesRequest](#sweetspot-api-v1-GetMakerVolumeSeriesRequest) - [GetMakerVolumeSeriesResponse](#sweetspot-api-v1-GetMakerVolumeSeriesResponse) - [MakerEventKindRate](#sweetspot-api-v1-MakerEventKindRate) - [SubscribeBalanceRequest](#sweetspot-api-v1-SubscribeBalanceRequest) - [SubscribeMakerFillsRequest](#sweetspot-api-v1-SubscribeMakerFillsRequest) - [SubscribeMakerMarketSnapshotsRequest](#sweetspot-api-v1-SubscribeMakerMarketSnapshotsRequest) - [sweetspot/api/v1/maker/service.proto](#sweetspot_api_v1_maker_service-proto) - [MakerService](#sweetspot-api-v1-MakerService) - [sweetspot/api/v1/stats/messages.proto](#sweetspot_api_v1_stats_messages-proto) - [AssetTvl](#sweetspot-api-v1-AssetTvl) - [CrossVenueWindowStats](#sweetspot-api-v1-CrossVenueWindowStats) - [GetAssetTvlsRequest](#sweetspot-api-v1-GetAssetTvlsRequest) - [GetAssetTvlsResponse](#sweetspot-api-v1-GetAssetTvlsResponse) - [GetSummaryRequest](#sweetspot-api-v1-GetSummaryRequest) - [GetSummaryResponse](#sweetspot-api-v1-GetSummaryResponse) - [GetVolumeBreakdownRequest](#sweetspot-api-v1-GetVolumeBreakdownRequest) - [GetVolumeBreakdownResponse](#sweetspot-api-v1-GetVolumeBreakdownResponse) - [GetVolumeSeriesRequest](#sweetspot-api-v1-GetVolumeSeriesRequest) - [GetVolumeSeriesResponse](#sweetspot-api-v1-GetVolumeSeriesResponse) - [LargestTrade](#sweetspot-api-v1-LargestTrade) - [PairVolume](#sweetspot-api-v1-PairVolume) - [UpdateRate](#sweetspot-api-v1-UpdateRate) - [VolumeBucket](#sweetspot-api-v1-VolumeBucket) - [WindowStats](#sweetspot-api-v1-WindowStats) - [StatsWindow](#sweetspot-api-v1-StatsWindow) - [sweetspot/api/v1/stats/service.proto](#sweetspot_api_v1_stats_service-proto) - [StatsService](#sweetspot-api-v1-StatsService) - [sweetspot/api/v1/common.proto](#sweetspot_api_v1_common-proto) - [BalanceUpdate](#sweetspot-api-v1-BalanceUpdate) - [Blockhash](#sweetspot-api-v1-Blockhash) - [Decimal](#sweetspot-api-v1-Decimal) - [L2Level](#sweetspot-api-v1-L2Level) - [L3Entry](#sweetspot-api-v1-L3Entry) - [MakerId](#sweetspot-api-v1-MakerId) - [MakerMarketState](#sweetspot-api-v1-MakerMarketState) - [Pair](#sweetspot-api-v1-Pair) - [PairMetadata](#sweetspot-api-v1-PairMetadata) - [PairSubscription](#sweetspot-api-v1-PairSubscription) - [Pubkey](#sweetspot-api-v1-Pubkey) - [Signature](#sweetspot-api-v1-Signature) - [SpotAssets](#sweetspot-api-v1-SpotAssets) - [SpotId](#sweetspot-api-v1-SpotId) - [SpotMetadata](#sweetspot-api-v1-SpotMetadata) - [StatusEvent](#sweetspot-api-v1-StatusEvent) - [Timestamp](#sweetspot-api-v1-Timestamp) - [FeedLevel](#sweetspot-api-v1-FeedLevel) - [HealthState](#sweetspot-api-v1-HealthState) - [Side](#sweetspot-api-v1-Side) - [Scalar Value Types](#scalar-value-types)

Top

## sweetspot/api/v1/auth/messages.proto ### AuthenticateRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pubkey | [Pubkey](#sweetspot-api-v1-Pubkey) | | | | signature | [Signature](#sweetspot-api-v1-Signature) | | Signature over AUTH_DOMAIN_PREFIX || nonce from the outstanding Challenge. | ### AuthenticateResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | session_token | [string](#string) | | Opaque bearer token. Clients pass this back as `authorization: Bearer <session_token>` metadata on all authenticated RPCs. | | maker_id | [MakerId](#sweetspot-api-v1-MakerId) | | Resolved maker identity the token speaks for. | | expires_at | [Timestamp](#sweetspot-api-v1-Timestamp) | | Absolute session expiration. Clients should re-auth before this moment; server will start returning UNAUTHENTICATED once it passes. | ### ChallengeRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pubkey | [Pubkey](#sweetspot-api-v1-Pubkey) | | The pubkey (quoting authority) that will sign the challenge. | ### ChallengeResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | nonce | [bytes](#bytes) | | Single-use random nonce. Client signs AUTH_DOMAIN_PREFIX || nonce. | | expires_at | [Timestamp](#sweetspot-api-v1-Timestamp) | | Absolute expiration. Server rejects Authenticate after this moment. | ### RequestLoginCodeRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | email | [string](#string) | | | ### RequestLoginCodeResponse ### RevokeRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | session_token | [string](#string) | | | ### RevokeResponse ### VerifyLoginCodeRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | email | [string](#string) | | | | code | [string](#string) | | | ### VerifyLoginCodeResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | session_token | [string](#string) | | Opaque bearer token. Clients pass this back as `authorization: Bearer <session_token>` metadata on authenticated RPCs that accept organization sessions. | | maker_id | [MakerId](#sweetspot-api-v1-MakerId) | | Resolved maker identity associated with the user's organization. | | expires_at | [Timestamp](#sweetspot-api-v1-Timestamp) | | Absolute session expiration. |

Top

## sweetspot/api/v1/auth/service.proto ### AuthService Issues session tokens. Keypair-auth tokens from Authenticate last 6 hours; passwordless organization tokens from VerifyLoginCode last 7 days. All RPCs are unauthenticated. | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | Challenge | [ChallengeRequest](#sweetspot-api-v1-ChallengeRequest) | [ChallengeResponse](#sweetspot-api-v1-ChallengeResponse) | Get a nonce to sign. Idempotent per pubkey within the nonce TTL; calling twice returns distinct nonces but only the most recent is accepted by Authenticate. | | Authenticate | [AuthenticateRequest](#sweetspot-api-v1-AuthenticateRequest) | [AuthenticateResponse](#sweetspot-api-v1-AuthenticateResponse) | Exchange a signed nonce for a 6-hour keypair session token. UNAUTHENTICATED if the pubkey has no outstanding nonce, the nonce has expired, the signature is invalid, or the pubkey is not registered as a quoting authority. | | Revoke | [RevokeRequest](#sweetspot-api-v1-RevokeRequest) | [RevokeResponse](#sweetspot-api-v1-RevokeResponse) | Invalidate a token before its natural expiration. | | RequestLoginCode | [RequestLoginCodeRequest](#sweetspot-api-v1-RequestLoginCodeRequest) | [RequestLoginCodeResponse](#sweetspot-api-v1-RequestLoginCodeResponse) | Send a 6-digit passwordless login code to an organization user's email. Requests are limited to one per email per minute. | | VerifyLoginCode | [VerifyLoginCodeRequest](#sweetspot-api-v1-VerifyLoginCodeRequest) | [VerifyLoginCodeResponse](#sweetspot-api-v1-VerifyLoginCodeResponse) | Exchange a passwordless login code for a 7-day bearer session token. |

Top

## sweetspot/api/v1/market_data/events.proto ### FillEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | side | [Side](#sweetspot-api-v1-Side) | | | | price | [Decimal](#sweetspot-api-v1-Decimal) | | | | size | [Decimal](#sweetspot-api-v1-Decimal) | | | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### L1Event | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | metadata | [PairMetadata](#sweetspot-api-v1-PairMetadata) | | | | bid | [L2Level](#sweetspot-api-v1-L2Level) | | | | ask | [L2Level](#sweetspot-api-v1-L2Level) | | | ### L2SnapshotEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | metadata | [PairMetadata](#sweetspot-api-v1-PairMetadata) | | | | bids | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | | asks | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | ### L2UpdateEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | metadata | [PairMetadata](#sweetspot-api-v1-PairMetadata) | | | | bids | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | | asks | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | ### L3SnapshotEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | metadata | [PairMetadata](#sweetspot-api-v1-PairMetadata) | | | | bids | [L3Entry](#sweetspot-api-v1-L3Entry) | repeated | | | asks | [L3Entry](#sweetspot-api-v1-L3Entry) | repeated | | ### L3UpdateEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | metadata | [PairMetadata](#sweetspot-api-v1-PairMetadata) | | | | bids | [L3Entry](#sweetspot-api-v1-L3Entry) | repeated | | | asks | [L3Entry](#sweetspot-api-v1-L3Entry) | repeated | | ### MarketDataEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | l1 | [L1Event](#sweetspot-api-v1-L1Event) | | | | l2_snapshot | [L2SnapshotEvent](#sweetspot-api-v1-L2SnapshotEvent) | | | | l2_update | [L2UpdateEvent](#sweetspot-api-v1-L2UpdateEvent) | | | | l3_snapshot | [L3SnapshotEvent](#sweetspot-api-v1-L3SnapshotEvent) | | | | l3_update | [L3UpdateEvent](#sweetspot-api-v1-L3UpdateEvent) | | | | status | [StatusEvent](#sweetspot-api-v1-StatusEvent) | | | ### MarketSnapshot `slot` is per-market: different entries may carry different slots when only a subset of markets updated in the most recent envelope. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | slot | [uint64](#uint64) | | | | best_bid | [L2Level](#sweetspot-api-v1-L2Level) | | | | best_ask | [L2Level](#sweetspot-api-v1-L2Level) | | | | bid_depth_levels | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | | ask_depth_levels | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | | last_price | [Decimal](#sweetspot-api-v1-Decimal) | | | ### MarketsSnapshotEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | | markets | [MarketSnapshot](#sweetspot-api-v1-MarketSnapshot) | repeated | |

Top

## sweetspot/api/v1/market_data/messages.proto ### GetBookRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | level | [FeedLevel](#sweetspot-api-v1-FeedLevel) | | | ### GetBookResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | l2 | [L2SnapshotEvent](#sweetspot-api-v1-L2SnapshotEvent) | | | | l3 | [L3SnapshotEvent](#sweetspot-api-v1-L3SnapshotEvent) | | | | l1 | [L1Event](#sweetspot-api-v1-L1Event) | | | ### ListPairsRequest ### ListPairsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [ListedPair](#sweetspot-api-v1-ListedPair) | repeated | | ### ListedPair | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | base | [SpotMetadata](#sweetspot-api-v1-SpotMetadata) | | | | last_price | [Decimal](#sweetspot-api-v1-Decimal) | | Last landed fill price for this pair. Absent until the server observes a fill. | | quote | [SpotMetadata](#sweetspot-api-v1-SpotMetadata) | | | ### SubscribeFillsRequest Separate stream so book subscribers don't have to pay to deserialize fills and vice versa. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [Pair](#sweetspot-api-v1-Pair) | repeated | Per-pair filter. Empty = fills from every pair. | ### SubscribeMarketSnapshotsRequest ### SubscribeRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [PairSubscription](#sweetspot-api-v1-PairSubscription) | repeated | Pairs to subscribe to with their desired feed level. |

Top

## sweetspot/api/v1/market_data/service.proto ### MarketDataService Public market data. | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | Subscribe | [SubscribeRequest](#sweetspot-api-v1-SubscribeRequest) | [MarketDataEvent](#sweetspot-api-v1-MarketDataEvent) stream | Subscribe to a multiplexed stream of book events (L1/L2/L3) and StatusEvents. The pair set is fixed at stream start; to change subscriptions open a new stream. | | SubscribeFills | [SubscribeFillsRequest](#sweetspot-api-v1-SubscribeFillsRequest) | [FillEvent](#sweetspot-api-v1-FillEvent) stream | Subscribe to fills on a separate stream. Optional per-pair filter; empty = fills from every pair. | | GetBook | [GetBookRequest](#sweetspot-api-v1-GetBookRequest) | [GetBookResponse](#sweetspot-api-v1-GetBookResponse) | One-shot book snapshot. Convenient for cold-start reconciliation without opening a streaming RPC. | | ListPairs | [ListPairsRequest](#sweetspot-api-v1-ListPairsRequest) | [ListPairsResponse](#sweetspot-api-v1-ListPairsResponse) | Return the catalog of discovered pairs + their spot metadata. | | SubscribeMarketSnapshots | [SubscribeMarketSnapshotsRequest](#sweetspot-api-v1-SubscribeMarketSnapshotsRequest) | [MarketsSnapshotEvent](#sweetspot-api-v1-MarketsSnapshotEvent) stream | Stream a consolidated snapshot of every registered market |

Top

## sweetspot/api/v1/tx/events.proto ### BlockhashEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | blockhash | [Blockhash](#sweetspot-api-v1-Blockhash) | | | | recommended_cu_price | [uint64](#uint64) | | Server's current priority-fee recommendation (microlamports per CU) sampled from recent landed txs. Zero = no recommendation yet. | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### SlotEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### TxAckEvent Server has accepted the tx for submission (pre-confirmation). | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | signature | [Signature](#sweetspot-api-v1-Signature) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### TxConfirmedEvent Tx landed and did not error. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | signature | [Signature](#sweetspot-api-v1-Signature) | | | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### TxFailedEvent Tx landed but reverted, or timed out before landing. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | signature | [Signature](#sweetspot-api-v1-Signature) | | | | reason | [string](#string) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### TxStatusEvent Multiplexed tx-status stream envelope. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | ack | [TxAckEvent](#sweetspot-api-v1-TxAckEvent) | | | | confirmed | [TxConfirmedEvent](#sweetspot-api-v1-TxConfirmedEvent) | | | | failed | [TxFailedEvent](#sweetspot-api-v1-TxFailedEvent) | | |

Top

## sweetspot/api/v1/tx/messages.proto ### GetSponsoredPayersRequest Retrieve the set of fee-payer pubkeys the server will pay tx fees for. Clients may use any returned pubkey as the `feePayer` of a submitted tx; the server then covers the SOL cost on landing. ### GetSponsoredPayersResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | payers | [Pubkey](#sweetspot-api-v1-Pubkey) | repeated | | ### SubmitTxRequest Submit a fully signed Solana transaction. The server forwards the tx to its upstream RPC/jito paths; lifecycle events (ack/confirmed/failed) arrive on `SubscribeTxStatus`. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | transaction | [bytes](#bytes) | | Serialized signed transaction bytes — legacy or v0. The server does not modify the tx; fee payer, recent blockhash, compute budget, and priority fee are all the client's choices. | ### SubmitTxResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | signature | [Signature](#sweetspot-api-v1-Signature) | | Parsed signature from the submitted tx. Use this to correlate with subsequent TxAckEvent / TxConfirmedEvent / TxFailedEvent received on `SubscribeTxStatus`. | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | Server wall-clock timestamp the submission was accepted. | ### SubscribeBlockhashRequest ### SubscribeSlotsRequest ### SubscribeTxStatusRequest Server streams status transitions for any tx submitted under the authenticated maker's session. No client-side filter — the maker_id on the session token scopes the stream.

Top

## sweetspot/api/v1/tx/service.proto ### TxService Transaction support: chain-tip feeds (blockhash, slot), transaction submission, and per-tx lifecycle status. Every RPC on this service requires a keypair-auth session token via `authorization: Bearer <token>` metadata. The token's maker_id is used for rate limiting on the public streams and to scope which signatures show up on the status stream. | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | SubmitTx | [SubmitTxRequest](#sweetspot-api-v1-SubmitTxRequest) | [SubmitTxResponse](#sweetspot-api-v1-SubmitTxResponse) | Forward a signed transaction to upstream submission paths. Returns the parsed signature immediately; landing/confirmation are reported asynchronously on `SubscribeTxStatus`. | | SubscribeBlockhash | [SubscribeBlockhashRequest](#sweetspot-api-v1-SubscribeBlockhashRequest) | [BlockhashEvent](#sweetspot-api-v1-BlockhashEvent) stream | | | SubscribeSlots | [SubscribeSlotsRequest](#sweetspot-api-v1-SubscribeSlotsRequest) | [SlotEvent](#sweetspot-api-v1-SlotEvent) stream | | | SubscribeTxStatus | [SubscribeTxStatusRequest](#sweetspot-api-v1-SubscribeTxStatusRequest) | [TxStatusEvent](#sweetspot-api-v1-TxStatusEvent) stream | |

Top

## sweetspot/api/v1/historical/messages.proto ### Candle | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | Candle open time. | | open | [Decimal](#sweetspot-api-v1-Decimal) | | | | high | [Decimal](#sweetspot-api-v1-Decimal) | | | | low | [Decimal](#sweetspot-api-v1-Decimal) | | | | close | [Decimal](#sweetspot-api-v1-Decimal) | | | | volume | [Decimal](#sweetspot-api-v1-Decimal) | | Base-asset volume over the interval. | ### GetCandlesRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | interval | [CandleInterval](#sweetspot-api-v1-CandleInterval) | | | | start | [Timestamp](#sweetspot-api-v1-Timestamp) | | Inclusive lower bound. | | end | [Timestamp](#sweetspot-api-v1-Timestamp) | | Exclusive upper bound. Unset / zero-micros = now. | ### GetCandlesResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | candles | [Candle](#sweetspot-api-v1-Candle) | repeated | Ordered oldest-first. Hard cap of 10_000 candles per response. | ### GetTradesRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | start | [Timestamp](#sweetspot-api-v1-Timestamp) | | Inclusive lower bound. Unset / zero-micros = unbounded (server uses its retention window). | | end | [Timestamp](#sweetspot-api-v1-Timestamp) | | Exclusive upper bound. Unset / zero-micros = now. | | limit | [uint32](#uint32) | | Max rows to return. Zero = server default (50). Hard cap 1000. | ### GetTradesResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | trades | [Trade](#sweetspot-api-v1-Trade) | repeated | Ordered newest-first. | ### Trade Historical trade row. Same shape as the live `FillEvent`. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | side | [Side](#sweetspot-api-v1-Side) | | | | price | [Decimal](#sweetspot-api-v1-Decimal) | | | | size | [Decimal](#sweetspot-api-v1-Decimal) | | | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### CandleInterval | Name | Number | Description | | ---- | ------ | ----------- | | CANDLE_INTERVAL_UNSPECIFIED | 0 | | | CANDLE_INTERVAL_1M | 1 | | | CANDLE_INTERVAL_5M | 2 | | | CANDLE_INTERVAL_15M | 3 | | | CANDLE_INTERVAL_30M | 4 | | | CANDLE_INTERVAL_1H | 5 | | | CANDLE_INTERVAL_4H | 6 | | | CANDLE_INTERVAL_1D | 7 | |

Top

## sweetspot/api/v1/historical/service.proto ### HistoricalService Historical (ClickHouse-backed) queries. Every RPC requires a session token via `authorization: Bearer <token>` metadata. Hard constraints enforced by the server: - Maximum time window: 30 days. - `GetTrades.limit` capped at 1000. - `GetCandles` returns at most 10_000 rows per call. If the backing ClickHouse is not configured, every RPC returns FAILED_PRECONDITION. | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | GetTrades | [GetTradesRequest](#sweetspot-api-v1-GetTradesRequest) | [GetTradesResponse](#sweetspot-api-v1-GetTradesResponse) | | | GetCandles | [GetCandlesRequest](#sweetspot-api-v1-GetCandlesRequest) | [GetCandlesResponse](#sweetspot-api-v1-GetCandlesResponse) | |

Top

## sweetspot/api/v1/maker/events.proto ### MakerBalanceEvent Streaming form of a per-maker balance observation. Emitted whenever the underlying on-chain account updates. Always scoped to the authenticated maker — servers filter by the token's maker_id before fan-out, so a client never sees another maker's balances. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | maker_id | [MakerId](#sweetspot-api-v1-MakerId) | | | | spot_id | [SpotId](#sweetspot-api-v1-SpotId) | | | | balance | [Decimal](#sweetspot-api-v1-Decimal) | | Token units (e.g. "1.5" SOL); server scales by SpotMetadata.decimals. | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | | notional | [Decimal](#sweetspot-api-v1-Decimal) | | | | soft_max_balance | [Decimal](#sweetspot-api-v1-Decimal) | | | ### MakerFillEvent Fill where the authenticated maker provided the liquidity. The `side` here is the maker's side (BUY = maker bought, SELL = maker sold) — the inverse of `FillEvent.side` on the public market-data stream, which carries the taker's side. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | maker_id | [MakerId](#sweetspot-api-v1-MakerId) | | | | side | [Side](#sweetspot-api-v1-Side) | | | | price | [Decimal](#sweetspot-api-v1-Decimal) | | | | size | [Decimal](#sweetspot-api-v1-Decimal) | | | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### MakerMarketSnapshot Per-pair view that augments `MarketSnapshot` with the authenticated maker's own best bid / best ask + depth level. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | slot | [uint64](#uint64) | | | | best_bid | [L2Level](#sweetspot-api-v1-L2Level) | | | | best_ask | [L2Level](#sweetspot-api-v1-L2Level) | | | | bid_depth_levels | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | | ask_depth_levels | [L2Level](#sweetspot-api-v1-L2Level) | repeated | | | last_price | [Decimal](#sweetspot-api-v1-Decimal) | | | | maker_bid | [MakerQuote](#sweetspot-api-v1-MakerQuote) | | Absent when the maker has no order on this side of this pair. | | maker_ask | [MakerQuote](#sweetspot-api-v1-MakerQuote) | | | ### MakerMarketsSnapshotEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | | markets | [MakerMarketSnapshot](#sweetspot-api-v1-MakerMarketSnapshot) | repeated | | ### MakerQuote The authenticated maker's resting quote on one side of a pair plus its 0-indexed L2 depth level (0 = top of book). Aggregates across the maker's individual orders at their best price for that side. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | price | [Decimal](#sweetspot-api-v1-Decimal) | | | | size | [Decimal](#sweetspot-api-v1-Decimal) | | | | level | [uint32](#uint32) | | |

Top

## sweetspot/api/v1/maker/messages.proto ### GetBalanceRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | spot_ids | [SpotId](#sweetspot-api-v1-SpotId) | repeated | Optional per-spot filter; empty = all spots. | ### GetBalanceResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | balances | [MakerBalanceEvent](#sweetspot-api-v1-MakerBalanceEvent) | repeated | | ### GetMakerStatsRequest ### GetMakerStatsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | maker_id | [MakerId](#sweetspot-api-v1-MakerId) | | Echoed from the bearer token so the client can sanity-check the scope. | | events_total | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Sum of every on-chain event attributable to this maker, regardless of kind. Mirrors `GetSummaryResponse.update_rate` but scoped to one maker. | | events_by_kind | [MakerEventKindRate](#sweetspot-api-v1-MakerEventKindRate) | repeated | Per-kind breakdown of `events_total`. Kinds the maker has never produced are omitted. Ordered by 5-minute rate descending. | | fills | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Match events where this maker provided liquidity (convenience copy from `events_by_kind`). | | transactions_sent | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Transactions accepted from this maker by the server's TxService (counts every TX whose initial outcome was `Submitted`). | | transactions_landed | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Transactions confirmed on-chain (TxOutcome::Confirmed). | | transactions_failed | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Transactions that failed — rejected pre-submit, expired before landing, or landed with an error. | | oracle_updates | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Venue-wide oracle-fair update rate. Same value every maker observes — included so MMs can compare their request rate against oracle volatility. | ### GetMakerVolumeBreakdownRequest Per-maker volume breakdown. The authenticated maker_id is taken from the bearer token; clients only optionally narrow the pair set. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [Pair](#sweetspot-api-v1-Pair) | repeated | Optional pair filter. Empty = every pair the maker has fills in across the largest window (30d). | ### GetMakerVolumeBreakdownResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [PairVolume](#sweetspot-api-v1-PairVolume) | repeated | Ordered by 24h volume descending. | ### GetMakerVolumeSeriesRequest Per-maker volume series. The authenticated maker_id is taken from the bearer token. Window selects both horizon and bucket width identically to `StatsService.GetVolumeSeries`. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | Optional pair filter. Absent = aggregate across every pair the maker has fills in. | | window | [StatsWindow](#sweetspot-api-v1-StatsWindow) | | | ### GetMakerVolumeSeriesResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | buckets | [VolumeBucket](#sweetspot-api-v1-VolumeBucket) | repeated | Ordered oldest-first. | | bucket_seconds | [uint32](#uint32) | | | ### MakerEventKindRate Rolling rate for a single SweetSpotEvent kind attributable to a maker. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | kind | [string](#string) | | SDK variant name — "Match", "CrossCancel", "DepositWithdraw", etc. | | rate | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | | ### SubscribeBalanceRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | spot_ids | [SpotId](#sweetspot-api-v1-SpotId) | repeated | Optional per-spot filter. Empty = every spot the authenticated maker has a balance in (base + quote of every pair they quote on). | ### SubscribeMakerFillsRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [Pair](#sweetspot-api-v1-Pair) | repeated | Optional per-pair filter. Empty = every pair the authenticated maker gets a fill on. | ### SubscribeMakerMarketSnapshotsRequest

Top

## sweetspot/api/v1/maker/service.proto ### MakerService Per-maker queries + streaming. Every RPC requires organization-oriented auth: either a session token via `authorization: Bearer <token>` metadata whose maker_id exists in the organizations table (see AuthService), or an `x-api-key` organization API key. The resolved maker_id scopes all responses. | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | SubscribeBalance | [SubscribeBalanceRequest](#sweetspot-api-v1-SubscribeBalanceRequest) | [MakerBalanceEvent](#sweetspot-api-v1-MakerBalanceEvent) stream | Stream balance updates as the on-chain accounts change. | | GetBalance | [GetBalanceRequest](#sweetspot-api-v1-GetBalanceRequest) | [GetBalanceResponse](#sweetspot-api-v1-GetBalanceResponse) | One-shot snapshot of current balances — useful on cold start before opening the stream. | | SubscribeMarketSnapshots | [SubscribeMakerMarketSnapshotsRequest](#sweetspot-api-v1-SubscribeMakerMarketSnapshotsRequest) | [MakerMarketsSnapshotEvent](#sweetspot-api-v1-MakerMarketsSnapshotEvent) stream | Stream a consolidated cross-market snapshot enriched with the authenticated maker's own best bid / ask + depth level per pair. | | GetVolumeBreakdown | [GetMakerVolumeBreakdownRequest](#sweetspot-api-v1-GetMakerVolumeBreakdownRequest) | [GetMakerVolumeBreakdownResponse](#sweetspot-api-v1-GetMakerVolumeBreakdownResponse) | 1h / 24h / 30d volume + fill count broken down by pair, scoped to the authenticated maker. | | GetVolumeSeries | [GetMakerVolumeSeriesRequest](#sweetspot-api-v1-GetMakerVolumeSeriesRequest) | [GetMakerVolumeSeriesResponse](#sweetspot-api-v1-GetMakerVolumeSeriesResponse) | Time-bucketed volume series scoped to the authenticated maker. | | SubscribeFills | [SubscribeMakerFillsRequest](#sweetspot-api-v1-SubscribeMakerFillsRequest) | [MakerFillEvent](#sweetspot-api-v1-MakerFillEvent) stream | Stream every fill where the authenticated maker provided the liquidity. The taker (counterparty) is the transaction signer; this stream does not include fills where the maker was the taker. | | GetStats | [GetMakerStatsRequest](#sweetspot-api-v1-GetMakerStatsRequest) | [GetMakerStatsResponse](#sweetspot-api-v1-GetMakerStatsResponse) | Rolling 1s / 1m / 5m activity rates scoped to the authenticated maker: per-event-kind breakdown, TX submission outcomes, plus the venue-wide oracle update rate for cross-reference. |

Top

## sweetspot/api/v1/stats/messages.proto ### AssetTvl | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | asset | [SpotMetadata](#sweetspot-api-v1-SpotMetadata) | | | | total_balance | [Decimal](#sweetspot-api-v1-Decimal) | | Sum of every maker's balance for this spot, in token units (e.g. "1500.5" SOL — not atoms). | | maker_count | [uint32](#uint32) | | How many makers currently hold a non-zero balance of this asset. | ### CrossVenueWindowStats Cross-venue equivalent of `WindowStats`. `volume_base` is dropped — summing base across pairs with different base assets mixes denominations. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | window | [StatsWindow](#sweetspot-api-v1-StatsWindow) | | | | volume_quote | [Decimal](#sweetspot-api-v1-Decimal) | | | | fill_count | [uint64](#uint64) | | | ### GetAssetTvlsRequest ### GetAssetTvlsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | assets | [AssetTvl](#sweetspot-api-v1-AssetTvl) | repeated | | ### GetSummaryRequest ### GetSummaryResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | active_pairs | [uint32](#uint32) | | How many pairs the indexer has discovered and is streaming books for. | | active_makers | [uint32](#uint32) | | How many makers are registered on-chain. | | one_hour | [CrossVenueWindowStats](#sweetspot-api-v1-CrossVenueWindowStats) | | Cross-venue aggregates over each window — quote volume + fill count totalled across every pair. | | twenty_four_hour | [CrossVenueWindowStats](#sweetspot-api-v1-CrossVenueWindowStats) | | | | thirty_day | [CrossVenueWindowStats](#sweetspot-api-v1-CrossVenueWindowStats) | | | | volume_quote_all_time | [Decimal](#sweetspot-api-v1-Decimal) | | All-time totals, computed from the events table since the archiver started recording. | | fill_count_all_time | [uint64](#uint64) | | | | unique_traders_24h | [uint32](#uint32) | | Distinct taker accounts seen in the last 24h. A rough proxy for active users. | | largest_trade_24h | [LargestTrade](#sweetspot-api-v1-LargestTrade) | | Largest single USDC-quoted trade in the last 24h by notional value (`size * price`). Absent if no USDC-quoted fills have happened. | | update_rate | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Cross-venue order-update rate — every observed change to a pair's virtual book (maker cancel/replace plus oracle moves). | | oracle_update_rate | [UpdateRate](#sweetspot-api-v1-UpdateRate) | | Cross-venue oracle-update rate — counts observed oracle fair changes across every spot market. | ### GetVolumeBreakdownRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [Pair](#sweetspot-api-v1-Pair) | repeated | Optional pair filter. Empty = every pair the venue has fills for in the largest window (30d). | ### GetVolumeBreakdownResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pairs | [PairVolume](#sweetspot-api-v1-PairVolume) | repeated | Ordered by 24h volume descending. | ### GetVolumeSeriesRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | Optional pair filter. Absent = cross-venue aggregate. | | window | [StatsWindow](#sweetspot-api-v1-StatsWindow) | | Window selects both the time horizon and the bucket size: 1H → 60 × 1m buckets 24H → 288 × 5m buckets 30D → 720 × 1h buckets | ### GetVolumeSeriesResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | buckets | [VolumeBucket](#sweetspot-api-v1-VolumeBucket) | repeated | Ordered oldest-first. | | bucket_seconds | [uint32](#uint32) | | Width of each bucket in seconds — clients can label the X axis without having to repeat the server's window→interval mapping. | ### LargestTrade | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | size | [Decimal](#sweetspot-api-v1-Decimal) | | | | price | [Decimal](#sweetspot-api-v1-Decimal) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | | notional | [Decimal](#sweetspot-api-v1-Decimal) | | `size * price`, in USDC for summary responses. | ### PairVolume 1h / 24h / 30d bundled together. All three are always populated even if a pair has no fills in the smaller windows (zero is meaningful). | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | one_hour | [WindowStats](#sweetspot-api-v1-WindowStats) | | | | twenty_four_hour | [WindowStats](#sweetspot-api-v1-WindowStats) | | | | thirty_day | [WindowStats](#sweetspot-api-v1-WindowStats) | | | | last_price | [Decimal](#sweetspot-api-v1-Decimal) | | Most recent landed fill price. Absent if no fills observed yet. | ### UpdateRate Rolling event rate. Used for both order updates and oracle updates on `GetSummaryResponse`. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | per_second_1s | [double](#double) | | Updates within the trailing 1 second. | | per_second_1m | [double](#double) | | Average updates / second over the trailing 1 minute. | | per_second_5m | [double](#double) | | Average updates / second over the trailing 5 minutes. | ### VolumeBucket | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | Bucket start (inclusive). | | volume_base | [Decimal](#sweetspot-api-v1-Decimal) | | | | volume_quote | [Decimal](#sweetspot-api-v1-Decimal) | | | | fill_count | [uint64](#uint64) | | | ### WindowStats Per-pair volume + fill count over a single rolling window. `volume_quote = sum(size * price)` in the pair's quote asset. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | window | [StatsWindow](#sweetspot-api-v1-StatsWindow) | | | | volume_base | [Decimal](#sweetspot-api-v1-Decimal) | | | | volume_quote | [Decimal](#sweetspot-api-v1-Decimal) | | | | fill_count | [uint64](#uint64) | | | ### StatsWindow | Name | Number | Description | | ---- | ------ | ----------- | | STATS_WINDOW_UNSPECIFIED | 0 | | | STATS_WINDOW_1H | 1 | | | STATS_WINDOW_24H | 2 | | | STATS_WINDOW_30D | 3 | |

Top

## sweetspot/api/v1/stats/service.proto ### StatsService Public (unauthenticated) aggregate stats about the exchange. | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | GetSummary | [GetSummaryRequest](#sweetspot-api-v1-GetSummaryRequest) | [GetSummaryResponse](#sweetspot-api-v1-GetSummaryResponse) | | | GetVolumeBreakdown | [GetVolumeBreakdownRequest](#sweetspot-api-v1-GetVolumeBreakdownRequest) | [GetVolumeBreakdownResponse](#sweetspot-api-v1-GetVolumeBreakdownResponse) | 1h / 24h / 30d volume + fill count broken down by pair. | | GetVolumeSeries | [GetVolumeSeriesRequest](#sweetspot-api-v1-GetVolumeSeriesRequest) | [GetVolumeSeriesResponse](#sweetspot-api-v1-GetVolumeSeriesResponse) | | | GetAssetTvls | [GetAssetTvlsRequest](#sweetspot-api-v1-GetAssetTvlsRequest) | [GetAssetTvlsResponse](#sweetspot-api-v1-GetAssetTvlsResponse) | |

Top

## sweetspot/api/v1/common.proto ### BalanceUpdate Low-level per-spot balance sample, used by the indexer and by historical queries. `MakerBalanceEvent` is the streaming form. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | spot_id | [SpotId](#sweetspot-api-v1-SpotId) | | | | maker_id | [MakerId](#sweetspot-api-v1-MakerId) | | | | balance | [Decimal](#sweetspot-api-v1-Decimal) | | Token units (e.g. "1.5" SOL), not raw atoms. | | slot | [uint64](#uint64) | | | ### Blockhash | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | hash | [bytes](#bytes) | | | ### Decimal Human-readable decimal string (e.g. "155.14"). The client must parse with a decimal library that matches rust_decimal semantics. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | value | [string](#string) | | | ### L2Level | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | price | [Decimal](#sweetspot-api-v1-Decimal) | | | | size | [Decimal](#sweetspot-api-v1-Decimal) | | | ### L3Entry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | maker_id | [MakerId](#sweetspot-api-v1-MakerId) | | | | price | [Decimal](#sweetspot-api-v1-Decimal) | | | | size | [Decimal](#sweetspot-api-v1-Decimal) | | | ### MakerId | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | id | [uint64](#uint64) | | | ### MakerMarketState | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | spot_id | [SpotId](#sweetspot-api-v1-SpotId) | | | | order_sequence | [uint64](#uint64) | | | ### Pair | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | base | [SpotId](#sweetspot-api-v1-SpotId) | | | | quote | [SpotId](#sweetspot-api-v1-SpotId) | | | ### PairMetadata | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | slot | [uint64](#uint64) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | | update_id | [uint64](#uint64) | | | ### PairSubscription | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pair | [Pair](#sweetspot-api-v1-Pair) | | | | level | [FeedLevel](#sweetspot-api-v1-FeedLevel) | | | | snapshot_only | [bool](#bool) | | When true, the server emits a full snapshot on every book update and never sends deltas. When false (default), one snapshot on subscribe followed by incremental deltas. | ### Pubkey | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | | ### Signature | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | signature | [bytes](#bytes) | | | ### SpotAssets | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | assets | [SpotMetadata](#sweetspot-api-v1-SpotMetadata) | repeated | | ### SpotId | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | id | [uint64](#uint64) | | | ### SpotMetadata | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | id | [SpotId](#sweetspot-api-v1-SpotId) | | | | name | [string](#string) | | | | mint | [Pubkey](#sweetspot-api-v1-Pubkey) | | | | program_id | [Pubkey](#sweetspot-api-v1-Pubkey) | | | | decimals | [uint32](#uint32) | | | | atoms_per_lots | [uint32](#uint32) | | | ### StatusEvent | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | state | [HealthState](#sweetspot-api-v1-HealthState) | | | | pair | [Pair](#sweetspot-api-v1-Pair) | | Absent = global scope, present = per-pair. | | reason | [string](#string) | | | | ts | [Timestamp](#sweetspot-api-v1-Timestamp) | | | ### Timestamp Unix microseconds since epoch. The single canonical timestamp unit across the entire API — events, responses, range queries. Field value `0` means "unspecified" (used on optional bounds in range queries and on absent event timestamps). | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | micros | [uint64](#uint64) | | | ### FeedLevel | Name | Number | Description | | ---- | ------ | ----------- | | FEED_LEVEL_UNSPECIFIED | 0 | | | FEED_LEVEL_L1 | 1 | | | FEED_LEVEL_L2 | 2 | | | FEED_LEVEL_L3 | 3 | | ### HealthState | Name | Number | Description | | ---- | ------ | ----------- | | HEALTH_STATE_UNSPECIFIED | 0 | | | HEALTH_STATE_HEALTHY | 1 | | | HEALTH_STATE_DEGRADED | 2 | | | HEALTH_STATE_HALTED | 3 | | ### Side | Name | Number | Description | | ---- | ------ | ----------- | | SIDE_UNSPECIFIED | 0 | | | SIDE_BUY | 1 | | | SIDE_SELL | 2 | | ## Scalar Value Types | .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | | ----------- | ----- | --- | ---- | ------ | -- | -- | --- | ---- | | double | | double | double | float | float64 | double | float | Float | | float | | float | float | float | float32 | float | float | Float | | int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | | uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | | uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | | sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | | fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | | fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | | sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | | bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | | string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | | bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | --- # api/schema # Proto schema The canonical schema for `sweetspot.api.v1`. Synced from the `sweetspot-protos` submodule; regenerate SDKs and docs after bumping that submodule. - Download bundled api.proto — every service in `sweetspot.api.v1`. - Download openapi.yaml — OpenAPI 3.1 schema for the gRPC-Web service URLs. <<< @/public/api.proto --- # api/transports # Protocols & transports | Protocol | Content-Type | Best for | | --- | --- | --- | | gRPC (binary) | `application/grpc` | Native Rust / Python clients — **hot path**. | | gRPC-Web (binary) | `application/grpc-web+proto` | Browser SDK default. | The FLINT SDKs use: | SDK | Default transport | | --- | --- | | Rust | gRPC (binary) over HTTPS | | Python | gRPC (binary) over HTTPS | | TypeScript-web | gRPC-Web binary over HTTPS | ## gRPC examples Use the SDKs for production clients. For quick checks from a shell, `grpcurl` can call the same RPCs over gRPC using the bundled proto: ```sh # Public — no auth needed. grpcurl \ -proto api.proto \ -d '{}' \ api.superis.exchange:443 \ sweetspot.api.v1.MarketDataService/ListPairs # Authenticated — pass the session token as gRPC metadata. grpcurl \ -proto api.proto \ -H "authorization: Bearer ${SUPERIS_TOKEN}" \ -d '{}' \ auth.api.superis.exchange:443 \ sweetspot.api.v1.MakerService/GetBalance # Historical candles — public, no auth. Timestamp.micros is unix microseconds. grpcurl \ -proto api.proto \ -d '{"pair":{"base":{"id":1},"quote":{"id":0}},"interval":"CANDLE_INTERVAL_5M","start":{"micros":"1735689600000000"},"end":{"micros":"1735776000000000"}}' \ api.superis.exchange:443 \ sweetspot.api.v1.HistoricalService/GetCandles ``` `grpcurl` accepts JSON input for convenience, then sends the request as gRPC. The on-the-wire schema remains protobuf. ## Streaming Long-lived server streams (`MarketDataService.Subscribe`, `MarketDataService.SubscribeFills`, `MarketDataService.SubscribeMarketSnapshots`, `MakerService.SubscribeBalance`, `MakerService.SubscribeFills`, `MakerService.SubscribeMarketSnapshots`, `TxService.SubscribeBlockhash` / `SubscribeSlots` / `SubscribeTxStatus`) are the recommended path for anything event-driven. The SDK helpers handle auto-reconnect and fan-out internally — see the per-language SDK pages. --- # sdks/rust # Rust SDK Async, tonic-based Rust client for every service in `sweetspot.api.v1`. Quoting layer (`OrderList`, `OracleOffset`, `LinearDistribution` — all built using `QuoteBuilder`) behind the default `quoting` feature; turn it off for read-only consumers to drop the on-chain Solana SDK from your build. ## Install ```toml [dependencies] sweetspot-api-client = { git = "https://github.com/superis-labs/sweetspot-maker-client", branch = "master" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` When the SDK is published to crates.io: ```toml sweetspot-api-client = "0.1" ``` Read-only (no quoting): ```toml sweetspot-api-client = { version = "0.1", default-features = false, features = ["tls"] } ``` ## Quickstart The ergonomic entry point is `Client` — one handle that owns two tonic [`Channel`]s (one per server listener — see [Onboarding](/overview/onboarding#1-get-an-endpoint)), an optional `AuthFlow`, a `ConfigCache`, a `ServerState`, and resilient stream supervisors. Use the builder for everyday integrations; reach past it (`client.public_channel()`, `client.auth_channel()`, `client.auth()`, `client.config_cache()`) only when you need finer control. ```rust use std::sync::Arc; use solana_sdk::signature::Keypair; use solana_sdk::signer::keypair::read_keypair_file; use sweetspot_api_client::api::client::Client; use sweetspot_api_client::api::proto::ListPairsRequest; #[tokio::main] async fn main() -> anyhow::Result<()> { // Public consumer — no wallet, no authed endpoint needed. let client = Client::builder() .public_endpoint("https://api.superis.exchange:443") .build() .await?; let mut market = client.market_data(); let pairs = market.list_pairs(ListPairsRequest {}).await?.into_inner(); for pair in &pairs.pairs { println!("{:?} / {:?}", pair.base, pair.quote); } // Maker — supply a wallet plus the authed endpoint, then authenticate // once. Authenticated service clients pick up the cached bearer // automatically and route to the authed listener. let kp = read_keypair_file("/path/to/id.json").map_err(|e| anyhow::anyhow!("{e}"))?; let client = Client::builder() .public_endpoint("https://api.superis.exchange:443") .auth_endpoint("https://auth.api.superis.exchange:443") .wallet(Arc::new(kp)) .build() .await?; let session = client.authenticate().await?; println!("authenticated as maker_id={}", session.maker_id); let mut _maker = client.maker()?; let mut _tx = client.tx()?; Ok(()) } ``` The bare building blocks remain available for callers who want to wire things up by hand. `AuthFlow`, `MakerServiceClient`, and `TxServiceClient` speak to the **authed** listener: ```rust use std::sync::Arc; use sweetspot_api_client::api::auth::AuthFlow; use sweetspot_api_client::api::proto::maker_service_client::MakerServiceClient; let auth_channel = tonic::transport::Channel::from_static("https://auth.api.superis.exchange:443") .connect() .await?; let auth = AuthFlow::new(auth_channel.clone(), Arc::new(my_keypair)); let session = auth.token().await?; let mut maker = MakerServiceClient::with_interceptor( auth_channel, auth.interceptor(), ); # let _ = (session, maker); ``` `MarketDataService`, `StatsService`, and `HistoricalService` live on the **public** listener — pass `client.public_channel()` to their clients directly, no auth needed. ## Where to go from here | You want | Page | | --- | --- | | Boot a maker bot | [Quoting](/sdks/quoting) | | Stream books and fills | [Market data](/sdks/market-data) | | Pull historical trades / candles | [Historical queries](/sdks/historical) | | Sign-in flow detail | [Auth flow](/sdks/auth) | ## Decimal handling Every wire numeric — book/fill prices and sizes, historical `Trade`/`Candle` OHLCV, and `MakerBalanceEvent.balance` — is wrapped as `Decimal { value: String }`. Parse with [`rust_decimal`](https://crates.io/crates/rust_decimal): ```rust use rust_decimal::Decimal; use std::str::FromStr; let price = Decimal::from_str(&trade.price.as_ref().unwrap().value)?; let size = Decimal::from_str(&trade.size.as_ref().unwrap().value)?; let notional = price * size; ``` ## Errors The SDK surfaces gRPC `tonic::Status` directly. Branch on `status.code()` for retry decisions — see [Errors](/overview/errors). ## Cargo features | Feature | Default | What it adds | | --- | --- | --- | | `tls` | yes | HTTPS via rustls + native roots. | | `quoting` | yes | Quoting layer (`QuoteBuilder` for `OrderList` / `OracleOffset` / `LinearDistribution`, `QuotingCore`, `Receipt`, sequence trackers). Pulls in the on-chain Solana SDK. | ## Source - Crate: [`rust/`](https://github.com/superis-labs/sweetspot-maker-client/tree/master/rust) - Examples: [`examples/rust/`](https://github.com/superis-labs/sweetspot-maker-client/tree/master/examples/rust) --- # sdks/python # Python SDK Use the Python SDK when your maker bot signs and submits Flint quote updates directly from Python. It gives you one `Client` for market data, maker authentication, balances, chain tips, transaction submission, and receipts, plus a `QuoteBuilder` for the three on-chain quoting modes: | Strategy | Use it when | | --- | --- | | Linear distribution | You quote a ladder around a fair price. | | Oracle offset | You quote offsets from an external oracle/fair. | | Order list | You want CEX-style explicit bid and ask orders. | Your bot still owns fair-value inputs, risk checks, inventory targets, persistence, and reconciliation. The SDK handles the Flint-specific work needed to publish those decisions: decimal-safe unit conversion, quote instruction construction, transaction packing/signing, and submit-status tracking. ## Runnable example bot The companion [`sweetspot-maker-example`](https://github.com/superis-labs/sweetspot-maker-example) repo is the best starting point for a real Python maker process. It is a small bot built on `flint-sdk`, not a second SDK. Read it when you want to see how the pieces on this page fit together in one loop. The example makes these decisions explicitly in `maker.toml` and the `sweetspot_maker` package: - **Markets**: quote named catalog markets such as `SOL/USDC`. - **Mode**: dry-run by default, or submit signed transactions when `submit_quotes = true`. - **Endpoints and trust**: configure public/auth URLs, keypair path, durable state path, pinned program id, and Solana RPC URL. - **Price source**: read USD prices from Pyth Hermes, with static fallback prices for local synthetic mints. - **Strategy**: use `SimpleOracleOffset` to center a two-sided ladder on the mid price. - **Risk shape**: configure spread, number of levels, level spacing, size per level, staleness slots, and base `soft_max_balance`. - **Inventory skew**: shift the center price from target inventory toward a maximum skew, with an optional minimum spread floor. - **Lifecycle**: authenticate, resolve markets, verify program ids, watch slots, keep chain tips warm, persist sequence counters before submit, and wait for receipts. ## What you need Before writing the bot, get: | Item | Why | | --- | --- | | Public endpoint | Market catalog, books, fills, and public stats. | | Authenticated endpoint | Maker balances, chain tips, submit, and receipts. | | Maker authority keypair | The registered Solana keypair allowed to quote for your maker id. | | Trusted program id | A config-pinned Flint program id, not just the id returned by an API call. | | Funded maker account | Deposits, quote-side caps, and cross-spread settings for the markets you quote. | | Durable bot state | Persist sequence counters and client order ids across restarts. | The examples below assume: - base spot `1` quotes against quote spot `0`; - your keypair is the registered maker authority; - `fair_slot` is the Solana slot associated with your current fair/oracle input; - `FLINT_PROGRAM_ID` and `SOLANA_RPC_URL` come from trusted bot config; - `state` is your own durable state object or database row. If you have not created a `maker_id`, registered the hot keypair, deposited inventory, and set the relevant `soft_max_balance` caps, start with [Onboarding](/overview/onboarding). Quotes can be accepted by the API and still never fill if the maker has no sell-side deposit or no buy-side cap headroom. ## Install Install the SDK package: ```sh pip install flint-sdk # from a local checkout pip install -e './python' ``` Install `flint-sdk`; import it as `flint` in Python. The SDK uses [`solders`](https://pypi.org/project/solders/) for Solana keys, pubkeys, blockhashes, instructions, and transaction signing. ## Connect Read-only processes only need the public endpoint: ```python from flint import Client, Endpoints client = Client(Endpoints(public_url="https://api.superis.exchange")) ``` Maker bots need both endpoints and a keypair: ```python from solders.keypair import Keypair from flint import Client, Endpoints keypair = Keypair.from_json(open("/path/to/id.json").read()) client = Client( Endpoints( public_url="https://api.superis.exchange", auth_url="https://auth.api.superis.exchange", ), keypair, ) ``` Use `https://` for production. For local development, pass explicit `http://localhost:...` URLs. `Endpoints(..., insecure=True)` and `SUPERIS_INSECURE=1` force plaintext even for HTTPS-looking URLs, so never set them in production. Always close long-lived clients on shutdown: ```python await client.close() ``` ## Discover markets Call `list_markets()` once on startup and whenever you need to refresh the catalog. It returns Python-friendly `MarketInfo` objects keyed by `(base_spot_id, quote_spot_id)`. ```python markets = await client.list_markets() market = markets[(1, 0)] print(market.name) print(market.program_id) print(market.last_price) ``` Each `MarketInfo` includes the mint, program id, and unit metadata the quoting builder needs to convert human prices and sizes into on-chain integers. ## Authenticate Authenticate once after startup. The client caches and refreshes the session when later maker or transaction calls need bearer metadata. ```python session = await client.authenticate() print("maker id", session.maker_id) balances = await client.maker_balance() stats = await client.maker_stats() ``` Revoke when you need best-effort early token invalidation, such as logout or key rotation: ```python await client.revoke() ``` If revoke fails during shutdown, the server-side token may remain live until expiry. Do not treat revoke as a substitute for short token lifetimes and key rotation controls. ## Initialize bot state Flint enforces strictly increasing per-maker/per-market sequence counters. The Python builder consumes counters when it builds instructions, before anything is submitted. For a brand-new durable state row, seed counters from wall-clock nanoseconds: ```python import time seed = time.time_ns() state.next_oracle_sequence = seed state.next_order_sequence = seed state.next_client_order_id = seed state.save() ``` On every tick, reserve the builder's next counters before submitting. Gaps are safe; reuse is not. Run one quoting process per `maker_id`, and stop quoting if your durable state is missing or uncertain until you reconcile. The example bot's `Sequences` type is intentionally boring: it seeds counters from `time.time_ns()`, stores them in `maker-state.json`, and writes them before any submit attempt. ## Submit one quote tick A quote tick usually does this: 1. Build quote instructions from your current fair, spread, and inventory. 2. Get a recent chain tip. 3. Pack and sign one or more transactions. 4. Submit each transaction and wait for a receipt. 5. Persist the next sequence counters. ```python import os from solders.pubkey import Pubkey from flint.onchain import CrossSpreadUpdate, QuoteBuilder, QuoteMarket, tx session = await client.authenticate() markets = await client.list_markets() market = markets[(1, 0)] quote_market = QuoteMarket.from_market_info(market) trusted_program_id = Pubkey.from_string(os.environ["FLINT_PROGRAM_ID"]) await client.verify_program_id( market.program_id, os.environ["SOLANA_RPC_URL"], expected_program_id=trusted_program_id, ) builder = QuoteBuilder( trusted_program_id, keypair.pubkey(), session.maker_id, oracle_sequence=state.next_oracle_sequence, order_sequence=state.next_order_sequence, client_order_id=state.next_client_order_id, ) builder.linear_fair( quote_market, last_oracle_slot=fair_slot, buy_start_price="155.10", buy_end_price="155.00", sell_start_price="155.20", sell_end_price="155.30", ) builder.linear_params( quote_market, bid_size_per_level="0.10", ask_size_per_level="0.10", soft_max_balance="10", # 10 base tokens for this market, not quote notional. cross_spread_update=[ # Nonzero spreads are raw oracle integers; compute them with # human_to_oracle_for_quoting(...) before passing them here. CrossSpreadUpdate(spot_index=market.quote_spot_id, spread=0), ], ) tip = await client.chain_tip() instruction_groups = builder.pack( keypair.pubkey(), tip.blockhash, tip.recommended_cu_price, ) # Reserve consumed counters before any submit attempt. A crash may leave a gap; # reusing counters can make later updates silently drop. state.next_oracle_sequence = builder.next_oracle_sequence state.next_order_sequence = builder.next_order_sequence state.next_client_order_id = builder.next_client_order_id state.save() for instructions in instruction_groups: signed = tx.sign_transaction( instructions, keypair.pubkey(), [keypair], tip.blockhash, ) receipt = await client.submit_transaction_receipt(bytes(signed)) await receipt.confirmed_within(5.0) ``` `QuoteBuilder` is one-shot. Create a new builder for each tick, add that tick's fair and parameter/order updates, pack, submit, then persist the builder's next counters. ## Keep chain tips warm For one-off scripts, `chain_tip()` fetches and caches a blockhash. For a maker loop, start the reconnecting stream once. `chain_tip()` returns the latest tip observed by this client; if freshness matters after startup or reconnect, wait for the next stream item before building: ```python from flint import ConnectionState stream = client.start_chain_tip() await stream.wait_for_state(ConnectionState.CONNECTED, timeout=5.0) tip = await stream.next_item() while True: if stream.state is not ConnectionState.CONNECTED: await stream.wait_for_state(ConnectionState.CONNECTED, timeout=5.0) tip = await stream.next_item() tip = await client.chain_tip() # build, sign, and submit this tick with tip.blockhash ``` If the status or chain-tip stream disconnects, reconcile before replacing orders that might already have landed. The example bot also watches `TxService.SubscribeSlots` so its oracle-fair updates carry a recent `last_oracle_slot`. The Python `Client` does not yet wrap slots directly, so the example uses the generated tx stub for that one stream. ## Strategy helpers Use one params helper per market, matching the strategy you want live on-chain: | Strategy | Fair helper | Params/helper updates | | --- | --- | --- | | Linear distribution | `linear_fair(...)` | `linear_params(...)` | | Oracle offset | `oracle_fair(...)` | `oracle_offset_params(...)` with already-lowered `SpotOffsetOrder` values | | Order list | None | `order_list_params(...)` with `order_list_place(...)` updates | The example bot uses Oracle offset. Its strategy produces human-unit `LevelSpec`s, then its quoting module lowers each level to `SpotOffsetOrder` with `human_size_to_lots(...)`, `human_to_oracle_for_quoting(...)`, staleness, and a monotonic client order id. Order-list helpers take human price and size strings and lower them for you: ```python bid = builder.order_list_place( quote_market, price="155.12", size="0.25", post_only=True, ) ask = builder.order_list_place_and_pop( quote_market, price="155.18", size="0.25", post_only=True, ) builder.order_list_params( quote_market, bids=[bid], asks=[ask], soft_max_balance="10", cross_spread_update=[ CrossSpreadUpdate(spot_index=market.quote_spot_id, spread=0), ], ) ``` ## Receipts `submit_transaction_receipt()` subscribes to transaction status before it submits, so the bot does not miss an early ack event. ```python receipt = await client.submit_transaction_receipt(raw_transaction) await receipt.accepted() await receipt.confirmed_within(5.0) status = receipt.status() print(status.accepted, status.confirmed, status.total) ``` Treat these receipt errors as operational signals: | Error | Meaning | | --- | --- | | `OnChainFailure` | The chain reported a failed transaction status. | | `CommitTimeout` | Confirmation did not arrive before your timeout; the transaction may still later confirm or fail. | | `CommitDisconnected` | The status stream disconnected before the outcome was known. | | `CommitLagged` | The subscriber lagged and may have dropped status events. | Treat timeout, disconnect, lag, and submit errors after signing as unknown outcomes. Reconcile before submitting replacement orders. If `submit_transaction_receipt()` raises `CommitDisconnected` before submit, the helper did not submit the transaction. If you abandon a receipt before confirmation, cancel any waiters and then close it. `close()` unsubscribes from future status events; it is not a way to make existing `accepted()` or `confirmed()` calls return. ```python receipt.close() ``` Finish, cancel, or close outstanding receipts before `await client.close()`. Closing the client tears down the status stream, so active receipt waiters can observe `CommitDisconnected`. ## Balances, stats, and history Use authenticated maker calls for inventory and per-maker activity: ```python balances = await client.maker_balance() maker_stats = await client.maker_stats() volume = await client.maker_volume_breakdown() for balance in balances.balances: print(balance.spot_id.id, balance.balance.value) ``` These helpers return protobuf response messages. Historical trades and candles also use request messages from the API schema: ```python import time from flint.gen.sweetspot.api.v1 import common_pb2 from flint.gen.sweetspot.api.v1.historical import messages_pb2 as hist now = int(time.time() * 1_000_000) pair = common_pb2.Pair( base=common_pb2.SpotId(id=1), quote=common_pb2.SpotId(id=0), ) trades = await client.historical_trades( hist.GetTradesRequest( pair=pair, start=common_pb2.Timestamp(micros=now - 60 * 60 * 1_000_000), end=common_pb2.Timestamp(micros=now), limit=100, ) ) ``` See [Historical queries](/sdks/historical) for pagination and live-plus-historical stitching. ## Decimals and units All wire money values are decimal strings. Parse response fields with `decimal.Decimal`, not `float`: ```python from decimal import Decimal notional = Decimal(trade.price.value) * Decimal(trade.size.value) ``` All human money inputs accepted by the quoting helpers should be decimal strings, `Decimal`, or integers. Floats are rejected because binary floating point can silently change prices and sizes. ::: warning Price conversion Use `human_to_oracle_for_quoting` only when you are manually building instruction payloads or nonzero `CrossSpreadUpdate.spread` values. Normal `QuoteBuilder` price and size parameters already call the correct conversion helper. See [Prices, sizes, units](/overview/units). ::: ## Advanced RPC access `Client` exposes public generated service clients for RPCs that do not yet have a handwritten helper: ```python from flint.gen.sweetspot.api.v1 import common_pb2 from flint.gen.sweetspot.api.v1.market_data import messages_pb2 as md pair = common_pb2.Pair( base=common_pb2.SpotId(id=1), quote=common_pb2.SpotId(id=0), ) response = await client.market_data.GetBook( md.GetBookRequest(pair=pair, level=common_pb2.FEED_LEVEL_L2) ) ``` Prefer the high-level helpers when they exist; they keep endpoint selection and auth metadata consistent. For authenticated RPCs without a helper, wire the generated stub with `AuthFlow` directly so every call gets fresh bearer metadata. ## Production checklist - Use a public-only `Client` for read-only processes. - Pin the program id from trusted config and call `verify_program_id()` before building or submitting maker transactions. - Start `start_chain_tip()` once in maker loops. - Reserve/persist `next_oracle_sequence`, `next_order_sequence`, and `next_client_order_id` before submitting packed transactions. - Use decimal strings or `Decimal` at money boundaries. - Treat `CommitTimeout`, `CommitDisconnected`, and `CommitLagged` as unknown outcomes and reconcile before submitting replacement orders. - Close the client on shutdown. ## Where to go next | You want | Page | | --- | --- | | Run or fork a complete Python maker bot | [`sweetspot-maker-example`](https://github.com/superis-labs/sweetspot-maker-example) | | Create and fund a maker | [Onboarding](/overview/onboarding) | | Understand quote models and sequence safety | [Quoting](/sdks/quoting) | | Stream books and fills | [Market data](/sdks/market-data) | | Query historical trades and candles | [Historical queries](/sdks/historical) | | Inspect a runnable script | [Python examples](https://github.com/superis-labs/sweetspot-maker-client/tree/master/examples/python) | ## Source - Package: [`python/`](https://github.com/superis-labs/sweetspot-maker-client/tree/master/python) - Example bot: [`sweetspot-maker-example`](https://github.com/superis-labs/sweetspot-maker-example) - Examples: [`examples/python/`](https://github.com/superis-labs/sweetspot-maker-client/tree/master/examples/python) --- # sdks/typescript # TypeScript (web) SDK ESM-only TypeScript SDK that speaks gRPC-Web directly to the upstream `sweetspot-server` (tonic + `tonic_web`). Works in any modern browser and in Node 18.14+. No build-time codegen required by consumers — the package ships pre-generated TypeScript. ::: info Surfaces | Surface | How | | --- | --- | | Public (`MarketDataService`, `StatsService`, `HistoricalService`) | `createClient`, `createServiceClient` | | Authenticated (`MakerService`) | `createMakerClient` + `AuthFlow` (passwordless email) or `apiKeyInterceptor(...)` (org API key) | | `TxService` + wallet signing | Not exposed — drive a wallet adapter or use the [Rust](/sdks/rust) / [Python](/sdks/python) SDK | ::: ## Install ```sh npm install @superis-labs/sweetspot-client @bufbuild/protobuf ``` The only runtime dependency is `@bufbuild/protobuf` for serialization. The gRPC-Web transport ships in-tree. ## Quickstart ```ts import { GrpcWebTransport, MarketDataService, StatsService, createServiceClient, } 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.superis.exchange", }); // Public RPC — no auth needed. const market = createServiceClient(MarketDataService, transport); const stats = createServiceClient(StatsService, transport); const pairs = await market.listPairs({}); const summary = await stats.getSummary({}); console.log(pairs.pairs); console.log(summary); // Server-streaming RPC — `for await` works directly. const pair = { base: { id: 1n }, quote: { id: 0n } }; for await (const event of market.subscribe({ pairs: [{ pair, level: FeedLevel.L2, snapshotOnly: false }], })) { console.log(event); } ``` ## Where to go from here | You want | Page | | --- | --- | | Stream books and fills | [Market data](/sdks/market-data) | | Pull historical trades / candles | [Historical queries](/sdks/historical) | | Authenticate against `MakerService` | [Auth flow](/sdks/auth) | ## MakerService (authenticated) `MakerService` requires organization-scoped credentials. Build a maker client via `createMakerClient`, passing either an `AuthFlow` (passwordless email login) or `apiKeyInterceptor(key)` (organization API key). ```ts import { AuthFlow, createMakerClient, } from "@superis-labs/sweetspot-client"; // Persist the bearer across reloads. Any localStorage-shaped store works // — pass a sessionStorage, an IndexedDB-backed shim, or your own. const auth = new AuthFlow({ baseUrl: "https://auth.api.superis.exchange", storage: globalThis.localStorage, onExpired: () => router.push("/login"), }); // Login form: await auth.requestLoginCode("trader@example.com"); // …prompt user, then… await auth.verifyLoginCode("trader@example.com", code); // Bind credentials to a maker client. const maker = createMakerClient({ baseUrl: "https://auth.api.superis.exchange", auth, }); const { balances } = await maker.getBalance({ spotIds: [] }); ``` See [Auth flow → TypeScript](/sdks/auth#typescript-passwordless-email-login) for the full walk-through, including API key auth. ## HistoricalService (public) `HistoricalService` is exposed as a generated descriptor. Build a service client with the public transport: ```ts import { GrpcWebTransport, HistoricalService, createServiceClient, } from "@superis-labs/sweetspot-client"; const historical = createServiceClient( HistoricalService, new GrpcWebTransport({ baseUrl: "https://api.superis.exchange", }), ); const { trades } = await historical.getTrades({ pair, limit: 100 }); ``` ## Decimal handling Live book and fill `price` / `size` fields come back as `Decimal { value: string }`. Parse with [`bignumber.js`](https://www.npmjs.com/package/bignumber.js) or [`decimal.js`](https://www.npmjs.com/package/decimal.js): ```ts import BigNumber from "bignumber.js"; const price = new BigNumber(trade.price.value); const size = new BigNumber(trade.size.value); const notional = price.multipliedBy(size); ``` Historical `Trade` and `Candle` responses use the same `Decimal { value: string }` wrapper for price, size, and OHLCV fields. ## Errors ```ts import { GrpcError, GrpcCode } from "@superis-labs/sweetspot-client"; try { await market.listPairs({}); } catch (err) { if (err instanceof GrpcError) { switch (err.code) { case GrpcCode.ResourceExhausted: await sleep(backoff); break; case GrpcCode.Unavailable: await sleep(backoff); break; } } } ``` See [Errors](/overview/errors) for the full code map. ## Wire format The transport speaks `application/grpc-web+proto` over HTTP/1.1, terminating at the server's `tonic_web::GrpcWebLayer`. Server streaming is supported via `AsyncIterable` on every streaming RPC. There is no gRPC-streaming-bidi or client-streaming over gRPC-Web — those RPCs throw at call time. ## Browser compatibility Modern browsers (Chrome 90+, Firefox 90+, Safari 14+). Requires `fetch` and `ReadableStream` — both baseline in every release-channel browser since 2022. ## Source - Package: [`typescript-web/`](https://github.com/superis-labs/sweetspot-maker-client/tree/master/typescript-web) - Examples: [`examples/typescript-web/`](https://github.com/superis-labs/sweetspot-maker-client/tree/master/examples/typescript-web) --- # sdks/auth # Auth flow `AuthService` issues two kinds of bearer **session tokens**, and `MakerService` additionally accepts a static organization API key. Pick the path that matches the caller: | Path | Header | Minted by | Lifetime | Accepted by | | --- | --- | --- | --- | --- | | Keypair session | `authorization: Bearer ` | `Authenticate` (signed nonce) | 6 h | `TxService`, `MakerService` | | Organization session | `authorization: Bearer ` | `VerifyLoginCode` (passwordless email) | 7 d | `MakerService` | | Organization API key | `x-api-key: ` | static, hashed server-side | n/a | `MakerService` only | `MarketDataService`, `StatsService`, `HistoricalService`, and every `AuthService` RPC are unauthenticated. `MakerService` additionally requires that the resolved `maker_id` be registered in the server's organizations table. The **keypair** path is multi-step and signed: 1. **`Challenge(pubkey)`** — server returns a single-use random `nonce` bound to your pubkey, valid for a short TTL. 2. **Sign** `AUTH_DOMAIN_PREFIX || nonce` (the raw bytes — no extra encoding) with the keypair owning the pubkey. The constant is `b"SWEETSPOT-AUTH-V1:"`. 3. **`Authenticate(pubkey, signature)`** — server verifies the signature, maps the pubkey to a `maker_id` via its registry, and returns a `session_token` plus an `expires_at`. 4. **Use** the token as `authorization: Bearer ` on subsequent RPCs. 5. **Re-auth** before `expires_at`. The SDK helpers cache the token and refresh `skew` ahead of expiry automatically. The **passwordless email** path is two RPCs: 1. **`RequestLoginCode(email)`** — server emails a 6-digit code, rate-limited to one request per email per minute. 2. **`VerifyLoginCode(email, code)`** — server returns a 7-day `session_token` + `maker_id` + `expires_at`. The Rust and Python SDKs each ship an `AuthFlow` helper that drives the keypair handshake — the same shape in both (`token` / `refresh` / `revoke`, refreshing `skew` ahead of expiry). The TypeScript SDK ships an `AuthFlow` for the **email** path and an `apiKeyInterceptor` for the API key path; the keypair signed-nonce flow is not exposed in TS (use a wallet adapter directly, or the Rust/Python SDK). ## Wallet signer The signer is intentionally narrow: produce a 64-byte ed25519 signature over an arbitrary byte slice, and return the corresponding 32-byte public key. This lets you back the same `AuthFlow` with a keypair file, hardware wallet, remote signer, or browser wallet adapter. ```rust // The Rust SDK takes any `solana_sdk::signer::Signer` (`Keypair`, // hardware wallet, presigner, remote signer) directly — typed as // `Wallet = Arc`. use std::sync::Arc; use solana_sdk::signer::keypair::{read_keypair_file, Keypair}; use sweetspot_api_client::api::auth::Wallet; // Local keypair file. let kp: Keypair = read_keypair_file("/path/to/id.json") .map_err(|e| anyhow::anyhow!("{e}"))?; let wallet: Wallet = Arc::new(kp); // Hardware wallets / remote signers: implement `solana_sdk::signer::Signer` // (`pubkey()`, `try_sign_message(...)`) and wrap with `Arc::new(...)`. ``` ## Refresh strategy `spawn_refresh_loop()` returns a `tokio::JoinHandle` and defaults to a 30 s skew before expiry. Override with `with_skew`. ## Revoke If you need to invalidate a token before its natural expiration (e.g. on logout), call `revoke()`: ```rust auth.revoke().await?; ``` The cached session is dropped; subsequent calls re-authenticate from scratch. ## Errors `AuthService.Authenticate` returns `UNAUTHENTICATED` when: - The pubkey has no outstanding nonce. - The nonce has expired (TTL exceeded). - The signature is invalid for the nonce. - The pubkey is not registered as a quoting authority. The SDK surfaces these as `AuthError::Service`, with the gRPC status preserved so callers can branch on the code. ## Python: keypair signed-nonce The Python `AuthFlow` drives the same handshake. `Client.authenticate()` runs it once; `token()` returns the cached session and re-authenticates `skew` seconds (default 30) before `expires_at`; `revoke()` invalidates the token server-side and drops the cache. Omit the keypair and `auth_url` when constructing a Python `Client` for public market-data, stats, or historical calls. Pass both for maker or tx RPCs. ```python import asyncio from solders.keypair import Keypair from flint import Client, Endpoints async def main() -> None: client = Client( Endpoints( public_url="https://api.superis.exchange", auth_url="https://auth.api.superis.exchange", ), Keypair.from_json(open("/path/to/id.json").read()), ) session = await client.authenticate() # Challenge -> sign -> Authenticate print("authenticated as maker_id", session.maker_id) await client.token() # cached; re-auths near expiry await client.revoke() # invalidate + drop the cache await client.close() asyncio.run(main()) ``` To wire it up by hand, construct `AuthFlow` over an `AuthServiceStub` and stamp `await auth.auth_metadata()` (`[("authorization", "Bearer ")]`) on each authed RPC. Pass `skew_seconds=` to tune the refresh window. ## TypeScript: passwordless email login Browser apps authenticate against `MakerService` with the passwordless email flow. `AuthFlow` drives the two RPCs, caches the resulting session, and produces an interceptor that stamps `authorization: Bearer ` on every outbound call. ```ts import { AuthFlow, createMakerClient, } from "@superis-labs/sweetspot-client"; const auth = new AuthFlow({ baseUrl: "https://api.superis.exchange", storage: globalThis.localStorage, // optional — survives reload onExpired: (session) => router.push("/login"), }); await auth.requestLoginCode("trader@example.com"); // …prompt the user… const session = await auth.verifyLoginCode("trader@example.com", code); console.log("logged in as maker", session.makerId); const maker = createMakerClient({ baseUrl: "https://auth.api.superis.exchange", auth, }); const { balances } = await maker.getBalance({ spotIds: [] }); ``` ### Session persistence `AuthFlow` accepts any `localStorage`-shaped store (`getItem` / `setItem` / `removeItem`). Pass `globalThis.localStorage` in a browser, `globalThis.sessionStorage` for tab-scoped persistence, or any custom adapter (IndexedDB-backed, cookie-backed, in-memory) that implements the same three methods. ```ts const auth = new AuthFlow({ baseUrl, storage: globalThis.localStorage, storageKey: "sweetspot.session", // default }); ``` On construction `AuthFlow` rehydrates a session from storage if one is present. On `verifyLoginCode()` it overwrites; on `clear()` and on detected expiry it removes the entry. ### Expiry callback `AuthFlow` does not auto-refresh — organization sessions are minted by a user-driven email flow, not a signer. Subscribe to `onExpired` and re-prompt: ```ts const auth = new AuthFlow({ baseUrl, onExpired: () => { // Storage already cleared. Send the user back to the login form. router.push("/login"); }, expirySkewMs: 60_000, // treat as expired this long before expiresAt; default 30s }); ``` The callback fires once per session, inside the interceptor, just before it throws `GrpcError(Unauthenticated)` — wrapping `for await` loops will unwind on the next emission. ### Organization API key For server-side Node consumers (or any context where embedding a long-lived secret in client JS is acceptable), pass an API key directly via `apiKeyInterceptor`: ```ts import { apiKeyInterceptor, createMakerClient, } from "@superis-labs/sweetspot-client"; const maker = createMakerClient({ baseUrl: "https://auth.api.superis.exchange", auth: apiKeyInterceptor(process.env.SUPERIS_API_KEY!), }); ``` API keys are hashed server-side (SHA-256) against `organizations.api_key_hashes`. They are accepted by `MakerService`, but not by `TxService`, and never expire. ::: warning Browser apps Don't ship an API key in browser JavaScript. Anyone viewing the page can read it and impersonate the organization. Use `AuthFlow` instead; API keys belong on a trusted server. ::: --- # sdks/market-data # 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 `StatusEvent`s 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: ::: code-group ```rust [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 [TypeScript] 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 `StatusEvent`s 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. --- # sdks/quoting # Quoting You're running a market-maker. You publish bids and asks; takers hit them; you earn the spread. On Flint the *shape* in which you publish those quotes is configurable — pick whichever **quoting model** matches how your pricing engine already works. This page is the practical guide: - the three quoting models, when each one fits, and how to call them; - the anatomy of a quoting tick (queue → commit → confirm); - the maker-account setup you have to do once before any quotes fill; - the few on-chain invariants the SDK doesn't hide from you. ::: tip You don't have to learn Solana to quote The SDK takes human prices and sizes (`155.14`, `10.0` SOL), batches your updates into the right number of on-chain messages, signs them, and submits fully signed transactions through the gateway. You don't compose transactions or touch RPC nodes, but your quoting key is the fee payer and must be able to pay Solana transaction fees. The few Solana-specific concepts that do leak through are called out inline — start [here](#what-actually-lands-on-chain) if you want the glossary first. ::: ::: warning TypeScript does not build quoting transactions The Rust SDK has the full `QuoteBuilder` + `QuotingCore` commit loop. The Python SDK exposes a maker-ready `QuoteBuilder`, instruction packing, chain-tip streaming, and transaction receipts; see [Python SDK](/sdks/python) for its API shape. The TypeScript SDK stops at gRPC-Web market data, stats, auth, maker RPCs, and generated descriptors — browser quoting clients should sign through a wallet adapter or use the Rust/Python SDK. ::: ## The three quoting models You pick **one model per market**. You can run different models on different markets, but you can't mix two models on the same market. | Model | Mental shortcut | What you publish per tick | Best for | | ---------------------- | -------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | **OrderList** | CEX-style. "Place this order, cancel that one." | A list of `submit` / `cancel` ops. | Porting an existing CEX market-maker with minimal logic changes. Strategies with sparse, intentional order placement. | | **OracleOffset** | "Here's my fair price; quote ±N around it." | A buy-side fair, a sell-side fair, and offset ladders. | Continuous quoting driven by an external oracle or your own mid. Cheapest *fair* refresh. | | **LinearDistribution** | "Quote evenly between A and B on each side." | Four anchor prices — best/worst bid, best/worst ask. | Passive liquidity bands. Smallest possible message. | The columns map directly to how often you'll send messages and how much you'll send per message — pick the row that matches the shape of the pricing decisions your bot is already making. ### Choosing between them A loose decision tree: 1. **Do you already think in "place / cancel" operations?** Use [OrderList](#orderlist-cex-style). You'll feel at home. 2. **Do you have a single fair price (oracle, internal mid) and just want a ladder around it?** Use [OracleOffset](#oracleoffset-fair--per-side-offsets). Per-tick you only re-send the fair; the ladder stays installed. 3. **Do you want the cheapest possible passive liquidity, sized uniformly across a band?** Use [LinearDistribution](#lineardistribution-server-side-interpolation). Four numbers per refresh. If you're not sure, OracleOffset is a good default — it's the model the bundled example bot uses. ## Anatomy of a quoting tick Every quoting model shares the same flow. ```rust use sweetspot_api_client::api::client::Client; use sweetspot_api_client::quoting::QuoteBuilder; // 1. Build the client + authenticate (covered in Onboarding). let client = Client::builder() .public_endpoint("https://api.superis.exchange:443") .auth_endpoint("https://auth.api.superis.exchange:443") .wallet(keypair.clone()) .build() .await?; client.authenticate().await?; // 2. Start the quoting "core" — it owns the catalog, sequence // counters, blockhash/slot streams, and the tx-status stream, // and auto-spawns the blockhash + slot streams it needs. let mut core = client.start_quoting_core(keypair.clone()).await?; // 3. Per tick: describe what you want, commit, await confirmation. let mut receipt = QuoteBuilder::new() .order_list("SOL", |b| b .submit(Side::Buy, 154.90, 10.0) .submit(Side::Sell, 155.30, 10.0)) .commit(&mut core) .await?; receipt.confirmed_within(std::time::Duration::from_secs(5)).await?; ``` A few things to notice: - **`"SOL"` is the base-token name**, not `"SOL/USDC"`. Flint has a single global quote (USDC), so naming the base is enough. - **`QuoteBuilder` is a one-shot builder.** Construct it, attach per-market updates, call `commit()`. Next tick — new builder. - **`commit()` returns a `Receipt`** you can await on (`accepted`, `confirmed`, `confirmed_within(timeout)`) or sample non-blockingly via `receipt.status()`. Dropping it is fine — the submission keeps going server-side. ## OrderList — CEX-style If you've integrated against a centralized exchange, this is the model that maps to your existing code. Each tick you describe a list of operations: - **submit** — place an order on a side at a price + size. - **cancel_by_id** — remove a specific order you placed earlier. - **cancel_all** — wipe all live orders (one side or both). ```rust use sweetspot_api_client::quoting::{QuoteBuilder, Side}; QuoteBuilder::new() .order_list("SOL", |b| b // Two-sided quote at the level you choose. .submit(Side::Buy, 154.90, 10.0) .submit(Side::Sell, 155.30, 10.0)) .commit(&mut core).await? .confirmed().await?; ``` ### Tracking your orders for cancels When you call `.submit(...)` the SDK auto-assigns a `client_order_id` under the hood. If you plan to cancel a specific order later, pin the id yourself: ```rust QuoteBuilder::new() .order_list("SOL", |b| b .submit_with_id(Side::Buy, 154.90, 10.0, /* id */ 1001) .submit_with_id(Side::Sell, 155.30, 10.0, /* id */ 1002)) .commit(&mut core).await?; // …later, when your mid moves: QuoteBuilder::new() .order_list("SOL", |b| b .cancel_by_id(Side::Buy, 1001) .cancel_by_id(Side::Sell, 1002) .submit_with_id(Side::Buy, 154.50, 10.0, 1003) .submit_with_id(Side::Sell, 155.50, 10.0, 1004)) .commit(&mut core).await?; ``` `client_order_id` is just a `u64`. The SDK seeds its auto-counter to a value derived from the current time, so even auto-allocated ids won't collide with what's already on the book — but if you supply your own, **keep them strictly increasing**. ### Wipe and re-quote For the common "every tick, replace the whole quote" pattern: ```rust QuoteBuilder::new() .order_list("SOL", |b| b .cancel_all() .submit(Side::Buy, fair - 0.05, 10.0) .submit(Side::Sell, fair + 0.05, 10.0)) .commit(&mut core).await?; ``` Cancels are processed before placements in the same batch, so the on-chain slots free up before the new orders need them. ### Post-only (subtler than on a CEX) For OrderList specifically, use `.submit_post_only(...)` instead of `.submit(...)`: ```rust b.submit_post_only(Side::Buy, 154.90, 10.0) ``` The semantics aren't the CEX semantics you may be used to — read [Post-only on Flint](#post-only-on-flint) below before you set the flag. ### When OrderList costs more Every submit and every cancel is a discrete op in the on-chain message. Replacing a 10-level ladder = 10 cancels + 10 submits = 20 ops. That's still fine — the SDK splits the work across multiple transactions as needed — but it's heavier than the other two models, which re-quote by changing a single number. If your bot ticks fast and re-quotes the entire book each tick, look at OracleOffset next. ## OracleOffset — fair + per-side offsets This is the model purpose-built for **continuous quoting against an external price source**. You publish two things: - a **fair price** per side (buy fair, sell fair — usually the same number, or split if you want asymmetry); - a **ladder of offsets** describing how far from fair each level sits, and how large it is. ```rust use sweetspot_api_client::quoting::{OffsetSpec, QuoteBuilder, RiskParams}; // Tick 1 — install the strategy with the ladder + fair. let ladder = vec![ OffsetSpec { price_offset: 0.05, size: 5.0, staleness: 5, client_order_id: None, post_only: false }, OffsetSpec { price_offset: 0.10, size: 10.0, staleness: 5, client_order_id: None, post_only: false }, OffsetSpec { price_offset: 0.20, size: 20.0, staleness: 5, client_order_id: None, post_only: false }, ]; QuoteBuilder::new() .oracle_offset("SOL", |b| b .with_fair((mid, mid)) .with_spread(ladder.clone(), ladder) .with_risk(RiskParams { per_slot_decay_factor: Some(0.99), ..Default::default() })) .commit(&mut core).await? .confirmed().await?; // Tick 2..N — only the fair changes. Cheap. QuoteBuilder::new() .oracle_offset("SOL", |b| b.with_fair((new_mid, new_mid))) .commit(&mut core).await?; ``` ### How offsets stack `OffsetSpec.price_offset` is **cumulative**, not absolute. Level 0 is measured from fair, level 1 is measured from level 0, and so on. So the ladder above quotes at `mid - 0.05`, `mid - 0.15`, `mid - 0.35` on the bid side (and symmetrically on the ask). ### Asymmetric fair Pass `(buy_fair, sell_fair)` as a tuple, or build an `OffsetFair` explicitly if you want to leave one side unchanged for this tick. Both sides write atomically on-chain; if you only set one side the SDK skips the fair update entirely rather than half-writing it. ### `staleness` Each level carries a `staleness` byte — how many Solana slots old the fair is allowed to be before the matcher refuses to fill that level. Lower = safer (you never fill on a stale price) at the cost of needing to re-publish more often. A few slots' worth is typical; `5` (≈ 2 seconds) is the value the bundled example uses. ### Risk dampening `RiskParams` lets you tell the matcher to back off your quotes after you've filled a certain notional volume. Useful guard rails for adversarial flow: | Field | What it does | | ----------------------------------- | ------------------------------------------------------------ | | `per_slot_decay_factor` | Per-slot multiplier on accumulated fill volume. Closer to 1 = slower decay (you carry "saturated" status longer). | | `risk_reduce_factor` | Multiplier applied to ladder sizes once you're saturated. | | `maker_volume_to_saturation` | Notional fill volume that flips you into the saturated regime. | | `maker_volume_backoff_at_saturation`| Price backoff applied when saturated. | Install risk once at startup; re-installing it every tick resets the accumulator on-chain. ### When OracleOffset is the right pick You already produce a mid from an oracle, a partner CEX, or your own internal pricing — and the only question is *what spread do I quote around it*. Re-quoting a fair is one number; the ladder stays installed across ticks. Cheap and natural. ## LinearDistribution — server-side interpolation The cheapest tick. You publish four prices, and the matcher fills buyers anywhere between `buy_start_price` (best, highest bid) and `buy_end_price` (worst, lowest bid), and symmetrically for sells. ```rust use sweetspot_api_client::quoting::{LinearFair, LinearParams, QuoteBuilder}; QuoteBuilder::new() .linear("SOL", |b| b .with_fair(LinearFair { buy_start_price: 155.00, buy_end_price: 154.50, sell_start_price: 155.30, sell_end_price: 155.80, }) .with_linear_params(LinearParams { spread_backoff_per_slot: Some(0.001), bid_size_per_level: Some(1.0), ask_size_per_level: Some(1.0), bid_post_only: Some(true), ask_post_only: Some(true), client_order_id: None, })) .commit(&mut core).await? .confirmed().await?; ``` Sizes are human token units (e.g. `1.0` SOL), lowered to on-chain lots at build time — same as `OffsetSpec.size` in oracle-offset mode. Use `None` to leave an existing side unchanged, or `Some(0.0)` to stop quoting that side. You don't write a ladder — the strategy still interpolates over the band, with `spread_backoff_per_slot` controlling how the spread widens over time if you go silent. This is the right model when you want passive participation across a range and don't have strong views on shape — think of it as "AMM-ish" liquidity provisioning with explicit boundaries. ## Post-only on Flint ::: danger Flint post-only ≠ CEX post-only On a CEX, `post_only` is checked **at order entry**: if your order would cross the book on arrival, it's rejected. Flint doesn't work that way. The on-chain placement path **does not look at any book** — yours or anyone else's. Your quote is recorded as a quoting intent and that's it. ::: Two consequences fall out of this: 1. **You can post a crossing quote and nothing will stop you.** If your bid is 158 and another maker's ask is 154, both quotes coexist on-chain. Until something resolves them, the book is simply crossed. 2. **Taker swaps don't resolve the cross either.** A taker is matched along its own side of the consolidated book; it never triggers a maker-to-maker fill. The only path that resolves crossed maker quotes is a separate on-chain instruction, **`CrossOrCancelMaker`**, which anyone can invoke against a spot. It walks the book and, for every crossing pair `(bid_maker, ask_maker)`: - If either side carries `post_only` → that order is **cancelled**. - If neither side is `post_only` → the two makers **fill each other at the midpoint** `(bid_price + ask_price) / 2`. Both makers get price improvement vs. what they posted. So `post_only` is really a flag about what you want to happen during a `CrossOrCancelMaker` sweep, not at submission time. ### How to think about it | You set `post_only` | What it means in practice | | ------------------- | ---------------------------------------------------------------------------------------------------------- | | **`false`** (default) | Fine with crossing another maker at midpoint. Best when your fair is robust and you'd rather take the trade than be cancelled. | | **`true`** | Refuse maker-vs-maker fills. Your order is cancelled if it ever ends up crossed against another maker. | There's no in-between. There's also no atomic "post if non-crossing, else reject" — the check happens later, not at submission. ### Cost note `CrossOrCancelMaker` walks the entire crossed region of the book and emits one event per cancel or fill. It's **CU-expensive** — nobody runs it on every block. In practice it's invoked by operators, by adversarial counterparties looking to extract the midpoint trade against a stale quote, or by the sweepers that maintain book health. If you publish a tight, accurate fair, your quotes generally won't cross other makers, so `post_only` rarely fires either way. If you expect to lag the market or publish defensive (wide) quotes against adversarial flow, `post_only` is the safer default. ### Where to set it The flag exists in all three models, but the field shape is different: ```rust // OrderList — per-order. b.submit_post_only(Side::Buy, 154.90, 10.0); // OracleOffset — per ladder level. OffsetSpec { price_offset: 0.05, size: 5.0, staleness: 5, client_order_id: None, post_only: true } // LinearDistribution — per side, in LinearParams. LinearParams { bid_post_only: Some(true), ask_post_only: Some(true), .. } ``` ## Initial setup, once per market Before your first quote fills, the maker's account on this market needs to be **enabled** and **balance-capped**. You do that on the first commit, alongside the strategy install: ```rust use sweetspot_api_client::quoting::{ParamsUpdate, QuoteBuilder}; QuoteBuilder::new() .order_list("SOL", |b| b // Force the params message to be emitted even though // no orders are queued in this tick yet. .init() .with_params(ParamsUpdate::new() .enable(true) .max_balance(100.0) // 100 SOL on the base side .with_cross(0_u16, Some(0.0)))) // see "Cross-spread" below .commit(&mut core).await? .confirmed().await?; ``` `ParamsUpdate` is shared across all three models — same shape, same semantics. Defaults if you skip it: `enable=true`, no balance cap, no cross-spread entries. ::: warning Liquidity is allocated at swap time The matcher computes your fill budget when a taker arrives, not when you publish. Each fill is capped at `min(available_to_sell` on the side you're selling, `available_to_buy` on the side you're buying`)`, and silently filtered only when one of those reads **literally zero**. You can publish quote sizes larger than your current inventory — the matcher fills up to the budget that exists at that moment, and a fill on one side feeds the budget on the other (a SOL ask that takes SOL out brings USDC in, which immediately backs any SOL bid you also posted). For a fresh sell-only SOL/USDC maker that means: deposit SOL **and** set a non-zero USDC `soft_max_balance` on the global market — no SOL cap needed. Full mechanics in [Budgets per leg](/overview/exchange#budgets-per-leg). ::: ### Cross-spread Flint supports two kinds of swaps: - **Global swap** — base ↔ USDC. The "normal" case. - **Cross swap** — base ↔ base (e.g. SOL ↔ ETH). The matcher bridges through both makers' books. Either way, the matcher reads `book.get_cross_params(counterparty)` on your micro-book; if the entry is missing, your maker is silently filtered. Counterparty is the other base token for cross swaps, or spot 0 (`SpotId::GLOBAL`) for global swaps. In practice the `cross_params` array is configured once at maker setup (`enable_all_zeroed` blanket-enables every counterparty with zero added spread). The bundled bot example skips `with_cross(...)` because the maker it talks to was set up that way. If you're cold-booting a maker or quoting against a cross-swap counterparty you've never touched, add the entry explicitly: ```rust .with_params(ParamsUpdate::new() .enable(true) .max_balance(100.0) .with_cross(0_u16, Some(0.0))) // global swap: counterparty = spot 0 ``` Cross-market quoting is covered in detail under [Cross-market spread](/overview/exchange#cross-market-spread). ## Receipts: knowing your quote landed `commit()` returns a `Receipt`. Three things to do with it: ```rust use sweetspot_api_client::quoting::{CommitError, ReceiptStatus}; use std::time::Duration; // (a) await the server's accept. receipt.accepted().await?; // (b) await full on-chain confirmation, with a deadline. match receipt.confirmed_within(Duration::from_secs(10)).await { Ok(()) => println!("confirmed"), Err(CommitError::OnChain { reason }) => eprintln!("reverted: {reason}"), Err(CommitError::Timeout) => eprintln!("timed out"), Err(CommitError::Disconnected) => eprintln!("status stream dropped"), Err(CommitError::Lagged { skipped }) => eprintln!("missed {skipped} events"), } // (c) non-blocking snapshot. match receipt.status() { ReceiptStatus::InFlight { accepted, confirmed, total } => println!("{accepted}/{total} acked, {confirmed}/{total} confirmed"), ReceiptStatus::Confirmed => println!("done"), ReceiptStatus::Failed(e) => eprintln!("failed: {e}"), } ``` Dropping the `Receipt` is fine. The submission continues server-side; you just stop awaiting. ## Re-quoting on your own fills A maker bot usually re-quotes when its inventory changes. The public fills feed doesn't tell you which fills are yours, so subscribe to the **maker-scoped** balance stream: ```rust use sweetspot_api_client::api::proto::SubscribeBalanceRequest; let bus = client.subscribe_maker_balance(SubscribeBalanceRequest { spot_ids: vec![] }); let mut rx = bus.subscribe().await; while let Ok(ev) = rx.recv().await { tracing::debug!(spot_id = ?ev.spot_id, balance = ?ev.balance, "inventory changed"); let mid = my_oracle.fetch().await; QuoteBuilder::new() .oracle_offset("SOL", |b| b.with_fair((mid, mid))) .commit(&mut core).await? .accepted().await?; } ``` `subscribe_maker_balance` is the resilient wrapper — auto-reconnects and replays the request. Symmetric wrappers exist for `subscribe_maker_fills`, `subscribe_maker_market_snapshots`, and the public streams. ## Sequence safety across process restarts Flint's matcher enforces strict monotonicity on a couple of per-market counters. The SDK handles this for you by seeding each counter from the current Unix time (in nanoseconds) at startup, so a process restart never collides with what's already on-chain. The one rule you have to obey yourself: ::: danger Don't run two quoting processes under the same maker Two processes sharing a `maker_id` will burn each other's sequence numbers and start writing updates that the matcher silently drops. One process per maker, always. ::: If you supply `client_order_id`s manually in OrderList mode, keep those strictly increasing too — the SDK doesn't double-check user-supplied ids. ## What actually lands on-chain You can stop reading here if you just want to quote. This section explains the few Solana-specific concepts that do leak through, so you know what you're seeing when something fails. - **Instruction** — one logical operation. Each `commit()` produces one *params* instruction per market (your enable/balance/strategy bundle) plus, for OracleOffset and LinearDistribution, a separate *fair* instruction. - **Transaction** — a bundle of instructions, signed and submitted as one unit. The SDK packs instructions greedily into the smallest number of transactions that fit Solana's 1232-byte cap. Fair and params instructions go in separate transactions (the on-chain order matters). - **Fee payer** — the wallet that pays the SOL fee. The quoting keypair is the fee payer, and `TxService.SubmitTx` forwards the fully signed transaction bytes unchanged. Keep enough SOL on that key for transaction fees. - **Blockhash / slot** — Solana's notion of "recent". The SDK streams both in the background (`spawn_chain_tip`) and refuses to build a transaction without them. - **Confirmed** — Solana cluster consensus on your transaction. `Receipt::confirmed()` resolves at that point. Most quoting flows await ack (cheap, sub-second) and let confirmation happen in the background. If `Receipt` reports `CommitError::OnChain { reason }`, the `reason` string comes straight from the on-chain program — common values are covered in [Errors](/overview/errors). ## Disabling the quoting layer The `quoting` Cargo feature is on by default. If you're only consuming market data, turn it off to drop the Solana dependencies from your build: ```toml sweetspot-api-client = { version = "0.1", default-features = false, features = ["tls"] } ``` ## Where to go next | You want | Page | | --------------------------------------- | --------------------------------------------- | | Read books, fills, the pair catalog | [Market data](/sdks/market-data) | | Cold-start a fresh maker account | [Onboarding](/overview/onboarding) | | Understand the matcher and unit system | [How Flint works](/overview/exchange) | | Backtest against historical data | [Historical queries](/sdks/historical) | | Run or fork a Python maker bot | [`sweetspot-maker-example`](https://github.com/superis-labs/sweetspot-maker-example) | | The end-to-end runnable example | [`examples/rust/src/quote.rs`](https://github.com/superis-labs/sweetspot-maker-client/tree/master/examples/rust/src/quote.rs) | --- # sdks/polling # Snapshots vs. streams Two ways to read state from the API. Pick the right one or you'll either burn rate-limit or lag the market. | You want | Use | | --- | --- | | One snapshot at app boot | Snapshot RPC (`ListPairs`, `GetBook`, `MakerService.GetBalance`) | | Continuous live updates | Streaming RPC (`MarketDataService.Subscribe` / `SubscribeFills` / `SubscribeMarketSnapshots`, `MakerService.SubscribeBalance` / `SubscribeFills` / `SubscribeMarketSnapshots`, `TxService.SubscribeBlockhash`/`SubscribeSlots`/`SubscribeTxStatus`) | | Historical backfill | `HistoricalService.GetTrades` / `GetCandles` | Streaming RPCs are long-lived. The SDK helpers (`Client::subscribe_market_data`, `Client::subscribe_market_data_fills`, `Client::subscribe_market_data_snapshots`, `Client::subscribe_maker_balance`, `Client::subscribe_maker_fills`, `Client::subscribe_maker_market_snapshots`, `Client::subscribe_tx_status`, `Client::spawn_chain_tip`, etc.) reconnect and fan out automatically; let HTTP/2 multiplex the rest of your traffic on the same channel. ## Cadence Snapshots are bucket-counted per IP. Streams are not. Recommended ceilings for snapshot RPCs: | Operation | Suggested cadence | | --- | --- | | `ListPairs` | Once at boot, then on schema changes | | `GetBook` (per pair) | Once at boot + once per reconnect | | `MakerService.GetBalance` | Once at boot + once per reconnect; otherwise use `SubscribeBalance` | | `HistoricalService.GetTrades` | As needed for backfill | If you find yourself polling above ~1 Hz, switch to a stream. ## The boot pattern For any UI or bot: 1. **`ListPairs`** — discover what's tradeable. 2. **`GetBook`** for each pair you care about — populate local book. 3. **`Subscribe`** to those pairs — apply deltas to the local book. 4. **On reconnect**, call `GetBook` again before re-applying stream events — you may have missed deltas in flight. The same pattern applies to `MakerService` (snapshot via `GetBalance`, deltas via `SubscribeBalance`; matching `SubscribeFills` and `SubscribeMarketSnapshots` streams are scoped to the authenticated maker) and to `TxService.SubscribeTxStatus` (no snapshot — just open the stream when you start submitting). ## Backoff on rate-limit `RESOURCE_EXHAUSTED` means the per-IP bucket emptied. Honour it; do not retry tighter. Suggested policy: exponential backoff starting at 500 ms, capped at 30 s — same schedule the SDK's `backoffDuration` helper returns. ## Don't poll the same data over two surfaces Subscribing to `MarketDataService.Subscribe` for a pair AND polling `GetBook` for the same pair just doubles your bandwidth. Subscribe for the live deltas and only call `GetBook` on a fresh boot or after a stream drop. --- # sdks/historical # Historical queries `HistoricalService` returns archived trades and candles for any pair within the deployment's retention window. It is public and runs on the public listener. Pulls from the deployment's historical archive when configured. ## What's available | RPC | Returns | Cap | | --- | --- | --- | | `GetTrades` | Trades within `[start, end)` for a pair. | 1,000 rows per call | | `GetCandles` | OHLCV candles at one of seven intervals. | 10,000 rows per call | If the deployment doesn't have the historical archive enabled, every RPC returns `FAILED_PRECONDITION`. Read it once at boot to feature-gate the UI. ## Range semantics - `start` and `end` are `Timestamp { micros }` values, i.e. Unix microseconds. - `start` is inclusive. - `end` is exclusive. Unset or `micros = 0` means "now". - `GetTrades` returns rows newest-first. - `GetCandles` returns rows oldest-first. - Maximum window is 30 days per call. Larger spans need to be paginated. - Historical `price`, `size`, and OHLCV values are `Decimal` strings — the same wire format as the live book and fill streams. Parse with a fixed-precision type (`rust_decimal`, `bignumber.js`). ## Backfill recipe To paginate a long span (e.g. a year of 1-minute candles), chunk by the row cap: ::: code-group ```rust [Rust] use sweetspot_api_client::api::proto::{CandleInterval, GetCandlesRequest, Pair, SpotId, Timestamp}; let mut historical = client.historical(); let interval_us = 5 * 60 * 1_000_000u64; let max_rows = 10_000u64; let window_us = max_rows * interval_us; let mut start = start_us; let mut all = Vec::new(); while start < end_us { let end = (start + window_us).min(end_us); let res = historical .get_candles(GetCandlesRequest { pair: Some(Pair { base: Some(SpotId { id: 1 }), quote: Some(SpotId { id: 0 }) }), interval: CandleInterval::CandleInterval5m.into(), start: Some(Timestamp { micros: start }), end: Some(Timestamp { micros: end }), }) .await? .into_inner(); all.extend(res.candles); start = end; } ``` ```ts [TypeScript] import { GrpcWebTransport, HistoricalService, createServiceClient, } from "@superis-labs/sweetspot-client"; import { CandleInterval, type Candle, } from "@superis-labs/sweetspot-client/gen/sweetspot/api/v1/historical/messages_pb.js"; const historical = createServiceClient( HistoricalService, new GrpcWebTransport({ baseUrl: "https://api.superis.exchange", }), ); const intervalUs = 5n * 60n * 1_000_000n; const maxRows = 10_000n; const windowUs = maxRows * intervalUs; let start = startUs; const all: Candle[] = []; while (start < endUs) { const end = start + windowUs > endUs ? endUs : start + windowUs; const { candles } = await historical.getCandles({ pair: { base: { id: 1n }, quote: { id: 0n } }, interval: CandleInterval.CANDLE_INTERVAL_5M, start: { micros: start }, end: { micros: end }, }); all.push(...candles); start = end; } ``` ```python [Python] from flint import Client, Endpoints from flint.gen.sweetspot.api.v1 import common_pb2 from flint.gen.sweetspot.api.v1.historical import messages_pb2 as hist client = Client( Endpoints( public_url="https://api.superis.exchange", ) ) request = hist.GetCandlesRequest( pair=common_pb2.Pair( base=common_pb2.SpotId(id=1), quote=common_pb2.SpotId(id=0), ), interval=hist.CANDLE_INTERVAL_5M, start=common_pb2.Timestamp(micros=start_us), end=common_pb2.Timestamp(micros=end_us), ) response = await client.historical_candles(request) ``` ::: ## Stitching with live For a chart that shows the last 24h: 1. `GetCandles` with `start.micros = now_us - 86_400_000_000` and `end.micros = now_us` to pull the historical body. 2. Subscribe to `MarketDataService.Subscribe` for the pair to drive live updates from `now` forward, computing the last candle yourself from the fill stream — or just `GetCandles` again every interval for less aggressive UIs. The historical and live paths align on the interval boundary. ## When to prefer live `HistoricalService.GetTrades` over a recent window is more expensive than just streaming `MarketDataService.SubscribeFills` and ringing your own buffer. Use the historical path for backfill or for ranges older than your in-memory retention; use the live path for anything inside the active session.