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
- Read the market grid, mark, account health and slot sequences. Use one writer per trader and perp.
- Choose a strategy, prices, sizes and an expiry. Keep quote sizes within the account's margin limits.
- Write the quote, save its signature before submission and confirm it through RPC.
- Check slot state and watch fills. Reconcile after a disconnect before allocating new sequences or replacing quotes.
- 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:
- Install or restore depth. Stop and reconcile prior writes before
resetting the strategy. Send one transaction
with
update_quotinginstructions forReset(Density)followed byDensityBands, orReset(RelativeDensity)followed byRelativeDensityBands, thenDensityFairwithenable: true. Thedensityandrelative-densitymodes in onboarding step 6 demonstrate this installation. - Refresh prices. Send
update_density_fairswith a current fair for each perp. Setfair.slotto the slot of the price observation. - Refresh before expiry. With a current price source, re-stamp the
fair before
slot_delta_expiryeven 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("ing)
.instruction();
let transactions = core.build_txs(&[vec![instruction]])?;
core.submit(transactions, RetryConfig::default()).await
}import {
AccountRole,
address,
type Address,
type TransactionSigner,
} from "@solana/kit";
import {
findQuotingPda,
getUpdateDensityFairsInstruction,
withRemainingAccounts,
} from "@superis-labs/flint-margin-maker-client";
const SYSVAR_CLOCK_ADDRESS = address("SysvarC1ock11111111111111111111111111111111");
export interface Fair {
perpId: number;
entryIndex: number;
bidTicks: number;
askTicks: number;
}
/** One instruction re-stamping the fair on every perp in `fairs`. */
export async function restamp(
quoter: TransactionSigner,
delegateTraderAccount: Address,
fairs: Fair[],
slot: bigint,
nextSequence: () => bigint,
) {
const quoting = await Promise.all(
fairs.map(async ({ perpId }) => (await findQuotingPda({ linearPerpId: perpId }))[0]),
);
const instruction = getUpdateDensityFairsInstruction({
signer: quoter,
authority: delegateTraderAccount,
clock: SYSVAR_CLOCK_ADDRESS,
allowPartialSuccess: true,
updates: fairs.map((fair) => ({
linearPerpId: fair.perpId,
entryIndex: fair.entryIndex,
sequence: nextSequence(),
fair: { slot, bidFair: fair.bidTicks, askFair: fair.askTicks },
})),
});
return withRemainingAccounts(
instruction,
quoting.map((address) => ({ address, role: AccountRole.WRITABLE })),
);
}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:
- Open
SubscribeTxStatusbefore you submit to receive the outcome. It streams every transaction submitted under your session's trader. Match each event by signature. - Save the signed transaction's signature and blockhash before dispatch.
Send it with
SubmitTxand compare the returned signature with the saved one. - Wait for
landed(seen at processed commitment with no error) orfailed(reverted, or expired before landing, with areason).ackmeans accepted for submission only. Before reporting success, requireerr == nullat confirmed or finalized RPC commitment for the same signature. A processed landing can still roll back. - 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:
| Stream | Carries |
|---|---|
SubscribeBlockhash | blockhash, last_valid_block_height and recommended_cu_price (micro-lamports per CU; 0 means unavailable) |
SubscribeSlots | the current slot, for stamping fairs and expiries |
SubscribeLeaders | upcoming 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):
| RPC | Use |
|---|---|
SubscribeFills(perp_ids) | Live fills where you were the maker. Empty perp_ids means all. |
GetFills | Paged history, newest first (limit up to 1,000, cursor). |
SubscribeMarketSnapshots | Each perp's best bid/ask, depth, last price, and your best bid and ask (maker_bid, maker_ask). |
GetQuotingState | Your slots as the program last saw them: entry_index, enabled, strategy, order_sequence, fair_sequence, and the decoded strategy as payload_json. |
SubscribeQuotes / ListRecentQuotes | Your 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 / GetVolumeSeries | Your maker volume by perp and window. |
GetMarkoutSummary | Markout totals, buckets and per-perp rows over a window. GetFills can also sort by markout (MAKER_FILL_ORDER_ABS_MARKOUT_DESC). |
GetStats | Rates 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.
| Tool | Signer | Effect | Undo |
|---|---|---|---|
update_quoting { enable: false, update: Keep } | Owner or Trade delegate | Slot offers no fillable quote; strategy bytes kept | enable: true with Keep restores retained levels that have not expired; check prices first |
| Empty sides | Owner, Trade (update_quoting) or Quote (update_order_lists with count: 0) | Clears levels | Write new levels |
| Stop re-stamping density fairs | None | Density bands expire slot_delta_expiry slots after the last fair | Publish a fresh fair; reset and reinstall if filled depth must be restored |
| Stop rewriting order-list levels | None | Each level expires at its absolute slot_expiry | Rewrite levels with current prices, quantities, and new slot_expiry values |
release_quoting_entry | Owner, Trade delegate, or Flint's admin | Slot off the book and open to takeover | open_quoting_entry again, starting from an empty slot |
cancel_all_perp_orders | Owner or Trade delegate | Cancels your resting orders on one perp, including reduce-only orders | Place 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
enabledflag (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 slot | Resting orders | |
|---|---|---|
| Access | Flint's MAY_QUOTE grant; one slot per perp | Open to anyone; up to 16 orders per trader per perp |
| Writes | Sequence-ordered batch writes | One place_perp_order / modify_perp_order per order |
| Margin | Checked only at fill time; nothing reserved | A non-reduce-only order reserves initial margin when placed |
| A level that fails margin | None of that level or deeper quote levels is offered for that taker order | That order is skipped; later ones can still trade |
| Liquidations | Every enabled slot must be included | Optional for the liquidator |
| Removing liquidity | Disable, or let it expire | cancel_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.
| Strategy | You publish | Each refresh | Capacity per side |
|---|---|---|---|
| OrderList | Explicit levels: price, quantity, your order_id, an expiry slot | The changed side(s), whole | 21 levels |
| Density | A fair (bid and ask) plus bands at fixed tick offsets from it | The fair | 30 bands |
| RelativeDensity | A fair plus bands at a fraction of the fair | The fair | 30 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:
| Field | Meaning |
|---|---|
price | Ticks. 0 ends the side; nothing after it is read. |
quantity | Lots available. 0 offers no quantity here but does not end the side. |
order_id | Your own identifier, u64. The program does not interpret it. |
slot_expiry | Absolute 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 frombid_fair; ask prices increase fromask_fair. Two fairs let you skew or widen without changing the bands.slotrecords when you obtained the price.DensityBandOffset { density, start_offset, end_offset, slot_delta_expiry }:densitylots on every tick fromstart_offsettoend_offsetticks away from the fair (inclusive;0is the fair itself). The band stops quoting once the current slot isslot_delta_expiryor 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
Orderstream for levels or bands. Density strategies also have aFairstream. The streams have separate counters. - A new stream starts at
0. Its first write uses1. Gaps are allowed. - Read
order_sequenceandfair_sequencefromGetQuotingStateat startup. Allocate counters above the accepted values. Use one writer per trader and perp. A timestamp does not guarantee a higher sequence. - Rust's
QuotingCoreallocates values above those it has read or sent.QuoteBuilderuses the same counters. Callrefresh_quoting_state()after opening a slot or after another process writes to it. QuotingUpdate::Resetclears the strategy and starts its counters at0. 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:
- 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.
- Read
GetQuotingState. Select the entry for the trader and perp. Save its exactentry_index, stream and accepted sequence asexpected_sequence. AnOrderListhas only anOrderstream. - Build one
reset_quoting_sequencesupdate, withallow_partial_success: false. Use the owner with the trader PDA asauthority, or a Quote delegate with its delegate PDA asauthority. Append the writable quoting PDA, as shown below. - Sign and submit through your configured RPC or
TxService. Save the signature and require confirmed or finalized status witherr: null. Apply transaction recovery if the outcome is unknown. - 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. - Restart the writer and allocate
1for the reset stream's next write. Recreate Rust'sQuotingCoreafter 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()
}import { AccountRole, address, type Address, type TransactionSigner } from "@solana/kit";
import {
findQuotingPda,
getResetQuotingSequencesInstruction,
QuoteStream,
withRemainingAccounts,
} from "@superis-labs/flint-margin-maker-client";
export async function resetSequence(
signer: TransactionSigner,
authority: Address,
perpId: number,
entryIndex: number,
stream: QuoteStream,
expectedSequence: bigint,
) {
const [quoting] = await findQuotingPda({ linearPerpId: perpId });
const instruction = getResetQuotingSequencesInstruction({
signer,
authority,
clock: address("SysvarC1ock11111111111111111111111111111111"),
allowPartialSuccess: false,
updates: [{ linearPerpId: perpId, entryIndex, stream, expectedSequence }],
});
return withRemainingAccounts(instruction, [{ address: quoting, role: AccountRole.WRITABLE }]);
}The instructions
| Instruction | Signer | Writes | Entry index |
|---|---|---|---|
update_quoting | Owner, or Trade delegate | One slot: one QuotingUpdate and the enable flag | Hint; the program searches if incorrect |
update_density_fairs | Owner, or Quote delegate | Fairs for many perps; keeps flags, bands and fill risk | Must be exact |
update_order_lists | Owner, or Quote delegate | Order-list sides for many perps; keeps flags | Must be exact |
reset_quoting_sequences | Owner, or Quote delegate | Restarts one stream per entry | Must be exact |
open_quoting_entry | Owner, or Trade delegate, with MAY_QUOTE | Opens your slot on one perp | None; read it from GetQuotingState |
release_quoting_entry | Owner, Trade delegate, or Flint's admin | Disables your slot and allows reuse | A hint |
update_quoting carries one QuotingUpdate:
| Variant | Effect | Stream |
|---|---|---|
Keep | Nothing; use it to flip enable alone | none |
Reset(kind) | Discard the strategy; start an empty OrderList, Density or RelativeDensity | resets all |
OrderList { sequence, bids, asks } | Replace sides of an order list | Order |
DensityBands { sequence, bids, asks } | Replace sides of a density strategy | Order |
RelativeDensityBands { sequence, bids, asks } | Replace sides of a relative-density strategy | Order |
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:
signer: the key signing the transaction;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 fromsigner;clock: the Clock sysvar;- 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 perps | CU ceiling in the program's benchmarks |
|---|---|
update_density_fairs, owner signing | 506 |
update_density_fairs, delegate (no expiry / with expiry) | 522 / 561 |
update_density_fairs, partial success | 607 |
update_order_lists, atomic / partial success | 1,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
| Goal | Page |
|---|---|
| Margin, caps, liquidation, key rotation | Risk & inventory |
| Tick and lot arithmetic | Prices, sizes, units |
| Maker rebates | Fees |
| Builders, PDAs and decoders | On-chain clients |
| Error codes from refused writes | Errors |