DeepBook Predict
DeepBook Predict is an expiry-based prediction market protocol on Sui. Each market covers one underlying and one expiry timestamp, and every position is a range contract: it pays a fixed amount when the settlement price lands inside a chosen strike range, and nothing otherwise. Liquidity providers supply USDC to a shared pool, receive PLP shares, and take the other side of every trade.
Predict runs on Sui Mainnet as deepbook-predict-mainnet and on Sui Testnet as deepbook-predict-testnet. The Mainnet publish comes from DeepBookV3 source commit 7b169bde and Testnet from 4d752fb8. Both have since upgraded predict to protocol version 3, which Mainnet runs. On 2026-10-09 Testnet moved on to protocol version 4, which adds delayed execution: every trade is a queued order, placed now and filled at Pyth's signed price for an instant about 800 ms later, or refunded. Mainnet still fills trades immediately until its own cutover. Version 2.7.0 or later of the @mysten/deepbook-v3 package ships a /predict subpath that carries both deployments, builds every transaction for either trade path, and answers every read, so trading needs no indexer. The examples on these pages select a network with one constant and default to Testnet.
These pages describe the 2 current deployments, which went live on 2026-09-12. A chain read on 2026-10-13 found Testnet trading, with a new market every window, and Mainnet's trading_paused flag set since 2026-09-30, so a trade aborts on Mainnet for now. Read read.markets(), read.pool(), the ProtocolConfig object, and read.executionMode() on the network you target rather than assuming its state, and treat the package IDs and object IDs on these pages as pinned to those 2 deployments.
The quickstart below runs on Testnet. A freshly created market also rejects a mint until plp::rebalance_expiry_cash funds it from the pool, as step 5 explains.
Quickstart: how to place a position
The following 7 steps install the DeepBook TypeScript SDK, prepare a funded Predict account, and plan and queue a position on a live market. They run on Testnet, where the quote coin is a mintable test coin and every trade is a queued order. The samples live in the examples/deepbook-predict package, which type-checks with npm run build (tsc --noEmit) against @mysten/deepbook-v3 version 2.7.0 or later and @mysten/sui version 2.30.0 or later. They pass compile checks but do not run against Testnet during the documentation build, so work through Verify on Testnet before you rely on them.
1. Install the SDK
Install the DeepBook SDK and its Sui peer dependency.
- npm
- Yarn
- pnpm
npm install @mysten/deepbook-v3 @mysten/sui
yarn add @mysten/deepbook-v3 @mysten/sui
pnpm add @mysten/deepbook-v3 @mysten/sui
The Predict client lives on the @mysten/deepbook-v3/predict subpath. You do not need a separate Predict package. DeepBook Predict SDK documents every function it exports.
2. Request Testnet tokens
You need 2 assets on Testnet, from 2 separate sources. Neither one substitutes for the other:
- SUI for gas: Every transaction on this page pays gas in SUI, including the ones that only move the quote coin. Request SUI from a Sui Testnet faucet, which lists the browser, Discord, and cURL routes.
- Test USDC as the quote asset: Predict denominates deposits, premiums, payouts, and pool supplies in its quote coin. On Testnet that is a test coin with the type
0xc028557a1ed49e42ed091e115aedefd70a442b184c18fbec5c48d5b6c0b8c184::usdc::USDC, which wallets display as DUSDC. Request it through the DeepBook Predict Testnet token request form. The quote coin never pays gas, so an address that holds it but no SUI cannot submit a Predict transaction.
On Mainnet the quote asset is Circle's native USDC, and there is no faucet. Read the exact type for either network from getConfig(network).quoteCoinType rather than from a symbol: the 2 coins share the usdc::USDC module path but live in different packages.
How much SUI do I need?
Keep at least 1 SUI on the address as a working reserve. That covers many Predict transactions with headroom, because a single faucet request supplies well above the cost of one transaction. The quickstart transactions cost more than a plain transfer: creating and sharing an account writes new objects, so it pays storage on top of computation. Sui refunds part of the storage cost as a rebate when you delete objects, so a session costs less in total than the sum of its gas budgets.
Treat 1 SUI as a starting reserve, not a budget. Before you rely on a fixed gas budget in your own code, measure the real cost of each transaction with a dry run, then set the budget with margin for variable costs such as shared object contention. See Gas Fees for the fee formula and budget rules.
How do I request more SUI?
Check your balance with sui client gas before you request. Faucets rate-limit per address and per IP, so request only when the balance actually runs low. If the browser faucet rate-limits you, use the Discord or cURL routes under alternative methods for getting SUI tokens. When you finish testing, return unused Testnet SUI to the faucet pool.
3. Set the configuration block
The SDK ships each deployment's package IDs, object IDs, coin types, and units, so you do not hardcode them. The NETWORK constant selects 'testnet' or 'mainnet', and everything else derives from it. Assert the deployment name at startup, because a later SDK release can move a network to a newer deployment: getDeployment('testnet').deployment reads deepbook-predict-testnet and getDeployment('mainnet').deployment reads deepbook-predict-mainnet for the deployments these pages describe. The same values appear on the Contract Information page.
import { getConfig, getDeployment, getUnits } from '@mysten/deepbook-v3/predict';
// The SDK carries a deployment record for Testnet and for Mainnet, so `getConfig`
// resolves either. This constant is the single place these examples select a
// network. Change it here and every other file follows. They default to Testnet,
// where the quote coin is a mintable test coin; on Mainnet it is native USDC.
export const NETWORK = 'testnet' as 'testnet' | 'mainnet';
export const FULLNODE_URL =
NETWORK === 'mainnet'
? 'https://fullnode.mainnet.sui.io:443'
: 'https://fullnode.testnet.sui.io:443';
// One underlying is live on this deployment.
export const UNDERLYING = 'BTC';
// The SDK carries the IDs of whichever deployments its release was cut against.
// Assert the name at startup, so a later SDK release that moves a network to a
// new deployment fails loudly here rather than quietly trading against a
// deployment these examples were never checked against.
export const EXPECTED_DEPLOYMENT = {
testnet: 'deepbook-predict-testnet',
mainnet: 'deepbook-predict-mainnet',
}[NETWORK];
export const DEPLOYMENT = getDeployment(NETWORK);
if (DEPLOYMENT.deployment !== EXPECTED_DEPLOYMENT) {
throw new Error(
`Expected DeepBook Predict deployment ${EXPECTED_DEPLOYMENT}, got ` +
`${DEPLOYMENT.deployment} (chain ${DEPLOYMENT.chainId}, ` +
`deepbookv3 commit ${DEPLOYMENT.sourceCommit}).`,
);
}
// Package IDs, the shared registry, protocol config, and pool vault objects, the
// quote coin type, and the per-underlying oracle IDs all come from the SDK, so
// no deployment identifier is hardcoded in these examples. Always read the quote
// coin from `CONFIG.quoteCoinType`: on Mainnet it is native USDC, and on Testnet
// it is a test coin with the same `usdc::USDC` module path that displays as DUSDC.
export const CONFIG = getConfig(NETWORK);
// Scale constants the deployment owns: position quantities are whole
// `positionLotSize` lots, amounts are `quoteCoinDecimals`-decimal USDC, and
// probabilities, prices, and rates are fixed point at `fixedPointScale`.
export const UNITS = getUnits(NETWORK);
Set up a client next. The following example uses the gRPC client, which matches the rest of the DeepBook SDK documentation, and extends it with the Predict client for the selected network.
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { predict } from '@mysten/deepbook-v3/predict';
import { FULLNODE_URL, NETWORK } from './config.js';
// `$extend` registers the Predict facade on the Sui client, so everything below
// reaches it at `client.predict`. Any client exposing the core API works,
// whether gRPC or JSON-RPC.
export const client = new SuiGrpcClient({
network: NETWORK,
baseUrl: FULLNODE_URL,
}).$extend(predict({ network: NETWORK }));
// Reads run through the client's own transaction simulation and object reads, so
// `client.predict.read.*` needs neither an indexer nor a Predict server.
//
// The SDK never signs and never holds keys. Every `client.predict.tx.*` builder
// returns a `Transaction` for a wallet, dapp-kit, or your own signer to execute.
// Execute with events included whenever you intend to decode a receipt, because
// the decoders read the events' canonical BCS bytes.
Every client.predict.read method runs against the node's core API through simulation and object reads, so reads need no indexer. Every client.predict.tx builder returns an unsigned transaction, and the SDK never signs or handles keys.
4. Create and fund an account
Each owner has exactly one Predict account: a shared account wrapper holding balances and Predict positions. Its address derives from the owner address, so client.predict.wrapperIdFor(owner) returns the ID without a chain read. The method name createManager follows DeepBook balance manager naming, and the object it creates is the account wrapper.
import type {
CreateManagerReceipt,
DecodableTransactionResult,
} from '@mysten/deepbook-v3/predict';
import type { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
// Each trader holds one canonical account: a shared `AccountWrapper` holding an
// `Account`. The builder keeps the legacy DeepBook balance-manager name, but the
// object it creates is the account wrapper.
export function createAccount(): Transaction {
return client.predict.tx.createManager();
}
// The wrapper ID is derived from the owner address, so you can compute it before
// the transaction lands. No chain read.
export function accountWrapperId(owner: string): string {
return client.predict.wrapperIdFor(owner);
}
// First-time setup in a single PTB: create the wrapper, deposit, and share it.
// `owner` must be the address that signs, because the wrapper is derived from
// the transaction sender. This aborts if the account already exists, so use it
// only on the create path.
export function createAndFund(owner: string, amountUsdc: number): Transaction {
return client.predict.tx.deposit(owner, amountUsdc, { create: true });
}
// Fund an account that already exists. The USDC is sourced from the owner's
// coin objects and address balance together.
export function deposit(owner: string, amountUsdc: number): Transaction {
return client.predict.tx.deposit(owner, amountUsdc);
}
// Take USDC back out of the account. It lands in the owner's address balance;
// pass `{ toCoinObject: true }` when you need a discrete coin object instead.
export function withdrawToWallet(owner: string, amountUsdc: number): Transaction {
return client.predict.tx.withdraw(owner, amountUsdc);
}
// The decoders are pure and touch no network. Execute the create transaction
// with events included, then read the IDs off the receipt.
export function decodeCreated(result: DecodableTransactionResult): CreateManagerReceipt {
return client.predict.decode.createManager(result);
}
// The account's internal custody balance in USDC, as a decimal number. This is
// the balance a mint is debited from, not the owner's wallet balance.
export async function accountBalance(owner: string): Promise<number> {
return client.predict.read.balance(owner);
}
client.predict.tx.deposit(owner, amount, { create: true }) composes creation, deposit, and sharing into one transaction, which is the fastest first run. Use that form only once per owner, because creating a second account for the same owner aborts, and pass the signing address as owner, because account creation derives the wrapper from the transaction sender.
The wrapper ID is the handle every onchain call takes. A second address, the canonical account ID, derives from the same owner: it is what every order event reports as account_id and what indexed read services key on. Contract Information covers how to get it.
Every transaction on this page asks the owner to sign. An application that would rather not prompt a wallet for each trade can use a delegated session key instead. A session key carries authority over everything the account holds, so decide its scope and lifetime before you ship it, not after. See Sessions.
5. Discover a live market
Predict creates markets on a fixed cadence, one object per underlying and expiry. Both networks configure BTC markets on the 1-minute and 5-minute cadences, with 2 expiries of each live at once. client.predict.read.markets() returns the active ones with their expiry, both tick sizes, mint pause flag, and reference price. Active means live and not yet settled, so an expired market nobody has settled is still in the list, and quoting against it aborts.
import type { ActiveMarket, MarketSummary } from '@mysten/deepbook-v3/predict';
import { client } from './client.js';
import { UNDERLYING } from './config.js';
// Markets are created on a fixed cadence and every expiry is an absolute
// timestamp, so never hardcode one. Read the live board and take an expiry from
// it. `read.markets()` returns the pool's active markets, which means live and
// not yet settled: settlement is permissionless and unrewarded, so a market that
// is past its expiry but that nobody has settled is still in this list, and
// quoting against it aborts.
export async function liveMarkets(): Promise<ActiveMarket[]> {
const markets = await client.predict.read.markets();
// `expiryMs` is a bigint, so order it by comparison rather than subtraction.
return [...markets].sort((a, b) =>
a.expiryMs < b.expiryMs ? -1 : a.expiryMs > b.expiryMs ? 1 : 0,
);
}
// A market with enough life left to quote, sign, and land a transaction.
//
// Taking the soonest expiry is a trap. On the one-minute cadence the entry
// probability converges toward 0 or 1 in the closing seconds, so a quote taken
// there is stale before the mint executes and the `maxCost` cap then aborts the
// trade. Requiring a minimum time to expiry is what makes the quote-then-cap
// flow hold. Raise `minTtlMs` for a wallet flow that waits on a human.
//
// `referencePrice` is null for a short time at the start of a window. Anyone can
// seed it with the permissionless `expiry_market::set_reference_tick`; this
// helper skips those markets instead.
export async function tradeableMarket(minTtlMs = 30_000): Promise<ActiveMarket> {
const now = Date.now();
const open = (await liveMarkets()).filter(
(m) =>
!m.mintPaused &&
m.referencePrice !== null &&
Number(m.expiryMs) - now >= minTtlMs,
);
if (open.length === 0) {
throw new Error(
`No DeepBook Predict market has ${minTtlMs} ms or more left before expiry.`,
);
}
return open[0];
}
// One market's on-chain state, including its live NAV. Returns null when no
// market exists at that expiry.
export async function marketState(expiryMs: bigint): Promise<MarketSummary | null> {
return client.predict.read.market({ underlying: UNDERLYING, expiryMs });
}
// A numeric strike must be a whole multiple of the market's `admissionTickSize`,
// which is deliberately coarser than `tickSize` and varies by cadence. Round the
// target onto that grid rather than assuming a step: an off-grid strike throws
// `PredictInputError` when the transaction is built. The market's own
// `referencePrice` is the single finite strike the chain admits off-grid.
export function admissibleStrike(market: ActiveMarket, targetUsd: number): number {
const snapped = Math.round(targetUsd / market.admissionTickSize) * market.admissionTickSize;
// Trim binary-float residue before returning. Strikes scale by 1e9 and the SDK
// throws when a value carries more than nine decimals, which the multiply above
// produces for sub-dollar steps: at a 0.1 step it lands on values such as
// 96519.90000000001. Both enabled cadences use a 1 USD step today, so this is
// defensive, but the step is mutable protocol state and is read from the market.
return Number(snapped.toFixed(9));
}
Pick a market that is not mint paused and whose expiry leaves you time to trade. The sample's tradeableMarket helper filters on both, and its minTtlMs default of 30 seconds is the reason it exists: on the 1-minute cadence a quote taken in the closing seconds is stale before the order fills, and its limits then refund it. The protocol also stops trading outright inside a no-trade window before expiry, 10 seconds on both deployments, and on Testnet the queue refuses an order whose price instant would fall inside it, so new orders stop about 11 seconds before expiry. The 30-second floor clears that window with room to spare. Raise it for a flow that waits on a human.
A numeric strike must land on that market's admissionTickSize grid, which the sample's admissibleStrike helper rounds onto. The market's own reference price is the one finite strike the protocol admits off that grid, and the descriptor spells it strike: 'reference'. A market reports a null reference price until it records its reference tick. The sample skips those markets, but you do not have to wait for a keeper: expiry_market::set_reference_tick is permissionless, so you can seed the tick yourself.
A brand-new market also opens with zero working cash and rejects a mint until pool capital funds it, and on Testnet it needs its order queue, which the keeper creates right after the market. plp::rebalance_expiry_cash moves that cash from the pool's idle balance, and anyone can call it. A mint that aborts on a freshly created market with everything else in order is usually a market nobody has funded yet.
6. Plan, then queue the order
On Testnet a mint is a queued order. Plan it first: client.predict.read.planMint quotes the order at the current price, turns a slippage in cents per contract into the limits the order carries onchain, and runs a preflight of the queue gates, the balance, and the limits, so plan.accepting is false, with a typed refusal, when the order would be refused now. A few checks run only onchain at placement, so a plan that accepts can still abort there. client.predict.tx.enqueuePlan then builds the enqueue. Once it executes, a keeper fills the order at Pyth's signed price for its price instant, about a second later, within those limits, or refunds it.
examples/deepbook-predict/src/queue-mint.ts. You probably need to run `pnpm prebuild` and restart the site.A directional position is a one-sided range: side: 'up' pays when settlement lands above the strike, and side: 'down' pays when settlement lands at or below it, $1 per contract. Slippage is cents per contract, never a percentage: slippageCents: 10 lets a contract quoted at 42 cents all in, before the fee subsidy, fill at up to 52 cents.
3 rules decide whether the order lands:
- A strike near the money. Admission refuses an entry probability outside the market's band,
0.15to0.85on Testnet markets created since 2026-10-13, and a window's reference strike drifts away from the money as the price moves. The sample picks the strike that prices near 50 cents and snaps it onto the admission grid. - A premium of at least 1 USDC. The premium is the quantity multiplied by the entry probability, so a budget too small to buy 1 USDC of premium is refused.
- An order fee on top. Each queued order pays a flat 0.02 USDC on Testnet.
plan.totalDebitis the budget plus the fee, and unused budget comes back when the order fills.
After the enqueue executes with events included, read the record ID with client.predict.decode.enqueue(result).recordId and persist it. client.predict.read.waitForOutcome polls the record until it is filled or refunded. A filled mint stays in the market's queue as an Open record rather than entering the account, the queue's settlement walk pays it automatically after expiry, and you can sell it earlier with another queued order. For range positions, early sells, refunds, settled results, and the liquidity provider flow, see the end-to-end tutorial.
On Mainnet, before its cutover, a mint fills immediately through tx.mint, tx.mintAmount, or tx.mintCost under explicit cost caps. Mint a position immediately covers that path and its caps, which default to uncapped.
7. Verify on Testnet
The samples above pass compile checks, but nothing runs them against Testnet. Confirm them end to end yourself, with NETWORK set to 'testnet':
- Fund a Testnet address with SUI, then confirm a nonzero balance with
sui client gas. - Request test USDC through the token request form, then confirm the balance with
sui client balance, where it appears as DUSDC. - Confirm the configuration block resolves:
getDeployment('testnet').deploymentisdeepbook-predict-testneton chain4c78adac. - Confirm at least one live market exists:
client.predict.read.markets()returns an entry whose expiry is in the future, withmintPausedfalse and a non-nullreferencePrice. - Run the account step, then confirm the derived wrapper resolves with
sui client object WRAPPER_ID. - Confirm
client.predict.read.executionMode()returns'delayed', then plan an order of about 5 USDC and confirmplan.acceptingistrue. - Queue it, confirm the effects report a success status and an
OrderEnqueuedevent, whoseaccount_idfield is the canonical account ID, then confirmread.waitForOutcomeends'filled'within a few seconds.
Key features
DeepBook Predict provides the following capabilities:
- Per-expiry markets: Each market settles once, at one timestamp, from one of 2 sources: the exact Pyth price at the expiry timestamp, or, when that is unavailable 30 seconds after expiry, the exact Block Scholes minute-boundary spot. Cadence slots run from 1 minute to 1 month in 6 sizes, and both deployments enable the 1-minute and 5-minute cadences. Trading closes 10 seconds before expiry, so no trade fills inside that no-trade window at a converged price.
- Range positions: Every position pays its full quantity when settlement lands inside its strike range and nothing otherwise. Directional positions are ranges with one open end.
- Queued orders: From protocol version 4, which Testnet runs, every mint and early sell is priced at Pyth's signed price for a fixed instant shortly after placement, so nobody can trade against a price that is already stale. Any caller can fill or refund a due order, and the queue pays filled positions at settlement with no claim step.
- Oracle pricing from external feeds: Pyth Lazer spot, plus Block Scholes forward and stochastic volatility inspired (SVI) data, drive every quote from shared objects in a separate
propbookpackage. - Shared account custody: One shared account per owner holds USDC balances, positions minted before the cutover, and builder code attribution, at an address derived from the owner address.
- Pooled liquidity: Liquidity providers queue USDC supply and
PLPwithdrawal requests, each with an optional floor, and a periodic flush fills them at one frozen pool value. - TypeScript SDK on both networks: The SDK carries the Mainnet and Testnet deployments and covers plans, quotes, transactions, and reads for both trade paths without an indexer. Events stream from any full node over gRPC.
For the objects behind these features, see Design.
Testnet Workflow
End-to-end DeepBook Predict walkthrough on Testnet with the TypeScript SDK: network selection, account wrapper setup, board pricing, planning and queuing directional and two-sided range mints, selling and refunding queued orders, settlement, and the queued liquidity provider flow with per-request floors.
Design
DeepBook Predict architecture: the Registry, ProtocolConfig, PoolVault, and ExpiryMarket shared objects, account custody, propbook oracle feeds, the absolute tick grid, queued orders from protocol version 4, settlement, the no-trade window, and the staged PLP flush.