Skip to content

How Flint works

Flint has two parts:

  1. An exchange that runs entirely on-chain on Solana.
  2. The off-chain services operated around it.

On-chain, Flint 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 swaps, 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.

Off-chain, there is a managed supporting stack: a gRPC gateway, a market-data feed, historical queries, a dashboard, and language SDKs. They speed up building on Flint, so you can quote and trade without running your own indexer or RPC fleet.

At a glance

You never manually build Solana transactions or talk to an RPC node directly. Your bot runs off-chain with the SDK; the SDK signs with your quoter key and submits through the Flint gateway, which relays to the on-chain program and streams chain + market state back to you.

OFF-CHAIN · YOUR BOT + FLINT GATEWAYON-CHAIN · SOLANAYour strategyprice source · mid · riskFlint Client SDKPacks human prices into messagesManages sequencing + chain tipSigns with your quoter keyFlint gateway · gRPCPublic APImarket data · stats · historyAuthed APIAuth · Maker · TxBearer session tokenFlint programmatcher · builds the virtualbook, fills pro-rataMarket accountsSpotMarket · GlobalMarketquoting intents + inventoryTakersswap your resting quotessigned txstreamsrelay txeventsswapDashboard · admin walletdelegate quoter · caps · depositone-time · admin-signedconfigures the program on-chain

Off-chain services

The exchange itself is fully on-chain. Around it, managed off-chain services accelerate building on Flint, giving you fast market data, history, and submission without standing up your own indexer or RPC fleet:

ServiceWhat you get
Market dataLive books, fills, snapshots, and venue stats (MarketDataService · StatsService).
Maker dataBalances, fills, and stats scoped to your maker_id (MakerService).
HistoryClickHouse-backed candles and fills (HistoricalService).
Tx relaySubmits your signed transactions; streams chain tip + status (TxService).
AuthSigned-nonce session tokens (AuthService).

A dashboard (onboarding, config, monitoring) and Rust / Python / TypeScript SDKs sit on top. See Endpoints for hosts and ports.

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.

StrategyWhen to use
OrderListExplicit order-by-order control (place / cancel by price + size). The CEX-shaped option.
OracleOffsetYou have an off-book fair price (oracle, internal mid). Quote a fair anchor + per-side delta vectors.
LinearDistributionServer interpolates linearly between buy/sell price ranges. Cheapest message size.
Each model publishes the same two-sided quote in a different shape.bid (buy)ask (sell)higher price ↑ · bar length = size · ask above the mid, bid belowOrderListplace / cancel · CEX-stylePUBLISHESA list of submit / cancel ops.You set every price & size.price ↑midSell 155.70 × 4Sell 155.30 × 10Buy 154.90 × 10Buy 154.50 × 14OracleOffsetfair ± offset ladderPUBLISHESOne fair price + an offsetladder, installed once.fair+0.05+0.15+0.35Tick 1: install fair + ladder.Tick 2…N: re-send just the fair.The cheapest refresh.Offsets are cumulative; sizesgrow outward (5 · 10 · 20).LinearDistributioneven band, 4 anchorsPUBLISHESFour prices: best & worston each side.midsell_end (worst)sell_start (best)buy_start (best)buy_end (worst)Server fills evenly across theband between the anchors.Uniform size per level.

Strategies are documented under 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:

UnitMeaning
AtomsSmallest token integer. 1 SOL = 10⁹ atoms.
LotsAtoms divided by the market's atoms-per-lot value. The matcher works in lot-space.
Oracle unitsCommon reference unit for price comparisons across markets.

The SDK's pricingflint.onchain helpers (human_to_oracle, human_size_to_lots, oracle_to_human) do every conversion. See Prices, sizes, 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.

CounterPurpose
oracle_sequence_numberAnti-replay on OracleFair updates. Bump on every fair-price flush.
order_sequenceAnti-replay on UpdateQuotingParams. Bump on every params/order flush.
client_order_idPer-maker monotonic id for OrderList placements.

The SDK seeds all three counters from nanoseconds 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_spreadCrossSpreadUpdate entry. The matcher looks up book.get_cross_params(SpotId::OF_COUNTERPARTY)CrossSpreadUpdate(spot_index=counterparty, ...) and silently filters your maker if absent.

For a global swap (e.g. SOL ↔ USDC), the counterparty is SpotId::GLOBALspot_index=0. You still need a cross_spreadCrossSpreadUpdate entry for 0 even though there is no second spot market. Some(0.0)CrossSpreadUpdate(0, 0) is enough to pass the gate without widening.

rust
QuoteBuilder::new()
    .order_list("SOL", |b| b
        .init()
        .with_params(ParamsUpdate::new()
            .enable(true)
            .with_cross(0_u16, Some(0.0))))   // counterparty + spread cap
    .commit(&mut core).await?;
python
from flint.onchain import CrossSpreadUpdate, ParamsUpdate

market = core.market("SOL")
builder = core.builder()
builder.order_list(market).init().params(
    ParamsUpdate(
        enable=True,
        cross_spread=[CrossSpreadUpdate(0, 0)],  # counterparty + spread cap
    )
)

await core.commit(builder)

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 fillBudget readWhat's actually needed
Sell side (token going out)available_to_sell = balanceDeposited 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 through the dashboard's quote deposit/withdraw and quote max-balance actions. 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: how funding gates fills for the boot sequence.

OracleFair staleness

Each oracle-offset order carries a staleness byte. The matcher computes slot_delay = current_slot - last_oracle_slot and rejects any order where slot_delay > staleness.

Built on Solana