Appearance
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.
| # | Step | Where |
|---|---|---|
| 1 | Get a maker_id from the Flint Team | Contact |
| 2 | Log in, add a quoting key, enable markets, deposit | Dashboard → Onchain |
| Using Python? Continue through steps 3-5 below, or use the Python Example Bot Quickstart. | ||
| 3 | Create a project and add the SDK | Your machine |
| 4 | Initialize the client and quoter | Your code |
| 5 | Submit your first quote | Your 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.
- Mainnet: mainnet.dash.flint.trade
- Devnet: devnet.dash.flint.trade
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:
- 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.
- 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 quote | Sell-side budget (deposited) | Buy-side budget (cap headroom) |
|---|---|---|
| Sell SOL on SOL/USDC | SOL deposited in the SOL micro-book | USDC soft_max_balance on the global market |
| Buy SOL on SOL/USDC | USDC deposited in the global market | SOL soft_max_balance on the SOL micro-book |
| Sell SOL on SOL/ETH | SOL deposited in the SOL micro-book | ETH soft_max_balance on the ETH micro-book |
| Buy SOL on SOL/ETH | ETH deposited in the ETH micro-book | SOL 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-clientBoth 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:
shcargo login --registry kellnr {TOKEN}Python — it rides in the index URL, in place of the
{TOKEN}in thepip installabove. Put that URL in arequirements.txtorpip.confrather 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 updatesIf 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
| Goal | Next page |
|---|---|
| Full quoting strategies + tick loop | Quoting |
| Stream books and fills | Market data |
| Build a maker in Python | Python SDK |
| Pull historical fills / candles | Historical queries |
| Sign-in flow detail | Auth flow |
