Skip to main content

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, with identical package sources: the Testnet publish comes from the deepbook-predict-testnet branch, and the Mainnet publish from the deepbook-predict-mainnet branch of the DeepBookV3 repository. Version 2.3.0 or later of the @mysten/deepbook-v3 package ships a /predict subpath that carries both deployments, builds every transaction, and answers every read, so trading needs no indexer. The examples on these pages select a network with one constant and default to Testnet.

caution

These pages describe the 2 current deployments. As of 2026-09-10 the Mainnet deployment is onchain with its configuration in place, but its pool holds only the bootstrap minimum and no market has been tradable there yet. Testnet creates and settles markets every minute. Read read.markets(), read.pool(), and the ProtocolConfig object 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.

Quickstart: mint a position

The following 7 steps install the DeepBook TypeScript SDK, prepare a funded Predict account, and quote and mint a position on a live market under a cost cap and a fill price cap. They run on Testnet, where the quote coin is a mintable test coin. The samples live in the examples/deepbook-predict package, which type-checks with npm run build (tsc --noEmit) against @mysten/deepbook-v3 version 2.3.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 install @mysten/deepbook-v3 @mysten/sui

The Predict client lives on the @mysten/deepbook-v3/predict subpath. You do not need a separate Predict package.

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 to hold

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.

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.

info

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 mint lands, and the cost cap then aborts the trade. The protocol also closes trading outright inside a no-trade window before expiry, 2 seconds on both deployments, where a mint or live redeem aborts with ETradeWindowClosed. 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. 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. Quote, then mint under both caps

Quote first. client.predict.read.quoteMint dry-runs the identical transaction the mint builder produces, against the real account and the real fee path, so it doubles as a preflight check and raises the same typed errors the mint would, insufficient balance included. It returns the all-in account debit as cost and the fill price as entryProbability.

The mint takes 2 independent caps, and they bound different things:

  • maxCost bounds the all-in account debit, in USDC. Derive it from the quote's cost with a small buffer, rounded to at most 6 decimals.
  • maxProbability bounds the fill price, between 0 and 1 per 1 USDC of payout. Derive it from the quote's entryProbability with a small buffer.

Both are optional. The following sample passes both.

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 };
}
caution

Omitting maxCost and maxProbability sends U64_MAX for each, which leaves the mint genuinely uncapped against a price move between the quote and execution. tx.mint mints an exact payout quantity, so the premium alone cannot exceed quantity, but the protocol charges fees on top of it and only maxCost bounds the total debit. tx.mintAmount is the riskier default: there the protocol charges fees on top of spend, and maxCost is the only bound on the whole withdrawal. Quote first, then pass both caps.

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. quantity is the maximum payout in USDC, and it must be a whole lot of 0.01 USDC.

The lot size is the granularity of a quantity, not a size you can trade. Every mint has to pay a premium of at least 1 USDC, and the premium is quantity multiplied by the quoted entryProbability. Size a mint by that product rather than by the lot grid: a quantity of 1 is below the floor on every live market and aborts in strike_exposure_config with code 3, EPremiumBelowMinimum. A strike quoting near a 50 percent chance needs roughly 2 contracts, a far strike quoting at a low probability needs proportionally more, and no strike ever clears the floor below about 1.02 contracts, so quote first and pick a quantity whose premium reaches 1 USDC.

The mint returns an order ID. Read it from the transaction result with client.predict.decode.mint(result).orderId and persist it, because redeeming or claiming a position needs the market and order ID together, and closing part of a position replaces the ID with a new one. For range positions, live redemption, settled claims, and the liquidity provider flow, see the end-to-end tutorial.

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':

  1. Fund a Testnet address with SUI, then confirm a nonzero balance with sui client gas.
  2. Request test USDC through the token request form, then confirm the balance with sui client balance, where it appears as DUSDC.
  3. Confirm the configuration block resolves: getDeployment('testnet').deployment is deepbook-predict-testnet on chain 4c78adac.
  4. Confirm at least one live market exists: client.predict.read.markets() returns an entry whose expiry is in the future, with mintPaused false and a non-null referencePrice.
  5. Run the account step, then confirm the derived wrapper resolves with sui client object WRAPPER_ID.
  6. Run the quote step, then set quantity so that quantity multiplied by the quote's entryProbability clears 1 USDC with margin. A quantity of 3 against a near-the-money strike is a safe first try, and a quantity of 1 aborts on every market.
  7. Run the mint step, then confirm the effects report a success status and an OrderMinted event. Its account_id field is the canonical account ID.

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 2 seconds before expiry, so a mint or live redeem inside that no-trade window aborts rather than filling 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.
  • 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 propbook package.
  • Shared account custody: One shared account per owner holds USDC balances, open positions, and builder code attribution, at an address derived from the owner address.
  • Pooled liquidity: Liquidity providers queue USDC supply and PLP withdrawal 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 quotes, transactions, and reads without an indexer. Events stream from any full node over gRPC.

For the objects behind these features, see Design.