Appearance
How Flint works
Flint has two parts:
- An exchange that runs entirely on-chain on Solana.
- 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 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:
| Service | What you get |
|---|---|
| Market data | Live books, fills, snapshots, and venue stats (MarketDataService · StatsService). |
| Maker data | Balances, fills, and stats scoped to your maker_id (MakerService). |
| History | ClickHouse-backed candles and fills (HistoricalService). |
| Tx relay | Submits your signed transactions; streams chain tip + status (TxService). |
| Auth | Signed-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.
| Strategy | When to use |
|---|---|
| OrderList | Explicit order-by-order control (place / cancel by price + size). The CEX-shaped option. |
| OracleOffset | You have an off-book fair price (oracle, internal mid). Quote a fair anchor + per-side delta vectors. |
| LinearDistribution | Server interpolates linearly between buy/sell price ranges. Cheapest message size. |
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:
| Unit | Meaning |
|---|---|
| Atoms | Smallest token integer. 1 SOL = 10⁹ atoms. |
| Lots | Atoms divided by the market's atoms-per-lot value. The matcher works in lot-space. |
| Oracle units | Common 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.
| Counter | Purpose |
|---|---|
oracle_sequence_number | Anti-replay on OracleFair updates. Bump on every fair-price flush. |
order_sequence | Anti-replay on UpdateQuotingParams. Bump on every params/order flush. |
client_order_id | Per-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 fill | Budget read | What's actually needed |
|---|---|---|
| Sell side (token going out) | available_to_sell = balance | Deposited 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.
