Skip to main content

Predict

Trading on DeepBook Predict runs through the deepbook_predict::expiry_market module and, from protocol version 4, through the queue module of the deepbook_predict_orders package. One shared ExpiryMarket object exists per underlying and expiry, and it owns that expiry's USDC custody, strike exposure index, congestion estimator, mint pause flag, and flush stamp. Every call on this page is a public fun that you invoke from a programmable transaction block with a moveCall. Several of them take values only another command in the same block can produce, a transaction-local Pricer and a single-use account Auth, so plan the whole flow as one block.

The module lives at packages/predict/sources/expiry_market.move on DeepBookV3 main. Unless a section says otherwise, the Move snippets on this page pin DeepBookV3 at abb4cfd9, the protocol version 3 source that Mainnet runs. For the package and object IDs of each network, see Contract Information.

caution

The 2 networks no longer trade the same way. Mainnet runs Predict protocol version 3, where the mint and live-redeem functions on this page fill inside the trader's own transaction. Testnet runs protocol version 4, which retires those 4 functions: they keep their signatures and always abort with EDelayedExecutionRequired, and every trade is a queued order placed through the deepbook_predict_orders package. Queued orders covers the queued path. Pricer loading, the quotes, settled redemption, settlement, and the read accessors on this page apply to both networks, with the version 4 differences noted where they arise.

info

Collateral is the Move type usdc::usdc::USDC. On Mainnet that resolves to native USDC. On Testnet it is a mintable test coin at the same module path that displays as DUSDC and has no market value. Read the exact coin type from getConfig(network).quoteCoinType rather than assuming a symbol.

What is the ExpiryMarket shared object?​

ExpiryMarket has the key ability, and the registry shares it at creation, so any transaction can name it as an input. Authorization comes from the arguments a function demands, not from object ownership.

FieldTypeDescription
idUIDObject ID of the shared market.
propbook_underlying_idu32Propbook underlying this market prices against. The market stores no oracle object IDs.
expiryu64Settlement timestamp in Unix milliseconds.
cashExpiryCashUSDC custody plus the isolated inventory-impact reserve.
fee_incentive_balanceBalance<USDC>Sponsor-funded USDC available to subsidize taker fees on this expiry.
strike_exposureStrikeExposureExposure lifecycle state across this expiry's strike ticks.
ewmaEwmaStateSmoothed gas-price statistics behind the congestion surcharge.
mint_pausedboolWhen true, new mints abort. Every other flow stays available.
valuation_stampOption<ValuationStamp>some from a pool flush's snapshot stage until this market's value_expiry runs or the stamp goes stale. Trading never reads it and never waits on it.

The field order in the table is declaration order, which is also the Binary Canonical Serialization (BCS) order of the object. ValuationStamp holds the flush sequence number and the market's cash and inventory-impact reserve at the snapshot instant; see Vault for the flush that writes it.

Click to open
ExpiryMarket source

Load a pricer​

Every priced flow takes a Pricer, a snapshot of the oracle surface for one market. Build it with load_live_pricer, which reads the current canonical Propbook feeds for the market's underlying:

public fun load_live_pricer(
market: &ExpiryMarket,
config: &ProtocolConfig,
propbook_registry: &OracleRegistry,
pyth: &PythFeed,
bs_values: &BlockScholesValueStore,
bs_svi: &BlockScholesSVIStore,
clock: &Clock,
ctx: &TxContext,
): Pricer

The result has 3 properties that govern how you use it:

  • Abilities: Pricer has copy and drop but no store. You cannot save it in an object or carry it between transactions, so every transaction that prices anything must call load_live_pricer first. Because it has drop, a block that ends up not using the value is still valid. Only the package constructs and thaws the storable FrozenPricer that a pool flush keeps between transactions, and no trade function accepts it.
  • Market binding: A pricer binds to one market. It records market.id(), and any priced call that receives a pricer built for a different market aborts with EWrongPricer.
  • Oracle validation: The load takes 4 Propbook objects and validates 3 of them: it checks the PythFeed, the BlockScholesValueStore, and the BlockScholesSVIStore each against the underlying's current canonical binding, and a mismatch aborts inside pricing. The load does not check the OracleRegistry, because it is the authority the load checks the other 3 against. The load also rejects an observation written earlier in the same transaction, and it aborts with pricing::ELivePricingExpired at or after the market's expiry.

In a programmable transaction block, a priced flow therefore runs in this order:

  1. Call load_live_pricer for the market.
  2. Pass the resulting Pricer to each priced call on that same market in the same block, such as a quote, a mint, redeem_live, current_nav, or live_order_value.

The load judges freshness against each input's own source timestamp: the Pyth feed timestamp and, for the Block Scholes price, forward, and stochastic volatility inspired (SVI) inputs, the provider's per-update timestamp. Both deployments run the Pyth spot and Block Scholes price windows at 2,000 ms. A chain read on 2026-10-13 found the SVI window at 60,000 ms on Mainnet and 10,000 ms on Testnet.

See Oracle for how to discover the PythFeed, BlockScholesValueStore, and BlockScholesSVIStore object IDs for an underlying.

Click to open
load_live_pricer source

Quote a mint​

The module has 3 Move functions that price a prospective mint without changing market state. On protocol version 3 they apply the live-mint and admission gates, including the no-trade window, so a quote that succeeds tells you the trade is admissible at the current oracle state. None is a full preflight, and the section below is careful about which layer each claim belongs to:

public fun quote_mint(
market: &ExpiryMarket,
config: &ProtocolConfig,
pricer: &Pricer,
lower_tick: u64,
higher_tick: u64,
max_premium: u64,
min_quantity: u64,
exact_quantity: bool,
clock: &Clock,
ctx: &mut TxContext,
): MintQuote

public fun quote_mint_for_account(
market: &ExpiryMarket,
wrapper: &AccountWrapper,
config: &ProtocolConfig,
pricer: &Pricer,
lower_tick: u64,
higher_tick: u64,
max_premium: u64,
min_quantity: u64,
exact_quantity: bool,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): MintQuote

public fun quote_mint_exact_cost_for_account(
market: &ExpiryMarket,
wrapper: &AccountWrapper,
config: &ProtocolConfig,
pricer: &Pricer,
lower_tick: u64,
higher_tick: u64,
max_cost: u64,
min_quantity: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): MintQuote

The difference is whose terms the quote reflects. quote_mint prices an anonymous taker with no builder code. quote_mint_for_account reads the account's sticky builder code so the quoted builder fee matches what that account pays, and in budget mode it caps max_premium by the account's available USDC, including funds delivered through the accumulator but not yet settled.

Set exact_quantity to true to price a fixed size, in which case min_quantity carries the requested quantity. Set it to false to size a lot-rounded fill, a quantity in whole lots, under the max_premium budget. The Testnet order-desk policy lists one lot as 10000 base units. The congestion surcharge each quote reports uses the pre-update congestion estimate, which is the same estimate a mint in the same state charges.

quote_mint_exact_cost_for_account, new in package version 2, is the read-only twin of mint_exact_cost: it runs the same budget search against the same state and returns the fill that budget buys, along with that fill's cost decomposition. It returns the same MintQuote struct as the other 2 quotes, so read that fill and its cost decomposition through the getters in The MintQuote struct. It takes the account for the same reasons quote_mint_for_account does, plus one more: the chain caps the budget at the account's available USDC before sizing, and the quote reproduces that cap. It is the natural source of a min_quantity floor for the mint that follows. Like the other 2 quotes, it does not preflight expiry cash backing or exposure-index capacity.

Both quotes run the entry-probability band before they size anything. For a two-sided range the band applies 3 times: to the range probability, to the probability that settlement ends above the lower strike, and to the probability that it ends at or below the higher strike. An open end contributes no check, and the finite end of a directional position has the same probability as the position itself. The quoted trading_fee sums one fee for each finite boundary, as Fee components describes.

Both Move functions price and stop there. Neither checks the max_cost or max_probability slippage caps, because neither takes them, and neither checks whether the expiry has the cash to back the resulting payout liability. quote_mint in particular knows nothing about an account: it has no AccountWrapper argument, so it cannot see a balance, a builder code, or a referrer. One bound both quotes do enforce is that the all-in cost can never exceed the position's maximum payout: a quote whose all_in_cost would exceed quantity aborts with EMintCostAboveMaxPayout.

The SDK's client.predict.read.quoteMint is a different thing at a different layer. On a network before the cutover, it dry-runs the transaction client.predict.tx.mint builds for the options you pass, against the real account and the real fee path, so it does double as preflight and raises the same typed errors the mint would, including insufficient balance. Called with only quantity, it carries no slippage cap. When a page says a quote is a preflight, it means that dry run, not the Move functions above.

Quotes from protocol version 4​

Protocol version 4 keeps all 3 signatures and re-bodies the functions to price the order the way a queued fill prices, at the current clock:

  • No congestion surcharge: penalty_fee is always 0.
  • Subsidy at the configured rate: The fee subsidy is the admin-set subsidy rate applied to the trading fee, capped by the market's incentive balance.
  • Fewer gates: They check only the pricer binding and that the clock is before expiry, aborting with EInvalidOrderTiming at or after it. They have no no-trade-window or trading-pause gate.
  • One refusal code: A mint that would be refused at the clock, for the entry-probability band, the minimum premium, or a cost above its maximum payout, aborts with EOrderFailsLimits rather than the version 3 error.

The queued fill itself prices at its committed Pyth tick, not at the quote's clock, and placement checks the order without the fee subsidy, so a quote is an estimate of the fill rather than a promise. The SDK's read.quoteMint and read.quoteMintCost read these functions on Testnet and preview a queued fill, with the order fee reported separately.

Click to open
Quote source

The MintQuote struct​

MintQuote has copy and drop, and it reports amounts in USDC base units except for entry_probability, which uses the 1e9 fixed-point scale. Read it through the 9 getters, each of which takes &MintQuote and returns u64:

GetterMeaning
quantitySized position quantity, which is also the maximum payout.
entry_probabilityQuoted per-contract range probability before fees, scaled by 1e9.
premiumNet premium, the contract's entry value.
trading_feeTrading fee before the sponsor subsidy, summed over the range's finite boundaries.
fee_incentive_subsidySponsor-funded portion of the trading fee the trader does not pay. The SDK calls the same amount subsidy.
builder_feeBuilder add-on, zero when the account carries no builder code.
penalty_feeCongestion surcharge. The SDK calls the same amount penalty, in both the quote and the decoded receipt.
inventory_impact_chargeIsolated risk escrow charge, separate from every ordinary fee.
all_in_costTotal USDC withdrawal from the account.

all_in_cost is premium + (trading_fee - fee_incentive_subsidy) + builder_fee + penalty_fee + inventory_impact_charge. Pass that value, not premium, as the max_cost cap on the mint. See the Design page for how the package computes each fee component.

The congestion surcharge is one mechanism with 2 names, penalty_fee onchain and penalty in the SDK. On protocol version 3 it is zero unless the transaction's gas price is a high statistical outlier against the market's smoothed gas statistics, and the exponentially weighted moving average (EWMA) penalty ships disabled, so it reads zero in practice. Protocol version 4 retires it: its setters abort with EEwmaRetired, and every quote and fill reports 0. The sponsor subsidy is the same pairing: fee_incentive_subsidy onchain, subsidy in the SDK.

Click to open
MintQuote and its getters

The referral split​

The Move MintQuote has no referral field. That is not an omission: the chain computes the split after all_in_cost is already fixed, so there is nothing about it a quote could report. The chain computes it as:

referral_fee_basis = trading_fee - fee_incentive_subsidy + penalty_fee
referral_fee = referral_fee_basis * referral_fee_rate / 1e9

Protocol version 4 drops penalty_fee from the basis, which changes nothing in practice, because the surcharge is always 0 there. The chain then splits the result out of the USDC payment the trader has already made. It comes out of fees already paid, is never an extra debit, and never changes all_in_cost, so a trader with a referrer and a trader without one pay exactly the same total for the same trade. The amount is zero unless new_with_referrer created the account.

The SDK's MintQuote.fees.referral does report it, because that value comes from dry-running the mint and reading the OrderMinted event rather than from the Move quote. It is there for visibility, and the SDK deliberately does not add it into cost. Referrals covers how an account gets a referrer, the rate, and how the payment reaches the referrer.

Queued orders​

From protocol version 4, a mint or an early sell no longer fills inside the trader's own transaction. It becomes a queued order: the trader places it with its limits and its money, it waits in its market's queue until Pyth Lazer publishes its signed price for the order's price instant, called τ, and then any caller can commit that price and fill or refund the order. τ falls about 800 ms after placement on Testnet.

info

Testnet passed the delayed-execution cutover on 2026-10-09 and runs protocol version 4, so every Testnet trade is a queued order. Mainnet runs protocol version 3, which fills trades immediately, until its own rollout, and its order-flow packages are not published yet. Read protocol_config::version_watermark, or call the SDK's client.predict.read.executionMode(), rather than assuming either network's path.

The Move snippets in this section pin DeepBookV3 main at 29117434, the commit that records the Testnet publication. The order-flow sources are at packages/predict_orders/sources, and the math library is at packages/predict_math/sources.

Why orders wait​

An immediate trade prices off a live pricer built in its own transaction, which normally re-anchors the forward on the onchain Pyth spot and falls back to the Block Scholes forward when that spot is stale. That spot reaches the chain a moment after the price has already moved on faster venues, so a trader watching those venues can know that the fill price is stale before sending the trade. A queued order is priced at Pyth's signed price for a fixed instant after it is placed. Nobody picks that instant or that price, and by τ the price already reflects what the trader could see when placing the order.

Order-flow packages​

Sui caps a package at 102,400 bytes, and Predict, which can grow only by compatible upgrade, is close to that limit. Delayed execution therefore spans 3 packages:

PackageRole
deepbook_predictUpgraded in place to protocol version 4. Keeps the markets, pricing, fills, fees, cash backing, settlement, and the pool. Adds the order-flow primitives the queue drives, one OrderReceipt for each queued order, and a per-market OrderFlowLedger.
deepbook_predict_ordersPublished fresh. Holds the shared OrderDesk with the delayed-execution policy, one shared MarketQueue for each market, each order's escrow, every order entrypoint, the queue reads, and the queue events.
deepbook_predict_mathPublished fresh. A pure library: the pricing math Predict calls, the order-ID decoder, and LazerPrice, a price that only a Pyth-verified Lazer update can produce.

The dependency runs one way: deepbook_predict_orders depends on Predict, and Predict depends on deepbook_predict_math. Predict never names a type from the order-flow package, so Predict's own surface does not change when the order-flow package does.

Predict makes every pricing, fill, and solvency decision itself. Each primitive the queue calls is one complete Predict operation that checks its own gates, decides the accounting, and moves the cash. The order-flow package owns what Predict leaves to it: τ and the deadline, capacities and per-account caps, cohort order, escrow custody, refund routing, and the events.

Shared objects​

Publishing deepbook_predict_orders created and shared 2 objects:

  • OrderDesk: Holds the delayed-execution policy and the order-flow package's own version floor. Every trading call reads it, and only the admin's policy setters write it.
  • QueueRegistry: The root that every market's queue ID derives from. Only queue creation writes it, so creating a queue never contends with trading.

Each market has one MarketQueue, a shared object at an ID derived from the QueueRegistry and the market. queue::queue_id(registry_id, expiry_market_id) returns that ID whether or not the queue exists yet, and the SDK's client.predict.queueIdFor(marketId) derives the same ID offchain. queue::create_and_share(registry, desk, market, ctx) creates a market's queue. Anyone can call it, the caller pays its storage, and a second call for the same market aborts. The protocol's market keeper creates each new market's queue right after it creates the market.

The cutover and what retired​

Predict protocol version 4 compiles constants::current_version!() as 4, and ProtocolConfig.version_watermark reaching 4 is the delayed-execution cutover:

  • Admission waits for the cutover. Predict refuses every queued order with protocol_config::ECutoverNotReached until the watermark reaches 4. The bump also retires every older Predict version, so no package that knows nothing about the queue can run while an order waits. The watermark only moves up, so the cutover cannot be undone.
  • Predict serves only an allowlisted order flow. An admin runs protocol_config::set_order_flow<OrderFlow>(true), where OrderFlow is the order-flow package's witness type. Until then, every admission, commit, and fill aborts with EOrderFlowNotAllowed. On Testnet the allowlisting landed on 2026-10-09 and emitted OrderFlowUpdated.
  • The immediate trades always abort. mint_exact_quantity, mint_exact_amount, mint_exact_cost, and redeem_live keep their signatures and abort with EDelayedExecutionRequired, at any watermark. Their Sessions wrappers abort through them. Mint a position immediately documents them for networks before the cutover.
  • The mint quotes price a queued fill. quote_mint, quote_mint_for_account, and quote_mint_exact_cost_for_account keep their signatures and now price the order like a queued fill at the current clock. See Quotes from protocol version 4.
  • The congestion surcharge is gone. set_ewma_params and set_ewma_enabled abort with EEwmaRetired, and every fill and quote reports penalty_fee as 0.
  • Settled redemption is unchanged for account positions. redeem_settled pays positions that immediate mints left in accounts before the cutover. A queued fill never enters an account, so it never needs it. Protocol version 4 also limits redeem_settled_permissionless to an admin allowlist of keepers, as Redeem a position describes.
  • Only flush operators finish a pool flush. plp::finish_flush aborts with ENotFlushOperator unless the sender is on an admin-managed allowlist. See Vault.

Who calls what​

A trader, or a session acting for one, places orders through the queue module of deepbook_predict_orders. Every step after placement is open to anyone and needs no account authority: commit, resolve, refund, settle_step, cleanup, pay_open, and claim_parked in the order-flow package, plus Predict's rebalance_expiry_cash and try_settle. The protocol's keepers normally make those calls, with no special rights. Only a holder of Predict's AdminCap can allowlist the order flow, change the policy, refund orders by ID, bump the order-flow package's version floor, or edit the flush-operator allowlist.

The order lifecycle​

The queue keeps each order as one record in a table, keyed by a sequential u64 record ID that restarts in every market. A record ID is not the packed u256 order ID of a position. Each record holds Predict's OrderReceipt for its order and that order's own escrowed USDC, so a refund pays exactly what that order escrowed.

StatusCodeMeaningLeaves the status by
Pending0Waiting for its cohort's priceA commit, or a refund
Committed1Priced, waiting to be resolvedA fill, or a refund
Open2Holds a position: a filled mint, the rest of a partial sell, or a refunded sellAn early sell, or the settlement payout
Refunded3A refunded mintcleanup after settlement
Closed4Its position was sold, moved to a sell, or paid at settlementcleanup after settlement
RefundDue5Reserved. Nothing sets it at launchA refund

The kind codes are 0 for an exact-quantity mint, 1 for a premium-budget mint, 2 for an all-in budget mint, and 4 for an early sell of an Open record. Code 3 is reserved and never used. Status, kind, and refund reason codes are never renumbered, but new ones may be added, so tolerate codes you do not know. The order_queue module exposes a getter for every code, such as order_queue::status_open(), so a client never has to hard-code the numbers.

There is no cancel. Once placed, an order fills or is refunded.

How do I place a queued order?​

4 entrypoints in the queue module place orders. Each takes the market's queue, the market, the account wrapper and an Auth, the OrderDesk, ProtocolConfig, the Propbook OracleRegistry, and the canonical PythFeed, BlockScholesValueStore, and BlockScholesSVIStore, and each returns the new record ID as a u64. None takes a Pricer: placement reads the oracle objects itself and snapshots the volatility inputs into the order. The all-in budget mint and the early sell look like this:

public fun enqueue_exact_cost(
queue: &mut MarketQueue,
market: &mut ExpiryMarket,
wrapper: &mut AccountWrapper,
auth: Auth,
desk: &OrderDesk,
config: &ProtocolConfig,
propbook_registry: &OracleRegistry,
pyth: &PythFeed,
bs_values: &BlockScholesValueStore,
bs_svi: &BlockScholesSVIStore,
lower_tick: u64,
higher_tick: u64,
max_cost: u64,
min_quantity: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): u64

public fun enqueue_redeem_open(
queue: &mut MarketQueue,
market: &mut ExpiryMarket,
wrapper: &mut AccountWrapper,
auth: Auth,
desk: &OrderDesk,
config: &ProtocolConfig,
propbook_registry: &OracleRegistry,
pyth: &PythFeed,
bs_values: &BlockScholesValueStore,
bs_svi: &BlockScholesSVIStore,
record_id: u64,
close_quantity: u64,
min_probability: u64,
min_proceeds: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): u64

The 4 entrypoints differ only in their sizing and limit arguments:

EntrypointSizing at τTrader limits
enqueue_exact_quantityExactly quantitymax_cost caps the all-in cost, and max_probability caps the entry probability.
enqueue_exact_amountThe largest lot-rounded quantity whose premium fits max_premiummin_quantity, and max_cost caps the all-in cost.
enqueue_exact_costThe largest lot-rounded quantity whose all-in cost fits max_costmin_quantity
enqueue_redeem_openclose_quantity of the Open record record_idmin_probability and min_proceeds

Every queued mint requires a finite max_cost: zero and std::u64::max_value!() both abort with EMintCostCapRequired. The immediate mint_exact_quantity accepted u64::MAX as no cap, and the queued form does not.

Click to open
Enqueue source

What placement checks​

Placement runs these checks in order, and an abort charges nothing:

  1. Queue checks. The desk's version floor, the queue's own desk and market (EWrongDesk, EWrongMarket), the stuck gate (EQueueStuck), the side's capacity (EQueueFull), and the account's cap on waiting orders in this market (EAccountOrderCap).
  2. Timing. The order gets its τ, deadline, and cutoff, and τ must fall before the cutoff (EPastCutoff).
  3. Source record, for sells. The record must be Open (ERecordNotOpen, also for a missing ID) and belong to the placing account (ENotRecordOwner). close_quantity must be at least min_sell_quantity, and a partial sell must leave at least that much (EBelowMinSell).
  4. Budget and order fee. A mint needs an available balance above the order fee (EFeeNotCovered), and its budget is min(max_cost, available - order_fee). An exact-quantity mint also caps the budget at quantity, because a fill never costs more than its quantity. A sell needs a balance of at least the order fee and escrows nothing else.
  5. Predict's gates. The order-flow allowlist, the version gate, so the emergency freeze blocks placement, and the cutover. Mints also check the trading pause and the market's mint pause. An early sell is open during both pauses.
  6. Predict's timing bounds. EInvalidOrderTiming unless τ sits on the policy channel's grid at most one tick before the clock, τ is before the deadline and before expiry - no_trade_window_ms, and the deadline is at least 5 seconds before expiry.
  7. The volatility snapshot. Placement validates the oracle objects the way load_live_pricer does and copies the Block Scholes spot, forward, and SVI into the order. It requires use_pyth_spot_for_forward (pricing::EPythForwardRequired) and an SVI no older than the policy's svi_max_age_ms. It does not require a fresh onchain Pyth spot.
  8. A dry run at placement. Predict prices the order at the placement clock, without the fee subsidy, using the same test a fill applies. An order that would already be refunded for its limits or for admission aborts with EOrderFailsLimits, and so does a sell above the held quantity.
  9. Spare cash, for mints. The mint's cash need must fit the market's spare cash (EInsufficientMarketCash). Sells skip this check.
  10. Boundary nodes, for mints. Placement creates both finite boundary nodes of the range in the payout tree if they are missing, and pins them, so a fill never creates a node. The payout tree's node cap applies here.
  11. Append. The budget and the order fee move from the account into the record's escrow, and OrderEnqueued reports the order.

An early sell moves the whole position, with its receipt, out of its source record into the sell's new record, and marks the source Closed. After any sell, the trader's position therefore lives under the sell's record ID, and OrderEnqueued.source_record_id names the record it came from.

τ, cohorts, the deadline, and the cutoff​

τ is the last tick of the policy's Pyth Lazer channel at or before t0 + delay_ms, where t0 is the onchain clock of the placing transaction. τ never falls below the market's last τ, and it moves to the next tick when it would otherwise join a price that is already committed. A cohort is every order in a market that shares one τ, so orders placed within one channel tick share one signed update and one commit.

The deadline is min(τ + stall_timeout_ms, expiry), never earlier than the market's last deadline. At or past its deadline an order can only be refunded in full. The cutoff is expiry - max(no_trade_window_ms, stall_timeout_ms + 5_000), and an order whose τ would fall at or past it aborts with EPastCutoff. Every deadline therefore falls at least 5 seconds before expiry, so every waiting order is due before the market can settle.

Testnet runs an 800 ms delay on the 200 ms channel, a 5-second stall timeout, and a 10-second no-trade window. τ then falls 600 to 800 ms after placement, the deadline falls 5 seconds after τ, and the cutoff falls 10 seconds before expiry, so new orders stop about 11 seconds before expiry. Each order keeps the τ, deadline, channel, and order fee from its placement, so a change to those reaches only new orders. A change to pyth_price_buffer_ms or gap_wait_ms also reaches cohorts already waiting, because commit reads both from the desk when it runs.

Commit and resolve​

2 permissionless calls move a waiting order to its outcome:

public fun commit(
queue: &mut MarketQueue,
market: &mut ExpiryMarket,
desk: &OrderDesk,
config: &ProtocolConfig,
updates: vector<Update>,
clock: &Clock,
ctx: &TxContext,
)

public fun resolve(
queue: &mut MarketQueue,
market: &mut ExpiryMarket,
desk: &OrderDesk,
config: &ProtocolConfig,
max_orders: u64,
deny_list: &DenyList,
clock: &Clock,
ctx: &TxContext,
): u64

commit attaches verified Pyth Lazer prices to waiting cohorts. Each pyth_lazer::update::Update in updates must come from the Pyth Lazer verifier earlier in the same programmable transaction block. A cohort accepts only the update stamped exactly at its τ on its own channel, or, when the policy's pyth_price_buffer_ms is above zero and the clock is at least gap_wait_ms past τ, the update one channel tick later. Testnet runs the buffer at zero, so only the exact τ update counts. A cohort commits whole or not at all, a cohort without a usable price never holds back another cohort, and a cohort at or past its deadline is never committed. Committing a mint also reserves its fee subsidy from the market's incentive balance. commit emits CohortCommitted.

resolve fills or refunds committed orders from the market's own cash, walking cohorts in τ order and records in placement order. It visits at most max_orders records, and never more than 450 in one call, which keeps a call under Sui's limit of 1,024 events for each transaction. That cap bounds events, not the objects a call loads, so size max_orders from measured costs. For each record:

  • Past the deadline: Refunded with reason 5, never filled.
  • Receive address denied: Refunded with reason 9 before anything is priced. See Denied recipients.
  • Committed: Filled at its committed price. Predict rebuilds a pricer from the order's own volatility snapshot, re-anchored on the committed Pyth price and rolled to the committed tick, so the trading fee is charged at the tick rather than at the resolve transaction's clock.
  • Fails at its tick: Refunded with the reason it failed on, before anything moves.
  • More than the market's cash covers: Refunded with reason 8 and the order fee returned. Fills never draw on the pool.

Orders usually fill in placement order, but not always: each cohort is priced and filled on its own, so a caller holding 2 updates can commit and resolve a later cohort first. Each order keeps its own τ price, so fill order changes only which order reaches scarce market cash first.

A mint fill pays from the record's escrow. Market cash receives the premium, the inventory-impact charge, which joins its isolated reserve, the whole trading fee net of any referral share, part of which the reserved subsidy pays, and the order fee. The builder fee and the referral share leave for their addresses, except that a fee owed to an address on USDC's deny list stays in market cash, unused subsidy returns to the market's incentive balance, and unused budget returns to the trader's receive address. A sell fill pays the trader the redeem value plus any inventory-impact rebate, less the trading and builder fees. Predict emits its usual OrderMinted or LiveOrderRedeemed, with penalty_fee 0, and the order-flow package emits QueuedOrderFilled beside it.

The protocol's keeper normally commits and resolves each cohort, usually in one transaction within a second of τ. The SDK's client.predict.tx.fill builds the same transaction for anyone who holds Pyth Lazer access: it verifies signed Lazer payloads against the current Lazer package, read from Pyth Lazer's State object, commits them, and resolves the market's due records.

Click to open
Commit and resolve source

Open records and early sells​

A queued fill never enters the trader's account. A filled mint stays in the market's queue as an Open record owned by the placing account, holding the open receipt with the position's packed order ID, its root ID, its open time, and its held quantity. The position itself sits in the market's payout tree like any other, so it is backed, valued in the pool's net asset value, and settled the same way. Because the account's position table never sees it, predict_account reads and the SDK's read.positions do not list it: follow it by its record ID.

An Open record leaves the Open status in one of 2 ways:

  • Sold early. enqueue_redeem_open sells some or all of it before the cutoff. A partial fill leaves the remainder Open under the sell's record ID, and a refunded sell returns the whole position to an Open record.
  • Paid at settlement. The settle_step payout walk pays it in cash to the account's receive address, or pay_open pays it later when the walk had to skip it.

quote_redeem_open prices a prospective early sell without changing anything:

public fun quote_redeem_open(
queue: &MarketQueue,
market: &ExpiryMarket,
wrapper: &AccountWrapper,
pricer: &Pricer,
record_id: u64,
close_quantity: u64,
clock: &Clock,
): RedeemQuote

It prices the close at a live Pricer from load_live_pricer, with the account's builder fee and the trading fee at the current clock, the way a sell fill prices at its tick. Its proceeds are before the order fee. It aborts with ERecordNotOpen for a missing or non-Open record, and it does not check that the account owns the record. Position reads such as live_order_value and settled_order_payout take the packed order ID, which queue::order(queue, record_id) returns in the record's OrderView.

Why was my queued order refunded?​

Every refund returns the record's own escrow to the trader's receive address, keeps the order fee for reasons 1 and 2, and returns any reserved subsidy to the market's incentive balance. A refunded mint becomes Refunded, and a refunded sell returns to Open, holding its position. Each refund emits QueuedOrderRefunded.

ReasonCodeWhenOrder fee
Limits1The order misses its own limits at its tick.Kept, into market cash
Admission2A mint fails admission at its tick, such as the entry-probability band or the minimum premium, or the order cannot be priced there.Kept, into market cash
No price3Reserved, unused.Returned
Missing node4A pinned payout-tree node is missing at the fill. A backstop, since placement pins both nodes.Returned
Deadline5The order reached its deadline unfinished, or the settlement drain refunded it.Returned
Freeze6Reserved, unused.Returned
Admin7admin_refund.Returned
No cash8Market cash could not cover the fill.Returned
Recipient denied9The receive address is on USDC's deny list, or USDC is globally paused.Returned, and parked in the record with the rest of the escrow

A mint misses its limits when its size is zero or below min_quantity, when an exact-quantity order's probability is above max_probability, or when its all-in cost is above min(max_cost, budget). A sell misses its limits when its probability is below min_probability or its proceeds are below min_proceeds.

Besides resolve and the settlement drain, 2 calls refund waiting orders:

public fun refund(
queue: &mut MarketQueue,
market: &mut ExpiryMarket,
desk: &OrderDesk,
config: &ProtocolConfig,
max_orders: u64,
deny_list: &DenyList,
clock: &Clock,
ctx: &TxContext,
): u64

public fun admin_refund(
queue: &mut MarketQueue,
market: &mut ExpiryMarket,
_admin_cap: &AdminCap,
desk: &OrderDesk,
config: &ProtocolConfig,
record_ids: vector<u64>,
deny_list: &DenyList,
clock: &Clock,
ctx: &TxContext,
)

refund is permissionless. It refunds waiting orders at or past their deadline with reason 5, stops at the first cohort that is not yet due, visits at most 450 records, and returns how many it refunded, or 0 without aborting when none is due. Keepers refund overdue orders on their own, so an app needs it only for a "refund my order" button that the SDK enables a few seconds past the deadline. It needs no account, and it works during the emergency freeze. admin_refund takes Predict's AdminCap and refunds the listed waiting orders at once with reason 7, skipping missing and finished IDs.

Denied recipients and parked funds​

Mainnet USDC is a regulated coin, and Sui aborts any transaction that credits USDC to an address on its deny list, or to anyone while USDC is globally paused. Every queue walk restarts at the same record, so one such send could block a market's fills, refunds, and payouts. Predict and the order-flow package therefore read Sui's shared DenyList object, 0x403, read-only, and never send USDC where the send would abort:

  • Fills: An order whose receive address is denied is refunded with reason 9 before anything moves.
  • Fees: A builder or referral fee whose recipient is denied stays in market cash, and the events still report it as charged.
  • Refunds and change: Funds the receive address cannot take stay in the record, which emits RecordFundsParked. Once the address is clear, queue::claim_parked(queue, desk, record_id, deny_list, clock, ctx) sends them there and emits RecordFundsClaimed. Anyone can call it, and it returns 0 and changes nothing while the address is still denied.
  • Settled payouts: The payout walk skips a nonzero payout to a denied address and emits OpenRecordPayoutSkipped. queue::pay_open(queue, market, desk, config, record_id, deny_list, clock, ctx) pays that record at any time after settlement, and it also pays a record the walk skipped because market cash was short.

Every call that can send USDC takes deny_list: &DenyList: resolve, refund, admin_refund, settle_step, pay_open, and claim_parked. Placement and commit send nothing and do not take it.

The settlement walk and cleanup​

Settlement is 2 jobs in 2 packages, and neither waits on the other. Predict's try_settle records the settlement price exactly as before and reads nothing from the queue. Settling while orders still wait is safe, because every deadline falls at least 5 seconds before expiry and a waiting order can then only be refunded.

The order-flow package drains and pays its queue with settle_step, one bounded phase per call:

public fun settle_step(
queue: &mut MarketQueue,
market: &mut ExpiryMarket,
desk: &OrderDesk,
config: &ProtocolConfig,
deny_list: &DenyList,
clock: &Clock,
ctx: &TxContext,
): u8

It returns the phase the next call runs. Anyone can call it, and it aborts with EMarketNotExpired before expiry:

PhaseCodeRuns whenDoesRecords visited for each call
DRAIN0Unfinished records remainRefunds them in τ order with reason 5. A refund the receive address cannot take is parked.Up to settle_refund_batch, at most 450
PAY1Nothing is unfinished and Predict has settledPays each Open record its settled payout, the full quantity for a winner and 0 for a loser, to the receipt's receive address, marks it Closed, and emits OpenRecordSettled. A record the market cannot pay, or whose address is denied, stays Open with OpenRecordPayoutSkipped.Up to settle_payout_batch, at most 900
DONE2The walk reached the last recordThe call that gets there emits MarketPayoutsCompleted once.None

The batch bounds come from Sui's limit of 1,000 dynamic-field loads for each transaction, so the keeper sends one settle_step per transaction. Its order is try_settle until the market settles, settle_step until it returns DONE, then cleanup, then rebalance_expiry_cash. On Testnet that sequence usually completes within seconds of expiry, so by the time an app looks, a filled record has usually been paid and deleted. Keep each filled record's order ID, and compute what it is owed from the market's settlement price, as the TypeScript example below does. The OpenRecordSettled event is the payment itself.

queue::cleanup(queue, market, desk, record_ids, clock) deletes Refunded and Closed records of a settled market, and aborts with EMarketNotSettled before settlement. Anyone can call it and keeps the storage rebate. It skips missing IDs, other statuses, and records that still hold a receipt or parked funds, and it never deletes an Open record.

Queue limits and market cash​

The policy bounds each queue, and placement aborts when an order would pass a bound:

  • Capacity: mint_capacity and sell_capacity bound the unfinished mints and sells in one market (EQueueFull). One side can be full while the other still accepts orders.
  • Per-account cap: per_account_cap bounds one account's unfinished orders in one market (EAccountOrderCap).
  • Stuck gate: Placement aborts with EQueueStuck while an uncommitted cohort is at least stuck_threshold_ms past its τ and no later cohort is committed, or while 2 or more uncommitted cohorts are each that far past their τ. queue::queue_stuck reads the same check, and an app can show it as "pricing delayed".
  • Minimum sell: min_sell_quantity bounds the smallest early sell and the smallest remainder (EBelowMinSell).

Each order also records its cash need, the most its fill could take out of the market's spare cash, which is market cash minus required cash. Predict computes it at admission, where p is the market's minimum entry probability:

exact quantity:  ceil(quantity * (1 - p)) + 1
premium budget: ceil((min(max_premium, budget) + 1) * (1 / p - 1)) + 1
all-in budget: ceil((budget + 1) * (1 / p - 1)) + 1
early sell: ceil(close_quantity * (1 - backing_buffer_lambda)) + 1

A premium-budget mint buys no more than its premium cap allows, so its cash need uses min(max_premium, budget) in place of the escrowed budget.

Admission checks only the mint's own cash need against spare cash, and reserves nothing while the order waits. Predict keeps a running total, waiting_cash_need, and rebalance_expiry_cash funds a live market to at least required cash plus that total. An order's escrow sits in its record, outside market cash, the pool's net asset value, and payout backing, so a waiting order changes nothing in the pool's mark until it fills.

Queued orders under pauses and the freeze​

  • Trading pause and market mint pause: Block queued mints only. Early sells, commits, fills, refunds, and settlement keep running.
  • Order flow disabled: set_order_flow<OrderFlow>(false) stops every admission, commit, and fill. Committed orders pass their deadlines and refund, and Open records keep their positions and are still paid at settlement.
  • Emergency freeze: Halts admission, commits, fills, try_settle, and the policy setters, so nothing fills. refund, admin_refund, settle_step, pay_open, claim_parked, and cleanup still run, so a waiting order is still refunded at its deadline. An Open record is paid only if its market settled before the freeze.
  • Pool flush snapshot: Admission, fills, and refunds that keep an order fee abort inside the single transaction that snapshots the markets, as other trades do.

The order-desk policy​

DelayedExecutionPolicy lives in the shared OrderDesk. The values below are the launch policy, which a chain read of the Testnet OrderDesk on 2026-10-13 matched field for field:

FieldTestnet valueBoundWhat it does
delay_ms8000 to 5,000The latest τ may fall after placement
pyth_channel3, fixed_rate@200ms2, fixed_rate@50ms, or 3The tick grid for new orders' τ
stall_timeout_ms50002,000 to 10,000Time from τ to the deadline
stuck_threshold_ms150050 to 10,000, and at least one tickWhen the stuck gate refuses new orders
gap_wait_ms200050 to 10,000How long past τ before a backup tick is allowed
pyth_price_buffer_ms00 or exactly one tickSwitches the backup tick on or off
svi_max_age_ms600001 to 120,000The oldest SVI an admission accepts
mint_capacity1001 to 300Unfinished mints in one market
sell_capacity1001 to 300Unfinished sells in one market
per_account_cap51 to 300, and at most the smaller capacityUnfinished orders for one account in one market
min_sell_quantity10000, one lotPositive whole lots, at most an order's maximum quantityThe smallest early sell and the smallest remainder
order_fee20000, 0.02 USDC0 to 1 USDCThe flat fee for each order
settle_refund_batch4501 to 450Records one settle_step call visits while draining
settle_payout_batch9001 to 900Records one settle_step call visits while paying

Times are milliseconds and USDC amounts are base units. desk::policy(desk) returns the policy. Three setters change it, each taking Predict's AdminCap, refusing while Predict is frozen (desk::EProtocolFrozen), and emitting DelayedExecutionPolicyUpdated with the complete policy:

  • desk::set_timing sets the delay, stall timeout, stuck threshold, gap wait, price buffer, channel, and SVI age together, and requires pyth_price_buffer_ms < stuck_threshold_ms <= gap_wait_ms < stall_timeout_ms (EInvalidTiming).
  • desk::set_limits sets both capacities, the per-account cap, the minimum sell, and both settle batches (EInvalidLimits when the per-account cap exceeds the smaller capacity).
  • desk::set_order_fee sets the order fee.

The order fee is per order, not per contract. A fill moves it into market cash, a refund for reason 1 or 2 also keeps it there, and every other refund returns it.

Version floors​

3 version floors govern the 3 trading packages, and the math library has none:

PackageFloorBumped byTestnet value
deepbook_predictProtocolConfig.version_watermarkprotocol_config::bump_version_watermark4
deepbook_predict_ordersOrderDesk.version_watermarkdesk::bump_version_watermark1
sessionsSessionsConfig.version_watermarksession_config::bump_version_watermark3

A package runs the dependency versions it linked when it was published, so the order-flow package and Sessions must be relinked to each new Predict version before Predict's watermark retires the old one. An order-flow package still linked to a retired Predict version aborts on every call into Predict. A Sessions package linked to one aborts its queued wrappers, while placing orders directly keeps working.

Queue reads​

Every read below is permissionless and non-mutating:

ReadReturns
queue::order(queue, record_id)One record as a copyable OrderView, or none for a missing or deleted ID
queue::queue_id(registry_id, expiry_market_id)A market's queue ID, whether or not the queue exists yet
desk::registry_id(registry)The queue registry's ID, for deriving queue IDs
queue::queue_heads(queue)(resolve_head, next_id, last_tau_ms, last_committed_tau_ms)
queue::payout_progress(queue)(payout_cursor, next_id, payouts_completed)
queue::waiting_cohorts(queue)The cohort count, the oldest uncommitted τ, and the oldest uncommitted τ above the newest committed τ
queue::queue_stuck(queue, desk, clock)Whether placement would refuse a new order as stuck now
queue::pending_counts(queue)(pending_mints, pending_sells)
queue::waiting_orders(queue, account_id)The account's unfinished orders in this market. Stale after expiry
queue::oldest_unfinished_tau_ms(queue)τ of the oldest cohort with an unfinished order
desk::policy(desk), desk::version_watermark(desk)The policy, and the order-flow package's version floor
expiry_market::order_flow_state(market)(waiting_cash_need, payout_tree_node_count, min_entry_probability)
protocol_config::is_order_flow<W>, is_flush_operator, version_watermarkOrder-flow allowlisting, flush-operator membership, and the watermark that marks the cutover

OrderView carries the record's status, kind, request, account and receive address, timing, escrow terms, position, committed price, result, the receipt's stage, and the USDC the record holds: escrow while unfinished, parked funds after. Owner, referrer, and builder fields live in the events instead.

Queue orders from TypeScript​

Version 2.7.0 of @mysten/deepbook-v3 adds the queued-order flow to client.predict and records the Testnet deployment: read.planMint quotes an order and turns a slippage in cents per contract into its limits, tx.enqueuePlan builds the enqueue after a preflight of the queue gates, the balance, and the limits, decode.enqueue reads the record ID from the result, and read.waitForOutcome polls the record until it fills or refunds. Positions SDK covers each method.

Click to open
Queue a mint and follow it
Click to open
Sell an Open record, refund an overdue order, and read a settled result

How do I mint a position immediately?​

caution

This section describes protocol version 3, which Mainnet runs until its delayed-execution cutover. From protocol version 4, which Testnet runs, all 3 mint functions abort with EDelayedExecutionRequired, and a mint is a queued order placed with enqueue_exact_quantity, enqueue_exact_amount, or enqueue_exact_cost. See Place an order.

Positions open through 3 functions, one per sizing shape: an exact payout quantity, a premium budget, or an all-in budget. All 3 take the strike range as the tick pair (lower_tick, higher_tick], consume an account Auth, and return the minted order ID as a u256. A u256 has drop, so the transaction is free to ignore that return value; recover the ID from the OrderMinted event instead when you do not chain another command onto it:

public fun mint_exact_quantity(
market: &mut ExpiryMarket,
wrapper: &mut AccountWrapper,
auth: Auth,
config: &ProtocolConfig,
pricer: &Pricer,
lower_tick: u64,
higher_tick: u64,
quantity: u64,
max_cost: u64,
max_probability: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): u256

public fun mint_exact_amount(
market: &mut ExpiryMarket,
wrapper: &mut AccountWrapper,
auth: Auth,
config: &ProtocolConfig,
pricer: &Pricer,
lower_tick: u64,
higher_tick: u64,
max_premium: u64,
min_quantity: u64,
max_cost: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): u256

public fun mint_exact_cost(
market: &mut ExpiryMarket,
wrapper: &mut AccountWrapper,
auth: Auth,
config: &ProtocolConfig,
pricer: &Pricer,
lower_tick: u64,
higher_tick: u64,
max_cost: u64,
min_quantity: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): u256
info

mint_exact_cost arrived in predict package version 2. On Mainnet, call it against the network's current call target, not the original package ID, which has no such function. See Contract Information.

mint_exact_quantity fixes the size. max_cost caps the all-in USDC withdrawal and max_probability caps the quoted per-contract probability before fees, and you can pass std::u64::max_value!() for either to leave it uncapped.

mint_exact_amount fixes the premium budget instead. The market first caps max_premium by the account's available USDC, computes the largest lot-rounded quantity whose premium fits, and aborts when that quantity falls below min_quantity. The market charges fees, the builder add-on, and the congestion surcharge on top of the budget, so max_cost is the only bound on the total withdrawal. You must pass max_cost here: zero aborts with EMintCostCapRequired, and no value disables it. This variant has no max_probability argument, because min_quantity against a fixed budget already bounds the price paid per contract.

mint_exact_cost fixes the all-in budget, which is the difference that matters. Where mint_exact_amount sizes on the pre-fee premium and then charges the trading fee, builder add-on, congestion surcharge, and inventory-impact charge on top of it, mint_exact_cost sizes inside the total: it caps max_cost at the account's available USDC, then searches for the largest lot-rounded quantity whose complete withdrawal fits that figure. The search probes the same cost computation the mint charges, against the fee, subsidy, congestion, and book state at execution, so the debit can never exceed max_cost and no off-chain fee estimate or padding is needed. Passing std::u64::max_value!() sizes against the whole balance. There is no max_cost requirement to satisfy here, because the budget is the sizing input; a budget too small to clear the minimum premium aborts with EPremiumBelowMinimum, and zero is such a budget.

min_quantity is this shape's slippage guard. The spend is fixed, so an adverse move in price, congestion surcharge, sponsor subsidy, or inventory impact between building the transaction and executing it arrives as fewer contracts rather than a larger debit, and a fill below the floor aborts with EMintQuantityBelowMin. It bounds the all-in price per contract at max_cost / min_quantity, which is why this shape carries no max_probability either. Zero accepts any fill the budget buys.

Three limits qualify the fill, and a caller sizing a spend should know all 3:

  • The one-lot remainder holds only when the budget binds. If the budget is what limits the fill, what goes unspent is less than the incremental all-in cost of one more lot. A fill limited by something else can leave much more.
  • The maximum-payout bound can step the fill down. The marginal inventory-impact rate rises with liability, so the largest fill a budget could pay for is sometimes one that would cost more than its own maximum payout. Sizing then falls back to a smaller fill. That fallback is conservative: integer rounding can make it miss a larger admissible fill, or abort on min_quantity or the minimum premium where another size would have succeeded. When no smaller fill clears the bound, the mint reports EMintCostAboveMaxPayout.
  • Backing is not a sizing input. Insufficient expiry cash backing aborts the mint rather than shrinking it, and the read-only quote does not preflight backing or exposure-index capacity.

quote_mint_exact_cost_for_account is the read-only twin. It returns the fill the same budget would buy, and that fill's cost decomposition, which is the natural source for a min_quantity floor.

All 3 calls run the same gates, in this order, before anything moves:

  1. The running package version must be at or above the protocol version watermark.
  2. No pool-flush snapshot stage can be open in this transaction.
  3. The pricer must match this market.
  4. The market must be outside its no-trade window.
  5. The global trading_paused flag must be false.
  6. This market's mint_paused flag must be false.

The quote then has to satisfy all_in_cost <= quantity, and the expiry must hold enough cash to back the post-mint payout liability. A pool flush that is in flight but past its snapshot transaction does not block a mint.

You address positions by absolute integer ticks, with 0 as the open lower end and 1073741823 as the open higher end. A directional trade is a one-sided range: down is (0, K] and up is (K, 1073741823]. See Strikes and Ticks for the tick coordinate system and the sentinel values.

Click to open
Mint source

The TypeScript SDK wraps all 3 calls, resolves the market from an underlying and expiry, and converts raw strikes to ticks for you. client.predict.tx.mint builds mint_exact_quantity, client.predict.tx.mintAmount builds mint_exact_amount, and client.predict.tx.mintCost builds mint_exact_cost, with client.predict.read.quoteMintCost as the matching dry run. Omitting the SDK's maxCost or maxProbability options sends u64::MAX, which caps nothing, so read a quote first and set a cap. tx.mintCost has no such hole, because its budget is required. Version 2.7.0 deprecates all 3 builders, because protocol version 4 always aborts them, and client.predict.read.executionMode() reports which path a network is on.

Click to open
Quote and mint with the SDK
import type { MarketDescriptor, MintQuote } from '@mysten/deepbook-v3/predict';
import type { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { UNDERLYING } from './config.js';
import { admissibleStrike, tradeableMarket } from './markets.js';

// Quote, then mint with a cap derived from the quote.
//
// Both caps are optional, and omitting them is not a safe default: the SDK sends
// U64_MAX for a missing `maxCost` or `maxProbability`, which leaves the mint
// genuinely uncapped against any price move between the quote and execution.
// This builder mints an exact payout quantity, so the premium alone cannot exceed
// `quantity`; fees are charged on top, and `maxCost` is what bounds the total
// debit. `tx.mintAmount` is the one that can reach the whole balance, because
// there fees are charged on top of `spend` and `maxCost` is the only bound on the
// full withdrawal. Always pass at least `maxCost`.
export async function mintDirectional(params: {
owner: string;
side: 'up' | 'down';
// Maximum payout in USD, at $1 per contract. Must be a whole $0.01 lot.
quantity: number;
// Omit to trade at the market's on-chain reference price, the window anchor.
targetStrikeUsd?: number;
}): Promise<{ tx: Transaction; quote: MintQuote; descriptor: MarketDescriptor }> {
const { owner, side, quantity, targetStrikeUsd } = params;
const market = await tradeableMarket();

const descriptor: MarketDescriptor = {
underlying: UNDERLYING,
expiryMs: market.expiryMs,
// Pin the exact market object that was read, rather than whatever the
// registry resolves to at submit time.
marketId: market.id,
side,
strike:
targetStrikeUsd === undefined
? 'reference'
: admissibleStrike(market, targetStrikeUsd),
};

// The quote dry-runs the identical transaction the mint builds, against the
// real account and the real fee path, so it doubles as preflight: it throws
// the same typed errors the mint would.
const quote = await client.predict.read.quoteMint(owner, descriptor, { quantity });

// `quote.cost` is the all-in account debit, not the premium. Raw amounts are
// integers at six decimals, so round the cap to six decimals: a finer value
// throws `PredictInputError`.
const maxCost = Math.ceil(quote.cost * 1.01 * 1e6) / 1e6;

// A second, independent ceiling on the fill price, 0..1 per $1 of payout.
const maxProbability = Math.min(1, Number((quote.entryProbability * 1.02).toFixed(6)));

const tx = await client.predict.tx.mint(owner, descriptor, {
quantity,
maxCost,
maxProbability,
});

// The transaction is ready to sign. Nothing here signs it, and the SDK never
// holds keys.
return { tx, quote, descriptor };
}

The no-trade window​

Live trading closes shortly before each expiry. ProtocolConfig.no_trade_window_ms defines a window at the end of a market's life. A chain read on 2026-10-13 found it at 10000 on both networks, above its compiled default of 2000. On protocol version 3, every live flow inside the window aborts with protocol_config::ETradeWindowClosed:

blocked when now >= expiry, or when expiry - now <= no_trade_window_ms

The gate applies to these functions and their sessions wrappers:

  • quote_mint
  • quote_mint_for_account
  • mint_exact_quantity
  • mint_exact_amount
  • mint_exact_cost
  • quote_mint_exact_cost_for_account
  • redeem_live

It does not apply to these functions or to any read accessor:

  • redeem_settled
  • redeem_settled_permissionless
  • try_settle
  • set_reference_tick
  • load_live_pricer

A position still open when the window closes is therefore neither stranded nor force-closed: it settles at expiry, and the holder redeems it through the settled path.

Read the live value with protocol_config::no_trade_window_ms(config): u64. A window of 0 disables the gate, and an admin can move the value between 0 and 15000 ms at any time, including while a flush is in flight, because nothing in a flush reads it. A change emits NoTradeWindowUpdated. The window is why a client that quotes at one instant and submits a moment later can see a quote succeed and the mint fail near expiry; the examples on these pages skip any market with less than 30 seconds to run, which clears the deployed window with margin.

Protocol version 4 applies the window to the order's price instant instead. Admission refuses a queued order whose τ would fall at or past expiry - no_trade_window_ms, with EInvalidOrderTiming, and the window is one term of the order-flow package's cutoff, so on Testnet new orders stop about 11 seconds before expiry. The version 4 quotes have no window gate, and no trade aborts with ETradeWindowClosed there. See τ, cohorts, the deadline, and the cutoff.

Redeem a position​

Positions close through 3 functions. Which one applies depends on whether the market has settled:

public fun redeem_live(
market: &mut ExpiryMarket,
wrapper: &mut AccountWrapper,
auth: Auth,
config: &ProtocolConfig,
pricer: &Pricer,
order_id: u256,
close_quantity: u64,
min_probability: u64,
min_proceeds: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): Option<u256>

public fun redeem_settled(
market: &mut ExpiryMarket,
wrapper: &mut AccountWrapper,
auth: Auth,
config: &ProtocolConfig,
order_id: u256,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
)

public fun redeem_settled_permissionless(
market: &mut ExpiryMarket,
account_registry: &AccountRegistry,
wrapper: &mut AccountWrapper,
config: &ProtocolConfig,
order_id: u256,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
)

redeem_live closes an unsettled position at the current range probability, in full or in part. It runs on protocol version 3 only. From protocol version 4 it always aborts with EDelayedExecutionRequired, so a position held in the account then has no early exit and is paid at settlement, while a position from a queued fill is an Open record that enqueue_redeem_open sells. A partial close removes the closed slice and returns a replacement order ID in the Option<u256>, and a full close returns none. Option<u256> has drop, so the transaction can ignore the return value; the replacement ID is also on the LiveOrderRedeemed event as replacement_order_id. min_probability floors the quoted per-contract probability and min_proceeds floors the net USDC credited after the trading fee, the builder fee, and the congestion surcharge, with inventory-impact rebate included. Pass 0 to disable either floor. The close charges the trading fee once for each finite boundary, the same way a mint does, and caps it at the gross close value. It applies no entry-probability band, so a position whose probability has left the band stays closable. A live redeem in the same clock.timestamp_ms() as the mint aborts with EMintRedeemSameTimestamp, which blocks an atomic mint, oracle update, and redeem inside one transaction. A live redeem inside the no-trade window aborts with ETradeWindowClosed.

redeem_settled closes a settled position for its terminal payout. A settled close is always full, so the function takes no quantity, and it runs no live pricing, so it takes no pricer. Payout is the full quantity when the settlement price lies inside (lower_tick, higher_tick] and zero otherwise.

redeem_settled_permissionless performs the same settled close without an account Auth. Instead of receiving authority from the caller, it mints Predict app authorization from the AccountRegistry, which is why it takes the registry as an argument. On protocol version 3 anyone can run it on any holder's behalf. Protocol version 4 limits it, despite its name, to senders an admin has added with protocol_config::add_settled_redeem_keeper, and any other sender aborts with ENotSettledRedeemKeeper. The allowlist starts empty, and remove_settled_redeem_keeper takes a keeper off it. The proceeds always land in the position holder's account. An admin can turn the path off with deauthorize_app<PredictApp> on the account registry, and owners keep the redeem_settled path either way. Treat that switch as a pause rather than a revocation: it removes Predict's row from the registry allowlist and clears no stored state, so positions, balances, and the app-data slot survive untouched and a later authorize_app<PredictApp> brings the permissionless path straight back.

Both settled paths run the version gate and refuse to run inside a pool flush's snapshot transaction, and they abort with EMarketNotSettled before settlement. A flush past its snapshot transaction does not block them.

Click to open
Redeem source

The SDK exposes client.predict.tx.redeem for the live close and client.predict.tx.claimSettled for the settled one. Version 2.7.0 deprecates tx.redeem and read.quoteRedeem along with the immediate mints, and keeps claimSettled for positions held in the account. The live-close builder pins min_probability and min_proceeds to 0, so it surfaces no close-side floor; redeem_settled has no floor parameters at all, because a settled claim pays a fixed amount.

You have 2 routes to the floors. The @mysten/deepbook-v3/sessions subpath's redeemLive wrapper takes minProbability and minProceeds directly and defaults each to 0, which is the shortest path for a session-signed close. See Sessions. Otherwise build the moveCall against redeem_live yourself with the signature above.

Click to open
Close and claim with the SDK
import type {
CloseOptions,
DecodableTransactionResult,
MarketDescriptor,
OpenPosition,
RedeemQuote,
RedeemReceipt,
} from '@mysten/deepbook-v3/predict';
import type { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { UNDERLYING } from './config.js';

// Close part or all of a live position. Reuse the descriptor the position was
// minted with: the close needs it only to resolve the market object, and the
// order ID identifies the position itself.
//
// The quote is the only protection available on a live close. `tx.redeem` sends
// `minProbability: 0` and `minProceeds: 0` unconditionally, and the facade
// exposes no option to raise either floor, so the proceeds are not capped
// against a price move between the quote and execution. Quote immediately before
// closing and treat the figure as an estimate.
export async function closeLive(
owner: string,
descriptor: MarketDescriptor,
opts: CloseOptions,
): Promise<{ tx: Transaction; quote: RedeemQuote }> {
const quote = await client.predict.read.quoteRedeem(owner, descriptor, opts);
const tx = await client.predict.tx.redeem(owner, descriptor, opts);
return { tx, quote };
}

// A partial close retires the old order ID and issues a new one, reported as
// `replacementOrderId`. It is null when the position closed in full. Store the
// replacement, or the next close targets an order that no longer exists.
export function decodeClose(result: DecodableTransactionResult): RedeemReceipt {
return client.predict.decode.redeem(result);
}

// Claim a position whose market has settled. The claim closes the order in full,
// so it takes no quantity, and it needs only the market coordinates rather than
// a side or a strike.
export async function claimSettled(params: {
owner: string;
expiryMs: bigint;
marketId: string;
orderId: bigint;
}): Promise<Transaction> {
const { owner, expiryMs, marketId, orderId } = params;
return client.predict.tx.claimSettled(
owner,
{ underlying: UNDERLYING, expiryMs, marketId },
{ orderId },
);
}

// Every open position for an owner, read straight from the account's on-chain
// position table. Use it to recover order IDs from a cold start.
export async function openPositions(owner: string): Promise<OpenPosition[]> {
return client.predict.read.positions(owner);
}

How does a market settle?​

try_settle is the single transition from live to settled:

public fun try_settle(
market: &mut ExpiryMarket,
config: &ProtocolConfig,
propbook_registry: &OracleRegistry,
pyth: &PythFeed,
bs_values: &BlockScholesValueStore,
clock: &Clock,
): bool

When you compose it, 5 properties matter:

  • Permissionless and idempotent: No capability and no Auth. Calling it on an already settled market returns true without changing anything.
  • Return value: It returns bool, not an abort. true means the market is now or was already settled, and false means the call could not record settlement yet. bool has drop, so a transaction that composes try_settle ahead of another command can ignore the value rather than routing it anywhere.
  • No SVI store: Settlement reads an exact spot, not a volatility surface, so the argument list is the oracle registry, the Pyth feed, and the Block Scholes value store.
  • Pyth first, then Block Scholes: The call returns false before expiry. At or after expiry it tries the exact Pyth spot at the expiry timestamp. If Pyth is unavailable it returns false until 30 seconds past expiry, then tries the exact Block Scholes minute-boundary spot. If neither exact source is usable, the market stays unsettled and you can retry the call.
  • Pool flushes: A pool flush never blocks it. A market snapshotted into an in-flight flush settles the instant it can, because the flush reads only frozen figures that settlement does not touch. The call also discards a stale flush stamp left on the market by a superseded flush.

Settlement emits MarketSettled from config_events, carrying the settlement price and a settlement_source byte where 0 is Pyth and 1 is Block Scholes. It also releases the residual inventory-impact escrow, because no live close can follow.

Settled redeems, pool rebalancing, and the flush's snapshot stage only read the current phase rather than driving the transition, so compose try_settle ahead of them in the same transaction whenever settlement might be due. The snapshot stage in particular aborts on a market that has expired without settling, so a keeper settles first and snapshots second.

Click to open
try_settle source

Reference ticks and pause control​

The module has 2 further functions that change market state, and they differ sharply in what they require:

FunctionGateEffect
set_reference_tickPermissionless and version-gated. A pool flush does not block it.Sets this expiry's reference fine-grid tick from the exact Propbook Pyth observation at reference_tick_source_timestamp_ms, floored to the market's tick_size. Returns the tick as a u64.
set_mint_pausedRequires &AdminCap and is version-gated.Sets or clears mint_paused. A PauseCap holder can force the pause on one-way through the registry, which is not version-gated and cannot unpause.

set_reference_tick seeds a new market's anchor strike, and it is permissionless: it takes no capability and no Auth. A keeper normally calls it for each new window, but any caller can, so a trader blocked by a market with no reference price can unblock themselves rather than wait. It takes the oracle registry and the Pyth feed only, needing no Block Scholes objects and no pricer. The source observation must already be in the feed at the exact source timestamp, otherwise the call aborts with EReferenceTickObservationMissing. Until a market has a reference tick, reference_tick returns none and strike helpers that resolve against the reference cannot run.

The call always returns the resolved tick as a u64, but it emits ReferenceTickSet only when that tick is new. A repeat call resolving to the same tick succeeds silently.

Click to open
Keeper and admin source

Read accessors​

Every accessor below is permissionless and non-mutating, so a devInspect or simulated transaction reads them without gas. Grouped by what they tell you:

GroupAccessors
Identity and phaseid, propbook_underlying_id, expiry, is_settled, settlement_price, try_settlement_price, mint_paused, is_pending_valuation
Strike gridtick_size, admission_tick_size, reference_tick, reference_tick_source_timestamp_ms
Cash and exposurecash_balance, inventory_impact_reserve, fee_incentive_balance, payout_liability, required_cash
Frozen policybacking_buffer_lambda, expiry_fee_window_ms, expiry_fee_max_multiplier, inventory_impact_max_rate, inventory_impact_scale

Of those accessors, 4 behave differently from the rest:

  • settlement_price: Aborts on an unsettled market with std::option::EOPTION_NOT_SET, because it unwraps an empty option rather than running a phase check. Use try_settlement_price, which returns Option<u64>, when you do not already know the phase.
  • reference_tick: Returns Option<u64> and is none until someone calls the permissionless set_reference_tick.
  • inventory_impact_scale: Reports the cadence allocation cap that the registry froze into the market at creation, not a separately configured value.
  • is_pending_valuation: Takes &ProtocolConfig as well as the market. It returns true while the in-flight pool flush holds this market's snapshot and its value_expiry has not run. It gates nothing: trading and settlement both proceed regardless, so do not defer a settlement attempt on it.

Another 3 accessors need a market-bound Pricer or a settled market, because they mark positions rather than read stored fields:

public fun current_nav(market: &ExpiryMarket, pricer: &Pricer): u64
public fun live_order_value(market: &ExpiryMarket, pricer: &Pricer, order_id: u256): u64
public fun settled_order_payout(market: &ExpiryMarket, order_id: u256): u64

current_nav is free expiry cash minus the marked liability of the exposure book, floored at zero. live_order_value is one order's full-close range value before fees. settled_order_payout is one order's terminal payout and aborts with EMarketNotSettled before settlement. None of the 3 proves that the caller holds order_id. The pool flush values a market with the package-internal snapshot_nav, which has current_nav's exact shape over the values frozen at the snapshot instant.

Click to open
Read accessor source

Protocol version 4 adds 3 public reads for the order flow:

  • order_flow_state(market): (u64, u64, u64) returns the market's waiting_cash_need, its payout-tree node count, and its snapshotted minimum entry probability.
  • receipt_info(receipt) returns a queued order's market, stage, account, order ID, Pyth feed, cash need, subsidy bound, and volatility snapshot.
  • quote_close(market, pricer, receipt, close_quantity, builder_code_id, clock): RedeemQuote prices an early sell of a queued position. The order-flow package's quote_redeem_open wraps it, and the RedeemQuote getters carry a redeem_ prefix, such as redeem_proceeds, so they do not clash with the MintQuote getters.

The SDK's client.predict.read.markets, read.market, and read.pricer cover the same ground over simulated transactions, with raw values converted to decimals. See the tutorial for the read path end to end.

Error codes​

Error numbers repeat across modules, so read the module out of the abort location before you look a number up in any table below.

expiry_market declares 12 abort codes on protocol version 3 and 23 on protocol version 4:

CodeConstantCause
0EMintPausedThis market's mint_paused flag is true. Other flows remain open.
1EMarketNotSettledA settled-only flow ran on an unsettled market, including redeem_settled and settled_order_payout.
2EMintCostAboveMaxThe quoted all_in_cost exceeds the max_cost you passed.
3EMintProbabilityAboveMaxThe quoted entry probability exceeds the max_probability you passed.
4EWrongPricerload_live_pricer built the pricer for a different market. Load one against this market in the same transaction.
5EReferenceTickObservationMissingNo exact Pyth observation exists at the market's reference_tick_source_timestamp_ms.
6EMintRedeemSameTimestampA live redeem ran in the same onchain millisecond as the mint that opened the position.
7ERedeemProbabilityBelowMinThe quoted range probability fell below the min_probability floor.
8ERedeemProceedsBelowMinThe net credit fell below the min_proceeds floor.
9EMintCostCapRequiredA caller ran mint_exact_amount with max_cost set to zero. mint_exact_cost never raises it: there the budget is the sizing input, and a zero budget aborts EPremiumBelowMinimum instead.
10EMarketNotPendingValuationThe pool flush tried to value a market that carries no snapshot stamp. Package-internal, and unreachable through the public flush sequence.
11EMintCostAboveMaxPayoutThe quoted all_in_cost exceeds quantity, the position's maximum payout. On protocol version 3 every quote and every mint raises it. On mint_exact_cost it means no admissible size cleared the bound, because that entrypoint first steps a breaching fill down.
12ENotSettledRedeemKeeperProtocol version 4 only. A sender that is not an allowlisted keeper called redeem_settled_permissionless.
13EDelayedExecutionRequiredProtocol version 4 only. A retired immediate mint or redeem_live ran. It always aborts.
14EOrderFailsLimitsProtocol version 4 only. The placement dry run would refund the order, or a quote would be refused at the clock.
15EInsufficientMarketCashProtocol version 4 only. A queued mint's cash need exceeds the market's spare cash.
16EInvalidOrderTimingProtocol version 4 only. An admission outside Predict's timing bounds, a commit at or past the deadline, or a quote at or past expiry.
17EInvalidOrderTermsProtocol version 4 only. The policy's SVI age exceeds Predict's 120-second ceiling.
18EWrongMarketProtocol version 4 only. A queued order's receipt belongs to another market.
19EWrongStageProtocol version 4 only. A receipt is at the wrong stage for the call, such as quoting a sell of a position that is not open.
20ENotRecordOwnerProtocol version 4 only. A sell admission named another account's position.
21EEscrowMismatchProtocol version 4 only. A fill or a refund was presented with less escrow than the order's budget, order fee, and reserved subsidy.
22EWrongPriceProtocol version 4 only. A committed price does not match the order's feed, channel, or τ.

Aborts from other modules reach these calls too. A mint can abort inside strike_exposure_config on entry-probability or premium policy, covered in the next section. A pricer load can abort inside pricing when an oracle object does not match the registry binding, when a contributing observation is stale or the same transaction wrote it, or with ELivePricingExpired at or after expiry. A gate can abort inside protocol_config with these codes:

  • ETradingPaused (0)
  • EPackageVersionDisabled (3)
  • EProtocolFrozen (5)
  • ESnapshotInProgress (6), when a trade lands inside a pool flush's snapshot transaction
  • ETradeWindowClosed (7), inside the no-trade window on protocol version 3
  • ECutoverNotReached (13), on protocol version 4, when a queued order arrives before the watermark reached 4
  • EOrderFlowNotAllowed (15), on protocol version 4, when an order-flow call comes from a witness that is not allowlisted
  • EEwmaRetired (14), on protocol version 4, from the retired EWMA setters

On protocol version 4, a queued order's placement can also abort inside pricing with EPythForwardRequired (19) while use_pyth_spot_for_forward is off, and the order-flow package raises the codes in Order-flow package errors.

An account borrow can abort inside account with EInvalidOwner or EBalanceTooLow.

Order-flow package errors​

The queue's own checks run before Predict's gates, so when several apply, the order-flow package's error fires first. A settled market is not an error for commit or resolve: they return without change. Error numbers restart in every module, so resolve the module from the abort location before you read the number.

deepbook_predict_orders::queue​

CodeConstantCause
0EWrongDeskThe queue or the registry belongs to another desk.
1EWrongMarketThe queue belongs to another market.
2EQueueStuckThe stuck gate refuses new orders.
3EQueueFullThe order's side is at capacity in this market.
4EAccountOrderCapThe account is at its cap on unfinished orders in this market.
5EPastCutoffτ would fall at or past the market's cutoff.
6EMintCostCapRequiredA queued mint passed max_cost as zero or u64::MAX.
7EFeeNotCoveredThe balance cannot pay the order fee. A mint needs more than the fee.
8EBelowMinSellA sell, or the remainder it leaves, is below min_sell_quantity.
9ERecordNotOpenThe record to sell or quote is missing or not Open.
10ENotRecordOwnerThe record belongs to another account.
11EMarketNotSettledcleanup ran before settlement.
12EMarketNotExpiredsettle_step ran before expiry.

deepbook_predict_orders::desk and delayed_execution_config​

ModuleCodeConstantCause
desk0EPackageVersionDisabledThe order-flow package version is below the desk's version floor.
desk1EVersionWatermarkNotAdvancedA watermark bump would not advance the floor.
desk2EProtocolFrozenA policy setter ran while Predict is frozen.
delayed_execution_config0 to 12EInvalidDelayMs, EInvalidStallTimeoutMs, EInvalidStuckThresholdMs, EInvalidGapWaitMs, EInvalidPythPriceBufferMs, EInvalidSviMaxAgeMs, EInvalidMintCapacity, EInvalidSellCapacity, EInvalidPerAccountCap, EInvalidMinSellQuantity, EInvalidOrderFee, EInvalidSettleRefundBatch, EInvalidSettlePayoutBatchOne field's value is outside its bound in the policy table, numbered in the order listed.
delayed_execution_config13EUnsupportedPythChannelThe channel is not a supported fixed-rate Lazer channel.
delayed_execution_config14EInvalidTimingThe timing fields break pyth_price_buffer_ms < stuck_threshold_ms <= gap_wait_ms < stall_timeout_ms.
delayed_execution_config15EInvalidLimitsThe per-account cap exceeds the smaller capacity.

deepbook_predict_math::lazer_price​

CodeConstantCause
0EPropertyNotRequestedThe update lacks the price, exponent, or update-time property.
1EGenerationAfterEnvelopeA feed claims an update time after the envelope that carries it.
2EFeedMissingThe order's feed is missing from the update.

These 3 abort commit when the caller passes an update the decoder cannot read. A cohort whose price is unusable for any other reason simply waits for a later commit or its deadline refund.

Mint admission errors​

The 2 ordinary failure modes of a first mint are not in the table above. Both live in deepbook_predict::strike_exposure_config, the module that holds each market's snapshot of entry-probability and fee policy and runs assert_range_mint_probability_policy and assert_mint_admission before any state moves:

CodeConstantCause
0EEntryProbabilityOutOfBoundsThe quoted range probability, or the probability at one of its finite boundaries, falls outside the market's band, from min_entry_probability to max_entry_probability inclusive.
1EInvalidEntryProbabilityBoundAn admin update would leave the minimum at or above the maximum. Traders do not reach this.
2EInvalidFeeProbabilityA probability above 1.0 reached the Bernoulli fee curve.
3EPremiumBelowMinimumThe premium is below the min_premium floor of 1_000_000 base units, which is 1 USDC.

EPremiumBelowMinimum is the size floor, and it is the one an integrator meets first. Premium is quantity * entry_probability, so the smallest admissible quantity is 1 / max_entry_probability USDC of payout at best, and it rises as the strike moves away from the money. Each market snapshots its band at creation. A chain read on 2026-10-13 found the template band at 0.25 to 0.75 on Mainnet, and at 0.15 to 0.85 on Testnet for markets created from that day. With a 0.75 maximum, the smallest admissible quantity is about 1.34 USDC of payout. At an entry probability of 0.25, the smallest admissible quantity is 4 USDC of payout, rounded up to the lot size.

EEntryProbabilityOutOfBounds is the band. A strike far enough out of the money prices below min_entry_probability, and the admission check rejects it before computing the premium. The band is narrow on both networks, so a strike far from the money, including a window's reference strike once the price has moved away from it, can fall outside it. Read the market's band with the SDK's read.feePolicy rather than assuming one.

A two-sided range meets the band at each finite boundary as well: the probability that settlement ends above the lower strike and the probability that it ends at or below the higher strike must each sit inside the band. On a market with a 0.25 to 0.75 band, a range with a 0.8 probability of settling above its lower strike and a 0.3 probability of settling above its higher strike has a range probability of 0.5, inside the band, yet the mint aborts with EEntryProbabilityOutOfBounds, because 0.8 exceeds the 0.75 maximum. On protocol version 4 the same failures refund a queued order with reason 2, or abort its placement with EOrderFailsLimits, rather than raising these codes.

Because error numbers repeat across modules, the number alone identifies nothing. Read the module out of the abort location before you look the number up. Code 3 is EPremiumBelowMinimum in strike_exposure_config, EMintProbabilityAboveMax in expiry_market, EReferenceTickAlreadySet in strike_exposure, and EInvalidQuantity in order. See Strikes and Ticks for the order and strike_exposure tables.

Order events​

Order-domain events live in deepbook_predict::order_events and all 3 carry copy, drop, and store. The field order below is declaration order, which is the order BCS decoding depends on. Do not reorder it.

OrderMinted​

Each mint function emits it once per successful mint on protocol version 3. On protocol version 4, resolve in the order-flow package emits it for each queued mint fill, with penalty_fee 0:

#FieldType
1expiry_market_idID
2account_idID
3order_idu256
4position_root_idu256
5owneraddress
6lower_ticku64
7higher_ticku64
8entry_probabilityu64
9quantityu64
10premiumu64
11trading_feeu64
12fee_incentive_subsidyu64
13builder_feeu64
14penalty_feeu64
15referral_feeu64
16inventory_impact_chargeu64
17builder_code_idOption<ID>
18referrer_account_idOption<ID>
19onchain_timestamp_msu64
20pyth_spot_source_timestamp_msu64
21block_scholes_spot_source_timestamp_msu64
22block_scholes_forward_source_timestamp_msu64
23block_scholes_svi_source_timestamp_msu64

referral_fee appears here but not in the Move MintQuote, because the chain computes it after all_in_cost is fixed and carves it out of the payment the trader already made. See The referral split. penalty_fee is the congestion surcharge the SDK calls penalty, and fee_incentive_subsidy is what the SDK calls subsidy. The 4 source-timestamp fields are each pricing input's own source clock, the timestamp the load validated its freshness against: the Pyth feed timestamp for the spot, and the provider's per-update timestamp for each Block Scholes value.

LiveOrderRedeemed​

redeem_live emits it once on protocol version 3, whether the close is partial or full. On protocol version 4, resolve emits it for each queued sell fill, with penalty_fee 0:

#FieldType
1expiry_market_idID
2account_idID
3order_idu256
4position_root_idu256
5owneraddress
6quantity_closedu64
7remaining_quantityu64
8replacement_order_idOption<u256>
9redeem_amountu64
10trading_feeu64
11builder_feeu64
12penalty_feeu64
13inventory_impact_rebateu64
14builder_code_idOption<ID>
15onchain_timestamp_msu64
16pyth_spot_source_timestamp_msu64
17block_scholes_spot_source_timestamp_msu64
18block_scholes_forward_source_timestamp_msu64
19block_scholes_svi_source_timestamp_msu64

replacement_order_id holds a value on a partial close and none on a full one. position_root_id stays constant across replacements, so join on it to follow one economic position through partial closes.

SettledOrderRedeemed​

Both redeem_settled and redeem_settled_permissionless emit it:

#FieldType
1expiry_market_idID
2account_idID
3order_idu256
4position_root_idu256
5owneraddress
6payout_amountu64
7onchain_timestamp_msu64

The event carries the payout but not the settlement price. Read that from the MarketSettled event or from settlement_price on the market.

Queue events​

Every queue event comes from deepbook_predict_orders::queue_events, so an indexer subscribes to both packages. OrderMinted and LiveOrderRedeemed keep their Predict type and layout, but a fill transaction calls the order-flow package, so a filter keyed on the called package must match on the event type instead.

EventModuleEmitted by
OrderEnqueuedqueue_eventsEach enqueue_*, with the request, timing, volatility snapshot, escrow terms, the source record of a sell, and the market's cash, required cash, and waiting cash need after the call
CohortCommittedqueue_eventscommit, once for each cohort, with the normalized spot, the tick, the generation time, the feed, the channel, and the sender
QueuedOrderFilledqueue_eventsresolve, beside Predict's OrderMinted or LiveOrderRedeemed
QueuedOrderRefundedqueue_eventsEvery refund, with the reason, what was returned, and whether a position went back to Open
QueuedOrdersCleanedqueue_eventscleanup, when it deleted at least one record
OpenRecordSettledqueue_eventsThe payout walk or pay_open, once for each Open record paid, with payout 0 for a loser
OpenRecordPayoutSkippedqueue_eventsThe payout walk or pay_open, for each Open record it cannot pay
RecordFundsParkedqueue_eventsA refund or a fill's change that the receive address could not take
RecordFundsClaimedqueue_eventsclaim_parked, when it sends a record's parked funds
MarketPayoutsCompletedqueue_eventsThe settle_step call that finishes the payout walk, once for each queue
DelayedExecutionPolicyUpdatedqueue_eventsEvery policy setter, with the complete policy
OrderFlowUpdatedPredict config_eventsset_order_flow, with the witness type name and the new state
FlushOperatorUpdatedPredict config_eventsadd_flush_operator and remove_flush_operator

OrderEnqueued, QueuedOrderFilled, and QueuedOrderRefunded carry the market's cash, required cash, and waiting cash need after the call, so a keeper can track spare cash from events alone. CohortCommitted, QueuedOrderFilled, and QueuedOrderRefunded carry the transaction sender, so monitoring sees fills by third parties. The 2 Predict events are new types in protocol version 4, so match them against the Predict package version that introduced them rather than Predict's original ID. Contract Information explains why.

Click to open
Order event source

Market lifecycle events such as MarketCreated, ReferenceTickSet, MarketSettled, ExpiryMarketMintPausedUpdated, and NoTradeWindowUpdated live in deepbook_predict::config_events. See Registry for those, and Vault for the pool, flush, and liquidity events.