Skip to main content

DeepBook Predict Testnet Workflow

A complete DeepBook Predict flow on Sui Testnet covers reading a live oracle, minting binary and vertical range positions, redeeming before and after settlement, and running the liquidity provider vault flow. It continues from the DeepBook Predict quickstart, which covers install, funding, and your first mint.

There is no dedicated Predict TypeScript SDK. You build every transaction with the Sui TypeScript SDK against the protocol's Move entry points. The transaction samples live in the examples/deepbook-predict package, which CI type-checks with tsc --noEmit against @mysten/sui version 2.22.1. This document does not execute them against Testnet. The oracle-list response shape matches the live Testnet server. Run the manual verification steps before you rely on the write paths.

caution

DeepBook Predict smart contracts might change before Mainnet deployment. Treat the current package IDs, object layouts, and entry points as Testnet integration targets. All package IDs and source references on these pages are pinned to the predict-testnet-4-16 branch and change at Mainnet launch.

Configuration and client

Keep every Testnet-only ID in one configuration block. These values come from the Contract Information page and change at Mainnet launch.

// Testnet-only DeepBook Predict IDs, pinned to the `predict-testnet-4-16` branch.
// These change at Mainnet launch. Source: Contract Information page.
export const PREDICT = {
network: 'testnet' as const,
packageId: '0xf5ea2b3749c65d6e56507cc35388719aadb28f9cab873696a2f8687f5c785138',
predictObjectId: '0xc8736204d12f0a7277c86388a68bf8a194b0a14c5538ad13f22cbd8e2a38028a',
// DeepBook Test USDC (DUSDC), 6 decimals.
quoteType:
'0xe95040085976bfd54a1a07225cd46c8a2b4e8e2b6732f140a0fc49850ba73e1a::dusdc::DUSDC',
serverUrl: 'https://predict-server.testnet.mystenlabs.com',
};

// Oracle ID, expiry, and strike are NOT hardcoded. Read a live oracle from the
// Predict server before minting: GET /predicts/:predict_id/oracles.
export type ActiveOracle = {
oracleId: string; // object ID of the OracleSVI
expiry: number; // ms timestamp
strike: number; // fixed-point strike, per oracle scale
};

The samples reuse the client from the quickstart:

import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
import { PREDICT } from './config.js';

export function getKeypair(privateKey: string): Ed25519Keypair {
const { secretKey } = decodeSuiPrivateKey(privateKey);
return Ed25519Keypair.fromSecretKey(secretKey);
}

export const client = new SuiGrpcClient({
network: PREDICT.network,
baseUrl: 'https://fullnode.testnet.sui.io:443',
});

Understand the oracle

Each market is an OracleSVI object for one underlying asset and one expiry. It holds spot, forward, SVI parameters, a lifecycle status, and, after expiry, a settlement price. Each oracle constrains strikes to a grid rather than free-form values: min_strike plus multiples of tick_size. You trade a binary position at one grid strike, or a vertical range between two grid strikes.

An oracle moves through 4 lifecycle states:

  • Inactive: exists but not yet activated.
  • Active: accepts live price and SVI updates. Minting requires this state.
  • Pending settlement: reached expiry, awaiting the first post-expiry price.
  • Settled: the first post-expiry price freezes the settlement price. No further live updates.

Minting requires a live (active) oracle. Redeeming works against a live or settled oracle.

Read a live oracle from the server

Read the current oracle list from the public Predict server, then select an active oracle and a strike from its grid. The server returns snake_case fields and the strike grid rather than a single strike.

import { PREDICT, type ActiveOracle } from './config.js';

type ServerOracle = {
oracle_id: string;
expiry: number;
min_strike: number;
tick_size: number;
status: string; // "inactive" | "active" | "pending_settlement" | "settled"
};

// Picks the first active oracle and a strike `tickIndex` ticks up the grid.
export async function getActiveOracle(
predictObjectId: string,
tickIndex = 0,
): Promise<ActiveOracle> {
const res = await fetch(`${PREDICT.serverUrl}/predicts/${predictObjectId}/oracles`);
if (!res.ok) throw new Error(`oracle fetch failed: ${res.status}`);
const oracles = (await res.json()) as ServerOracle[];
const live = oracles.find((o) => o.status === 'active');
if (!live) throw new Error('no active oracle available');
return {
oracleId: live.oracle_id,
expiry: live.expiry,
strike: live.min_strike + tickIndex * live.tick_size,
};
}

Set up the PredictManager

Each user creates one PredictManager and reuses it. See the quickstart for create_manager and reading the new ID from effects. Because the manager is shared during creation, deposit into it and mint from it in later transactions.

The manager stores your deposited quote balance, binary position quantities keyed by MarketKey, and range quantities keyed by RangeKey. Read them with predict_manager::balance, predict_manager::position, and predict_manager::range_position. The manager does not store positions as separate objects.

Deposit and mint a binary position

A binary position pays out when settlement lands above the strike (an up position) or at or below it (a down position). Deposit DUSDC into the manager and mint one up position in a single transaction. Build the MarketKey with market_key::up.

import { Transaction } from '@mysten/sui/transactions';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { client } from './client.js';
import { PREDICT, type ActiveOracle } from './config.js';

// Deposits DUSDC into the manager and mints one binary "up" position, in a
// single PTB. `dusdcCoinId` is a DUSDC coin object owned by the signer.
export async function mintBinaryUp(params: {
signer: Ed25519Keypair;
managerId: string;
oracle: ActiveOracle;
dusdcCoinId: string;
depositAmount: bigint; // DUSDC base units (6 decimals)
quantity: bigint; // position quantity
}) {
const { signer, managerId, oracle, dusdcCoinId, depositAmount, quantity } =
params;
const tx = new Transaction();

// 1. Split the deposit amount off a DUSDC coin and deposit it into the manager.
const [deposit] = tx.splitCoins(tx.object(dusdcCoinId), [depositAmount]);
tx.moveCall({
target: `${PREDICT.packageId}::predict_manager::deposit`,
typeArguments: [PREDICT.quoteType],
arguments: [tx.object(managerId), deposit],
});

// 2. Build the MarketKey for an "up" binary position.
const key = tx.moveCall({
target: `${PREDICT.packageId}::market_key::up`,
arguments: [
tx.pure.id(oracle.oracleId),
tx.pure.u64(oracle.expiry),
tx.pure.u64(oracle.strike),
],
});

// 3. Mint the position, paying from the manager's deposited balance.
tx.moveCall({
target: `${PREDICT.packageId}::predict::mint`,
typeArguments: [PREDICT.quoteType],
arguments: [
tx.object(PREDICT.predictObjectId),
tx.object(managerId),
tx.object(oracle.oracleId),
key,
tx.pure.u64(quantity),
tx.object.clock(),
],
});

const result = await client.core.signAndExecuteTransaction({
transaction: tx,
signer,
include: { effects: true },
});
if (result.$kind === 'FailedTransaction') {
throw new Error('mint transaction failed');
}
return result.Transaction;
}

For a down position, use market_key::down with the same oracle, expiry, and strike.

Preview mint cost

Before minting, preview the cost. The predict::get_trade_amounts read function returns (mint_cost, redeem_payout) per the requested quantity, priced from oracle fair value plus protocol spread. For rendering, the public server also exposes oracle state and resolved ask bounds through GET /oracles/:oracle_id/state and GET /oracles/:oracle_id/ask-bounds. Deposit at least the previewed mint_cost before you mint.

Mint a vertical range position

A vertical range pays out when settlement lands in the half-open band (lower_strike, higher_strike]. Build the key with range_key::new, which aborts if lower_strike is not less than higher_strike. This example assumes the manager already holds enough DUSDC.

import { Transaction } from '@mysten/sui/transactions';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { client } from './client.js';
import { PREDICT, type ActiveOracle } from './config.js';

export async function mintRange(params: {
signer: Ed25519Keypair;
managerId: string;
oracle: ActiveOracle;
lowerStrike: bigint;
higherStrike: bigint;
quantity: bigint;
}) {
const { signer, managerId, oracle, lowerStrike, higherStrike, quantity } = params;
const tx = new Transaction();

const key = tx.moveCall({
target: `${PREDICT.packageId}::range_key::new`,
arguments: [
tx.pure.id(oracle.oracleId),
tx.pure.u64(oracle.expiry),
tx.pure.u64(lowerStrike),
tx.pure.u64(higherStrike),
],
});

tx.moveCall({
target: `${PREDICT.packageId}::predict::mint_range`,
typeArguments: [PREDICT.quoteType],
arguments: [
tx.object(PREDICT.predictObjectId),
tx.object(managerId),
tx.object(oracle.oracleId),
key,
tx.pure.u64(quantity),
tx.object.clock(),
],
});

const result = await client.core.signAndExecuteTransaction({
transaction: tx,
signer,
include: { effects: true },
});
if (result.$kind === 'FailedTransaction') throw new Error('mint_range failed');
return result.Transaction;
}

Preview range amounts with predict::get_range_trade_amounts, which mirrors get_trade_amounts for a RangeKey.

Redeem and settlement

Redeeming sells a position back to the vault and deposits the payout into the owner's manager. The payout depends on the oracle lifecycle:

  • Before settlement: the payout is the post-trade bid value at current oracle prices.
  • After settlement: a binary position pays its settled fair value, and a vertical range pays its full value when settlement landed in the band, or zero otherwise.

The owner redeems a position with predict::redeem.

import { Transaction } from '@mysten/sui/transactions';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { client } from './client.js';
import { PREDICT, type ActiveOracle } from './config.js';

export async function redeemBinaryUp(params: {
signer: Ed25519Keypair;
managerId: string;
oracle: ActiveOracle;
quantity: bigint;
permissionless?: boolean; // for settled positions redeemed by anyone
}) {
const { signer, managerId, oracle, quantity, permissionless } = params;
const tx = new Transaction();

const key = tx.moveCall({
target: `${PREDICT.packageId}::market_key::up`,
arguments: [
tx.pure.id(oracle.oracleId),
tx.pure.u64(oracle.expiry),
tx.pure.u64(oracle.strike),
],
});

tx.moveCall({
target: `${PREDICT.packageId}::predict::${permissionless ? 'redeem_permissionless' : 'redeem'}`,
typeArguments: [PREDICT.quoteType],
arguments: [
tx.object(PREDICT.predictObjectId),
tx.object(managerId),
tx.object(oracle.oracleId),
key,
tx.pure.u64(quantity),
tx.object.clock(),
],
});

const result = await client.core.signAndExecuteTransaction({
transaction: tx,
signer,
include: { effects: true },
});
if (result.$kind === 'FailedTransaction') throw new Error('redeem failed');
return result.Transaction;
}

After an oracle settles, anyone can redeem a settled position on the owner's behalf with predict::redeem_permissionless. The payout still lands in the owner's manager. Use this to close out settled positions for users without requiring their signature. Redeem a vertical range with predict::redeem_range.

Liquidity provider flow

Liquidity providers supply an accepted quote asset into the shared vault and receive PLP shares. The vault takes the opposite side of every Predict trade, so LP returns track vault profit and loss. The first supplier receives shares one-to-one with the amount they supply. Later suppliers receive shares proportional to their deposit relative to current vault value.

Supply liquidity

predict::supply returns a Coin<PLP>. Transfer it to the supplier to keep it.

import { Transaction } from '@mysten/sui/transactions';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { client } from './client.js';
import { PREDICT } from './config.js';

export async function supplyLiquidity(params: {
signer: Ed25519Keypair;
dusdcCoinId: string;
amount: bigint;
}) {
const { signer, dusdcCoinId, amount } = params;
const tx = new Transaction();

const [supply] = tx.splitCoins(tx.object(dusdcCoinId), [amount]);
const plp = tx.moveCall({
target: `${PREDICT.packageId}::predict::supply`,
typeArguments: [PREDICT.quoteType],
arguments: [tx.object(PREDICT.predictObjectId), supply, tx.object.clock()],
});
tx.transferObjects([plp], signer.toSuiAddress());

const result = await client.core.signAndExecuteTransaction({
transaction: tx,
signer,
include: { effects: true },
});
if (result.$kind === 'FailedTransaction') throw new Error('supply failed');
return result.Transaction;
}

Withdraw liquidity

predict::withdraw burns a Coin<PLP> and returns the selected quote asset. The withdrawal succeeds only when enough funds remain after the vault covers its current maximum payout and the withdrawal limiter allows the amount.

import { Transaction } from '@mysten/sui/transactions';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { client } from './client.js';
import { PREDICT } from './config.js';

export async function withdrawLiquidity(params: {
signer: Ed25519Keypair;
plpCoinId: string;
}) {
const { signer, plpCoinId } = params;
const tx = new Transaction();

const quote = tx.moveCall({
target: `${PREDICT.packageId}::predict::withdraw`,
typeArguments: [PREDICT.quoteType],
arguments: [tx.object(PREDICT.predictObjectId), tx.object(plpCoinId), tx.object.clock()],
});
tx.transferObjects([quote], signer.toSuiAddress());

const result = await client.core.signAndExecuteTransaction({
transaction: tx,
signer,
include: { effects: true },
});
if (result.$kind === 'FailedTransaction') throw new Error('withdraw failed');
return result.Transaction;
}

Vault strategy considerations

These points follow directly from the protocol's documented mechanics. They are not investment advice.

  • Withdrawals can be capped by liability. predict::withdraw only releases funds available after the vault reserves its current total maximum payout. When open positions carry large payout coverage, less is withdrawable. Read the current withdrawable amount with predict::available_withdrawal.
  • A rate limiter can throttle outflows. The vault includes a withdrawal limiter with a configured capacity and refill rate. Large withdrawals can be limited even when funds are otherwise available.
  • LP value tracks vault profit and loss. Because the vault is the counterparty to every trade, PLP share value rises and falls with trader outcomes, not with a fixed yield.
  • Minting is bounded by an exposure cap. After each mint, the vault asserts that total mark-to-market liability stays within a configured percentage of vault value, which protects existing LPs.

For the accounting model behind these limits, see Design.

Verify on Testnet

The write paths above pass compile checks but do not execute. To confirm them end to end:

  1. Fund a Testnet address with SUI, then confirm with sui client gas.
  2. Request DUSDC, then confirm you own a DUSDC coin with sui client objects.
  3. Fetch a live oracle with getActiveOracle. Confirm the JSON includes an oracle with "status": "active".
  4. Create a PredictManager (see the quickstart) and confirm the ID resolves with sui client object MANAGER_ID.
  5. Run mintBinaryUp with a depositAmount at or above the previewed mint cost. Confirm a success status and a PositionMinted event.
  6. Run mintRange with two grid strikes where lowerStrike < higherStrike. Confirm a RangeMinted event.
  7. Run redeemBinaryUp. Confirm a PositionRedeemed event and that the payout lands in the manager balance (predict_manager::balance).
  8. Run supplyLiquidity, then confirm you hold a PLP coin with sui client objects.
  9. Run withdrawLiquidity with that PLP coin. Confirm a Withdrawn event.