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.
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.
| Field | Type | Description |
|---|---|---|
id | UID | Object ID of the shared market. |
propbook_underlying_id | u32 | Propbook underlying this market prices against. The market stores no oracle object IDs. |
expiry | u64 | Settlement timestamp in Unix milliseconds. |
cash | ExpiryCash | USDC custody plus the isolated inventory-impact reserve. |
fee_incentive_balance | Balance<USDC> | Sponsor-funded USDC available to subsidize taker fees on this expiry. |
strike_exposure | StrikeExposure | Exposure lifecycle state across this expiry's strike ticks. |
ewma | EwmaState | Smoothed gas-price statistics behind the congestion surcharge. |
mint_paused | bool | When true, new mints abort. Every other flow stays available. |
valuation_stamp | Option<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.
ExpiryMarket source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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:
Pricerhascopyanddropbut nostore. You cannot save it in an object or carry it between transactions, so every transaction that prices anything must callload_live_pricerfirst. Because it hasdrop, a block that ends up not using the value is still valid. Only the package constructs and thaws the storableFrozenPricerthat 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 withEWrongPricer. - Oracle validation: The load takes 4 Propbook objects and validates 3 of them: it checks the
PythFeed, theBlockScholesValueStore, and theBlockScholesSVIStoreeach against the underlying's current canonical binding, and a mismatch aborts insidepricing. The load does not check theOracleRegistry, 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 withpricing::ELivePricingExpiredat 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.
load_live_pricer source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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.
Quote source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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:
| Getter | Meaning |
|---|---|
quantity | Sized position quantity, which is also the maximum payout. |
entry_probability | Quoted per-contract range probability before fees, scaled by 1e9. |
premium | Net premium, the contract's entry value. |
trading_fee | Trading fee before the sponsor subsidy. |
fee_incentive_subsidy | Sponsor-funded portion of the trading fee the trader does not pay. The SDK calls the same amount subsidy. |
builder_fee | Builder add-on, zero when the account carries no builder code. |
penalty_fee | Congestion surcharge. The SDK calls the same amount penalty, in both the quote and the decoded receipt. |
inventory_impact_charge | Isolated risk escrow charge, separate from every ordinary fee. |
all_in_cost | Total 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.
MintQuote and its getters
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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:
- The running package version must be at or above the protocol version watermark.
- No pool-flush snapshot stage can be open in this transaction.
- The pricer must match this market.
- The market must be outside its no-trade window.
- The global
trading_pausedflag must be false. - This market's
mint_pausedflag 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.
Mint source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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.
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_mintquote_mint_for_accountmint_exact_quantitymint_exact_amountredeem_live
It does not apply to these functions or to any read accessor:
redeem_settledredeem_settled_permissionlesstry_settleset_reference_tickload_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.
Redeem source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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.
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 returnstruewithout changing anything. - Return value: It returns
bool, not an abort.truemeans the market is now or was already settled, andfalsemeans the call could not record settlement yet.boolhasdrop, so a transaction that composestry_settleahead 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
falsebefore expiry. At or after expiry it tries the exact Pyth spot at the expiry timestamp. If Pyth is unavailable it returnsfalseuntil 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.
try_settle source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.Reference ticks and pause control
The module has 2 further functions that change market state, and they differ sharply in what they require:
| Function | Gate | Effect |
|---|---|---|
set_reference_tick | Permissionless 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_paused | Requires &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.
Keeper and admin source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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:
| Group | Accessors |
|---|---|
| Identity and phase | id, propbook_underlying_id, expiry, is_settled, settlement_price, try_settlement_price, mint_paused, is_pending_valuation |
| Strike grid | tick_size, admission_tick_size, reference_tick, reference_tick_source_timestamp_ms |
| Cash and exposure | cash_balance, inventory_impact_reserve, fee_incentive_balance, payout_liability, required_cash |
| Frozen policy | backing_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 withstd::option::EOPTION_NOT_SET, because it unwraps an empty option rather than running a phase check. Usetry_settlement_price, which returnsOption<u64>, when you do not already know the phase.reference_tick: ReturnsOption<u64>and isnoneuntil someone calls the permissionlessset_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&ProtocolConfigas well as the market. It returnstruewhile the in-flight pool flush holds this market's snapshot and itsvalue_expiryhas 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.
Read accessor source
packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.packages/predict/sources/expiry_market.move. You probably need to run `pnpm prebuild` and restart the site.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:
| Code | Constant | Cause |
|---|---|---|
0 | EMintPaused | This market's mint_paused flag is true. Other flows remain open. |
1 | EMarketNotSettled | A settled-only flow ran on an unsettled market, including redeem_settled and settled_order_payout. |
2 | EMintCostAboveMax | The quoted all_in_cost exceeds the max_cost you passed. |
3 | EMintProbabilityAboveMax | The quoted entry probability exceeds the max_probability you passed. |
4 | EWrongPricer | load_live_pricer built the pricer for a different market. Load one against this market in the same transaction. |
5 | EReferenceTickObservationMissing | No exact Pyth observation exists at the market's reference_tick_source_timestamp_ms. |
6 | EMintRedeemSameTimestamp | A live redeem ran in the same onchain millisecond as the mint that opened the position. |
7 | ERedeemProbabilityBelowMin | The quoted range probability fell below the min_probability floor. |
8 | ERedeemProceedsBelowMin | The net credit fell below the min_proceeds floor. |
9 | EMintCostCapRequired | A caller ran mint_exact_amount with max_cost set to zero. |
10 | EMarketNotPendingValuation | The pool flush tried to value a market that carries no snapshot stamp. Package-internal, and unreachable through the public flush sequence. |
11 | EMintCostAboveMaxPayout | The 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 transactionETradeWindowClosed(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:
| Code | Constant | Cause |
|---|---|---|
0 | EEntryProbabilityOutOfBounds | The quoted entry probability falls outside the market's band, from min_entry_probability to max_entry_probability inclusive. |
1 | EInvalidEntryProbabilityBound | An admin update would leave the minimum at or above the maximum. Traders do not reach this. |
2 | EInvalidFeeProbability | A probability above 1.0 reached the Bernoulli fee curve. |
3 | EPremiumBelowMinimum | The 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:
| # | Field | Type |
|---|---|---|
| 1 | expiry_market_id | ID |
| 2 | account_id | ID |
| 3 | order_id | u256 |
| 4 | position_root_id | u256 |
| 5 | owner | address |
| 6 | lower_tick | u64 |
| 7 | higher_tick | u64 |
| 8 | entry_probability | u64 |
| 9 | quantity | u64 |
| 10 | premium | u64 |
| 11 | trading_fee | u64 |
| 12 | fee_incentive_subsidy | u64 |
| 13 | builder_fee | u64 |
| 14 | penalty_fee | u64 |
| 15 | referral_fee | u64 |
| 16 | inventory_impact_charge | u64 |
| 17 | builder_code_id | Option<ID> |
| 18 | referrer_account_id | Option<ID> |
| 19 | onchain_timestamp_ms | u64 |
| 20 | pyth_spot_source_timestamp_ms | u64 |
| 21 | block_scholes_spot_source_timestamp_ms | u64 |
| 22 | block_scholes_forward_source_timestamp_ms | u64 |
| 23 | block_scholes_svi_source_timestamp_ms | u64 |
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:
| # | Field | Type |
|---|---|---|
| 1 | expiry_market_id | ID |
| 2 | account_id | ID |
| 3 | order_id | u256 |
| 4 | position_root_id | u256 |
| 5 | owner | address |
| 6 | quantity_closed | u64 |
| 7 | remaining_quantity | u64 |
| 8 | replacement_order_id | Option<u256> |
| 9 | redeem_amount | u64 |
| 10 | trading_fee | u64 |
| 11 | builder_fee | u64 |
| 12 | penalty_fee | u64 |
| 13 | inventory_impact_rebate | u64 |
| 14 | builder_code_id | Option<ID> |
| 15 | onchain_timestamp_ms | u64 |
| 16 | pyth_spot_source_timestamp_ms | u64 |
| 17 | block_scholes_spot_source_timestamp_ms | u64 |
| 18 | block_scholes_forward_source_timestamp_ms | u64 |
| 19 | block_scholes_svi_source_timestamp_ms | u64 |
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:
| # | Field | Type |
|---|---|---|
| 1 | expiry_market_id | ID |
| 2 | account_id | ID |
| 3 | order_id | u256 |
| 4 | position_root_id | u256 |
| 5 | owner | address |
| 6 | payout_amount | u64 |
| 7 | onchain_timestamp_ms | u64 |
The event carries the payout but not the settlement price. Read that from the MarketSettled event or from settlement_price on the market.
Order event source
packages/predict/sources/events/order_events.move. You probably need to run `pnpm prebuild` and restart the site.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.