Skip to content

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:

StrategyUse it when
Linear distributionYou quote a ladder around a fair price.
Oracle offsetYou quote offsets from an external oracle/fair.
Order listYou 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 SimpleOracleOffset to 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:

ItemWhy
Public endpointMarket catalog, books, fills, and public stats.
Authenticated endpointMaker balances, chain tips, submit, and receipts.
Maker authority keypairThe registered Solana keypair allowed to quote for your maker id.
Trusted program idA config-pinned Flint program id, not just the id returned by an API call.
Funded maker accountDeposits, quote-side caps, and cross-spread settings for the markets you quote.
Durable bot statePersist sequence counters and client order ids across restarts.

The examples below assume:

  • base spot 1 quotes against quote spot 0;
  • your keypair is the registered maker authority;
  • fair_slot is the Solana slot associated with your current fair/oracle input;
  • FLINT_PROGRAM_ID and SOLANA_RPC_URL come from trusted bot config;
  • state is 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:

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

python
from flint import Client, Endpoints

client = Client(Endpoints(public_url="https://api.superis.exchange"))

Maker bots need both endpoints and a keypair:

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

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

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

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

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

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

  1. Build quote instructions from your current fair, spread, and inventory.
  2. Get a recent chain tip.
  3. Pack and sign one or more transactions.
  4. Submit each transaction and wait for a receipt.
  5. Persist the next sequence counters.
python
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:

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

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

StrategyFair helperParams/helper updates
Linear distributionlinear_fair(...)linear_params(...)
Oracle offsetoracle_fair(...)oracle_offset_params(...) with already-lowered SpotOffsetOrder values
Order listNoneorder_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:

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

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

ErrorMeaning
OnChainFailureThe chain reported a failed transaction status.
CommitTimeoutConfirmation did not arrive before your timeout; the transaction may still later confirm or fail.
CommitDisconnectedThe status stream disconnected before the outcome was known.
CommitLaggedThe 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.

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

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

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

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

python
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 Client for 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, and next_client_order_id before submitting packed transactions.
  • Use decimal strings or Decimal at money boundaries.
  • Treat CommitTimeout, CommitDisconnected, and CommitLagged as unknown outcomes and reconcile before submitting replacement orders.
  • Close the client on shutdown.

Where to go next

You wantPage
Run or fork a complete Python maker botsweetspot-maker-example
Create and fund a makerOnboarding
Understand quote models and sequence safetyQuoting
Stream books and fillsMarket data
Query historical trades and candlesHistorical queries
Inspect a runnable scriptPython examples

Source

Build on Solana