Skip to main content

Predict

Trading on DeepBook Predict runs through the deepbook_predict::expiry_market module. 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 the deepbook-predict-mainnet branch. The Move snippets on this page pin the Mainnet source commit, which serves both deployments. For the package and object IDs of each network, see Contract Information.

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.

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.

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 and the SVI window at 60,000 ms.

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 2 Move functions that price a prospective mint without changing market state. Both 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. Neither 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

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 under the max_premium budget. The congestion surcharge each quote reports uses the pre-update congestion estimate, which is the same estimate a mint in the same state charges.

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. It dry-runs the identical transaction client.predict.tx.mint builds, against the real account and the real fee path, so it does double as preflight and raises the same typed errors the mint would, insufficient balance included. When a page says a quote is a preflight, it means that dry run, not the Move functions above.

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.
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. 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 on both deployments, so it reads zero in practice. 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

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.

Mint a position

Positions open through 2 functions. Both take the strike range as the tick pair (lower_tick, higher_tick], both consume an account Auth, and both 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

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.

Both 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 both 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 and client.predict.tx.mintAmount builds mint_exact_amount. Omitting the SDK's maxCost or maxProbability options sends u64::MAX, which caps nothing, so read a quote first and set a cap.

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, deployed at 2000 on both networks, defines a window at the end of a market's life in which every live flow 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
  • 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.

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. 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. 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 and why anyone can run it on any holder's behalf. 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. 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);
}

Settle a market

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

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

expiry_market declares 12 abort codes:

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.
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. Both quotes and both mints raise it.

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

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

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_mint_admission before any state moves:

CodeConstantCause
0EEntryProbabilityOutOfBoundsThe quoted entry probability 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, and max_entry_probability is 0.99 on both deployments, so the smallest admissible quantity never falls below about 1.02 USDC of payout, and it rises as the strike moves away from the money. At an entry probability of 0.08, for example, the smallest admissible quantity is 12.5 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, which is 0.01 on both deployments, and the admission check rejects it before computing the premium.

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

Either mint function emits it once per successful mint:

#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, whether the close is partial or full:

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

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.