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