Skip to content

Rust SDK

Async, tonic-based Rust client for every service in flint.spot.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

From the Kellnr registry (needs a registry token):

toml
flint-api-client = { version = "1.6", registry = "kellnr" }

Read-only (no quoting):

toml
flint-api-client = { version = "1.6", registry = "kellnr", default-features = false, features = ["tls"] }

Registry token

Pulling from Kellnr requires a read-only registry token. Grab yours from the dashboard (Maker → Quoters → SDK access) — it is minted on first view, and the same token is shown on every visit.

Point Cargo at the registry in ~/.cargo/config.toml:

toml
[registries.kellnr]
index = "sparse+https://kellnr.xantasoft.com/api/v1/crates/"

Then store the token with cargo login (paste it at the prompt):

sh
cargo login --registry kellnr

Or skip the login and supply it via the environment (e.g. in CI):

sh
export CARGO_REGISTRIES_KELLNR_TOKEN=<token>

Quickstart

The ergonomic entry point is Client — one handle that owns two tonic [Channel]s (one per server listener — see Endpoints), 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 flint_api_client::api::client::Client;
use flint_api_client::api::proto::ListPairsRequest;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Public consumer — defaults to the mainnet endpoint.
    let client = Client::builder()
        .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, then authenticate once. The authed endpoint is
    // assumed from the public one, so it defaults to mainnet too. 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()
        .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 flint_api_client::DEFAULT_MAKER_ENDPOINT;
use flint_api_client::api::auth::AuthFlow;
use flint_api_client::api::proto::maker_service_client::MakerServiceClient;

// DEFAULT_MAKER_ENDPOINT is the mainnet authed listener; see Onboarding
// for the other networks' constants.
let auth_channel =
    tonic::transport::Channel::from_static(DEFAULT_MAKER_ENDPOINT)
        .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.

Endpoint discovery

By default, Client::builder().build() resolves every DNS record behind the public hostname, races a probe across the addresses (median of three NodeService.GetNodeInfo calls each), and pins both channels to the fastest healthy node — each deployment node serves api and makerapi from the same address, so the authed channel follows the public winner rather than running a weaker probe of its own. A background task re-probes every 10 s:

  • If the pinned node stops answering (or reports degraded health), the channel switches to the best alternative immediately.
  • A merely faster node must beat the pinned one by ≥10 % and ≥5 ms, in two consecutive rounds (so ~20 s), before a switch — a transient network blip never cuts live streams.

Every executed switch force-reconnects the client's resilient streams and session heartbeat onto the new node (swapping the channel's endpoint alone would only affect new RPCs — streams already in flight stay on the old connection otherwise). Switches and deferred candidates are logged at debug; anything that needs attention — a pinned node gone, a whole probe round failing — is warn.

rust
let client = Client::builder()
    .endpoint_discovery(false)              // opt out entirely, or
    .discovery_interval(Duration::from_secs(60)) // slow the re-probe cadence
    .build()
    .await?;

// Observability: the node both channels are pinned to right now.
let pinned = client.discovered_endpoint();
// Saw a disconnect yourself? Trigger a re-probe immediately.
client.reprobe_endpoints();

Discovery is skipped for IP-literal endpoints and pre-built channels (public_channel / auth_channel), which always connect directly.

The Python SDK has the same feature with the same thresholds — see Python: endpoint discovery. The browser TypeScript SDKs do not: gRPC-Web can't resolve DNS or pin TLS to a raw IP.

Where to go from here

You wantPage
Boot a maker botQuoting
Shorten quote transaction lifetimeQuoting: controlling transaction lifetime
Widen spread ahead of risky slotsQuoting: SpreadProtect
Stream books and fillsMarket data
Pull historical fills / candlesHistorical queries
Sign-in flow detailAuth flow

Decimal handling

Every wire numeric — book/fill prices and sizes, historical FillEvent/Candle OHLCV, and MakerBalanceEvent.balance — is wrapped as Decimal { value: String }. Parse with 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.

Logging

The Rust SDK uses tracing. By default it logs connection, auth, and stream lifecycle events only if your application installs a tracing subscriber and enables debug or trace. At debug, client construction logs the SDK version, proto hash, and proto revision stamped on outbound requests.

To turn on full protobuf message payload logs for SDK-owned calls, enable it on the builder:

rust
let client = Client::builder()
    .log_messages(true)
    .message_log_limit(50_000)
    .build()
    .await?;

Notes:

  • Message payload logs are opt-in.
  • Payload logging covers the SDK-managed auth flow, config refresh, and stream helpers.
  • Payloads are emitted at trace.
  • message_log_limit caps each payload in characters. Use 0 to disable truncation.
  • Auth fields such as nonce, signature, code, and session_token are redacted.
  • Bearer headers and API key headers are not logged.

Cargo features

FeatureDefaultWhat it adds
tlsyesHTTPS via rustls + native roots.
quotingyesQuoting layer (QuoteBuilder for OrderList / OracleOffset / LinearDistribution, QuotingCore, Receipt, sequence trackers). Pulls in the on-chain Solana SDK.

Built on Solana