Skip to content
Logo

Quoting

Publish quotes through your bot into one slot per perp. Taker orders can fill those quotes, and maker fills earn a rebate. Use Onboarding to prepare the account, bot key and first slot. The SDK examples publish once; your bot controls continuous quoting.

Operating flow

  1. Read the market grid, mark, account health and slot sequences. Use one writer per trader and perp.
  2. Choose a strategy, prices, sizes and an expiry. Keep quote sizes within the account's margin limits.
  3. Write the quote, save its signature before submission and confirm it through RPC.
  4. Check slot state and watch fills. Reconcile after a disconnect before allocating new sequences or replacing quotes.
  5. Stop and reconcile pending writes, then disable quotes. Cancel resting orders and close positions separately.

Quote update loop

For OrderList, replace each changed side with current levels and absolute expiry slots. A fill consumes quantity until you rewrite that side.

For Density or RelativeDensity, install the bands and fair, then update the fair on each refresh cycle:

  1. Install or restore depth. Stop and reconcile prior writes before resetting the strategy. Send one transaction with update_quoting instructions for Reset(Density) followed by DensityBands, or Reset(RelativeDensity) followed by RelativeDensityBands, then DensityFair with enable: true. The density and relative-density modes in onboarding step 6 demonstrate this installation.
  2. Refresh prices. Send update_density_fairs with a current fair for each perp. Set fair.slot to the slot of the price observation.
  3. Refresh before expiry. With a current price source, re-stamp the fair before slot_delta_expiry even when the price is unchanged. If the price source fails, stop the loop and disable the quote.

Choose the refresh interval and expiry from your maximum acceptable price age. Measure the network's slot rate when converting slots to time.

SDK helpers for batched fair updates

These helpers update existing density strategies. They use partial success; read each perp's quoting state to check which updates applied. Keep each instruction within the transaction size limit.

use flint_margin_maker_client::Result;
use flint_margin_maker_client::onchain::accounts::LinearPerpQuoting;
use flint_margin_maker_client::onchain::instructions::UpdateDensityFairsBuilder;
use flint_margin_maker_client::onchain::types::{DensityFair, DensityFairUpdate};
use flint_margin_maker_client::quoting::{QuotingCore, RetryConfig, SubmitResult};
use solana_sdk::instruction::AccountMeta;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signer::Signer;
use solana_sdk_ids::sysvar::clock;
 
pub struct Fair {
    pub perp_id: u32,
    pub entry_index: u32,
    pub bid_ticks: u32,
    pub ask_ticks: u32,
}
 
/// Re-stamps the fair on every perp in `fairs` with one `update_density_fairs`.
pub async fn restamp(
    core: &mut QuotingCore,
    delegate_account: Pubkey,
    fairs: &[Fair],
    slot: u64,
) -> Result<SubmitResult> {
    let mut updates = Vec::with_capacity(fairs.len());
    let mut quoting = Vec::with_capacity(fairs.len());
    for fair in fairs {
        updates.push(DensityFairUpdate {
            linear_perp_id: fair.perp_id,
            entry_index: fair.entry_index,
            sequence: core.next_fair_sequence(fair.perp_id)?,
            fair: DensityFair { slot, bid_fair: fair.bid_ticks, ask_fair: fair.ask_ticks },
        });
        let (account, _) = LinearPerpQuoting::find_pda(fair.perp_id);
        quoting.push(AccountMeta::new(account, false));
    }
    let instruction = UpdateDensityFairsBuilder::new()
        .signer(core.signer().pubkey())
        .authority(delegate_account)
        .clock(clock::ID)
        .allow_partial_success(true)
        .updates(updates)
        .add_remaining_accounts(&quoting)
        .instruction();
    let transactions = core.build_txs(&[vec![instruction]])?;
    core.submit(transactions, RetryConfig::default()).await
}

The Rust helper allocates sequences from the same core as QuoteBuilder. build_txs adds the compute budget and signs the transactions; submit opens status tracking and submits them. The caller must wait for the receipt and confirm each signature through RPC.

A Quote delegate can send these batch updates. Using QuoteBuilder with one density_fair per perp sends one update_quoting per perp, requires Trade permission and sets enable: true on every refresh.

update_order_lists is the batch equivalent for order lists. It carries one shared pool of levels plus, per perp, a bids and asks range into that pool (offset, count): a count of 255 preserves the side, 0 empties it.

Submitting

Through TxService

TxService.SubmitTx takes a fully signed legacy or v0 transaction and returns its signature once the server accepts it for submission. SubscribeTxStatus reports processed outcomes. Confirm the signature through RPC before reporting success.

The server rejects a transaction when:

  • the authenticated quoting key is not one of its required signers;
  • any required signature is missing;
  • it invokes any program other than the Flint margin program, the Compute Budget program and the System program (or does not invoke the margin program at all).

Submit and track each transaction:

  1. Open SubscribeTxStatus before you submit to receive the outcome. It streams every transaction submitted under your session's trader. Match each event by signature.
  2. Save the signed transaction's signature and blockhash before dispatch. Send it with SubmitTx and compare the returned signature with the saved one.
  3. Wait for landed (seen at processed commitment with no error) or failed (reverted, or expired before landing, with a reason). ack means accepted for submission only. Before reporting success, require err == null at confirmed or finalized RPC commitment for the same signature. A processed landing can still roll back.
  4. Reconcile failures against RPC status and current account state before rebuilding. A transport error or missing acknowledgement can leave execution unknown. Follow transaction recovery, then use a fresh blockhash, fresh sequence numbers and current prices for any replacement.

In Rust, QuotingCore::submit opens the status stream before submission. QuoteBuilder::commit calls it. Transient submission failures (UNAVAILABLE, a deadline, an expired session or a full queue) trigger retries of the same signed bytes: three attempts by default (RetryConfig). The signature stays unchanged.

submit returns a SubmitResult after submission attempts, without waiting for execution. Inspect failed for rejected or unacknowledged submissions. Wait on receipt.settled_within(timeout) for acknowledged transactions; the receipt tracks processed outcomes only. With QuoteBuilder::commit, it also sorts the builder's intents into landed, failed and unknown. Then require confirmed or finalized RPC status with err: null for each signature. Treat unknown outcomes, timeouts and lost status streams as unresolved execution; check signatures and state before rebuilding.

With the crate's rpc feature, with_rpcs also broadcasts each signed transaction through your own Solana RPC nodes and with_quic sends it directly to the upcoming leaders over QUIC. The receipt still learns outcomes only from the server's status stream.

The other TxService streams provide chain data:

StreamCarries
SubscribeBlockhashblockhash, last_valid_block_height and recommended_cu_price (micro-lamports per CU; 0 means unavailable)
SubscribeSlotsthe current slot, for stamping fairs and expiries
SubscribeLeadersupcoming leader windows (start_slot, end_slot, TPU address)

Directly over RPC

You can submit quote transactions directly through Solana RPC. Save each signature before dispatch and confirm it through RPC. The server's SubscribeTxStatus does not track direct submissions.

In TypeScript, withTxFallback(txClient, rpc) sends SubmitTx, SubscribeBlockhash and SubscribeSlots requests to RPC when opening the server request fails with a supported transport error. It does not switch a stream that disconnects after delivering data. pollTxStatus tracks one explicit signature at processed commitment; confirm it separately through RPC. It does not replace the trader-scoped SubscribeTxStatus stream.

Watching fills

Fills reach you on MakerService (same bearer session as TxService):

RPCUse
SubscribeFills(perp_ids)Live fills where you were the maker. Empty perp_ids means all.
GetFillsPaged history, newest first (limit up to 1,000, cursor).
SubscribeMarketSnapshotsEach perp's best bid/ask, depth, last price, and your best bid and ask (maker_bid, maker_ask).
GetQuotingStateYour slots as the program last saw them: entry_index, enabled, strategy, order_sequence, fair_sequence, and the decoded strategy as payload_json.
SubscribeQuotes / ListRecentQuotesYour quote writes that landed, decoded: kind, sequence, payload_json, applied and enabled_after when available. Partial-success batches do not report individual results; reconcile current quoting state. Filter by perp_ids and quoters. GetQuote fetches one by signature and instruction.
GetVolumeBreakdown / GetVolumeSeriesYour maker volume by perp and window.
GetMarkoutSummaryMarkout totals, buckets and per-perp rows over a window. GetFills can also sort by markout (MAKER_FILL_ORDER_ABS_MARKOUT_DESC).
GetStatsRates of fills, transactions sent, landed and failed, and quote updates.

Read collateral and margin from the public AccountService.GetAccount / SubscribeAccount (in Rust, client.account() and client.subscribe_account(...)). See Positions & PnL.

A FillEvent gives perp_id, your side (the maker's direction), price and size (human units, decimal strings), slot, ts, signature, taker_id and maker_rebate (in the perp's PnL currency).

After a fill, reconcile positions and remaining quote depth before updating prices or sizes. A density fill consumes depth until a strategy reset; see Fill risk.

The Rust Client wraps the streams: subscribe_maker_fills, subscribe_quotes and subscribe_maker_market_snapshots reconnect, resend their filters and re-authenticate on their own. Fill and quote streams do not replay events missed while disconnected. After a gap (when disconnects() increases), reconcile fills with GetFills and quote writes with ListRecentQuotes (or GetQuotingState if you only need the current state).

Stop quoting

First stop and reconcile pending writes. A signed update can execute after a disable and set enable: true again. This step also applies before planned key rotation, a strategy reset or reopening a released slot.

Disable each slot and require confirmed or finalized RPC success. Read its state to verify enabled: false. Cancel resting orders separately. Close existing positions with reduce-only orders if leaving the market.

ToolSignerEffectUndo
update_quoting { enable: false, update: Keep }Owner or Trade delegateSlot offers no fillable quote; strategy bytes keptenable: true with Keep restores retained levels that have not expired; check prices first
Empty sidesOwner, Trade (update_quoting) or Quote (update_order_lists with count: 0)Clears levelsWrite new levels
Stop re-stamping density fairsNoneDensity bands expire slot_delta_expiry slots after the last fairPublish a fresh fair; reset and reinstall if filled depth must be restored
Stop rewriting order-list levelsNoneEach level expires at its absolute slot_expiryRewrite levels with current prices, quantities, and new slot_expiry values
release_quoting_entryOwner, Trade delegate, or Flint's adminSlot off the book and open to takeoveropen_quoting_entry again, starting from an empty slot
cancel_all_perp_ordersOwner or Trade delegateCancels your resting orders on one perp, including reduce-only ordersPlace new orders

A disable removes the slot's liquidity when it executes. It uses one instruction per perp; several can fit in one transaction. In Rust, build QuoteBuilder::new().enable(perp_id, false) for each perp, call commit, then wait for the receipt and RPC confirmation.

Revoking a bot key does not remove stored quotes or resting orders. It removes that key's delegated authority on the trader. Disable quotes and cancel orders separately, or verify that they have expired. The revoked key cannot receive another grant on this trader. Follow key rotation for the full procedure.

Expiry stops quotes automatically if your process, network, or TxService fails. Choose the maximum price age you can accept in slots. For density bands, use that age as slot_delta_expiry. For order-list levels, set the absolute slot_expiry to the price's slot plus that age. Batched fair updates cannot change the enabled flag. A Quote delegate can stop re-stamping; a Trade delegate can also disable the slot. Grant Trade if your bot must disable its quotes. QuoteBuilder strategy writes set enable: true, so reconcile pending writes before the final disable.

One slot per perp

Each perp has one quoting account (PDA ["quoting", perp_id]) holding up to 32 maker slots. Your slot holds:

  • your trader_id;
  • an enabled flag (a disabled slot offers no fillable quote, even when it contains levels or bands);
  • one strategy, a fixed 1 KiB buffer holding your levels or bands.

You open your own slot with open_quoting_entry once Flint has granted your account MAY_QUOTE (Onboarding step 5). It starts disabled, running an empty order list. Slots never move, so the entry_index from MakerService.GetQuotingState stays valid while you hold the slot.

release_quoting_entry disables your slot and allows another maker to reuse it. The owner, a Trade delegate or Flint's admin can sign it. The slot stays where it is and stays yours (you can open it again) until another maker opening a slot on that perp takes it over. Its rent stays with the quoting account. Opening again resets it: disabled, an empty order list, sequences back at 0. Before reopening, follow Stop and reconcile pending writes. If another maker took your old slot, you get a different one. Read GetQuotingState after opening.

When a taker crosses, the program reads the maker slots included in the order and merges their live levels by price. Each quoted level must pass a margin check for its full quantity after the position cap is applied (see Risk → Margin at fill time).

Quotes and resting orders

Each perp also has an order book of resting limit orders, placed with place_perp_order (Gtc or Gtd, with optional post_only; see Orders). It needs no grant: any trader can rest orders, makers included, alongside a slot or instead of one.

A taker's order crosses both in one pass:

  • quotes and resting orders are merged best price first;
  • levels and orders at the same price share partial fills pro rata, with no time priority;
  • the taker can cross only the maker accounts included in its order, for both quoting slots and resting orders;
  • your slot and your orders use one margin allowance per crossing, so together they cannot fill more than your account can support.

The slot and order book have these rules:

Quoting slotResting orders
AccessFlint's MAY_QUOTE grant; one slot per perpOpen to anyone; up to 16 orders per trader per perp
WritesSequence-ordered batch writesOne place_perp_order / modify_perp_order per order
MarginChecked only at fill time; nothing reservedA non-reduce-only order reserves initial margin when placed
A level that fails marginNone of that level or deeper quote levels is offered for that taker orderThat order is skipped; later ones can still trade
LiquidationsEvery enabled slot must be includedOptional for the liquidator
Removing liquidityDisable, or let it expirecancel_all_perp_orders per perp, or a Gtd order's expires_at_slot

Once your account is below initial margin, anyone can send force_cancel_perp_orders to cancel your margin-reserving orders on a perp; reduce-only orders stay. The order book is not indexed by the API yet, so watch your own orders on chain.

Strategies

A slot runs exactly one strategy at a time. Switching means a Reset, which discards the old one.

StrategyYou publishEach refreshCapacity per side
OrderListExplicit levels: price, quantity, your order_id, an expiry slotThe changed side(s), whole21 levels
DensityA fair (bid and ask) plus bands at fixed tick offsets from itThe fair30 bands
RelativeDensityA fair plus bands at a fraction of the fairThe fair30 bands

These capacities fit the 1 KiB strategy buffer. A write that exceeds capacity is refused. The transaction limit (4,096 bytes for v1, 1,232 for legacy or v0) usually binds before the strategy buffer.

Prices and sizes

Every price in a quote is in ticks and every size in lots on the perp's grid. Convert from human units with the market's tick_size and lot_size (PerpMarketService.ListPerpMarkets on the public API); Prices, sizes, units has the arithmetic. Fills and balances come back from MakerService as decimal strings already scaled to human units; parse them with a decimal library, never a float.

OrderList

A list of levels per side, best price first (bids descending, asks ascending). Each OrderListLevel is:

FieldMeaning
priceTicks. 0 ends the side; nothing after it is read.
quantityLots available. 0 offers no quantity here but does not end the side.
order_idYour own identifier, u64. The program does not interpret it.
slot_expiryAbsolute slot at which the level stops quoting.

The program stops at the first level whose price is not strictly deeper than the previous price. It ignores that level and all later levels without returning an error. Expired or empty levels are skipped without ending the side, so an expired level can precede live levels.

Fills reduce quantity on the filled levels, best price first. A filled level stays empty until you rewrite that side.

Density

A fair, your chosen reference price, plus bands at offsets from it:

  • DensityFair { slot, bid_fair, ask_fair }: bid prices decrease from bid_fair; ask prices increase from ask_fair. Two fairs let you skew or widen without changing the bands. slot records when you obtained the price.
  • DensityBandOffset { density, start_offset, end_offset, slot_delta_expiry }: density lots on every tick from start_offset to end_offset ticks away from the fair (inclusive; 0 is the fair itself). The band stops quoting once the current slot is slot_delta_expiry or more slots past the fair's stamp.

Bands are listed nearest the fair first. A zero density, or an end offset below its start, ends the side.

Every band's expiry counts from the fair's stamp. Re-stamp the fair to extend the bands' expiry. If you stop publishing fairs, the quote expires automatically. See Stop quoting.

RelativeDensity

The same as Density, but each band's start_offset and end_offset are fractions of the fair (DensityBandRelative, scaled by 2^32 − 1). Both edges round away from the fair, so a band never quotes tighter than you asked. A ladder written once keeps its width in basis points as the price moves.

Fill risk on density quotes

A density strategy stores a per-side filled-lot counter. Each fill adds to that counter, and that many lots are subtracted from the nearest ticks of that side before anything is quoted. After a 30-lot hit on your bid, the first 30 lots of bid depth stay consumed while the bands stay as written.

Fair prices

Every fair and level is an absolute tick price that you publish. RelativeDensity scales offsets with that fair. Perps have no oracle-pegged strategy. The market's oracle-derived mark affects quote execution:

  • your margin is checked against the perp's mark at fill time, and a fill priced worse than the mark reduces available margin;
  • when the perp's mark goes stale, the program refuses orders on it, so nothing fills (see Risk → Stale prices).

Compare your fair with the mark and set deviation limits in your bot. Poll the public PerpMarketService.GetMarketStats for mark_price, index_price and oracle_price; SubscribeMarketStats is not served yet.

Sequence numbers

Transactions can execute in a different order from submission. Level, band and fair writes carry sequence numbers. The program refuses a write at or below its stream's accepted value. Accepted values increase until a reset. A refused write can fail its transaction.

  • Each slot has an Order stream for levels or bands. Density strategies also have a Fair stream. The streams have separate counters.
  • A new stream starts at 0. Its first write uses 1. Gaps are allowed.
  • Read order_sequence and fair_sequence from GetQuotingState at startup. Allocate counters above the accepted values. Use one writer per trader and perp. A timestamp does not guarantee a higher sequence.
  • Rust's QuotingCore allocates values above those it has read or sent. QuoteBuilder uses the same counters. Call refresh_quoting_state() after opening a slot or after another process writes to it.
  • QuotingUpdate::Reset clears the strategy and starts its counters at 0. Stop and reconcile pending writes before resetting, then reinstall the quote data before enabling the slot.
  • A guarded sequence reset sets only the selected counter to 0. It keeps the strategy, levels, fairs, filled-depth counters and enabled flag.

Reset and the enabled flag have no sequence counter. Concurrent enable and disable writes apply in execution order. Resetting a counter can also allow a pending older transaction to pass its sequence check. Follow Stop and reconcile pending writes before a reset, reopening, shutdown or planned key rotation.

Guarded sequence reset

Use this procedure when a counter reaches u64::MAX, or when you need to restart a counter without clearing quote data. If the counter is only ahead of your saved value, you can read it and allocate a higher value instead. The downloadable CLI examples do not expose a sequence-reset command. Build this instruction through the SDK:

  1. Stop and reconcile pending writes for the trader and perp. No earlier signed transaction may remain able to execute after the reset. Disable and confirm the slot if its stored quote should remain unavailable during recovery.
  2. Read GetQuotingState. Select the entry for the trader and perp. Save its exact entry_index, stream and accepted sequence as expected_sequence. An OrderList has only an Order stream.
  3. Build one reset_quoting_sequences update, with allow_partial_success: false. Use the owner with the trader PDA as authority, or a Quote delegate with its delegate PDA as authority. Append the writable quoting PDA, as shown below.
  4. Sign and submit through your configured RPC or TxService. Save the signature and require confirmed or finalized status with err: null. Apply transaction recovery if the outcome is unknown.
  5. Read the entry again. The selected counter must be 0; the other counter and quote data must match the saved state. If the guard failed, read the current state and investigate another writer before trying again.
  6. Restart the writer and allocate 1 for the reset stream's next write. Recreate Rust's QuotingCore after a confirmed reset. Refreshing an existing core keeps its highest local counter and cannot clear an exhausted counter. Re-read both counters before enabling or updating the quote.
SDK helpers for a guarded reset

These helpers build the instruction. Use the transaction submission path to sign, submit and confirm it.

use flint_margin_maker_client::onchain::accounts::LinearPerpQuoting;
use flint_margin_maker_client::onchain::instructions::ResetQuotingSequencesBuilder;
use flint_margin_maker_client::onchain::types::{QuoteStream, QuotingSequenceReset};
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::pubkey::Pubkey;
use solana_sdk_ids::sysvar::clock;
 
pub fn reset_sequence(
    signer: Pubkey,
    authority: Pubkey,
    perp_id: u32,
    entry_index: u32,
    stream: QuoteStream,
    expected_sequence: u64,
) -> Instruction {
    let (quoting, _) = LinearPerpQuoting::find_pda(perp_id);
    ResetQuotingSequencesBuilder::new()
        .signer(signer)
        .authority(authority)
        .clock(clock::ID)
        .allow_partial_success(false)
        .updates(vec![QuotingSequenceReset {
            linear_perp_id: perp_id,
            entry_index,
            stream,
            expected_sequence,
        }])
        .add_remaining_account(AccountMeta::new(quoting, false))
        .instruction()
}

The instructions

InstructionSignerWritesEntry index
update_quotingOwner, or Trade delegateOne slot: one QuotingUpdate and the enable flagHint; the program searches if incorrect
update_density_fairsOwner, or Quote delegateFairs for many perps; keeps flags, bands and fill riskMust be exact
update_order_listsOwner, or Quote delegateOrder-list sides for many perps; keeps flagsMust be exact
reset_quoting_sequencesOwner, or Quote delegateRestarts one stream per entryMust be exact
open_quoting_entryOwner, or Trade delegate, with MAY_QUOTEOpens your slot on one perpNone; read it from GetQuotingState
release_quoting_entryOwner, Trade delegate, or Flint's adminDisables your slot and allows reuseA hint

update_quoting carries one QuotingUpdate:

VariantEffectStream
KeepNothing; use it to flip enable alonenone
Reset(kind)Discard the strategy; start an empty OrderList, Density or RelativeDensityresets all
OrderList { sequence, bids, asks }Replace sides of an order listOrder
DensityBands { sequence, bids, asks }Replace sides of a density strategyOrder
RelativeDensityBands { sequence, bids, asks }Replace sides of a relative-density strategyOrder
DensityFair { sequence, fair }Move the fair (either density kind)Fair

For each side, None leaves it as it was, Some([]) empties it, and Some(levels) replaces it whole, clearing anything past the new run. Update both sides when both prices change. An unchanged stale side can still fill at its old price. The strategy write applies first and the flag second, so a rejected write cannot enable the slot with an unintended quote.

In Rust, QuoteBuilder has a method per variant: reset, enable, order_list, density_bands, relative_density_bands and density_fair, plus update for a raw QuotingUpdate. Each write uses one update_quoting, and all of one perp's writes in a build go in one transaction. reset writes enable: false; the four strategy writes all write enable: true, so any of them switches a disabled slot back on. To write a slot and keep it disabled, use update(perp_id, false, ...).

The three batch instructions update many perps in one instruction. Their accounts are:

  1. signer: the key signing the transaction;
  2. authority: your trader account if the owner signs, or the delegate account (PDA ["delegate_trader", trader_id, quoter]) if a delegate signs. It must differ from signer;
  3. clock: the Clock sysvar;
  4. then one writable quoting account per update, in the same order as the updates, each one distinct. Use one update per perp per batch.

allow_partial_success: true skips entries that fail, such as a stale sequence, and succeeds if any applied; false makes the batch atomic.

Size and compute units

A v1 transaction (SIMD-0385) is capped at 4,096 bytes and 64 distinct accounts; legacy and v0 transactions at 1,232 bytes. Pack updates within the limit and split batches across transactions when needed.

The program's density-fair benchmarks and order-list tests use these compute-unit ceilings for eight-perp batches. Measure your own transactions before choosing a limit:

Instruction, 8 perpsCU ceiling in the program's benchmarks
update_density_fairs, owner signing506
update_density_fairs, delegate (no expiry / with expiry)522 / 561
update_density_fairs, partial success607
update_order_lists, atomic / partial success1,999 / 2,191

The priority fee is charged on the requested limit. Add a Compute Budget SetComputeUnitLimit sized to your measured usage plus headroom, and a SetComputeUnitPrice from recommended_cu_price. Simulate your own transaction once to get the number; account count and level count affect compute usage.

A v1 transaction carries these in its message header instead of as instructions, and states the priority fee as total lamports: ceil(price × limit / 1,000,000), the same fee the legacy instructions cost. An absent v1 limit means 0, not the runtime default, so always set the CU limit and the loaded-accounts cap.

The Rust QuotingCore sets both on every transaction it builds: the limit from QuotingConfig::compute_unit_limit, which defaults to 200,000, and the price from compute_unit_price or, when that is unset, the stream's recommended_cu_price. It builds v1 transactions by default (QuotingConfig::tx_version). Lower the limit to your measured usage. It packs the builder's per-perp groups into as few transactions as fit. Each perp's group must fit in one transaction; the core rejects a group that exceeds the limit.

Leader-window advisories

SpreadProtectService.SubscribeSpreadProtect (authenticated listeners only) streams Flint's advice on when to quote wider. It opens with a ResetState carrying the baseline default_spread_mult, then sends a WidenWindow (start_slot, end_slot inclusive, spread_mult of at least 1.0 as a decimal string) for each upcoming leader window Flint flags. The same windows go to every maker.

The program does not read this advice or widen your quote. In Rust, QuotingCore follows the feed itself: core.spread_mult() returns the multiplier for the current slot as a Decimal, and core.spread_protect_window() the window in force. Your bot chooses whether to apply the multiplier to its spreads.

Where to go next

GoalPage
Margin, caps, liquidation, key rotationRisk & inventory
Tick and lot arithmeticPrices, sizes, units
Maker rebatesFees
Builders, PDAs and decodersOn-chain clients
Error codes from refused writesErrors