Turbo Accounts API
Turbo Accounts use the same signed session-action API as regular o2 accounts, with additional actions for credit, debt, collateral, duration, and settlement.
For the product model and lifecycle, start with Turbo Accounts. This page is the raw API reference for building a Turbo integration.
Integration Model
A Turbo integration manages two contracts:
- the parent trading account, which creates, opens, funds, and closes its Turbo children;
- the Turbo Account, which places orders and performs draw, borrow, repay, collateral, and prolong actions.
All writes are signed session actions submitted through POST /v1/session/actions, except registration, which sends the same signed envelope to POST /v1/margin/accounts so the API can deploy and register the child.
The parent session scope must include every Turbo child it controls and the required pool, registry, and market contracts. Value withdrawn from a Turbo Account always returns to its parent.
Units and Rounding
- Asset amounts are decimal strings in the asset's raw base units.
- Prices are decimal strings scaled by
1e18. - Collateral-denominated results use the collateral asset's base units.
- Owned value is rounded down.
- Owed value is rounded up.
Do not calculate with JavaScript number. Use integers such as bigint and preserve the rounding direction shown in Calculations.
Read Endpoints
| Endpoint | Purpose |
|---|---|
GET /v1/accounts?owner={owner} | List a user's parent and registered Turbo Accounts |
GET /v1/margin/tiers | Get the latest version of each available tier |
GET /v1/margin/tiers/history | Get historical tier versions |
GET /v1/margin/next-account | Predict a deterministic Turbo Account ID and registration configurables |
GET /v1/margin/state?account={id} | Get the complete state needed for valuation and limit calculations |
GET /v1/margin/pool | Get pool pause state and current per-asset inventory |
GET /v1/margin/statistics | Get aggregate active-session and pool risk statistics |
GET /v1/margin/activity?account={id} | Get account lifecycle and margin activity |
GET /v1/margin/close-cleanups?account={id} | Get the order-book cleanup list required for a clean close |
GET /v1/margin/discount?user={owner} | Get the owner's current Turbo fee discounts |
Lifecycle
1. Select a Tier
GET /v1/margin/tiersA tier supplies the session's collateral requirement, k, duration, fees, supported assets and books, credit-line cap, threshold, open buffer, and price controls. Store the exact tier_id and version used by the session; tier terms can change between sessions.
Cohorts
A tier version sells one of two products, and the difference changes the open call, the liquidation rule, and settlement. The discriminator is the presence of a turbo object on the tier:
| Aspect | Legacy | Turbo (prepaid) |
|---|---|---|
tier.turbo | Key absent | Present |
| Collateral posted | The trader's, refundable | None. The session opens on zero collateral |
| Entry payment | A deposit plus fees | A price, forfeited at settlement |
| Liquidation rule | V <= tier.threshold | An anchored floor. See Health, Freeze, and Liquidation |
| Extra terms | Purchased with ProlongSession | Earned free by hitting a profit target, on tiers with max_rollovers > 0 |
The key is omitted rather than nulled, so test for presence. A live session also carries session.cohort, either "Legacy" or "Turbo", pinned at open and stable for the session's life. Prefer session.cohort when a session exists and fall back to the tier test when it does not.
tier.turbo contains:
| Field | Meaning |
|---|---|
forfeit_collateral | Settlement routes the posted collateral to the platform rather than back to the parent |
max_loss_bps | Four-entry drawdown escalator, bps of tier.line, indexed by rollovers_used and clamped at the last entry |
rollover_profit_bps | Profit above the anchor that earns one free term, bps of tier.line |
max_rollovers | How many free terms can be earned |
term_seconds | How long one term runs. Authoritative on a prepaid tier; do not derive it from duration + prolong_seconds[period] |
rollover_window_seconds | Vestigial. Nothing schedules against it, and there is no early claim. Do not build a claim window from it |
An all-zero max_loss_bps means the tier sells no anchored drawdown, and the static tier.threshold stands alone.
A tier without an expiry publishes duration and turbo.term_seconds as 18446744073709551615 (u64::MAX), and its sessions carry the same value in expires_at. Treat it as "no expiry": do not add it to a timestamp or render it as a countdown. A tier with max_rollovers: 0 sells no free terms, so every max_loss_bps entry is equal, the floor never moves, and GET /v1/margin/state omits the rollover object. The tiers on sale today are of this kind; read them from GET /v1/margin/tiers rather than assuming it.
2. Predict and Register a Turbo Account
Predict the next child of a parent trading account:
GET /v1/margin/next-account?parent_contract=0xPARENTThe response contains the deterministic contract_id, its index, and the configurables covered by registration.
Register that child using the standard signed session envelope with this action:
POST /v1/margin/accounts{
"RegisterMarginAccount": {
"margin_account_id": "0xTURBO_ACCOUNT",
"index": "0"
}
}The trade_account_id in the envelope is the parent account. See session action request structure for the signature, nonce, session identity, and market grouping.
3. Start a Session
Submit StartMarginSession from the parent account:
{
"StartMarginSession": {
"margin_account_id": "0xTURBO_ACCOUNT",
"tier_id": "12",
"amount": "250000000",
"period": "Week"
}
}amount is the value forwarded with the call. period is one of SixHours, Day, Week, or Month. Starting a session purchases the base duration plus the selected initial period and charges the applicable opening and period fees.
What amount has to be depends on the cohort:
On a legacy tier the discount reaches the fee leg only. required_collateral is the trader's own returnable margin, so discounting it would under-fund the session below the tier's floor and the open reverts.
On a prepaid tier the discount reaches both legs. The entry is a price rather than a deposit, and the session opens on zero collateral.
Discount each leg separately and then sum, mirroring the chain: discounted_price truncates, so discounting the total instead can differ by one base unit. A prepaid open requires an exact amount and is refused rather than clamped, which makes that one unit a revert.
A prepaid tier sells one term flat and refuses every period but Week at open.
4. Read or Subscribe to State
GET /v1/margin/state?account=0xTURBO_ACCOUNTThe response contains:
session: collateral, credit line, quote drawn, in-kind debts, fees, timestamps, andcohort;tier: the exact tier version used by the active session, including itsturbooverlay if it has one;rollover: the prepaid session's rollover position, described below;balances:on_account,received,locked,settled, anddebtfor each asset;prices: bid, ask, decimals, timestamp, and stale status;collateral_decimals: the result scale for collateral-denominated formulas;discount: the parent's active fee-discount state.
Use this snapshot as one internally consistent input to the formulas below. Do not mix balances, prices, and session fields fetched at different times.
The rollover object
Emitted only when the tier's terms enable a rollover and an anchor was seeded, so it is absent on every legacy session and on a forfeit-only prepaid tier.
| Field | Meaning |
|---|---|
anchor | V at the current term's start, raw collateral units. Account value, not equity: equity = V - k, so comparing the two directly is off by k |
withdrawn | Collateral value withdrawn this term. Added back when judging the profit bar, so banked profit still counts. Resets on rollover |
outside | Value that arrived this term rather than being earned. Subtracted when judging the bar, which is what stops a transfer in from buying a free term |
rollovers_used | Which term the session is in, and therefore which max_loss_bps entry applies |
max_rollovers | How many free terms this session can earn |
profit_required | The absolute profit above anchor one rollover demands, computed as line * rollover_profit_bps / 10_000 |
eligible_now | The backend's hint, from its own marked valuation. The chain re-verifies regardless |
anchor is fixed for the term. A withdrawal moves withdrawn, never the anchor.
5. Trade and Manage Credit
Regular CreateOrder, CancelOrder, and SettleBalance actions work on a Turbo Account. Submit them with the Turbo Account as trade_account_id.
A tier whose books are Turbo order books trades with "turbo": true on each market group. Match tier.books against GET /v1/markets?turbo=true to tell which kind a tier uses. See Turbo Order Books.
Turbo Account actions are:
| Action | Purpose |
|---|---|
Draw | Draw the pool's collateral asset against the credit line |
ReturnQuote | Return previously drawn quote held by the account |
Borrow | Borrow a supported non-collateral asset in kind |
Repay | Repay an in-kind debt with that asset |
RepayFromCollateral | Reduce drawn quote directly against posted collateral |
RepayBaseFromCollateral | Buy out an in-kind debt from posted collateral at the ask plus fee |
AddCollateral | Convert collateral-asset coins already on the Turbo Account into posted collateral |
WithdrawFromMargin | Withdraw eligible active-session funds to the parent |
ProlongSession | Purchase one or more additional periods |
SetAutoProlong | Enable or disable automatic prolongation |
Draw, ReturnQuote, Borrow and Repay behave identically in both cohorts. The three collateral actions and ProlongSession are legacy remedies: a prepaid session posts no collateral to repay from, and it gains extra terms by clearing rollover.profit_required rather than by buying them. A prepaid term is rolled at its deadline through the cosigned expire_or_rollover, not by a client action.
Example action fragments:
{ "Draw": { "amount": "1000000000" } }{
"Borrow": {
"asset_id": "0xASSET",
"amount": "100000000"
}
}{
"Repay": {
"asset_id": "0xASSET",
"amount": "100000000"
}
}{
"ProlongSession": {
"period": "Day",
"times": "1"
}
}{ "SetAutoProlong": { "period": null } }Margin actions share the request's market-grouped action list. Their market_id positions the action in the batch; the action targets the pool rather than that order book. This lets a client atomically return quote before borrowing, or draw quote before creating an order.
6. Close the Session
Before a clean close, cancel open orders, settle book balances, return quote, and repay every in-kind debt. Then fetch the current cleanup list:
GET /v1/margin/close-cleanups?account=0xTURBO_ACCOUNTSubmit CloseMarginSession from the parent account:
{
"CloseMarginSession": {
"margin_account_id": "0xTURBO_ACCOUNT",
"cleanups": []
}
}Pass the exact cleanups returned by the endpoint. A stale or incomplete list reverts rather than leaving funds on a book.
With no active session, the parent can move a remaining child balance back using WithdrawFromMarginAccount.
Settlement
Settlement terms depend on the cohort, and every figure below is published per tier rather than fixed by the product. Read them from GET /v1/margin/tiers.
| Aspect | Legacy | Turbo (prepaid) |
|---|---|---|
| Liquidation level | tier.threshold | The anchored floor, below |
| Posted collateral at settlement | Returned to the parent | Forfeited to the platform (turbo.forfeit_collateral) |
| Trader's share of realized profit | 10_000 - tier.profit_share_bps, in bps | The same field. Zero share on the shipped prepaid tiers, so the trader keeps all of it |
| Additional terms | Bought with ProlongSession | Earned free by clearing profit_required |
At expiry or liquidation, positions are unwound off market to avoid slippage on the o2 order book. The amount returned to the parent account may include supported assets rather than only USDC.
A prepaid entry is a price and does not pro-rate, so closing early neither reduces nor refunds it.
The last escalator step is wider than the deposit, so a final-term floor can legitimately land below the pool's own principal k. On a 10,000 line that is a floor of 9,700 against a k of 9,750. Settlement books that tail as bad debt deliberately. Do not clamp the floor at k or at tier.threshold: clamping makes distance-to-liquidation read wrong for exactly the sessions that earned the room.
Turbo Order Books
Each market can have a Turbo order book paired with its public (canonical) order book. Turbo orders match only against shared liquidity that the house market maker and whitelisted market makers post through the canonical book. They never match ordinary public orders, and public takers never match resting Turbo orders.
Selecting a Turbo book
Add turbo=true to a market-scoped request to read the Turbo book instead of the canonical one. The market_id stays the same.
| Endpoint | With turbo=true |
|---|---|
GET /v1/markets | Lists only markets that have a Turbo book, rewritten for it (below) |
GET /v1/markets/summary, GET /v1/markets/ticker, GET /v1/markets/fees | Computed from the Turbo book. The ticker's bid and ask are the best executable shared levels. Markets without a Turbo book are skipped |
GET /v1/depth | Executable or demand depth. See Turbo depth views |
GET /v1/bars | Prices from the Turbo book. History before the Turbo book was attached is inherited from the canonical book |
GET /v1/trades, GET /v1/trades_by_account, GET /v1/trade_by_account, GET /v1/fee_for_trade | Trades and fees on the Turbo book |
GET /v1/order, GET /v1/orders, GET /v1/orders/book, GET /v1/orders/active | Orders on the Turbo book |
turbo defaults to false, and a canonical response is unchanged. A Turbo response adds "turbo": true. Requesting turbo=true for a market with no Turbo book returns MarketNotFound with the message Market {market_id} has no Turbo order book.
Account-wide endpoints (/v1/balance, /v1/accounts, /v1/margin/*, /v1/events) do not take turbo.
Turbo market rows
GET /v1/markets?turbo=true returns each market's canonical row with these changes:
| Field | Value |
|---|---|
contract_id | The Turbo book's contract ID. Sign Turbo orders against this contract |
canonical_contract_id | The paired canonical book |
turbo | true |
connected | Whether the Turbo book is connected to its canonical book. Creates are refused while it is disconnected |
paused | true if either the Turbo book or its canonical book is paused |
maker_fee, taker_fee | The Turbo book's own rates, in ppm (100 is 0.01%) |
min_order, dust, price_window, trigger_max_slippage, asset decimals and precision | The Turbo book's own configuration |
Placing orders
In POST /v1/session/actions, set turbo on each market group that targets a Turbo book:
{
"actions": [
{
"market_id": "0xMARKET_ID",
"turbo": true,
"actions": [{ "CreateOrder": { "...": "..." } }]
}
],
"signature": "...",
"trade_account_id": "0xTURBO_ACCOUNT"
}CreateOrder, CreateOrderWithTriggers, CreateTriggerOrder(s), CancelOrder, CancelTriggerOrder and SettleBalance work as they do on a canonical book. Creates are refused with market with id {market_id} is paused if either book is paused, and with House book is disconnected while the Turbo book is disconnected. Each returned order carries "turbo": true when it sits on a Turbo book.
In POST /v1/accounts/actions, SettleBalance and Whitelist take the same optional turbo flag to target the Turbo book.
How Turbo orders fill
| Order | Fills against |
|---|---|
| Turbo market or crossing limit order | Shared quotes resting on the canonical book |
| Resting Turbo order | A shared quote placed at or through its price, or an explicit house execution |
A trade filled from shared liquidity carries a shared object, { "funding_source": { "order_id": "0x..." } }, naming the canonical order that funded it. It appears on REST and WebSocket trade responses.
Market maker actions
Shared liquidity is posted with two session actions, available only to trading accounts that hold the house role on the market's Turbo book. Both are sent on the canonical market group ("turbo": false); sending them with "turbo": true is refused with House posts and selects its liquidity through the canonical book.
| Action | Fields | Purpose |
|---|---|---|
CreateSharedOrder | side, price, quantity, order_type ("Spot" or "PostOnly", default "PostOnly") | Post a quote on the canonical book that Turbo takers can also fill. A Spot order first fills any resting Turbo orders it crosses |
ExecuteTurboOrders | source_order_id, max_base_quantity, max_fills | Fill resting Turbo orders from an existing shared quote. Bounds must be positive |
Without a paired Turbo book, a shared order is placed as an ordinary order of the same type. Market makers interested in providing Turbo liquidity can contact the o2 team.
WebSocket
| Request | Turbo fields |
|---|---|
subscribe_depth, subscribe_depth_view and their unsubscribes | turbo: true, and optional view ("executable" or "demand") |
subscribe_trades, unsubscribe_trades | turbo: true |
subscribe_orders, unsubscribe_orders | turbo: true. Turbo and canonical order subscriptions are separate and can run together; unsubscribe with the same flag |
Turbo trade messages add turbo: true, confirmed and indexed_height. If the Turbo trade stream has to restart, it sends { "action": "error", "turbo": true, "resync_required": true, "message": "..." } and closes the subscription; resubscribe to continue. subscribe_balances, subscribe_nonce and subscribe_margin are account-wide and ignore turbo.
Events
GET /v1/events reports Turbo book activity with these event types:
| Event | Fields |
|---|---|
SharedExecution | house_order_id, source_order_id, turbo_order_id, house_trader, turbo_trader, turbo_side, house_execution_role (Maker or Taker), quantity, price, house_fee_amount, turbo_fee_amount, timestamp |
SharedTradePerformed | source_order_id, turbo_order_id, house_execution_role, quantity, price, timestamp |
HouseOrderCreated | canonical_book, order_id, source_order_id, trader, side, price, quantity, timestamp |
HouseOrderCompleted | order_id, timestamp |
SidecarChanged | previous, current: the Turbo book paired with a canonical book, before and after |
Calculations
The raw API returns inputs rather than precomputed action limits. A custom client must reproduce these calculations before presenting or signing a value.
The formulas are shown in calculation order. Each display is one value a client can compute and inspect independently.
Symbols
| Symbol | Definition |
|---|---|
cd | Collateral asset decimals |
ad[a] | Decimals of asset a |
bid[a], ask[a] | 1e18-scaled prices for asset a |
k | Tier capital constant |
C | Posted collateral, including fees charged from it |
F | Fees accrued for the active session |
L | Current credit line |
D_q | Quote currently drawn |
debt[a] | In-kind debt in asset a |
H[a] | Total holdings of asset a |
V | Conservatively marked account value |
Price Conversion
First compute the scale for asset a:
Round down when valuing something the account owns:
Round up when valuing something the account owes:
The ceiling operation is:
Credit Line
The tier first sets the maximum possible line:
Fees reduce the collateral that contributes to the line:
The current credit line is the lower of the collateral-derived line and the tier cap:
Additional collateral that can still increase the line is:
A prepaid session holds C = 0, so its line is min(k, line cap) and the collateral-denominated remedies have nothing to draw on. AddCollateral is still accepted, and value added that way counts toward rollover.outside rather than toward the profit bar.
Holdings and Net Position
For each asset:
Net holdings against debt before applying a price:
For the collateral asset, its value calculation also includes:
Collateral posted above the amount converted into credit remains part of value:
Account Value, Equity, and PnL
Mark a positive position at the bid and a negative position at the ask:
Then:
Remove the tier's capital to get the trader's equity:
Fees are removed from posted collateral to establish the trading basis:
Trading PnL compares equity with that basis:
Draws and borrows are balance-sheet movements, not PnL. Fees are reported separately because they reduce both equity and trading basis.
Loan Cap and Draw Capacity
Value every in-kind debt at the ask and round up:
Add quote already drawn:
The unused part of the line is:
Quote inventory can further restrict a draw:
The protocol requires outstanding <= L after the action batch.
Borrow Capacity
For target asset a:
1. Quote That Can Be Returned First
returnable quote supports a batch that returns quote before borrowing. Without that leading action, use only the current loan headroom.
2. Limit Imposed by the Loan Cap
3. Stress the Ask Price
4. Limit Imposed by Exposure
5. Final Borrow Limit
Take the smallest of the two risk limits and the asset inventory:
The collateral asset cannot be borrowed in kind; use Draw.
Reserve and Spendable Quote
Cash needed to buy back open net shorts is reserved:
Count quote held directly or settled on a book:
Only quote above the reserve is spendable:
The on-chain calculation may reduce the reserve by quote already committed to a resting buyback on the same book. A client that cannot attribute locked quote to a specific book should omit that credit; this is conservative.
Gross Exposure
Gross exposure includes current positions, quote locked by buy orders, and the possible filled state of resting sell orders. Let:
For each spot and stressed price pass, the non-collateral contribution is the larger absolute value of the current and effective positions. Add quote locked by buys at par:
Stress moves prices against the position by the configured stress band. Longs use a lower stressed bid; shorts use a higher stressed ask.
Use the largest result from the spot and stressed passes:
The remaining exposure capacity is:
Health, Freeze, and Liquidation
The threshold itself depends on the cohort. A legacy session uses the tier's static value; a prepaid session replaces it with an anchored floor plus the cost of unwinding what the account currently holds.
Legacy:
Prepaid. First the loss floor, from the escalator entry for the current term:
The allowance rounds down, so a fraction of a unit tightens the floor.
The drawdown is measured on tier.line, a constant, not on the anchor. The anchor served as both baselines until 0.4.1-rc, and because withdraw decremented it, a trader who made his week and withdrew it sat back at the line while his floor had dropped by the same amount. Repeating a donate-and-withdraw cycle walked the floor toward zero. The two baselines are now separate: anchor carries the rollover bar alone, and this floor reads the line, which nothing the session does can move. rollovers_used is read only to index the escalator.
Then add the absorption cost, which is what unwinding the current basket would cost the pool:
Note that the prepaid rule adds the absorption cost rather than taking a maximum with it, so the effective floor rises with position size.
This threshold replaces the static one. Do not take a maximum of the two.
Liquidation becomes permissionless at whichever threshold applies:
The maximum value an action may concede while remaining strictly above that line is:
Use the worse of the two stressed values:
Risk-increasing actions freeze below the freeze line, which is also cohort-dependent:
Under the prepaid regime the freeze line sits one open_buffer - maintenance gap above whatever the threshold turned out to be, which is what keeps freeze > threshold true in both regimes and at every price. The absorption cost must be added to both lines: adding it to the threshold alone inverts the ordering.
When frozen, a batch must not increase exposure. Reducing risk can still be accepted. The one-unit subtraction in giveaway allowance preserves the strict safe side because liquidation begins at equality.
Buy-Side Capacity
A buy must leave enough quote reserved for shorts and stay inside the exposure line:
This is still subject to price freshness, custody, the giveaway allowance, order bounds, market support, and the batch-level stressed checks.
Stream State with subscribe_margin
Connect to the o2 WebSocket and subscribe to one Turbo Account:
{
"action": "subscribe_margin",
"margin_account_id": "0xTURBO_ACCOUNT"
}The first message is a complete state snapshot:
{
"action": "subscribe_margin",
"state": {}
}Subsequent messages carry changes:
{
"action": "subscribe_margin_update",
"event": {},
"onchain_timestamp": 0,
"seen_timestamp": 0
}Updates include activity, prices, tier publication, pause state, balances, and session closure. Activity fields such as drawn_total, debt_total, and received_total are post-event totals: replace the local value rather than adding the event amount again.
Start from the snapshot, fold updates in order, and run the formulas locally after each relevant change. On reconnect, a sequence gap, or a failed fold, fetch a fresh GET /v1/margin/state snapshot before signing another action.
To unsubscribe:
{ "action": "unsubscribe_margin" }Raw API vs SDK
The raw API intentionally exposes state and protocol inputs. Raw clients are responsible for integer conversion, rounding, action ordering, price freshness, and every applicable risk limit.
Turbo is supported in the TypeScript SDK, which composes these calls for you. long() and short() build the sweep, the funding leg and the order into one signed batch, so you never assemble a draw or a borrow by hand.
Requires @o2exchange/sdk 0.4.0 or later
Turbo is not in 0.3.0. If client.turbo is undefined, you are on an older release — upgrade, or install from main in the SDK repository until 0.4.0 is published.
import { Network, O2Client, boundedMarketFromSlippage } from "@o2exchange/sdk";
const client = new O2Client({ network: Network.MAINNET });
const wallet = O2Client.loadWallet(process.env.PRIVATE_KEY!);
await client.setupAccount(wallet);
// `turbo: true` also scopes the session to the margin contracts. That
// scope is signed and cannot be widened afterwards, so a session created
// without it can do nothing with a Turbo account.
await client.createSession(wallet, ["ETH/USDC"], { turbo: true });
const tier = await client.turbo.cheapestTier();
await client.turbo.open({
tierId: tier.tier_id,
collateral: client.turbo.openingCost(tier), // margin plus the premium
});
await client.turbo.long("ETH/USDC", { notional: "2000" });
await client.turbo.closePosition("ETH/USDC");Two rules the SDK enforces that a raw client must handle itself:
- Market orders must be bounded. The pool funds an order at its worst-case execution price, and an unbounded market order has none, so it is rejected. Pass
boundedMarketFromSlippage(rawPrice, 100, tick)for a 1% band. - Attached take-profit and stop-loss inherit the position's size. The contract accepts nothing else for a protective leg created alongside its parent order.
The Python and Rust SDKs do not cover Turbo yet; use the raw API from those languages.
For the full surface — tiers, positions, risk limits, take-profit and stop-loss, extensions, referrals and closing — see the Turbo guide and the SDK documentation on GitHub.
Using the raw API directly, your integration is responsible for integer conversion, rounding, state updates, action ordering, and risk calculations. Treat an API rejection as authoritative if local state is stale.