Python SDK
Use the Python SDK when your maker bot signs and submits Flint quote updates directly from Python. It gives you one Client for market data, maker authentication, balances, chain tips, transaction submission, and receipts, plus a QuoteBuilder for the three on-chain quoting modes:
| Strategy | Use it when |
|---|---|
| Linear distribution | You quote a ladder around a fair price. |
| Oracle offset | You quote offsets from an external oracle/fair. |
| Order list | You want CEX-style explicit bid and ask orders. |
Your bot still owns fair-value inputs, risk checks, inventory targets, persistence, and reconciliation. The SDK handles the Flint-specific work needed to publish those decisions: decimal-safe unit conversion, quote instruction construction, transaction packing/signing, and submit-status tracking.
Runnable example bot
The companion sweetspot-maker-example repo is the best starting point for a real Python maker process. It is a small bot built on flint-sdk, not a second SDK. Read it when you want to see how the pieces on this page fit together in one loop.
The example makes these decisions explicitly in maker.toml and the sweetspot_maker package:
- Markets: quote named catalog markets such as
SOL/USDC. - Mode: dry-run by default, or submit signed transactions when
submit_quotes = true. - Endpoints and trust: configure public/auth URLs, keypair path, durable state path, pinned program id, and Solana RPC URL.
- Price source: read USD prices from Pyth Hermes, with static fallback prices for local synthetic mints.
- Strategy: use
SimpleOracleOffsetto center a two-sided ladder on the mid price. - Risk shape: configure spread, number of levels, level spacing, size per level, staleness slots, and base
soft_max_balance. - Inventory skew: shift the center price from target inventory toward a maximum skew, with an optional minimum spread floor.
- Lifecycle: authenticate, resolve markets, verify program ids, watch slots, keep chain tips warm, persist sequence counters before submit, and wait for receipts.
What you need
Before writing the bot, get:
| Item | Why |
|---|---|
| Public endpoint | Market catalog, books, fills, and public stats. |
| Authenticated endpoint | Maker balances, chain tips, submit, and receipts. |
| Maker authority keypair | The registered Solana keypair allowed to quote for your maker id. |
| Trusted program id | A config-pinned Flint program id, not just the id returned by an API call. |
| Funded maker account | Deposits, quote-side caps, and cross-spread settings for the markets you quote. |
| Durable bot state | Persist sequence counters and client order ids across restarts. |
The examples below assume:
- base spot
1quotes against quote spot0; - your keypair is the registered maker authority;
fair_slotis the Solana slot associated with your current fair/oracle input;FLINT_PROGRAM_IDandSOLANA_RPC_URLcome from trusted bot config;stateis your own durable state object or database row.
If you have not created a maker_id, registered the hot keypair, deposited inventory, and set the relevant soft_max_balance caps, start with Onboarding. Quotes can be accepted by the API and still never fill if the maker has no sell-side deposit or no buy-side cap headroom.
Install
Install the SDK package:
pip install flint-sdk
# from a local checkout
pip install -e './python'Install flint-sdk; import it as flint in Python.
The SDK uses solders for Solana keys, pubkeys, blockhashes, instructions, and transaction signing.
Connect
Read-only processes only need the public endpoint:
from flint import Client, Endpoints
client = Client(Endpoints(public_url="https://api.superis.exchange"))Maker bots need both endpoints and a keypair:
from solders.keypair import Keypair
from flint import Client, Endpoints
keypair = Keypair.from_json(open("/path/to/id.json").read())
client = Client(
Endpoints(
public_url="https://api.superis.exchange",
auth_url="https://auth.api.superis.exchange",
),
keypair,
)Use https:// for production. For local development, pass explicit http://localhost:... URLs. Endpoints(..., insecure=True) and SUPERIS_INSECURE=1 force plaintext even for HTTPS-looking URLs, so never set them in production.
Always close long-lived clients on shutdown:
await client.close()Discover markets
Call list_markets() once on startup and whenever you need to refresh the catalog. It returns Python-friendly MarketInfo objects keyed by (base_spot_id, quote_spot_id).
markets = await client.list_markets()
market = markets[(1, 0)]
print(market.name)
print(market.program_id)
print(market.last_price)Each MarketInfo includes the mint, program id, and unit metadata the quoting builder needs to convert human prices and sizes into on-chain integers.
Authenticate
Authenticate once after startup. The client caches and refreshes the session when later maker or transaction calls need bearer metadata.
session = await client.authenticate()
print("maker id", session.maker_id)
balances = await client.maker_balance()
stats = await client.maker_stats()Revoke when you need best-effort early token invalidation, such as logout or key rotation:
await client.revoke()If revoke fails during shutdown, the server-side token may remain live until expiry. Do not treat revoke as a substitute for short token lifetimes and key rotation controls.
Initialize bot state
Flint enforces strictly increasing per-maker/per-market sequence counters. The Python builder consumes counters when it builds instructions, before anything is submitted.
For a brand-new durable state row, seed counters from wall-clock nanoseconds:
import time
seed = time.time_ns()
state.next_oracle_sequence = seed
state.next_order_sequence = seed
state.next_client_order_id = seed
state.save()On every tick, reserve the builder's next counters before submitting. Gaps are safe; reuse is not. Run one quoting process per maker_id, and stop quoting if your durable state is missing or uncertain until you reconcile.
The example bot's Sequences type is intentionally boring: it seeds counters from time.time_ns(), stores them in maker-state.json, and writes them before any submit attempt.
Submit one quote tick
A quote tick usually does this:
- Build quote instructions from your current fair, spread, and inventory.
- Get a recent chain tip.
- Pack and sign one or more transactions.
- Submit each transaction and wait for a receipt.
- Persist the next sequence counters.
import os
from solders.pubkey import Pubkey
from flint.onchain import CrossSpreadUpdate, QuoteBuilder, QuoteMarket, tx
session = await client.authenticate()
markets = await client.list_markets()
market = markets[(1, 0)]
quote_market = QuoteMarket.from_market_info(market)
trusted_program_id = Pubkey.from_string(os.environ["FLINT_PROGRAM_ID"])
await client.verify_program_id(
market.program_id,
os.environ["SOLANA_RPC_URL"],
expected_program_id=trusted_program_id,
)
builder = QuoteBuilder(
trusted_program_id,
keypair.pubkey(),
session.maker_id,
oracle_sequence=state.next_oracle_sequence,
order_sequence=state.next_order_sequence,
client_order_id=state.next_client_order_id,
)
builder.linear_fair(
quote_market,
last_oracle_slot=fair_slot,
buy_start_price="155.10",
buy_end_price="155.00",
sell_start_price="155.20",
sell_end_price="155.30",
)
builder.linear_params(
quote_market,
bid_size_per_level="0.10",
ask_size_per_level="0.10",
soft_max_balance="10", # 10 base tokens for this market, not quote notional.
cross_spread_update=[
# Nonzero spreads are raw oracle integers; compute them with
# human_to_oracle_for_quoting(...) before passing them here.
CrossSpreadUpdate(spot_index=market.quote_spot_id, spread=0),
],
)
tip = await client.chain_tip()
instruction_groups = builder.pack(
keypair.pubkey(),
tip.blockhash,
tip.recommended_cu_price,
)
# Reserve consumed counters before any submit attempt. A crash may leave a gap;
# reusing counters can make later updates silently drop.
state.next_oracle_sequence = builder.next_oracle_sequence
state.next_order_sequence = builder.next_order_sequence
state.next_client_order_id = builder.next_client_order_id
state.save()
for instructions in instruction_groups:
signed = tx.sign_transaction(
instructions,
keypair.pubkey(),
[keypair],
tip.blockhash,
)
receipt = await client.submit_transaction_receipt(bytes(signed))
await receipt.confirmed_within(5.0)QuoteBuilder is one-shot. Create a new builder for each tick, add that tick's fair and parameter/order updates, pack, submit, then persist the builder's next counters.
Keep chain tips warm
For one-off scripts, chain_tip() fetches and caches a blockhash. For a maker loop, start the reconnecting stream once. chain_tip() returns the latest tip observed by this client; if freshness matters after startup or reconnect, wait for the next stream item before building:
from flint import ConnectionState
stream = client.start_chain_tip()
await stream.wait_for_state(ConnectionState.CONNECTED, timeout=5.0)
tip = await stream.next_item()
while True:
if stream.state is not ConnectionState.CONNECTED:
await stream.wait_for_state(ConnectionState.CONNECTED, timeout=5.0)
tip = await stream.next_item()
tip = await client.chain_tip()
# build, sign, and submit this tick with tip.blockhashIf the status or chain-tip stream disconnects, reconcile before replacing orders that might already have landed.
The example bot also watches TxService.SubscribeSlots so its oracle-fair updates carry a recent last_oracle_slot. The Python Client does not yet wrap slots directly, so the example uses the generated tx stub for that one stream.
Strategy helpers
Use one params helper per market, matching the strategy you want live on-chain:
| Strategy | Fair helper | Params/helper updates |
|---|---|---|
| Linear distribution | linear_fair(...) | linear_params(...) |
| Oracle offset | oracle_fair(...) | oracle_offset_params(...) with already-lowered SpotOffsetOrder values |
| Order list | None | order_list_params(...) with order_list_place(...) updates |
The example bot uses Oracle offset. Its strategy produces human-unit LevelSpecs, then its quoting module lowers each level to SpotOffsetOrder with human_size_to_lots(...), human_to_oracle_for_quoting(...), staleness, and a monotonic client order id.
Order-list helpers take human price and size strings and lower them for you:
bid = builder.order_list_place(
quote_market,
price="155.12",
size="0.25",
post_only=True,
)
ask = builder.order_list_place_and_pop(
quote_market,
price="155.18",
size="0.25",
post_only=True,
)
builder.order_list_params(
quote_market,
bids=[bid],
asks=[ask],
soft_max_balance="10",
cross_spread_update=[
CrossSpreadUpdate(spot_index=market.quote_spot_id, spread=0),
],
)Receipts
submit_transaction_receipt() subscribes to transaction status before it submits, so the bot does not miss an early ack event.
receipt = await client.submit_transaction_receipt(raw_transaction)
await receipt.accepted()
await receipt.confirmed_within(5.0)
status = receipt.status()
print(status.accepted, status.confirmed, status.total)Treat these receipt errors as operational signals:
| Error | Meaning |
|---|---|
OnChainFailure | The chain reported a failed transaction status. |
CommitTimeout | Confirmation did not arrive before your timeout; the transaction may still later confirm or fail. |
CommitDisconnected | The status stream disconnected before the outcome was known. |
CommitLagged | The subscriber lagged and may have dropped status events. |
Treat timeout, disconnect, lag, and submit errors after signing as unknown outcomes. Reconcile before submitting replacement orders. If submit_transaction_receipt() raises CommitDisconnected before submit, the helper did not submit the transaction.
If you abandon a receipt before confirmation, cancel any waiters and then close it. close() unsubscribes from future status events; it is not a way to make existing accepted() or confirmed() calls return.
receipt.close()Finish, cancel, or close outstanding receipts before await client.close(). Closing the client tears down the status stream, so active receipt waiters can observe CommitDisconnected.
Balances, stats, and history
Use authenticated maker calls for inventory and per-maker activity:
from flint.gen.sweetspot.api.v1.stats import messages_pb2 as stats
balances = await client.maker_balance()
maker_stats = await client.maker_stats()
volume = await client.maker_volume_breakdown()
nav = await client.maker_nav_history(stats.STATS_WINDOW_30D)
for balance in balances.balances:
print(balance.spot_id.id, balance.balance.value)
for point in nav.points:
print(point.ts.micros, point.nav_usd.value)These helpers return protobuf response messages. Historical trades and candles also use request messages from the API schema:
import time
from flint.gen.sweetspot.api.v1 import common_pb2
from flint.gen.sweetspot.api.v1.historical import messages_pb2 as hist
now = int(time.time() * 1_000_000)
pair = common_pb2.Pair(
base=common_pb2.SpotId(id=1),
quote=common_pb2.SpotId(id=0),
)
trades = await client.historical_trades(
hist.GetTradesRequest(
pair=pair,
start=common_pb2.Timestamp(micros=now - 60 * 60 * 1_000_000),
end=common_pb2.Timestamp(micros=now),
limit=100,
)
)See Historical queries for pagination and live-plus-historical stitching.
Decimals and units
All wire money values are decimal strings. Parse response fields with decimal.Decimal, not float:
from decimal import Decimal
notional = Decimal(trade.price.value) * Decimal(trade.size.value)All human money inputs accepted by the quoting helpers should be decimal strings, Decimal, or integers. Floats are rejected because binary floating point can silently change prices and sizes.
Price conversion
Use human_to_oracle_for_quoting only when you are manually building instruction payloads or nonzero CrossSpreadUpdate.spread values. Normal QuoteBuilder price and size parameters already call the correct conversion helper. See Prices, sizes, units.
Advanced RPC access
Client exposes public generated service clients for RPCs that do not yet have a handwritten helper:
from flint.gen.sweetspot.api.v1 import common_pb2
from flint.gen.sweetspot.api.v1.market_data import messages_pb2 as md
pair = common_pb2.Pair(
base=common_pb2.SpotId(id=1),
quote=common_pb2.SpotId(id=0),
)
response = await client.market_data.GetBook(
md.GetBookRequest(pair=pair, level=common_pb2.FEED_LEVEL_L2)
)Prefer the high-level helpers when they exist; they keep endpoint selection and auth metadata consistent. For authenticated RPCs without a helper, wire the generated stub with AuthFlow directly so every call gets fresh bearer metadata.
Production checklist
- Use a public-only
Clientfor read-only processes. - Pin the program id from trusted config and call
verify_program_id()before building or submitting maker transactions. - Start
start_chain_tip()once in maker loops. - Reserve/persist
next_oracle_sequence,next_order_sequence, andnext_client_order_idbefore submitting packed transactions. - Use decimal strings or
Decimalat money boundaries. - Treat
CommitTimeout,CommitDisconnected, andCommitLaggedas unknown outcomes and reconcile before submitting replacement orders. - Close the client on shutdown.
Where to go next
| You want | Page |
|---|---|
| Run or fork a complete Python maker bot | sweetspot-maker-example |
| Create and fund a maker | Onboarding |
| Understand quote models and sequence safety | Quoting |
| Stream books and fills | Market data |
| Query historical trades and candles | Historical queries |
| Inspect a runnable script | Python examples |
Source
- Package:
python/ - Example bot:
sweetspot-maker-example - Examples:
examples/python/
