Skip to content

Onboarding

Five steps from zero to a live quote on Flint.

This is the maker path; you'll finish with a maker_id, funded inventory, and a bot that submits a quote. Only need read-only data (analytics, charts, alerts)? Skip to Market data; public books, stats, and history need no account.

#StepWhere
1Get a maker_id from the Flint TeamContact
2Log in, add a quoting key, enable markets, depositDashboard → Onchain
Using Python? Continue through steps 3-5 below, or use the Python Example Bot Quickstart.
3Create a project and add the SDKYour machine
4Initialize the client and quoterYour code
5Submit your first quoteYour code

1. Get a maker_id from the Flint Team

Your maker_id is your identity on the exchange; every balance, fill, and quote is keyed to it.

Flint provisions your maker_id

Only the Flint admin key can create a maker_id; you cannot create one yourself. Contact The Flint Team to get set up. You can't authenticate, deposit, or quote until this is done.

Your maker_id is registered against an admin wallet you provide. Keep that wallet cold; it controls your funds. You'll delegate everyday quoting to a separate hot key in step 2.

Two kinds of access attach to a maker_id, and the difference matters:

  • Management emails can sign into the dashboard to view balances, fills, and config, and to prepare on-chain changes. You can connect several emails to one maker_id; this is purely an ergonomic convenience so a team shares one view.
  • The admin key is the on-chain authority. Every program change must be signed by this key: adding a quoter, setting caps, depositing, withdrawing. A dashboard login alone changes nothing.

Guard the admin key

Dashboard access and on-chain authority are not the same thing. A management email can look but cannot move funds or alter your program state without the admin key's signature. Treat the admin key as your root of trust: keep it cold, back it up, and prefer a multisig or hardware signer for anything that touches funds. Adding or removing a management email never grants spend authority; only the admin key does.

2. Log in, add a quoting key, enable markets, deposit

Open the dashboard for your environment and sign in with your email; the dashboard sends a 6-digit one-time code, no password.

Pick the one that matches the gRPC endpoints your bot will use (step 3); a maker_id and its deposits live in a single environment.

Two different logins

Dashboard sign-in (email + code) is separate from how your bot signs in. The bot authenticates with its quoting keypair (step 4); both resolve to the same maker_id.

The dashboard handles all your on-chain actions so you never build a transaction by hand. It builds and simulates each one, then signs and sends it through a connected Solana wallet, or gives you the unsigned transaction (base58 / base64) to sign elsewhere, such as a multisig or air-gapped key.

Everything below happens on the dashboard's Onchain page. Connect your admin wallet to sign these actions.

a. Delegate a quoting key. In the quoting-authority list, add your quoter pubkey, a hot wallet that signs quotes for your bot. Your admin wallet stays cold; this quoter key is the one your bot loads in step 4. You can also set a separate withdraw authority here.

b. Enable your markets. In the books table, tick each pair you want to quote. Only enabled markets accept your quotes.

c. Set soft caps. Give each market a soft cap (soft_max_balance): the most of that asset you'll hold. Set one per market and on the quote (USDC) side.

d. Deposit inventory. In the balances panel, deposit base and quote tokens from your wallet. These deposits back the sell side of your fills.

How funding gates fills

Flint allocates liquidity at swap time, not when you publish a quote. When a taker hits your book, the matcher reads your current balance and caps the fill against:

  • Sell side (token going out): your deposited balance of that token.
  • Buy side (token coming in): soft_max_balance − balance, your remaining headroom under the cap.

Two things surprise people coming from a CEX:

  1. You can post levels bigger than you hold. Quotes aren't rejected on size; the matcher fills up to whatever budget exists at swap time. A 10 SOL ask with 5 SOL deposited fills 5 SOL.
  2. Fills feed your other quotes. A SOL ask pays out SOL and brings USDC in; that USDC becomes sell-side budget for any SOL bid you posted. So you can run a two-sided book from a one-sided deposit; inventory cycles through your quotes.

Which balance gates which side of each fill:

Your quoteSell-side budget (deposited)Buy-side budget (cap headroom)
Sell SOL on SOL/USDCSOL deposited in the SOL micro-bookUSDC soft_max_balance on the global market
Buy SOL on SOL/USDCUSDC deposited in the global marketSOL soft_max_balance on the SOL micro-book
Sell SOL on SOL/ETHSOL deposited in the SOL micro-bookETH soft_max_balance on the ETH micro-book
Buy SOL on SOL/ETHETH deposited in the ETH micro-bookSOL soft_max_balance on the SOL micro-book

The silent filter: the #1 cold-start mistake

A quote is dropped only when a side's budget is zero at swap time: no deposit on the sell side, or no soft-cap headroom on the buy side. A fresh maker has soft_max_balance = 0 everywhere, so every buy-side budget is 0. The classic mistake: quoting SOL/USDC with SOL deposited but no cap on the USDC side; sells post but never fill. Set caps on both sides in step 2c.

Once you're set up, the dashboard's Overview and Markets pages show your inventory value, NAV history, 24h volume, and live fills.

Choose your path

3. Create a project and add the SDK

sh
cargo new my-flint-maker
cd my-flint-maker

# Point Cargo at the Kellnr registry; the token comes next.
mkdir -p .cargo
cat >> .cargo/config.toml <<'EOF'
[registries.kellnr]
credential-provider = "cargo:token"
index = "sparse+https://kellnr.xantasoft.com/api/v1/crates/"
EOF

cat >> Cargo.toml <<'EOF'
flint-api-client = { version = "1.6", registry = "kellnr" }
solana-sdk = "3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
anyhow = "1"
EOF

# The quoting feature is on by default; read-only consumers can turn it off.
sh
mkdir my-flint-maker
cd my-flint-maker

python -m venv .venv
source .venv/bin/activate
python -m pip install --index-url https://__token__:{TOKEN}@pypi.xantasoft.com/simple/ flint-api-client

Both SDKs live on private registries — Rust on Kellnr, Python on a private PyPI — so both installs need a read-only token. It's the same token for either language: grab it from the Developer tab of the dashboard, mainnet or devnet.

Where it goes differs by language:

  • Rust — store it once with Cargo; the config above already points at the registry, so nothing else references the token:

    sh
    cargo login --registry kellnr {TOKEN}
  • Python — it rides in the index URL, in place of the {TOKEN} in the pip install above. Put that URL in a requirements.txt or pip.conf rather than retyping it per install.

Your gRPC endpoints

Your bot connects to two gRPC listeners: a public one for market data and an authed one for quoting. The SDKs default to mainnet, so you usually don't set an endpoint at all. Use the environment helpers when you want the environment to be explicit:

rust
// Mainnet is the default, but this makes it explicit.
let client = Client::builder().mainnet().build().await?;

// Devnet sets the public endpoint and derives the matching authed endpoint.
let client = Client::builder().devnet().build().await?;
python
from flint import Client, DEVNET_MAKER_ENDPOINT, DEVNET_PUBLIC_ENDPOINT, Endpoints

# Mainnet is the default.
client = Client()

# Devnet uses the public and authed endpoints for the same network.
client = Client(Endpoints(public_url=DEVNET_PUBLIC_ENDPOINT, auth_url=DEVNET_MAKER_ENDPOINT))

Full mainnet / devnet URLs and how to override them are on the Endpoints page. For mainnet:

  • Public: https://mainnet.api.flint.trade
  • Authed: https://mainnet.makerapi.flint.trade

Set only one endpoint for a known network (mainnet or devnet) and the other is assumed automatically, so a devnet deployment only needs one endpoint spelled out. Custom hosts aren't paired; set both explicitly.

Market-data, stats, historical, and browser login integrations only need the public endpoint. Makers need the authed endpoint too.

4. Initialize the client and quoter

Give the client the quoter keypair you delegated in step 2; that one key both signs in and signs your quotes. Endpoints default to mainnet, so you don't set them here; see Endpoints → Overriding the endpoint to target another network.

rust
use std::sync::Arc;
use solana_sdk::signer::keypair::read_keypair_file;
use flint_api_client::Client;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let keypair_path = std::env::args().nth(1).unwrap();
    let my_keypair = Arc::new(read_keypair_file(&keypair_path).unwrap());

    let client = Client::builder()
        .wallet(my_keypair.clone())
        .build()
        .await?;

    let session = client.authenticate().await?;
    let mut core = client.start_quoting_core(my_keypair.clone()).await?;

    Ok(())
}
python
import sys

from solders.keypair import Keypair
from solders.pubkey import Pubkey

from flint import Client

keypair = Keypair.from_json(open(sys.argv[1]).read())

client = Client(keypair=keypair)
session = await client.authenticate()

trusted_program_id = Pubkey.from_string(sys.argv[2])
core = await client.start_quoting_core(program_id=trusted_program_id)
await client.wait_ready()  # warms blockhash + slot streams before fair updates

If this fails with UNAUTHENTICATED: pubkey not registered as a quoting authority, the quoter key isn't delegated yet; return to step 2a.

Verify the program id at boot

Extra safety: confirm the program id the server advertises matches a trusted, config-pinned id before you quote against it.

rust
use flint_api_client::api::config::verify_program_id;

let cfg = client.refresh_config(None).await?;
verify_program_id("https://api.mainnet-beta.solana.com", &cfg.program_id).await?;
python
import sys

from solders.pubkey import Pubkey

trusted_program_id = Pubkey.from_string(sys.argv[2])
markets = await client.list_markets()
market = markets[(1, 0)]

await client.verify_program_id(
    market.program_id,
    "https://api.mainnet-beta.solana.com",
    expected_program_id=trusted_program_id,
)

5. Submit your first quote

Build a quote with QuoteBuildercore.builder() and submit it. This one posts a single level on each side, offset from a fair price you supply, then waits for on-chain confirmation.

Each tab is a whole program, not a fragment — it repeats the setup from step 4. Rust takes the quoter keypair path as its only argument; Python takes the keypair path first and your trusted program id second.

rust
use std::sync::Arc;
use std::time::Duration;
use flint_api_client::Client;
use flint_api_client::quoting::{OffsetSpec, QuoteBuilder, RiskParams};
use solana_sdk::signer::keypair::read_keypair_file;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let keypair_path = std::env::args().nth(1).unwrap();
    let my_keypair = Arc::new(read_keypair_file(&keypair_path).unwrap());

    let client = Client::builder()
        .wallet(my_keypair.clone())
        .build()
        .await?;

    let session = client.authenticate().await?;
    println!("authenticated as maker_id={}", session.maker_id);

    let mut core = client.start_quoting_core(my_keypair.clone()).await?;

    // One level per side: quote 0.25 (quote-asset units) off the fair,
    // 1.0 base unit in size.
    let level = OffsetSpec {
        price_offset: 0.25,
        size: 1.0,
        staleness: 50,
        client_order_id: None,
        post_only: false,
    };

    let mut receipt = QuoteBuilder::new()
        .oracle_offset("WSOL", |b| {
            b.with_risk(RiskParams::default())
                .with_spread(vec![level.clone()], vec![level]) // (bids, asks)
                .with_fair((100.0, 100.0))                     // (buy_fair, sell_fair)
        })
        .commit(&mut core)
        .await?;

    receipt.landed_within(Duration::from_secs(20)).await?;
    println!("quote landed");

    Ok(())
}
python
import asyncio
import sys

from solders.keypair import Keypair
from solders.pubkey import Pubkey

from flint import Client
from flint.onchain import OffsetSpec, RiskParams


async def main() -> None:
    keypair = Keypair.from_json(open(sys.argv[1]).read())

    client = Client(keypair=keypair)
    session = await client.authenticate()
    print("authenticated as maker_id", session.maker_id)

    trusted_program_id = Pubkey.from_string(sys.argv[2])
    core = await client.start_quoting_core(program_id=trusted_program_id)
    await client.wait_ready()  # warms blockhash + slot streams

    # One level per side: quote 0.25 (quote-asset units) off the fair,
    # 1.0 base unit in size.
    level = OffsetSpec(price_offset="0.25", size="1.0", staleness=50)

    market = core.market("WSOL/USDC")
    builder = core.builder()
    builder.oracle_offset(market).risk(RiskParams()).spread(
        [level], [level]  # (bids, asks)
    ).fair(
        buy_price="100.0",
        sell_price="100.0",
    )

    receipt = await core.commit(builder)
    await receipt.landed_within(20.0)
    print("quote landed")

    await client.close()


if __name__ == "__main__":
    asyncio.run(main())

Run it:

sh
cargo run -- {QUOTER_KEYPAIR_FILE_PATH}
sh
python main.py {QUOTER_KEYPAIR_FILE_PATH} {FLINT_PROGRAM_ID}

In a real bot, install risk + spread once, then send only a fresh fair each tick; re-sending params resets your accumulated risk state. The Quoting recipe covers the full tick loop, and the examples/ directory in the SDK distribution has runnable quote commands for each SDK.

Pick your path

GoalNext page
Full quoting strategies + tick loopQuoting
Stream books and fillsMarket data
Build a maker in PythonPython SDK
Pull historical fills / candlesHistorical queries
Sign-in flow detailAuth flow

Built on Solana