Quoting
You're building a maker bot. This page is the integrator-facing reference for the SDK's quoting layer — how to wire it up, how to keep it submitting, and the on-chain gotchas that will silently drop your fills if you skip them.
Read How Sweetspot works first
The on-chain matcher enforces sequence-number monotonicity, the two-balance trap, and the cross-spread requirement. The SDK handles the first; you have to handle the other two.
Three strategies
Pick one per market.
| Strategy | When to use | SDK type |
|---|---|---|
| OracleOffset | You have an off-book fair price. Quote a fair anchor + per-side offset vectors. | OracleOffsetQuotingClient |
| OrderList | Explicit order-by-order Place/Cancel. | OrderListQuotingClient |
| LinearDistribution | Server interpolates linearly between buy/sell ranges. Cheapest message size. | LinearDistributionQuotingClient |
Strategies are mode-exclusive — one per maker, per market. Don't mix.
Rust + Go only
Quoting is gated on the on-chain Rust SDK (encoding the UpdateOracleFair / UpdateQuotingParams instructions). The TS SDK intentionally stops at AuthFlow + read-only client wrappers — browser trading clients should sign and submit transactions through a wallet adapter, not the SDK.
The Go SDK ships QuotingCore (Receipt, sequence tracking, status fan-out, packing scaffolding) but not the strategy clients. Build instructions in Rust and ship the bytes through Go's SubmitTx, or use a Go Solana library to encode against the on-chain program directly.
Bot loop, end-to-end (Rust)
The minimal shape of a quoting bot:
use std::sync::Arc;
use superis::auth::AuthFlow;
use superis::quoting::{
OffsetOrderSpec, OracleOffsetQuotingClient, ParamsUpdate, QuotingCore, RiskParams,
};
use superis::{ConfigCache, ResilientStream, ServerState};
use tonic::transport::Channel;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. Connect once + share the channel everywhere.
let channel = Channel::from_static("https://api.superis.exchange:443")
.connect()
.await?;
// 2. Authenticate.
let auth = AuthFlow::new(channel.clone(), Arc::new(my_signer));
let session = auth.token().await?;
let _refresher = auth.spawn_refresh_loop();
// 3. Cache config + verify program id.
let mut market = MarketDataServiceClient::new(channel.clone());
let mut tx = TxServiceClient::with_interceptor(channel.clone(), auth.interceptor());
let cache = ConfigCache::new();
let cfg = superis::config::refresh(&cache, &mut market, &mut tx, server_program_id).await?;
superis::config::verify_program_id(
"https://api.mainnet-beta.solana.com",
&cfg.program_id,
).await?;
// 4. Hook server state to blockhash + slot streams (resilience recipe).
let server = Arc::new(ServerState::new());
wire_blockhash_and_slots(&server, channel.clone(), auth.interceptor()).await?;
// 5. Spin up the quoting core.
let core = QuotingCore::start(
channel.clone(),
auth.interceptor(),
keypair.clone(),
session.maker_id as u16,
Arc::new(cfg),
server.clone(),
).await?;
let oc = OracleOffsetQuotingClient::new(core.clone());
// 6. One-time params (enable + balance + cross_spread).
oc.queue_params_update(
"SOL/USDC",
ParamsUpdate {
enable: true,
max_balance: 100_000_000_000, // 100 SOL atoms
cross_spread: vec![(0, Some(0))], // required even for SOL/USDC
},
).await?;
oc.queue_risk_params_update(
"SOL/USDC",
RiskParams { per_slot_decay_factor: Some(0.999), ..Default::default() },
).await?;
oc.commit().await?.confirmed().await?;
// 7. Quoting loop. Fair price comes from your strategy.
loop {
let fair = my_oracle.fetch().await; // your decision
oc.queue_fair_price("SOL/USDC", fair - 0.05, fair + 0.05).await?;
oc.queue_spread(
"SOL/USDC",
buy_ladder(fair),
sell_ladder(fair),
).await?;
let mut receipt = oc.commit().await?;
if let Err(e) = receipt.confirmed_within(std::time::Duration::from_secs(5)).await {
tracing::warn!(?e, "quote commit failed");
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
fn buy_ladder(fair: f64) -> Vec<OffsetOrderSpec> {
(1..=5).map(|i| OffsetOrderSpec {
price_offset_delta: 50 * i,
lots: 100,
staleness: 30,
client_order_id: None,
}).collect()
}The same shape works for OrderListQuotingClient (replace queue_fair_price + queue_spread with queue_order(...)) and LinearDistributionQuotingClient (replace with queue_linear_params(...)).
OracleOffset queue → commit
oc.queue_fair_price("SOL/USDC", 155.10, 155.20).await?;
oc.queue_spread(
"SOL/USDC",
vec![
OffsetOrderSpec { price_offset_delta: 50, lots: 100, staleness: 30, client_order_id: None },
OffsetOrderSpec { price_offset_delta: 100, lots: 200, staleness: 30, client_order_id: None },
],
vec![
OffsetOrderSpec { price_offset_delta: 50, lots: 100, staleness: 30, client_order_id: None },
],
).await?;
oc.queue_params_update("SOL/USDC", ParamsUpdate {
enable: true,
max_balance: 100_000_000_000,
cross_spread: vec![(0, Some(0))],
}).await?;
let mut receipt = oc.commit().await?;
receipt.confirmed_within(std::time::Duration::from_secs(30)).await?;commit() packs every queued change into the smallest set of transactions that fit, partial-signs each with your quoting key, submits via TxService.SubmitTx, and returns a Receipt you await.
OrderList queue → commit
use superis::quoting::OrderAction;
use sweetspot_sdk::core::side::Side;
let ol = OrderListQuotingClient::new(core.clone());
ol.queue_order("SOL/USDC", OrderAction::Submit, Side::Buy, 154.90, 10.0).await?;
ol.queue_order("SOL/USDC", OrderAction::Submit, Side::Sell, 155.30, 10.0).await?;
ol.commit().await?.confirmed().await?;The local tracker remembers (price, size) → client_order_id so a follow-up Cancel at the same price/size resolves the right id without you wiring it.
LinearDistribution queue → commit
let ld = LinearDistributionQuotingClient::new(core.clone());
ld.queue_linear_params(
"SOL/USDC",
LinearParamsUpdate {
buy_start_price: 155.00,
buy_end_price: 154.50,
sell_start_price: 155.30,
sell_end_price: 155.80,
spread_backoff_per_slot: Some(2),
bid_post_only: Some(true),
ask_post_only: Some(true),
client_order_id: None,
},
).await?;
ld.commit().await?.confirmed_within(std::time::Duration::from_secs(30)).await?;Receipts
Receipt resolves when every submission in a commit() has acked or confirmed. Cheap to drop — the underlying submissions continue server-side regardless. The mechanism is fed by TxService.SubscribeTxStatus, which the quoting core opens and auto-reconnects in the background.
use superis::quoting::{CommitError, ReceiptStatus};
// Block until acked.
receipt.accepted().await?;
// Block until confirmed, with timeout.
match receipt.confirmed_within(std::time::Duration::from_secs(10)).await {
Ok(()) => println!("confirmed"),
Err(CommitError::OnChain { reason }) => eprintln!("reverted: {reason}"),
Err(CommitError::Timeout) => eprintln!("timeout"),
Err(CommitError::Disconnected) => eprintln!("status stream dropped"),
}
// Or non-blocking snapshot.
match receipt.status() {
ReceiptStatus::InFlight { accepted, confirmed, total } =>
println!("{accepted}/{total} acked, {confirmed}/{total} confirmed"),
ReceiptStatus::Confirmed => println!("done"),
ReceiptStatus::Failed(e) => eprintln!("failed: {e}"),
}Re-quoting on fills
Subscribe to fills (your maker_id is scoped automatically by the session) and re-quote when your inventory shifts:
use superis::proto::{
market_data_service_client::MarketDataServiceClient, SubscribeFillsRequest,
};
let mut market = MarketDataServiceClient::new(channel.clone());
let mut fills = market
.subscribe_fills(SubscribeFillsRequest { pairs: vec![] })
.await?
.into_inner();
while let Some(fill) = fills.message().await? {
if fill.maker_id.as_ref().map(|m| m.id) == Some(session.maker_id) {
// Your fill — refresh fair, requeue, commit.
let fair = my_oracle.fetch().await;
oc.queue_fair_price("SOL/USDC", fair - 0.05, fair + 0.05).await?;
oc.commit().await?.accepted().await?;
}
}Sequence-number invariants
The matcher rejects any oracle_sequence_number or order_sequence that isn't strictly increasing per spot. The SDK seeds both counters from SystemTime::now().as_micros() at startup so a process restart never collides with prior on-chain state.
client_order_id is allocated from a single global counter, also seeded from microseconds-since-epoch. If you supply your own ids (OffsetOrderSpec.client_order_id = Some(n)), it's your job to keep them monotonic — the SDK does not check.
Don't share a maker_id across processes
Two processes using the same maker_id will burn each other's sequence numbers and cause OracleFairSequenceNotMonotonic reverts. Run one quoting process per maker_id.
Tx batching
Every commit() produces a flat Vec<Instruction> that the SDK packs greedily into as few 1232-byte transactions as fit. A single instruction that overflows on its own is still emitted (the server will reject it); keep individual instructions reasonable.
Each tx is partial-signed with the quoting keypair. The fee-payer signature slot is left empty for the server to fill via its sponsored-payer wallet (you select one from TxService.GetSponsoredPayers, which superis::config::refresh() caches for you).
Capacity planning
Three knobs control whether your fills land:
ParamsUpdate.max_balance— base-side balance ceiling, per spot. Set above your expected inventory.- Global quote balance — set via
DepositWithdrawQuote(admin path). Required for sells to land — see the two-balance trap in How Sweetspot works. ParamsUpdate.cross_spread— must include an entry for the counterpartyspot_id(typically0for global swaps). Without it the matcher silently filters your maker.
If your orders show up in the L3 stream but never fill, walk through those three first.
Go
Go ships the transport layer — Receipt, sequence tracking, status fan-out, packing scaffolding — without the strategy clients (encoding the on-chain instructions is Rust-only):
import (
"github.com/superis/sweetspot-maker-client/go/superis"
pb "github.com/superis/sweetspot-maker-client/go/sweetspot/api/v1"
)
txClient := pb.NewTxServiceClient(authedConn)
state := superis.NewServerState()
seq := superis.NewSequenceTracker()
core := superis.NewQuotingCore(txClient, state, seq)
stop := core.Start(ctx)
defer stop()
// Build + sign your tx with your preferred Solana Go library, then:
receipt, err := core.SubmitTx(ctx, txBytes, []string{signature})
if err := receipt.ConfirmedWithin(ctx, 30*time.Second); err != nil {
log.Fatal(err)
}SequenceTracker exposes NextOracleSeq(spotID), NextOrderSeq(spotID), and AllocClientOrderID(). Match the seeding strategy by calling them when building each on-chain instruction.
Disabling the quoting layer (Rust)
The quoting Cargo feature is on by default. Read-only consumers can turn it off to drop the on-chain SDK + Solana deps from the build:
superis = { version = "0.1", default-features = false, features = ["tls"] }