Skip to content

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.

StrategyWhen to useRust builder entry
OrderListExplicit order-by-order control (place / cancel by price + size). The CEX-shaped option.QuoteBuilder::new().order_list("SOL", ...)
OracleOffsetYou have an off-book fair price (oracle, internal mid). Quote a fair anchor + per-side delta vectors.QuoteBuilder::new().oracle_offset("SOL", ...)
LinearDistributionServer interpolates linearly between buy/sell price ranges. Cheapest message size.QuoteBuilder::new().linear("SOL", ...)

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 / atoms_per_contract_lots. The matcher works in lot-space.
Oracle unitsCommon 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.

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

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

Build on Solana