Skip to main content

Predict Sessions SDK

The @mysten/deepbook-v3/sessions subpath exports SessionsContract, which builds the owner-signed calls that grant and revoke a time-limited session key and the session-signed calls that trade DeepBook Predict positions for the account. Sessions explains the contract mechanism behind each call.

danger

A session key carries authority over everything the account holds, as the Sessions contract page states. It cannot withdraw to an address, grant or revoke sessions, or outlive its expiry, but nothing caps notional, restricts which markets or pools it reaches, or bounds loss to adverse pricing. Fund an account that hands out session keys with only what you are willing to put at risk.

SessionsContract builds commands and never signs or executes them. Each call has a single signer:

  • Owner key: The account owner signs authorizeSession and revokeSession, because the contract derives owner authority from the transaction sender.
  • Session key: The session address signs the 4 Predict builders and the 5 DeepBook spot calls, because each wrapper checks the sender's grant on the account.
  • Any sender: You simulate sessionExpirationMs rather than execute it, so any sender address works.

Configuration

Build a SessionsContract for each network from the deployed IDs that getSessionsConfig returns:

import { SessionsContract, getSessionsConfig } from '@mysten/deepbook-v3/sessions';

const ids = getSessionsConfig('testnet');
const sessions = new SessionsContract(ids);

// Every builder takes the owner's wrapper ID.
const wrapperId = sessions.deriveAccountWrapperId(OWNER_ADDRESS);

// Every Predict builder also takes the Predict ProtocolConfig object ID.
const protocolConfig = ids.protocolConfig;

OWNER_ADDRESS is the address that owns the account. The same sessions instance serves the owner-signed and the session-signed calls, because the signer is whoever signs the transaction, not a property of the contract instance.

getSessionsConfig

Use getSessionsConfig to read the deployed sessions IDs for a network, so you never transcribe them. It returns the frozen deployment record for that network, typed SessionsConfig & SessionsSpotIds & SessionsPredictIds, and it throws an Error for any network other than Testnet or Mainnet. For a deployment of your own, construct a SessionsConfig and pass it to the SessionsContract constructor directly.

It takes this parameter:

  • network: The network to resolve. Its type is NetworkArg, the Sui client's network type, so client.network passes directly.

SessionsContract

Use the SessionsContract constructor to bind every builder to a single deployment. It reads only the 4 SessionsConfig IDs, so the wider getSessionsConfig result passes directly, and each Predict builder takes protocolConfig as its own parameter.

It takes this parameter:

  • config: The deployed sessions and account IDs, usually the getSessionsConfig result.

SessionsConfig

SessionsConfig holds the 4 IDs every builder needs: the deepbook_sessions package, the shared SessionsConfig object, the account package, and the shared AccountRegistry. The account IDs are the same ones AccountContract takes on Predict Accounts SDK, because sessions is an account app on the same registry.

SessionsSpotIds

SessionsSpotIds holds 2 IDs that only DeepBook spot work needs: deepbookRegistry, which the 2 order-placement calls take as deepbook_registry, and deepbookCoreAccountPackageId, which you need to check that registry's authorization. No Predict builder takes either, so they sit outside SessionsConfig.

SessionsPredictIds

SessionsPredictIds holds the Predict ProtocolConfig object ID, which every Predict builder takes as protocolConfig.

MAX_SESSION_DURATION_MS

MAX_SESSION_DURATION_MS is the longest grant the contract accepts, 30 days in milliseconds. authorizeSession does not check durationMs against it, so a duration of 0, or one longer than the maximum, reaches the chain and aborts with EInvalidSessionDuration. Check the duration locally, as the authorize example does, to get the error before you sign.

MAX_SESSIONS_PER_ACCOUNT

MAX_SESSIONS_PER_ACCOUNT is the number of distinct session addresses an account can store, 20. Expired grants keep counting toward it until the owner revokes them, and the SDK does not check it, so authorizing a new address on a full account aborts with ESessionLimitExceeded. The list example reclaims the slots that expired grants hold.

ID derivation functions

An owner has 2 derived objects in the account registry, and SessionsContract computes both IDs offchain with no chain read. The shared AccountWrapper is the object every builder takes as wrapperId. The canonical account is the identity that session grants attach to, and the one that SessionAuthorized and SessionRevoked report as account_id. Accounts and Custody describes both objects.

deriveAccountWrapperId

Use deriveAccountWrapperId to compute the owner's AccountWrapper ID. It returns a string. It delegates to AccountContract with the account package and registry that the Predict facade also uses, so it returns the same ID as the wrapper derivation on Predict Accounts SDK.

It takes this parameter:

  • owner: The address that owns the account.

deriveAccountId

Use deriveAccountId to compute the owner's canonical account ID, a different derived object from the wrapper. It returns a string. Session grants attach to this object, and deriveSessionsFieldId derives from it.

It takes this parameter:

  • owner: The address that owns the account.

deriveSessionsFieldId

Use deriveSessionsFieldId to compute the object ID of the owner's DataKey<SessionsApp> dynamic field, which holds every grant. It returns a string. It derives the account ID from owner first, so pass the owner address, not an account or wrapper ID.

It takes this parameter:

  • owner: The address that owns the account.

Authorize a session

The owner grants a session key with a single owner-signed call. The contract computes the expiry from the clock at execution time, so a queued or retried transaction still gets its full duration.

authorizeSession

Use authorizeSession to grant session authority over the account until the execution-time clock plus durationMs. It returns a (tx: Transaction) => void function that adds the authorize_session call, and the owner must sign it. Re-authorizing an address that already holds a grant replaces its expiry in place and uses no additional slot, which is how you extend a running session.

It takes these parameters:

  • wrapperId: The owner's AccountWrapper ID, from deriveAccountWrapperId.
  • session: The address to authorize.
  • durationMs: The grant length in milliseconds, greater than 0 and at most MAX_SESSION_DURATION_MS.

Grant a session from the owner key

The following example checks the duration locally, then builds an owner-signed grant for a session address:

import {
MAX_SESSION_DURATION_MS,
SessionsContract,
getSessionsConfig,
} from '@mysten/deepbook-v3/sessions';
import { Transaction } from '@mysten/sui/transactions';
import { NETWORK } from './config.js';

// One contract instance drives every sessions call in these examples.
// `getSessionsConfig` returns the deployed sessions package, the shared
// `SessionsConfig` object, the account registry the app is authorized against,
// the Predict protocol config, and the two extra IDs the DeepBook spot wrappers
// need. It resolves both recorded networks; an unrecorded one throws.
export const SESSIONS_CONFIG = getSessionsConfig(NETWORK);

export const sessions = new SessionsContract(SESSIONS_CONFIG);

// The owner's shared `AccountWrapper` ID, derived from the owner address with no
// chain read. Every builder below takes it as `wrapperId`.
export function wrapperId(owner: string): string {
return sessions.deriveAccountWrapperId(owner);
}

// Authorize `session` to trade the owner's account until `now + durationMs`.
//
// A grant is trading authority over the whole account, not a scoped permission.
// The session key cannot withdraw to an address, cannot grant or revoke
// sessions, and cannot outlive its expiry, but within those limits it chooses
// the market, the size, and the price bounds, and the spot wrappers reach the
// account's entire Base, Quote, and DEEP balance. Fund an account that hands out
// ephemeral session keys with only what you are willing to put at risk.
//
// The owner must sign, because the contract derives owner authority from the
// transaction sender. Re-authorizing an address that already holds a grant
// replaces its expiry in place and consumes no additional slot.
export function authorizeSession(params: {
owner: string;
session: string;
durationMs: number;
}): Transaction {
const { owner, session, durationMs } = params;

// The chain asserts the same bounds and aborts `EInvalidSessionDuration`.
// Checking here turns a wasted transaction into a local error.
if (durationMs <= 0 || durationMs > MAX_SESSION_DURATION_MS) {
throw new Error(
`durationMs must be greater than 0 and at most ${MAX_SESSION_DURATION_MS} (30 days), got ${durationMs}.`,
);
}

const tx = new Transaction();
tx.setSender(owner);
tx.add(sessions.authorizeSession({ wrapperId: wrapperId(owner), session, durationMs }));

// Ready to sign with the owner's key. Nothing here signs it.
return tx;
}

// A short-lived grant for one trading run. Expiry is computed from the onchain
// clock at execution time, not from when you build the transaction, so a queued
// or retried transaction still gets its full duration.
export function authorizeForOneHour(owner: string, session: string): Transaction {
return authorizeSession({ owner, session, durationMs: 60 * 60 * 1000 });
}

List grants

No onchain call returns every grant at once. sessionExpirationMs answers for a single known address, so listing every grant means fetching the account's DataKey<SessionsApp> field and decoding it offchain. The Sessions page shows the stored SessionsData layout.

Listing takes 4 steps, and the SDK covers each one:

  1. Derive the canonical account ID from the owner with deriveAccountId.
  2. Derive the DataKey<SessionsApp> field ID under that account with deriveSessionsFieldId.
  3. Fetch that object with the core getObject, and request its Binary Canonical Serialization (BCS) content with include: { content: true }.
  4. Decode the bytes with the static decodeSessions, then split the result with activeSessions and expiredSessions.

deriveSessionsFieldId runs the first 2 steps together, because it derives the account ID from the owner address internally. The field does not exist until the owner's first authorize_session, so the fetch finds no object for an owner who has never granted a session. Treat only that not-found result as an empty list, because a transport failure says nothing about the account's grants.

That path has 3 traps:

  • Account, not wrapper: The field hangs off the canonical account, not the wrapper. Deriving the field under the wrapper ID yields an ID that points at nothing.
  • Plain dynamic field: The contract writes the slot with dynamic_field::add, while the registry claims the account and wrapper IDs through derived_object::claim, so deriveSessionsFieldId uses deriveDynamicFieldID. Deriving the ID yourself with deriveObjectID wraps the type tag in a DerivedObjectKey and produces a different, empty address.
  • Whole-field decoding: Pass decodeSessions the whole Field<DataKey<SessionsApp>, SessionsData> bytes, not the inner value. DataKey is empty in source, but Move inserts a hidden dummy_field: bool, so a single zero byte sits between the field ID and the value, and decoding the value alone reads the field ID's first byte as the map length.

Split the result with the 2 helpers rather than by hand. The chain asserts now < expires_at_ms, so activeSessions keeps grants where nowMs < expiresAtMs and expiredSessions keeps grants where nowMs >= expiresAtMs. A filter written as nowMs > expiresAtMs leaves the grant that expires exactly at nowMs in neither list, so your cleanup skips it and the grant keeps its slot.

Expired grants keep their slots until the owner revokes them, so on a busy account, list the grants, revoke what expiredSessions returns, and then grant.

sessionExpirationMs

Use sessionExpirationMs to read the absolute expiry of a single session address. It returns a (tx: Transaction) => TransactionResult function that adds the session_expiration_ms call. Simulate the transaction and decode the returned BCS as Option<u64>: the stored expiry in milliseconds, or none for an address the owner never authorized or has revoked. The call has no version gate, and it does not classify the timestamp, so compare it with the current time yourself.

It takes these parameters:

  • wrapperId: The owner's AccountWrapper ID.
  • session: The session address to look up.

SessionGrant

decodeSessions returns a SessionGrant for each stored address. session is the authorized address, and expiresAtMs is its absolute expiry in milliseconds since the Unix epoch. The grant is dead at that timestamp, not after it.

decodeSessions

Use the static SessionsContract.decodeSessions to decode an account's stored grants from the raw BCS content of its DataKey<SessionsApp> field object. It returns a SessionGrant[]. It re-encodes what it parsed and throws an Error when the length differs from the input, so truncated or padded bytes fail instead of decoding to an empty list, and an empty result means the account holds no grants.

It takes this parameter:

  • contents: The whole field object's BCS content, as client.core.getObject returns it with include: { content: true }.

activeSessions

Use the static SessionsContract.activeSessions to keep the grants that are still live at nowMs. It returns a SessionGrant[] of the grants where nowMs < expiresAtMs.

It takes these parameters:

  • grants: The grants from decodeSessions.
  • nowMs: The current time in milliseconds since the Unix epoch.

expiredSessions

Use the static SessionsContract.expiredSessions to keep the grants that are already dead at nowMs, which are the grants to revoke when you reclaim slots. It returns a SessionGrant[] of the grants where nowMs >= expiresAtMs, the exact complement of activeSessions.

It takes these parameters:

  • grants: The grants from decodeSessions.
  • nowMs: The current time in milliseconds since the Unix epoch.

List and reclaim expired slots

The following example lists an account's grants, splits them at the current time, and builds a single owner-signed transaction that revokes every expired grant:

import type { SessionGrant } from '@mysten/deepbook-v3/sessions';
import { MAX_SESSIONS_PER_ACCOUNT, SessionsContract } from '@mysten/deepbook-v3/sessions';
import { ObjectError } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { sessions, wrapperId } from './sessions-authorize.js';

// Every grant an account holds.
//
// There is no bulk onchain read: `session_expiration_ms` answers one address at a
// time. Listing means fetching the account's `DataKey<SessionsApp>` dynamic field
// and decoding it locally. Two details make or break this:
//
// 1. The field hangs off the derived ACCOUNT address, not off the wrapper. They
// are different objects. `deriveSessionsFieldId` derives the account first,
// then the field, so pass the owner address and let it do both steps.
// 2. `decodeSessions` takes the whole `Field<DataKey, SessionsData>` bytes that
// `getObject` returns, not the inner value. It throws on truncated bytes
// rather than decoding to an empty list, so an empty result means the account
// really holds no grants.
//
// The field does not exist until the first `authorize_session`, so a missing
// object is an empty list rather than an error. Only a missing object: any other
// failure rethrows, because a transport error says nothing about the grants.
// Once attached the field stays attached, even after every grant is revoked.
export async function listGrants(owner: string): Promise<SessionGrant[]> {
const fieldId = sessions.deriveSessionsFieldId(owner);

let content: Uint8Array | undefined;
try {
const { object } = await client.core.getObject({
objectId: fieldId,
include: { content: true },
});
content = object.content;
} catch (error) {
// No sessions data attached to this account yet.
if (error instanceof ObjectError && error.reason === 'notFound') return [];
throw error;
}
if (!content) return [];

return SessionsContract.decodeSessions(content);
}

// Split a listing at the current time. Use the helpers rather than comparing by
// hand: the chain asserts `now < expiresAtMs`, so a grant is dead AT its expiry,
// and a filter written as `nowMs > expiresAtMs` leaves the grant expiring exactly
// at `nowMs` occupying a slot forever.
export async function grantsByState(
owner: string,
nowMs: number = Date.now(),
): Promise<{ active: SessionGrant[]; expired: SessionGrant[]; slotsFree: number }> {
const grants = await listGrants(owner);
return {
active: SessionsContract.activeSessions(grants, nowMs),
expired: SessionsContract.expiredSessions(grants, nowMs),
// Expired grants still occupy slots, so free slots count against every
// stored address, not just the live ones.
slotsFree: MAX_SESSIONS_PER_ACCOUNT - grants.length,
};
}

// Reclaim the slots expired grants are still holding.
//
// Time passing executes no Move code, so nothing prunes expired entries. They
// count toward the 20 stored addresses until the owner revokes them, and the
// twenty-first `authorize_session` aborts `ESessionLimitExceeded` even when every
// stored grant is long dead. Run this before granting on a busy account.
//
// Returns null when there is nothing to reclaim, so the caller does not sign an
// empty transaction.
export async function reclaimExpiredSlots(
owner: string,
nowMs: number = Date.now(),
): Promise<Transaction | null> {
const { expired } = await grantsByState(owner, nowMs);
if (expired.length === 0) return null;

const tx = new Transaction();
tx.setSender(owner);
for (const grant of expired) {
tx.add(sessions.revokeSession({ wrapperId: wrapperId(owner), session: grant.session }));
}
return tx;
}

Trade as a session key

The 4 Predict builders mirror the expiry_market entrypoints of the same name, but the session key signs them instead of the owner. Compared with the owner-signed call, you pass no Auth, because the wrapper generates app authorization internally, and each builder adds the accountRegistry and sessionsConfig arguments from its SessionsConfig. Predict still runs its own validation, and the Sessions page lists the aborts a session caller still meets. If the account registry does not authorize SessionsApp, every trading call aborts with EAppNotAuthorized, as the Sessions page explains under deauthorizing the app.

Every Predict builder follows these conventions:

  • The session address signs as the transaction sender, so it needs SUI for gas unless another address sponsors the transaction.
  • pricer is a programmable transaction block (PTB) result, not an object ID. Add loadLivePricer from @mysten/deepbook-v3/predict in an earlier command of the same transaction and pass its result.
  • protocolConfig is the Predict ProtocolConfig object ID, which the getSessionsConfig result carries.
  • lowerTick and higherTick are absolute ticks. Predict Markets and Pricing SDK shows how to resolve a strike to ticks.
  • Quantities, costs, and probabilities are raw onchain integers, and the builders pass them through unchanged. Convert them with usdcToRaw and probabilityToRaw from @mysten/deepbook-v3/predict, which DeepBook Predict SDK describes.

mintExactQuantity

Use mintExactQuantity to mint a position of an exact payout quantity as the session key. It returns a (tx: Transaction) => TransactionResult function whose result is the new order ID, a u256. The builder requires both caps and gives them no default. The contract asserts that each value is at most its cap, so a cap of u64::MAX never trips.

It takes these parameters:

  • expiryMarketId: The expiry market object ID.
  • wrapperId: The owner's AccountWrapper ID, not an ID derived from the session address.
  • protocolConfig: The Predict ProtocolConfig object ID.
  • pricer: The loadLivePricer result from an earlier command in the same transaction.
  • lowerTick: The lower bound of the position's strike range, as an absolute tick.
  • higherTick: The upper bound of the position's strike range, as an absolute tick.
  • quantity: The payout quantity to mint, in raw units.
  • maxCost: The all-in cost ceiling, premium plus fees, in raw quote units.
  • maxProbability: The ceiling on the fill probability, in raw fixed point.

mintExactAmount

Use mintExactAmount to mint by spending up to a premium budget and flooring the quantity received, as the session key. It returns a (tx: Transaction) => TransactionResult function whose result is the new order ID, a u256. The builder requires maxCost, and the contract requires it to be greater than 0.

It takes these parameters:

  • expiryMarketId: The expiry market object ID.
  • wrapperId: The owner's AccountWrapper ID.
  • protocolConfig: The Predict ProtocolConfig object ID.
  • pricer: The loadLivePricer result from an earlier command in the same transaction.
  • lowerTick: The lower bound of the position's strike range, as an absolute tick.
  • higherTick: The upper bound of the position's strike range, as an absolute tick.
  • maxPremium: The premium budget, in raw quote units.
  • minQuantity: The smallest payout quantity you accept, in raw units.
  • maxCost: The all-in cost ceiling, premium plus fees, in raw quote units.

redeemLive

Use redeemLive to close part or all of a live position at the pricer's mark as the session key. It returns a (tx: Transaction) => TransactionResult function whose result is an Option<u256>, the replacement order ID when a partial close leaves quantity open. Store that replacement ID, because the next close must target it. minProbability and minProceeds are close-side floors, and the builder sends 0, which disables a floor, for each one you omit.

caution

redeemLive sends 0 for an omitted minProbability or minProceeds, and a floor of 0 lets the session key close the position at whatever price the mark gives. Pass real floors on every close a session key signs.

It takes these parameters:

  • expiryMarketId: The expiry market object ID.
  • wrapperId: The owner's AccountWrapper ID.
  • protocolConfig: The Predict ProtocolConfig object ID.
  • pricer: The loadLivePricer result from an earlier command in the same transaction.
  • orderId: The ID of the order to close.
  • closeQuantity: The payout quantity to close, in raw units.
  • minProbability: The lowest fill probability you accept, in raw fixed point. The builder sends 0 when you omit it.
  • minProceeds: The lowest proceeds you accept for the whole close, in raw quote units. The builder sends 0 when you omit it.

redeemSettled

Use redeemSettled to claim a settled position in full as the session key. It returns a (tx: Transaction) => void function. It takes no pricer, because settlement has already fixed the price, and no quantity, because a settled claim closes the order in full.

It takes these parameters:

  • expiryMarketId: The expiry market object ID.
  • wrapperId: The owner's AccountWrapper ID.
  • protocolConfig: The Predict ProtocolConfig object ID.
  • orderId: The ID of the settled order to claim.

Slippage bounds differ from the facade

The session builders and the @mysten/deepbook-v3/predict facade differ on slippage protection in both directions, and both differences make the session key holder set bounds explicitly:

  • Required mint caps: The session mint builders require their caps. mintExactQuantity takes maxCost and maxProbability as required parameters, and mintExactAmount requires maxCost. The facade's tx.mint, on Predict Positions SDK, sends U64_MAX for a cap you omit, which is a cap that can never trip.
  • Close-side floors on redeemLive: The session builder passes minProbability and minProceeds to the contract. The facade's tx.redeem, on Predict Positions SDK, sends 0 for both and has no option to raise them. Omitting a floor on the session builder also sends 0, so pass real floors on anything a session key can reach.

Mint and close as the session key

The following example quotes a mint through the owner's facade, then mints, closes, and claims as the session key with explicit caps and floors:

import type { MarketDescriptor, MintQuote, Side } from '@mysten/deepbook-v3/predict';
import {
binaryRangeTicks,
getConfig,
loadLivePricer,
priceToRaw,
probabilityToRaw,
toGeneratedConfig,
usdcToRaw,
} from '@mysten/deepbook-v3/predict';
import { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { NETWORK, UNDERLYING } from './config.js';
import { admissibleStrike, tradeableMarket } from './markets.js';
import { SESSIONS_CONFIG, sessions, wrapperId } from './sessions-authorize.js';

// The Predict session wrappers take the same market parameters as Predict itself,
// so they need the Predict deployment's IDs alongside the sessions IDs. The
// `/predict` subpath exports the projection and the pricer loader precisely so
// these wrappers can be composed.
const PREDICT = getConfig(NETWORK);
const GENERATED = toGeneratedConfig(PREDICT);
const FEEDS = PREDICT.underlyings[UNDERLYING];

// Mint a directional position as the session key.
//
// Three things differ from the owner-signed `/predict` flow:
//
// 1. The session key is the sender. The wrapper derives the caller from the
// sender, mints app authorization internally, and consumes it in the same
// call, so no `Auth` value is built or passed.
// 2. `pricer` is a PTB result, not an object ID. Load it with
// `loadLivePricer` in a preceding command of the same transaction.
// 3. `maxCost` and `maxProbability` are required. The `/predict` facade defaults
// a missing cap to `U64_MAX`, which is no cap at all; the session builder
// makes you name both. Quantities and costs are raw six-decimal USDC, and
// probabilities are raw fixed point at 1e9.
export async function mintAsSession(params: {
owner: string;
session: string;
side: Side;
// Maximum payout in USD, at $1 per contract. Must be a whole $0.01 lot.
quantity: number;
// Omit to trade at the market's onchain reference price, the window anchor.
targetStrikeUsd?: number;
}): Promise<{ tx: Transaction; quote: MintQuote; descriptor: MarketDescriptor }> {
const { owner, session, side, quantity, targetStrikeUsd } = params;
const market = await tradeableMarket();

// The session wrappers take absolute ticks, so resolve the strike to a number
// before converting. `tradeableMarket` already skips markets whose reference
// price is unseeded.
if (market.referencePrice === null) {
throw new Error(`Market ${market.id} has no reference price yet.`);
}
const strikeUsd =
targetStrikeUsd === undefined
? market.referencePrice
: admissibleStrike(market, targetStrikeUsd);

const { lowerTick, higherTick } = binaryRangeTicks(
priceToRaw(strikeUsd),
side,
priceToRaw(market.tickSize),
);

// Quote through the owner path first. The quote dry-runs the same market, the
// same account, and the same fee path, so its `cost` is the figure to size the
// cap against. Quote immediately before sending: on the short cadences the
// entry probability moves fast.
const descriptor: MarketDescriptor = {
underlying: UNDERLYING,
expiryMs: market.expiryMs,
marketId: market.id,
side,
strike: strikeUsd,
};
const quote = await client.predict.read.quoteMint(owner, descriptor, { quantity });

const tx = new Transaction();
// The session key signs, not the owner.
tx.setSender(session);

const pricer = tx.add(
loadLivePricer(GENERATED, {
expiryMarketId: market.id,
pythFeed: FEEDS.pythFeed,
blockScholesValueStore: FEEDS.blockScholesValueStore,
blockScholesSviStore: FEEDS.blockScholesSviStore,
}),
);

tx.add(
sessions.mintExactQuantity({
expiryMarketId: market.id,
wrapperId: wrapperId(owner),
protocolConfig: SESSIONS_CONFIG.protocolConfig,
pricer,
lowerTick,
higherTick,
quantity: usdcToRaw(quantity),
// All-in debit ceiling, one percent above the quoted cost.
maxCost: usdcToRaw(Math.ceil(quote.cost * 1.01 * 1e6) / 1e6),
// Independent ceiling on the fill price, 0 to 1 per $1 of payout.
maxProbability: probabilityToRaw(
Math.min(1, Number((quote.entryProbability * 1.02).toFixed(9))),
),
}),
);

return { tx, quote, descriptor };
}

// Close part or all of a live position as the session key.
//
// `minProbability` and `minProceeds` are close-side floors, and omitting either
// sends zero. Zero on a delegated key means the position closes at whatever the
// mark gives, so pass real floors on anything a session key can reach. The
// `/predict` facade sends zero for both and exposes no way to raise them, which
// is why the session builder is the one that can protect a close.
export async function closeAsSession(params: {
owner: string;
session: string;
expiryMarketId: string;
orderId: bigint;
// Payout quantity to close, in USD. Must be a whole $0.01 lot.
quantity: number;
// Minimum acceptable fill probability, 0 to 1.
minProbability: number;
// Minimum acceptable USDC proceeds for the whole close.
minProceeds: number;
}): Promise<Transaction> {
const { owner, session, expiryMarketId, orderId, quantity } = params;

const tx = new Transaction();
tx.setSender(session);

const pricer = tx.add(
loadLivePricer(GENERATED, {
expiryMarketId,
pythFeed: FEEDS.pythFeed,
blockScholesValueStore: FEEDS.blockScholesValueStore,
blockScholesSviStore: FEEDS.blockScholesSviStore,
}),
);

tx.add(
sessions.redeemLive({
expiryMarketId,
wrapperId: wrapperId(owner),
protocolConfig: SESSIONS_CONFIG.protocolConfig,
pricer,
orderId,
closeQuantity: usdcToRaw(quantity),
minProbability: probabilityToRaw(params.minProbability),
minProceeds: usdcToRaw(params.minProceeds),
}),
);

// A partial close retires the order ID and returns a replacement as
// `Option<u256>`. Read it from the transaction result and store it, or the
// next close targets an order that no longer exists.
return tx;
}

// Claim a settled position as the session key. No pricer, because the settlement
// price is fixed, and no quantity, because a settled claim closes the order in
// full.
export function claimSettledAsSession(params: {
owner: string;
session: string;
expiryMarketId: string;
orderId: bigint;
}): Transaction {
const { owner, session, expiryMarketId, orderId } = params;

const tx = new Transaction();
tx.setSender(session);
tx.add(
sessions.redeemSettled({
expiryMarketId,
wrapperId: wrapperId(owner),
protocolConfig: SESSIONS_CONFIG.protocolConfig,
orderId,
}),
);
return tx;
}

Revoke a session

Revocation removes a grant and frees its slot immediately, and the revoked address cannot trade from the next transaction on. It stops future calls only: it does not unwind positions, cancel resting orders, or reverse executed trades.

revokeSession

Use revokeSession to remove the grant for session. It returns a (tx: Transaction) => void function that adds the revoke_session call, and the owner must sign it. The call takes no SessionsConfig object and has no version gate, so it keeps working after a watermark bump retires the package version.

Revoking an address that holds no grant is a silent no-op: the call neither aborts nor emits SessionRevoked, so a successful transaction does not prove that the call removed a grant. Read sessionExpirationMs before and after when the difference matters. The Sessions page explains why the transaction result cannot confirm a revocation.

It takes these parameters:

  • wrapperId: The owner's AccountWrapper ID.
  • session: The session address to revoke.

Revoke and confirm

The following example builds an owner-signed revocation, reads a session's expiry through a simulation, and records whether a grant existed before the revocation:

import { bcs } from '@mysten/sui/bcs';
import { Transaction } from '@mysten/sui/transactions';
import { client } from './client.js';
import { sessions, wrapperId } from './sessions-authorize.js';

// Revoke one grant. The owner signs, the slot frees immediately, and the address
// stops being able to trade from the next transaction on.
//
// Revocation takes no `SessionsConfig` and is not version gated, so it keeps
// working after the sessions package is retired by a version bump. It does not
// unwind anything the session already did: positions, resting spot orders, and
// executed trades all survive.
export function revokeSession(owner: string, session: string): Transaction {
const tx = new Transaction();
tx.setSender(owner);
tx.add(sessions.revokeSession({ wrapperId: wrapperId(owner), session }));
return tx;
}

// Read one address's absolute expiry, in milliseconds since the epoch.
//
// `session_expiration_ms` returns `Option<u64>`: `none` for an address that was
// never granted or has been revoked, and the stored timestamp otherwise. It does
// not classify the timestamp, so compare it with the current time yourself, and
// remember the comparison is strict: the grant is dead AT `expiresAtMs`.
//
// The call is a `public fun` with a return value rather than an `entry` call, so
// simulate it and decode the returned BCS instead of executing it.
export async function sessionExpirationMs(
owner: string,
session: string,
): Promise<bigint | null> {
const tx = new Transaction();
// A simulation needs a sender; any address works for a read.
tx.setSender(owner);
tx.add(sessions.sessionExpirationMs({ wrapperId: wrapperId(owner), session }));

const result = await client.core.simulateTransaction({
transaction: tx,
// Public non-entry functions are only inspectable with checks disabled.
checksEnabled: false,
include: { commandResults: true },
});
if (result.$kind === 'FailedTransaction') {
throw new Error(`session_expiration_ms simulation failed for ${session}.`);
}

const returned = result.commandResults?.[0]?.returnValues?.[0]?.bcs;
if (!returned) throw new Error('simulateTransaction returned no command results.');

const expiry = bcs.option(bcs.u64()).parse(returned);
return expiry === null ? null : BigInt(expiry);
}

// Whether `session` can trade right now. `false` covers both an address that
// never held a grant and one whose grant has expired or been revoked.
export async function isSessionActive(
owner: string,
session: string,
nowMs: number = Date.now(),
): Promise<boolean> {
const expiry = await sessionExpirationMs(owner, session);
return expiry !== null && BigInt(nowMs) < expiry;
}

// Confirm a revocation by reading, because the transaction result cannot.
//
// Revoking an address that holds no grant is a silent no-op: it does not abort
// and it emits no `SessionRevoked` event, so a successful transaction proves
// nothing about whether a grant was removed or was never there. Read before and
// after when the difference matters, for example when you are auditing a key you
// believe you granted.
export async function revokeAndConfirm(
owner: string,
session: string,
): Promise<{ tx: Transaction; hadGrant: boolean }> {
const before = await sessionExpirationMs(owner, session);
return { tx: revokeSession(owner, session), hadGrant: before !== null };
}

DeepBook spot wrappers

SessionsContract does not wrap the 5 DeepBook spot session calls, because the SDK does not model the surrounding workflow of discovering the account's embedded balance manager and reading its resting orders and locked balances. Reach them through the generated sessionsMoveCalls namespace instead:

  • placeLimitOrder: Places a DeepBook spot limit order for the account.
  • placeMarketOrder: Places a DeepBook spot market order for the account.
  • cancelLiveOrder: Cancels a single DeepBook spot order.
  • cancelLiveOrders: Cancels several DeepBook spot orders.
  • withdrawSettledAmounts: Sweeps settled DeepBook spot proceeds into account custody.

Whether these calls succeed depends on a DeepBook registry authorization that differs by network, and the Sessions page covers how to check it.

Each generated function takes an options object with these fields:

  • arguments: The Move arguments, in an object whose keys are the parameter names.
  • typeArguments: The pool's base and quote coin types.
  • config: The sessionsPackageId and sessionsConfig IDs. The function resolves the package address from it and fills in the sessionsConfig argument, so you can omit that argument.
  • package: An explicit package ID, which overrides config.sessionsPackageId.

The functions also add the Clock, and the AccumulatorRoot that the order-placement calls take, so you never pass either. The following snippet cancels a spot order as the session key:

import { getSessionsConfig, sessionsMoveCalls } from '@mysten/deepbook-v3/sessions';
import { Transaction } from '@mysten/sui/transactions';

const ids = getSessionsConfig('testnet');

const tx = new Transaction();
tx.setSender(SESSION_ADDRESS);
tx.add(
sessionsMoveCalls.cancelLiveOrder({
config: { sessionsPackageId: ids.sessionsPackageId, sessionsConfig: ids.sessionsConfig },
arguments: {
pool: POOL_ID,
accountRegistry: ids.accountRegistry,
wrapper: WRAPPER_ID,
orderId: ORDER_ID,
},
typeArguments: [BASE_COIN_TYPE, QUOTE_COIN_TYPE],
}),
);

The functions take arguments by name, so argument positions matter only when you build the moveCall yourself. place_limit_order and place_market_order carry deepbook_registry between pool and account_registry, which puts account_registry at index 2 and sessions_config at index 4. The other 3 take no deepbook_registry, so they keep those arguments at indexes 1 and 3, matching the Predict wrappers.

Other exports

Besides SessionsContract and the spot bindings, the subpath exports the session_config bindings, 4 generated Move structs, and a set of deployment helpers.

sessionConfigMoveCalls

The generated sessionConfigMoveCalls namespace covers the session_config module. id and versionWatermark read the shared SessionsConfig object, and bumpVersionWatermark advances the package version floor, which only the SessionsAdminCap holder can call. The namespace also exports that module's BCS structs, and sessionConfigMoveCalls.SessionsConfig is the onchain object's layout, not the SessionsConfig interface. The Sessions page explains the version watermark.

Move structs

The subpath re-exports 4 generated BCS structs from the sessions module. Each one is a MoveStruct, so its parse method decodes the matching bytes:

  • SessionsApp: The app type the account registry must authorize before any trading call works.
  • SessionsData: The grant map stored under the account, a VecMap from session address to expiry. decodeSessions decodes the dynamic field that wraps it.
  • SessionAuthorized: The event authorize_session emits for a new grant or a re-authorization, with account_id, session, and expires_at_ms.
  • SessionRevoked: The event revoke_session emits when it removes a grant, with the same 3 fields.

Deployment helpers

The subpath re-exports these deployment helpers, so code that imports only @mysten/deepbook-v3/sessions can name the deployment its IDs came from and format a custody balance:

  • getDeployment and getUnits
  • TESTNET_DEPLOYMENT and TESTNET_UNITS
  • The DeployedNetwork and NetworkArg types

DeepBook Predict SDK describes the deployment record they read.