---
# 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