Accounts and Custody
DeepBook Predict has no PredictManager and no predict_manager module. Custody and identity moved into a standalone account package, where each trader owns one shared AccountWrapper holding an embedded Account. Predict attaches its own per-account data to that Account rather than storing positions itself.
If you arrived from the old Predict Manager title, you are in the right place. That title and the SDK's createManager() method both survive from older DeepBook balance-manager vocabulary, and both name the account wrapper described here. The SDK's own name for the object is account wrapper.
This surface belongs to 2 modules, both under packages/account/sources/ rather than under the Predict package:
account::account: TheAccountWrapperobject, the embeddedAccount, theAuthtype, and the custody functions, ataccount.move.account::account_registry: Account creation, address derivation, and the app authorization allowlist, ataccount_registry.move.
Predict's own per-account slot lives in deepbook_predict::predict_account.
Sui Mainnet and Sui Testnet each run a separate deployment of the account package from identical sources, and the Move snippets on this page pin the Mainnet source commit, which is also the deepbook-predict-mainnet branch. An account on one network does not exist on the other, and neither deployment shares accounts with the previous-generation predict-8-21 deployment. Take the IDs from getAccountConfig(network) rather than transcribing them:
| Object | Mainnet | Testnet |
|---|---|---|
account package | 0xa6f1b22aaeb429f6fd8f01c13f605256e00876457fd122da8a0e2c1045c75929 | 0x543139156cb90d1a73df33b5dca37d7c8bdce62506c7874ebc854afd319b98f1 |
AccountRegistry | 0x210ba485d973b5356e9078318837137efabdd5ffb9eeb3705ba7eef3340324fc | 0xb889caefe327cbdea58e529e3b9e10dc93f1e661ddef32824d28527edf8d6385 |
The registry authorizes PredictApp, SessionsApp, and DeepbookCoreAccountApp on both networks, verified 2026-09-10.
Lifecycle
An account moves through 5 stages. Only creation happens once.
| Stage | Call | What changes |
|---|---|---|
| Create | account_registry::new then account::share | Claims both derived IDs for the sender and shares a new AccountWrapper. |
| Deposit | account::deposit_funds<T> | Settles pending accumulator funds, then adds a whole Coin<T> to stored balance. |
| Mint | expiry_market::mint_exact_quantity, expiry_market::mint_exact_amount | Debits the all-in cost and records a position in Predict's account slot. |
| Redeem | expiry_market::redeem_live, expiry_market::redeem_settled, expiry_market::redeem_settled_permissionless | Removes or replaces the position and credits the payout. |
| Withdraw | account::withdraw_funds<T> | Settles pending funds, debits stored balance, and returns a Coin<T>. |
Every stage shares 3 properties:
- Owner: Creation fixes the owner.
newrecordsctx.sender()asowner, and no function changes it. To move an account to a different address, create a new one under that address. - Ownership: The wrapper is shared, not owned.
AccountWrapperhas thekeyability only, and creation hands it back foraccount::share. Any transaction can name it as an input, so authorization comes from theAuthvalue a function demands. - Derived IDs: Both IDs derive from the registry. The wrapper address and the account identity are derived objects claimed from it, so they are deterministic and computable before the account exists.
Create an account
The registry has 3 constructors, and all 3 are permissionless:
public fun new(registry: &mut AccountRegistry, ctx: &mut TxContext): AccountWrapper
public fun new_with_referrer(
registry: &mut AccountRegistry,
referrer: &AccountWrapper,
ctx: &mut TxContext,
): AccountWrapper
public fun new_self_owned(
registry: &mut AccountRegistry,
owner_uid: &mut UID,
ctx: &mut TxContext,
): AccountWrapper
new and new_with_referrer set the owner to the transaction sender. new_self_owned sets it to an object's address, so a package can own an account. new_with_referrer permanently records the referring account's canonical ID and receive address, which is what makes later mints pay a referral share to that account.
Each constructor returns the AccountWrapper by value. You cannot drop a key-only value, so the same transaction must consume it, and the only sensible consumer is:
public fun share(self: AccountWrapper)
Call account::share on the returned wrapper in the same programmable transaction block. Forgetting it makes the transaction fail with an unused value error.
Creating an account twice for the same owner aborts with EAccountAlreadyExists. Check first with derived_exists or derived_wrapper_exists, or catch the abort.
Creation source
packages/account/sources/account_registry.move. You probably need to run `pnpm prebuild` and restart the site.Derive the IDs before you create
Both object IDs derive from the shared AccountRegistry and the owner address, so you can compute them offchain and hardcode them into the transaction that creates the account:
public fun derived_address(registry: &AccountRegistry, owner: address): address
public fun derived_wrapper_address(registry: &AccountRegistry, owner: address): address
public fun derived_exists(registry: &AccountRegistry, owner: address): bool
public fun derived_wrapper_exists(registry: &AccountRegistry, owner: address): bool
derived_wrapper_address returns the shared object's address, which is what every Predict call takes as the wrapper argument and what the accumulator delivers funds to. derived_address returns the canonical account identity, which is the parent of every app-data slot and the value that appears as account_id in events.
That determinism removes the 2-transaction sequence the old manager required. You no longer need to execute a creation transaction, read the new object ID out of effects, and wait for finality before depositing. Compose creation, sharing, and the first deposit in one block. The TypeScript SDK does exactly this with client.predict.tx.deposit(owner, amount, { create: true }), and it exposes the derivation directly as client.predict.wrapperIdFor(owner) and the free function deriveAccountWrapperId(config, owner).
Derivation source
packages/account/sources/account_registry.move. You probably need to run `pnpm prebuild` and restart the site.The SDK's client.predict.tx.createManager() builds account_registry::new plus account::share as one transaction and takes no arguments. Decode the result with client.predict.decode.createManager(result), which returns accountId, wrapperId, owner, and selfOwned.
Create the account with the SDK
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);
}
Structs
The layer has 2 structs that describe the object and 1 that describes the authority that opens it.
AccountWrapper and Account
AccountWrapper is the shared object. Account is a store field inside it, not a separate object, so it has no independent object ID that a transaction can name:
| Struct | Field | Type | Description |
|---|---|---|---|
AccountWrapper | id | UID | Object ID of the shared wrapper. This is the argument Predict calls take. |
AccountWrapper | account | Account | The embedded account state. |
Account | account_id | UID | Canonical identity and the dynamic-field parent for app data. |
Account | owner | address | Owning address, either an external address or an object ID as an address. |
Account | receive_address | address | The wrapper's address, used as the accumulator delivery and withdrawal anchor. |
Account | balances | Bag | Stored Balance<T> values, indexed by coin type. |
Account | settlements | Bag | Per-coin timestamp of the latest settlement attempt. |
Account | referrer_account_id | Option<ID> | Referring account's canonical ID, set only at referral creation. |
Account | referrer_receive_address | Option<address> | Referring account's wrapper address, the destination for referral payments. |
Field order in the table is declaration order. Creation writes the 2 referrer fields once, and they never change, which is why you cannot add or move a referral relationship afterward.
AccountWrapper and Account source
packages/account/sources/account.move. You probably need to run `pnpm prebuild` and restart the site.The Auth hot potato
Auth carries the authority to open an account mutably:
public struct Auth {
kind: u8,
owner: address,
}
Auth declares no abilities at all. You cannot copy, drop, or store it, so the transaction that creates one must consume it, and one Auth authorizes exactly one call. Build one per gated call in the block.
Owner authority comes from 2 constructors, and app authority from 1 package-issued path:
| Constructor | Binds to | Use |
|---|---|---|
account::generate_auth(ctx: &mut TxContext): Auth | ctx.sender() | An address-owned account. This is the ordinary path. |
account::generate_auth_as_object(uid: &mut UID): Auth | The object's address | An account created with new_self_owned, opened by the owning object. |
account_registry::generate_auth_as_app<App> | The app witness | Requires a Permit<App> plus registry authorization, so only the app's own package can call it. Predict uses it for redeem_settled_permissionless. |
load_account_mut(wrapper, auth) consumes the value and returns &mut Account. Owner authority must match the stored owner, otherwise the call aborts with EInvalidOwner. App authority carries no owner restriction, which is why the allowlist behind it is admin-controlled and revocable with deauthorize_app.
Auth and authority source
packages/account/sources/account.move. You probably need to run `pnpm prebuild` and restart the site.packages/account/sources/account.move. You probably need to run `pnpm prebuild` and restart the site.Balances and custody
Funds reach an account in 2 ways. Some arrive as stored balance directly: a deposit, and a redemption payout, which Predict credits to the account inside the redeeming transaction. The rest arrive at the wrapper address through Sui's fund accumulator, which is how builder fees, referral shares, and filled liquidity requests arrive, and a settling call has to fold them into stored balance before the account can spend them.
settle does that folding and is permissionless:
public fun settle<T>(wrapper: &mut AccountWrapper, root: &AccumulatorRoot, clock: &Clock)
Anyone can call it for any account and coin type, because only the wrapper's own UID can authenticate the address-balance withdrawal and no value leaves the account. It latches a per-coin timestamp before reading the accumulator, so it is safe to call repeatedly and is a no-op once it has run for that coin type in the same millisecond. It emits FundsSettled when it moves a nonzero amount.
In practice you rarely call it alone. The 2 custody functions settle first, then act:
public fun deposit_funds<T>(
wrapper: &mut AccountWrapper,
auth: Auth,
coin: Coin<T>,
root: &AccumulatorRoot,
clock: &Clock,
)
public fun withdraw_funds<T>(
wrapper: &mut AccountWrapper,
auth: Auth,
amount: u64,
root: &AccumulatorRoot,
clock: &Clock,
ctx: &mut TxContext,
): Coin<T>
Predict's own trading functions settle USDC through the wrapper before they price anything, so an integrator seldom calls settle directly. Because the per-coin latch is timestamp-based, a second settle for the same coin type inside one transaction moves nothing.
Custody source
packages/account/sources/account.move. You probably need to run `pnpm prebuild` and restart the site.Predict does not gate withdrawal
withdraw_funds lives in the account package, which knows nothing about Predict's ProtocolConfig. It therefore runs regardless of Predict's package version watermark, its trading_paused flag, and its emergency freeze. Pausing or freezing Predict stops trading, not the exit of idle balance.
This has 2 consequences. You can always retrieve funds that are not committed to a live position. Risk tooling must not treat a Predict pause as a custody halt.
Deposit and withdraw
deposit_funds consumes a whole Coin<T>, so split the exact amount off a larger coin first. withdraw_funds takes an amount and returns a Coin<T> that the transaction must consume, by transferring it or by sending it to an address balance.
Both take an Auth, and both abort with EInvalidOwner when owner authority does not match the stored owner. withdraw_funds aborts with EBalanceTooLow when the amount exceeds stored balance or when the account has never held that coin type.
The SDK's client.predict.tx.deposit(owner, amountUsdc) sources the quote coin from coin objects or the address balance and can create the account in the same block with { create: true }. Its client.predict.tx.withdraw(owner, amountUsdc) sends the result to the owner's address balance by default, or returns a coin object with { toCoinObject: true }.
Coin types
deposit_funds<T> and withdraw_funds<T> are generic over the coin type and place no restriction on T. The restriction lives one level up, in Predict: USDC is the settlement asset for every trade, and PLP is the pool share coin. On Mainnet the quote coin is native USDC, 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC. On Testnet it is a mintable test coin at the same usdc::USDC module path, 0xc028557a1ed49e42ed091e115aedefd70a442b184c18fbec5c48d5b6c0b8c184::usdc::USDC, which displays as DUSDC. Read the exact type from getConfig(network).quoteCoinType. Depositing an unrelated coin type succeeds and you can always withdraw it again, but you cannot trade with it.
Read a balance with the account accessor, which counts stored balance plus funds delivered to the wrapper and not yet settled:
public fun balance<T>(self: &Account, root: &AccumulatorRoot, clock: &Clock): u64
Because it includes unsettled funds, it does not go stale between a payout and the next settle. The SDK's client.predict.read.balance(owner) returns the same figure as a decimal, and client.predict.read.plpBalance(owner) returns raw PLP shares.
Read accessor source
packages/account/sources/account.move. You probably need to run `pnpm prebuild` and restart the site.Predict's account slot
Predict does not store positions in its own objects. It attaches a PredictData value to the account under DataKey<PredictApp>, where PredictApp is a witness type only the predict_account module can construct. That namespacing means no other package can write Predict's slot, and Predict cannot write anyone else's. Predict creates the slot lazily on first use.
| Struct | Field | Type | Description |
|---|---|---|---|
PredictData | positions | Table<PositionKey, Position> | Open positions, scoped by expiry market. |
PredictData | builder_code_id | Option<ID> | Sticky builder-code attribution applied to future trades. |
PositionKey | expiry_market_id | ID | The market that minted the order. |
PositionKey | order_id | u256 | The packed order ID. |
Position | root_id | u256 | Original mint's order ID, carried forward across partial-close replacements. |
Position | opened_at_ms | u64 | Onchain time at which the position opened, also carried forward. |
An order ID alone does not identify a position. It carries the quantity and the tick range but not the market, so only the (expiry_market_id, order_id) pair binds a position to its market. Never infer market facts from an order ID.
root_id is what makes one economic position traceable. A partial close retires the current order ID and issues a replacement, but root_id stays fixed, so joining events on position_root_id follows the position through its whole life. Predict likewise carries opened_at_ms forward, which keeps a seasoned position closable while still rejecting a live redeem in the same millisecond as the mint.
PredictData source
packages/predict/sources/predict_account.move. You probably need to run `pnpm prebuild` and restart the site.Read a position
The predict_account module has 2 reads that work against a borrowed &Account, so a simulated transaction resolves them without gas:
public fun has_position(account: &Account, expiry_market_id: ID, order_id: u256): bool
public fun builder_code_id(account: &Account): Option<ID>
Both return the empty answer rather than aborting when the account has no Predict slot yet, so false and none do not distinguish an unused account from a closed position.
has_position answers one exact question and needs the market ID and order ID up front, so it cannot enumerate a portfolio. To list what an account holds, read the dynamic fields of the positions table, where each field key is a PositionKey serialized with Binary Canonical Serialization (BCS). The SDK's client.predict.read.positions(owner) does this on either network and returns { marketId, orderId } pairs, and client.predict.read.hasPosition(owner, marketId, orderId) wraps the single read. The public offchain read services answer the same question over HTTP for the previous-generation predict-8-21 deployment only, and their account and position paths key on the canonical account ID rather than the wrapper ID; see The account SDK subpath.
To value a position, pass its order ID to the market: expiry_market::live_order_value with a market-bound pricer while the market is live, or expiry_market::settled_order_payout once it has settled. See Predict for both.
Builder codes
An account can carry one builder code, which routes an add-on fee to the code's owner on every trade the account makes:
public fun set_builder_code(
wrapper: &mut AccountWrapper,
auth: Auth,
code: &BuilderCode,
ctx: &mut TxContext,
)
public fun unset_builder_code(wrapper: &mut AccountWrapper, auth: Auth, ctx: &mut TxContext)
Both consume an Auth and both emit BuilderCodeSet from deepbook_predict::builder_code_events, with builder_code_id set on one and absent on the other. Attribution is sticky: it applies to trades made after the call, not retroactively. The SDK exposes client.predict.tx.setBuilderCode(owner, builderCodeId) and client.predict.tx.unsetBuilderCode(owner).
Predict account function source
packages/predict/sources/predict_account.move. You probably need to run `pnpm prebuild` and restart the site.Function reference
Complete public signatures for the account layer. Copy them as written when you generate calls or bindings. Every one is a public fun you invoke with a moveCall.
public fun derived_address(registry: &AccountRegistry, owner: address): address
public fun derived_wrapper_address(registry: &AccountRegistry, owner: address): address
public fun derived_exists(registry: &AccountRegistry, owner: address): bool
public fun derived_wrapper_exists(registry: &AccountRegistry, owner: address): bool
public fun new(registry: &mut AccountRegistry, ctx: &mut TxContext): AccountWrapper
public fun new_with_referrer(registry: &mut AccountRegistry, referrer: &AccountWrapper, ctx: &mut TxContext): AccountWrapper
public fun new_self_owned(registry: &mut AccountRegistry, owner_uid: &mut UID, ctx: &mut TxContext): AccountWrapper
public fun is_app_authorized<App>(registry: &AccountRegistry): bool
public fun assert_app_is_authorized<App>(registry: &AccountRegistry)
public fun id(self: &AccountWrapper): ID
public fun load_account(self: &AccountWrapper): &Account
public fun owner(self: &Account): address
public fun account_id(self: &Account): ID
public fun receive_address(self: &Account): address
public fun referrer_account_id(self: &Account): Option<ID>
public fun referrer_receive_address(self: &Account): Option<address>
public fun balance<T>(self: &Account, root: &AccumulatorRoot, clock: &Clock): u64
public fun has_data<App>(self: &Account): bool
public fun generate_auth(ctx: &mut TxContext): Auth
public fun generate_auth_as_object(uid: &mut UID): Auth
public fun share(self: AccountWrapper)
public fun load_account_mut(self: &mut AccountWrapper, auth: Auth): &mut Account
public fun settle<T>(wrapper: &mut AccountWrapper, root: &AccumulatorRoot, clock: &Clock)
public fun deposit<T>(self: &mut Account, coin: Coin<T>)
public fun withdraw<T>(self: &mut Account, amount: u64, ctx: &mut TxContext): Coin<T>
public fun deposit_funds<T>(wrapper: &mut AccountWrapper, auth: Auth, coin: Coin<T>, root: &AccumulatorRoot, clock: &Clock)
public fun withdraw_funds<T>(wrapper: &mut AccountWrapper, auth: Auth, amount: u64, root: &AccumulatorRoot, clock: &Clock, ctx: &mut TxContext): Coin<T>
public fun has_position(account: &Account, expiry_market_id: ID, order_id: u256): bool
public fun builder_code_id(account: &Account): Option<ID>
public fun set_builder_code(wrapper: &mut AccountWrapper, auth: Auth, code: &BuilderCode, ctx: &mut TxContext)
public fun unset_builder_code(wrapper: &mut AccountWrapper, auth: Auth, ctx: &mut TxContext)
deposit and withdraw take a &mut Account, which only load_account_mut produces, so reaching them still costs an Auth. authorize_app and deauthorize_app need the AccountAdminCap, and generate_auth_as_app needs a Permit<App>, so none of the 3 is an integrator call.
The account SDK subpath
The account primitive serves more than Predict, so @mysten/deepbook-v3 publishes it on its own subpath, @mysten/deepbook-v3/account, separate from /predict. Reach for it when you drive custody without the Predict facade, or when you compose account commands into a block of your own.
That subpath exports 2 things before any builder:
getAccountConfig(network): Returns the deployedaccountPackageIdandaccountRegistryfor'mainnet'or'testnet', so you never transcribe them. From@mysten/deepbook-v32.3.0 both networks resolve; any other value throws a plainErrorrather than handing back placeholder IDs.AccountContract: The class every builder hangs off. Construct it with that configuration, or with your own{ accountPackageId, accountRegistry }if you run your own deployment of the account package.
import { AccountContract, getAccountConfig } from '@mysten/deepbook-v3/account';
const network = 'mainnet'; // or 'testnet'
const account = new AccountContract(getAccountConfig(network));
Its methods each return a function you add to a Transaction, so they compose into one block:
| Method | Builds |
|---|---|
deriveAccountWrapperId(owner) | No commands. Computes the wrapper's object ID offchain. |
generateAuth() | account::generate_auth for the transaction sender. |
createAccount() | account_registry::new then account::share. |
createAccountAndDeposit({ coin, coinType }) | new, generate_auth, deposit_funds, then share last. |
depositFunds({ wrapperId, coin, coinType }) | generate_auth then deposit_funds. |
withdrawFunds({ wrapperId, amount, coinType }) | generate_auth then withdraw_funds, returning the Coin<T>. |
loadAccount({ wrapperId }) | account::load_account, the read-side handle. |
balance({ owner, coinType }) | load_account chained into balance<T>, for a simulated read. |
createAccountAndDeposit is the one that actually implements the create-and-fund workflow in a single block, and you cannot decompose it into createAccount plus depositFunds. An object input can only name an object that already existed when the block started, so a wrapper created inside the block is reachable only through the result handle new returns, and sharing it ends by-value use of that handle. That is why share comes last.
Only the wrapper ID derives from this class. The canonical account identity is a second, different derived object, keyed by AccountKey(owner) rather than AccountWrapperKey(owner), and its derivation lives on SessionsContract.deriveAccountId in the /sessions subpath. Pass the wrapper ID to Predict calls, and expect the account ID in the account_id field of events. A live OrderMinted carries the account ID, not the wrapper ID.
Which ID an offchain read service wants is the same split, and getting it wrong fails quietly. As of 2026-09-10 the only public read services are the Testnet v4 hosts listed on Contract Information, and they index the previous-generation predict-8-21 deployment, not the 2 deployments this page describes; no read service exists yet for either, and the SDK reads above need none. On those services the account and position paths key on the canonical account ID. A wrapper ID passed there returns HTTP 200 with an empty array rather than an error, so a wrong key is indistinguishable from an empty portfolio until you compare both: verified 2026-09-03 for a real trader on that deployment, the wrapper ID returned zero balances and zero positions while the account ID returned 2 balance rows and 5 open positions. Derive the key with deriveAccountId, documented in Sessions, where the same wrapper-against-account trap applies to session grants. The wrapper ID stays correct for every onchain call that takes the shared AccountWrapper object.
The package root exports a different type named Account. import { Account } from '@mysten/deepbook-v3' gives you @deepbook/core::account::Account, the per-pool trading account, whose layout is unrelated to the account primitive's custody account. Parsing an account-primitive object with it yields nonsense rather than an error, because BCS decoding does not check which struct the bytes came from. Import Account from @mysten/deepbook-v3/account for anything on this page.
The generated bindings sit on the same subpath for callers building their own Move calls: accountMoveCalls, accountRegistryMoveCalls, accountEvents, and the Account and AccountWrapper BCS structs.
End-to-end workflow
You can compose creating an account, funding it, and trading into one programmable transaction block, because the wrapper's object ID is derivable before the account exists. You have 2 layers that reach that, and they are not interchangeable.
The facade builders under client.predict.tx.* do not compose. Each one returns its own fresh Transaction carrying just that builder's commands, so you cannot chain 2 of them into one block. What the facade does offer is 2 prebuilt compositions:
client.predict.tx.deposit(owner, amount, { create: true }): Creates the wrapper, deposits through the fresh handle, and shares last, in one block. Theownermust be the signer, becauseaccount_registry::newderives from the transaction sender, and the call aborts if the account already exists.client.predict.tx.mint(owner, descriptor, opts): A standalone transaction containingload_live_pricer,generate_auth, andmint_exact_quantity, and nothing else. There is no create, share, split, or deposit in it.
The example below is the second of those. It reads a quote, derives a maxCost cap from it, and returns an unsigned mint transaction:
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 };
}
Composing create, fund, and trade into a single block of your own is generated-bindings work rather than facade work. Build it from the AccountContract methods above and the loadLivePricer escape hatch the /predict subpath exports, adding commands to one Transaction in this order: account_registry::new, account::generate_auth, account::deposit_funds, account::share, expiry_market::load_live_pricer, a second account::generate_auth because deposit_funds consumed the first, and expiry_market::mint_exact_quantity. Share the wrapper after the deposit, not before: an object input can only name an object that already existed when the block started, so a wrapper created inside the block is reachable only through the result handle from new.
The tutorial walks the whole flow with quotes, ranges, redemption, and settlement.
Events
Account events live in account::account_events. All of them carry copy and drop but not store, so they are readable from transaction results and no object can hold them. Field order below is declaration order, which is the order BCS decoding depends on.
AccountCreated
All 3 constructors emit it, once per account:
| # | Field | Type | Description |
|---|---|---|---|
| 1 | account_id | ID | Canonical account identity, matching derived_address. |
| 2 | wrapper_id | ID | Shared wrapper object, matching derived_wrapper_address. |
| 3 | owner | address | Owning address, or the owning object's address. |
| 4 | self_owned | bool | True when created with new_self_owned. |
| 5 | referrer_account_id | Option<ID> | Referring account, set only by new_with_referrer. |
To recover a lost wrapper ID, prefer derived_wrapper_address over an event query. The derivation needs no indexer and works before the account exists.
Balance events
Value moves in and out under 3 events, and each carries the same 4 fields:
| # | Field | Type | Description |
|---|---|---|---|
| 1 | account_id | ID | Canonical account identity. |
| 2 | coin_type | String | Fully qualified coin type. |
| 3 | amount | u64 | Amount moved, in the coin's base units. |
| 4 | new_balance | u64 | Stored balance after the move, excluding unsettled funds. |
The 3 differ only in what caused the move:
Deposited: A coin entered stored balance throughdeposit, reached fromdeposit_funds.Withdrawn: Stored balance left as a coin throughwithdraw, reached fromwithdraw_funds.FundsSettled: Accumulator funds at the wrapper address folded into stored balance.settleemits it only when the settled amount is greater than zero, so a no-opsettleis silent.
A redemption payout credits stored balance inside the redeeming transaction, so it surfaces as Deposited. Builder fees, referral shares, and filled liquidity requests travel through the accumulator instead and surface as FundsSettled on the next settling call. Index all 3 events to reconstruct a full account ledger, and read the order events described in Predict for the trades themselves.
The module declares 2 further events, AppAuthorized and AppDeauthorized, which each carry a single app string and record changes to the registry allowlist that governs app authority.
Account event source
packages/account/sources/account_events.move. You probably need to run `pnpm prebuild` and restart the site.Error codes
| Code | Constant | Module | Cause |
|---|---|---|---|
0 | EInvalidOwner | account::account | Owner authority does not match the account's stored owner. |
1 | EBalanceTooLow | account::account | A withdrawal exceeds stored balance, or the account has never held that coin type. |
2 | EInvalidAuth | account::account | The Auth value carries neither owner nor app authority. |
0 | EAppAlreadyAuthorized | account::account_registry | An admin authorized an app that is already on the allowlist. |
1 | EAppNotAuthorized | account::account_registry | A caller requested app authority, or an admin attempted a deauthorization, for an app not on the allowlist. |
2 | EAccountAlreadyExists | account::account_registry | Either derived ID already exists for that owner. |
0 | EPositionAlreadyExists | deepbook_predict::predict_account | A mint tried to record a duplicate market and order ID pair. |
1 | EPositionNotFound | deepbook_predict::predict_account | A redeem referenced a position the account does not hold. |
Codes repeat across modules, so always resolve an abort against the module named in the abort location rather than against the number alone. Over gRPC or GraphQL the abort also carries the constant name; over JSON-RPC it does not.