# private/get_account
Source: https://docs.derive.xyz/api-reference/account/privateget_account
/openapi.json post /private/get_account
Returns account-level information for a given wallet address: the list of its subaccount IDs, WebSocket rate limits (matching, non-matching, perp, and option messages per second) and any per-endpoint overrides, cancel-on-disconnect and RFQ-maker flags, the account creation timestamp, and a fee_info block with fee discounts and per-instrument maker/taker fee overrides expressed as decimals. Returns an account-not-found error if the wallet has never been registered.
# Auctions watch
Source: https://docs.derive.xyz/api-reference/channels/auctionswatch
Channel name: `auctions.watch`. Public channel that streams the live state of all ongoing liquidation and insolvency auctions across the exchange. Takes no parameters.
# Margin watch
Source: https://docs.derive.xyz/api-reference/channels/marginwatch
Channel name: `margin.watch`. Public channel that streams margin and mark-to-market state for all accounts, letting you monitor collateralization and liquidation risk across the exchange. Takes no parameters.
# Orderbook
Source: https://docs.derive.xyz/api-reference/channels/orderbook
Channel name: `orderbook.{instrument_name}.{group}.{depth}`. Public channel that periodically publishes aggregated bids and asks for a single instrument, where {instrument_name} is the market (e.g. ETH-PERP), {group} is the price-level aggregation size (1, 10, or 100), and {depth} is the number of levels returned per side (1, 10, 20, or 100).
# Spot feed
Source: https://docs.derive.xyz/api-reference/channels/spotfeed
Channel name: `spot_feed.{currency}`. Public channel that streams the spot price feed for a given underlying, where {currency} selects the asset (e.g. ETH or BTC).
# Subaccount balances
Source: https://docs.derive.xyz/api-reference/channels/subaccountbalances
Channel name: `{subaccount_id}.balances`. Private channel (requires authentication) that streams balance and position updates for one of your subaccounts, where {subaccount_id} is the numeric subaccount identifier.
# Subaccount best quotes
Source: https://docs.derive.xyz/api-reference/channels/subaccountbestquotes
Channel name: `{subaccount_id}.best.quotes`. Private channel (requires authentication) that streams best-quote updates relevant to your subaccount, where {subaccount_id} is the numeric subaccount identifier.
# Subaccount orders
Source: https://docs.derive.xyz/api-reference/channels/subaccountorders
Channel name: `{subaccount_id}.orders`. Private channel (requires authentication) that streams live order updates (opens, fills, cancels, and status changes) for your subaccount, where {subaccount_id} is the numeric subaccount identifier.
# Subaccount quotes
Source: https://docs.derive.xyz/api-reference/channels/subaccountquotes
Channel name: `{subaccount_id}.quotes`. Private channel (requires authentication) that streams updates to your RFQ quotes for a subaccount, where {subaccount_id} is the numeric subaccount identifier.
# Subaccount trades
Source: https://docs.derive.xyz/api-reference/channels/subaccounttrades
Channel name: `{subaccount_id}.trades`. Private channel (requires authentication) that streams your subaccount's own trade fills as they occur, where {subaccount_id} is the numeric subaccount identifier.
# Ticker slim
Source: https://docs.derive.xyz/api-reference/channels/tickerslim
Channel name: `ticker_slim.{instrument_name}.{interval}`. Public channel that streams a lightweight ticker (prices and key stats) for an instrument, where {instrument_name} is the market and {interval} is the update cadence in milliseconds (100 or 1000).
# Trades by instrument
Source: https://docs.derive.xyz/api-reference/channels/tradesbyinstrument
Channel name: `trades.{instrument_name}`. Public channel that streams the public trade tape for a single instrument, where {instrument_name} is the market whose fills you want to follow.
# Trades by instrument type currency
Source: https://docs.derive.xyz/api-reference/channels/tradesbyinstrumenttypecurrency
Channel name: `trades.{instrument_type}.{currency}`. Public channel that streams public trades filtered by market category, where {instrument_type} is the kind of instrument (erc20, option, or perp) and {currency} is the underlying asset (e.g. ETH or BTC).
# Wallet rfqs
Source: https://docs.derive.xyz/api-reference/channels/walletrfqs
Channel name: `{wallet}.rfqs`. Private channel (requires authentication) that streams RFQ (request-for-quote) updates for all subaccounts under a wallet, where {wallet} is your wallet address.
# private/get_deposit_history
Source: https://docs.derive.xyz/api-reference/history/privateget_deposit_history
/openapi.json post /private/get_deposit_history
Returns settled deposits for a single subaccount or an entire wallet (specify exactly one), optionally bounded by a start/end timestamp window. Each entry reports the deposited amount as a decimal, the fee routed to the security module (the net credited amount is amount minus fee), and the resolved settlement batch and status.
# private/get_erc20_transfer_history
Source: https://docs.derive.xyz/api-reference/history/privateget_erc20_transfer_history
/openapi.json post /private/get_erc20_transfer_history
Returns settled spot (ERC-20) transfers involving a single subaccount or an entire wallet (specify exactly one), optionally bounded by a start/end timestamp window. Each entry reports the transfer amount and fee as decimals along with direction — the sender sees the gross amount plus fee, the receiver sees the net credit — plus the resolved settlement batch and status.
# private/get_funding_history
Source: https://docs.derive.xyz/api-reference/history/privateget_funding_history
/openapi.json post /private/get_funding_history
Returns a paginated history of perpetual funding (settlement) events for a single subaccount or an entire wallet (specify exactly one), optionally bounded by a start/end timestamp window and filtered by perpetual instrument name. The response includes the funding events with their instrument name and settled amounts, plus pagination info with total count and page count.
# private/get_interest_history
Source: https://docs.derive.xyz/api-reference/history/privateget_interest_history
/openapi.json post /private/get_interest_history
Returns realized interest settlements per subaccount, for a single subaccount or an entire wallet (specify exactly one), optionally bounded by a start/end timestamp window. Each entry gives the settled interest as a decimal, where a negative value was paid (borrowed) and a positive value was received (supplied).
# private/get_option_settlement_history
Source: https://docs.derive.xyz/api-reference/history/privateget_option_settlement_history
/openapi.json post /private/get_option_settlement_history
Returns option settlement (expiry) events for a single subaccount or an entire wallet (specify exactly one). Each settlement includes the reconstructed option instrument name, the settled amount and value for that expired position, and the realized settlement PnL against the position's cost basis, with and without trading fees.
# private/get_order_history
Source: https://docs.derive.xyz/api-reference/history/privateget_order_history
/openapi.json post /private/get_order_history
Returns a paginated history of orders for a single subaccount or for an entire wallet (specify exactly one), optionally bounded by a from/to timestamp window. Only orders in a terminal state (filled, cancelled, expired) are recorded here, one record per order. Trigger and algo orders appear once they terminate, carrying their accumulated filled amount; while they are still live — including partially executed algo orders and trigger orders that have not yet fired — read them from get_trigger_orders and get_algo_orders instead. Each page includes the order records plus pagination info with the total count and number of pages.
# private/get_trade_history
Source: https://docs.derive.xyz/api-reference/history/privateget_trade_history
/openapi.json post /private/get_trade_history
Returns a paginated history of executed trades for a single subaccount or an entire wallet (specify exactly one), with optional filters for a time window, order id, instrument name, or quote id. The response includes the trade records and pagination info with total count and page count.
# private/get_withdrawal_history
Source: https://docs.derive.xyz/api-reference/history/privateget_withdrawal_history
/openapi.json post /private/get_withdrawal_history
Returns settled withdrawals for a single subaccount or an entire wallet (specify exactly one), optionally bounded by a start/end timestamp window. Each entry reports the withdrawn amount as a decimal, the fee routed to the security module (the net amount sent to the recipient is amount minus fee), and the resolved settlement batch and status.
# public/get_liquidation_history
Source: https://docs.derive.xyz/api-reference/history/publicget_liquidation_history
/openapi.json post /public/get_liquidation_history
Returns a paginated history of liquidation auctions, newest first, for a single subaccount or across all of them, optionally bounded by a start/end timestamp window over the auction start. Each entry gives the auctioned subaccount, whether the auction was solvent or insolvent, the fee charged at start, the start and end timestamps, and the bids that filled it with the fraction of the account each absorbed and the cash it moved.
# public/get_maker_program_scores
Source: https://docs.derive.xyz/api-reference/maker-scoring/publicget_maker_program_scores
/openapi.json post /public/get_maker_program_scores
Returns the wallet-level score breakdown for one maker-program epoch, including square-rooted coverage and quality components, traded-volume multiplier, holder boost, volume, and epoch totals. Scores, trades, program configuration, and the temporary enrolled-wallet set are read from ClickHouse.
# public/get_maker_programs
Source: https://docs.derive.xyz/api-reference/maker-scoring/publicget_maker_programs
/openapi.json post /public/get_maker_programs
Returns every maker-program epoch, including historical epochs, with its asset and currency scope, minimum eligible notional, timestamps, and reward-token amounts. Takes no parameters and reads the configuration from ClickHouse.
# public/get_all_currencies
Source: https://docs.derive.xyz/api-reference/market-data/publicget_all_currencies
/openapi.json post /public/get_all_currencies
Returns detailed metadata for every configured currency, including supported instrument types, risk managers, spot price and 24h-ago price, SRM/portfolio margin collateral discounts, lending borrow/supply APYs, open-interest caps and utilization per manager, and protocol asset addresses. Currencies that exist but have no configured assets are omitted. Public endpoint, no authentication required.
# public/get_all_instruments
Source: https://docs.derive.xyz/api-reference/market-data/publicget_all_instruments
/openapi.json post /public/get_all_instruments
Returns a paginated list of full instrument definitions filtered by `instrument_type`, optional `currency`, and `expired` flag, with `page`/`page_size` (max 1000) controls and pagination metadata. Expired options are included when the `expired` flag is set. Public endpoint.
# public/get_all_live_instruments
Source: https://docs.derive.xyz/api-reference/market-data/publicget_all_live_instruments
/openapi.json post /public/get_all_live_instruments
Returns a sorted list of the names of every currently live instrument (active and within its scheduled activation window). Takes no parameters and returns names only, not full definitions. Public endpoint.
# public/get_assets
Source: https://docs.derive.xyz/api-reference/market-data/publicget_assets
/openapi.json post /public/get_assets
Returns the assets of a given `asset_type` (option, perp, or erc20) for a `currency`, with `expired` controlling whether past-expiry options are included. Each entry includes the asset id, name, on-chain address and sub_id, collateral/position flags, and type-specific details (option strike/expiry/settlement price, perp funding config, or ERC20 lending indices). Public endpoint.
# public/get_currency
Source: https://docs.derive.xyz/api-reference/market-data/publicget_currency
/openapi.json post /public/get_currency
Returns the full detail record for one currency named by the required `currency` parameter: supported instrument types, risk managers, current and 24h spot price, margin collateral discounts, lending APYs and totals, per-manager open-interest caps and current OI, and protocol asset addresses. Returns a not-found error for unknown currencies. Public endpoint.
# public/get_funding_rate_history
Source: https://docs.derive.xyz/api-reference/market-data/publicget_funding_rate_history
/openapi.json post /public/get_funding_rate_history
Returns funding-rate OHLC candles for a perpetual instrument (`instrument_name`) over an optional `start_timestamp`/`end_timestamp` window (UTC milliseconds) at a `period` granularity (defaults to 1h; 60s up to 1w supported). Each candle carries open/high/low/close per-hour rates plus a `funding_rate` mirroring the close. Public endpoint.
# public/get_index_chart_data
Source: https://docs.derive.xyz/api-reference/market-data/publicget_index_chart_data
/openapi.json post /public/get_index_chart_data
Returns spot index OHLC candles for a `currency` between required `start_timestamp` and `end_timestamp` (UTC seconds) at the given `period` granularity (1m up to 1w). Each candle includes open/high/low/close and a `price` mirroring the close. The requested range is clamped to a maximum number of buckets. Public endpoint.
# public/get_instrument
Source: https://docs.derive.xyz/api-reference/market-data/publicget_instrument
/openapi.json post /public/get_instrument
Returns the full public definition of one instrument named by `instrument_name`: type, activation window and active status, tick size, min/max/step amounts, maker/taker/base fee rates, pro-rata matching parameters, base/quote currencies, and type-specific details (option, perp, or spot). Expired options remain queryable by `instrument_name`. Public endpoint.
# public/get_interest_rate_history
Source: https://docs.derive.xyz/api-reference/market-data/publicget_interest_rate_history
/openapi.json post /public/get_interest_rate_history
Returns lending borrow/supply APY OHLC candles for a `currency`'s pool over an optional time window (UTC milliseconds) at a `period` of 1h or larger (defaults to 1h; sub-hour periods are rejected). An optional `risk_universe_id` restricts to one pool; otherwise every universe's candles are returned, each tagged and carrying pool total supply/borrow. Public endpoint.
# public/get_latest_signed_feeds
Source: https://docs.derive.xyz/api-reference/market-data/publicget_latest_signed_feeds
/openapi.json post /public/get_latest_signed_feeds
Returns the most recent oracle-signed feed data — spot, forward, volatility (SVI), rate, perp, and perpetual funding-rate feeds — each with signer addresses and signatures for on-chain submission. Optional `currency` and `expiry` filters narrow the result (use `expiry` 0 for the non-expiry feeds: spot, perp and funding); both default to all. Public endpoint.
# public/get_option_settlement_prices
Source: https://docs.derive.xyz/api-reference/market-data/publicget_option_settlement_prices
/openapi.json post /public/get_option_settlement_prices
Returns the settlement price for each settled option expiry of a `currency`, with the expiry date (YYYYMMDD), unix expiry timestamp, and price. Only already-settled expiries are returned. Unknown currencies return a not-found error. Public endpoint.
# public/get_risk_universes
Source: https://docs.derive.xyz/api-reference/market-data/publicget_risk_universes
/openapi.json post /public/get_risk_universes
Returns every risk universe (the isolated risk-containment boundaries trades never cross), each with its display name and description, its managers — at most one Standard (`SM`) and one Portfolio (`PM2`), each listing the derivative instruments it risk-prices and the collateral assets it accepts with their margin discounts and deposit metadata — and its Security Module. The one call that answers which universe and manager to choose: pick a universe, pick a manager in it, and pass its `manager_id` when depositing to a new subaccount (the accepted collateral entries carry the `asset` address for the deposit). The no-margin fallback universe (id 0) is included; it custodies dead-lettered deposits and lists no tradeable instruments. Public endpoint, no authentication required.
# public/get_ticker
Source: https://docs.derive.xyz/api-reference/market-data/publicget_ticker
/openapi.json post /public/get_ticker
Returns the latest ticker snapshot for a single instrument named by `instrument_name`. The payload matches the data of a `ticker_slim.{instrument}.{interval}` subscription update (mark/index prices, best bid/ask, sizes, greeks, and related fields). Returns an error if no ticker is available. Public endpoint.
# public/get_tickers
Source: https://docs.derive.xyz/api-reference/market-data/publicget_tickers
/openapi.json post /public/get_tickers
Returns a map of instrument name to latest ticker snapshot for an `instrument_type`. Options require both `currency` and an 8-digit `expiry_date` (YYYYMMDD); perps and spot accept an optional `currency` (omit to fetch all currencies) and reject `expiry_date`. Public endpoint.
# public/get_trade_history
Source: https://docs.derive.xyz/api-reference/market-data/publicget_trade_history
/openapi.json post /public/get_trade_history
Returns paginated, anonymized trades with optional filters: `trade_id` (a UUID, which overrides all other filters), `instrument_name`, `instrument_type` (erc20/option/perp), `currency`, `subaccount_id`, `batch_status` (a batch-status name; unset returns trades in every batch state), and `from_timestamp`/`to_timestamp`. Each trade is enriched with its settlement status and transaction hash. Public endpoint.
# public/get_tradingview_chart_data
Source: https://docs.derive.xyz/api-reference/market-data/publicget_tradingview_chart_data
/openapi.json post /public/get_tradingview_chart_data
Returns traded-price OHLCV candles for an `instrument_name` between required `start_timestamp` and `end_timestamp` (UTC seconds) at the given `period` (1m up to 1w), aggregated from executed trade history. Each candle includes open/high/low/close prices plus USD and contract volume, formatted for charting. Public endpoint.
# private/get_mmp_config
Source: https://docs.derive.xyz/api-reference/market-maker-protection/privateget_mmp_config
/openapi.json post /private/get_mmp_config
Returns the market maker protection (MMP) settings for a subaccount, optionally filtered to a single currency. Each entry reports the amount and delta limits (as decimal strings), the rolling interval and freeze duration (in milliseconds), and the current freeze state, including whether MMP is currently frozen and the timestamp at which it unfreezes.
# private/reset_mmp
Source: https://docs.derive.xyz/api-reference/market-maker-protection/privatereset_mmp
/openapi.json post /private/reset_mmp
Clears an active market maker protection freeze and resets the rolling MMP window for a subaccount, optionally scoped to a single currency. Use this to resume quoting after MMP has frozen a subaccount. Requires a trading-scoped session key and returns "ok" on success.
# private/set_mmp_config
Source: https://docs.derive.xyz/api-reference/market-maker-protection/privateset_mmp_config
/openapi.json post /private/set_mmp_config
Creates or fully replaces the market maker protection settings for a subaccount and currency. Accepts an amount limit and delta limit (as decimals) plus the rolling interval and freeze duration in milliseconds; omitted limits default to zero. Requires a trading-scoped session key and echoes the applied configuration back.
# Authenticate a WebSocket connection
Source: https://docs.derive.xyz/api-reference/methods/authenticate-a-websocket-connection
WebSocket-only method that authenticates the connection so subsequent private/* calls and private channel subscriptions act on your wallet without per-request headers. Pass the wallet address, a millisecond UTC timestamp, and an EIP-191 signature of that timestamp made by either the wallet itself or one of its registered session keys. Returns the subaccount ids the authenticated key can access.
# Enable or disable cancel on disconnect
Source: https://docs.derive.xyz/api-reference/methods/enable-or-disable-cancel-on-disconnect
WebSocket-only method that toggles cancel on disconnect for the authenticated wallet. While enabled, dropping the WebSocket connection cancels the wallet's open orders, trigger and algo orders, and quotes. Pass enabled true or false; omitting it leaves the setting unchanged. Requires a session key with full trade scope; returns "ok".
# Subscribe to notification channels
Source: https://docs.derive.xyz/api-reference/methods/subscribe-to-notification-channels
WebSocket-only method that subscribes the connection to one or more notification channels by name — see the Subscriptions section for the available channels and their address formats (e.g. orderbook.ETH-PERP.10.20). Duplicate names are deduplicated; private channels require a logged-in connection with access to the referenced subaccount. Returns a per-channel status map ("ok" or an error string) plus the connection's full set of subscriptions after the call.
# Unsubscribe from notification channels
Source: https://docs.derive.xyz/api-reference/methods/unsubscribe-from-notification-channels
WebSocket-only method that removes the connection's subscriptions for the given channel names; omit channels (or pass null) to unsubscribe from everything. Returns a per-channel status map ("ok" or an error string) plus the subscriptions that remain after the call.
# public/get_onchain_action_history
Source: https://docs.derive.xyz/api-reference/onchain-actions/publicget_onchain_action_history
/openapi.json post /public/get_onchain_action_history
Returns the lifecycle of onchain actions submitted via `OnchainActionManager.sol`. Public endpoint.
Onchain actions are either applied directly or with a `fallback` flag. Each onchain action is affected by the `fallback` flag uniquely:
- Deposits: sequencer moved the deposit into the `fallback_subaccount`
- SetSessionKey: no-op
- Admin actions: no-op
The onchain action can have several states:
- `applied`: action was successfully applied by the sequencer
- `applied_with_fallback`: action was applied by the sequencer with fallback=true
- `instant_fallback`: action failed its initial attempt, will be re-submitted with fallback=true
- `retry_then_fallback`: action failed its initial attempt, will be re-submitted with fallback=false until the retry budget is exceeded, then re-submitted with fallback=true
- `never_escalate`: action failed its initial attempt but will retry until successful, never with fallback=true
# public/get_pending_deposits
Source: https://docs.derive.xyz/api-reference/onchain-actions/publicget_pending_deposits
/openapi.json post /public/get_pending_deposits
Returns deposits queued on L1 but not yet applied by the sequencer, indexed for a wallet and keyed by (tx_hash, log_index). Once a deposit is applied it moves to private/get_deposit_history; this endpoint covers the window in between. Public because a first-time depositor has no account (and thus no session key) until their deposit is applied.
# public/register_deposit_address
Source: https://docs.derive.xyz/api-reference/onchain-actions/publicregister_deposit_address
/openapi.json post /public/register_deposit_address
Returns the deterministic on-chain deposit address for a wallet and records it so incoming deposits are watched and credited. Pass the wallet and optionally an existing subaccount id; when creating a new subaccount (subaccount omitted or 0) a non-zero manager_id is required. Repeated calls return the same cached address and keep it alive, while new registrations are rate limited per rolling window; unused addresses are dropped after 7 days.
# private/cancel
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel
/openapi.json post /private/cancel
Cancels one resting order identified by order_id, subaccount_id and instrument_name. Requires any trade scope on the session; returns the cancelled order with its final status.
# private/cancel_algo_order
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_algo_order
/openapi.json post /private/cancel_algo_order
Cancels a single active algo order identified by order_id and subaccount_id, stopping any further child-order execution. Requires any trade scope; returns the cancelled order.
# private/cancel_all
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_all
/openapi.json post /private/cancel_all
Cancels every open order on the given subaccount. Optional cancel_trigger_orders and cancel_algo_orders flags additionally clear the subaccount's trigger and algo orders in the same call. Requires any trade scope; returns "ok" on success.
# private/cancel_all_algo_orders
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_all_algo_orders
/openapi.json post /private/cancel_all_algo_orders
Cancels every active algo order on the given subaccount. Requires any trade scope; returns "ok" on success.
# private/cancel_all_trigger_orders
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_all_trigger_orders
/openapi.json post /private/cancel_all_trigger_orders
Cancels every pending trigger order on the given subaccount. Requires any trade scope; returns "ok" on success.
# private/cancel_by_instrument
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_by_instrument
/openapi.json post /private/cancel_by_instrument
Cancels every open order for a subaccount on a single instrument. Requires any trade scope; returns the number of orders cancelled.
# private/cancel_by_label
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_by_label
/openapi.json post /private/cancel_by_label
Cancels a subaccount's open orders that carry the given client label, optionally scoped to a single instrument_name. When an instrument is supplied the call is acknowledged with the number of orders cancelled; without one it runs as a bulk fire-and-forget cancel and returns a -1 sentinel count. Requires any trade scope.
# private/cancel_by_nonce
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_by_nonce
/openapi.json post /private/cancel_by_nonce
Cancels a single order identified by its signing nonce, instrument_name and subaccount_id. The owning wallet is taken from the authenticated session rather than the request. Requires any trade scope; returns the cancelled order.
# private/cancel_trigger_order
Source: https://docs.derive.xyz/api-reference/orderbook/privatecancel_trigger_order
/openapi.json post /private/cancel_trigger_order
Cancels a single pending trigger order identified by order_id and subaccount_id. Requires any trade scope; returns the cancelled order.
# private/get_algo_orders
Source: https://docs.derive.xyz/api-reference/orderbook/privateget_algo_orders
/openapi.json post /private/get_algo_orders
Returns all active algo orders (e.g. time-sliced execution orders) for the given subaccount as a flat list. Read-only query.
# private/get_open_orders
Source: https://docs.derive.xyz/api-reference/orderbook/privateget_open_orders
/openapi.json post /private/get_open_orders
Returns all currently open orders for the given subaccount, including each order's instrument, direction, prices, amounts and status. Only orders resting on the order book are returned: pending trigger orders that have not yet fired are available from get_trigger_orders, and active algo orders from get_algo_orders. Once a trigger order fires, the resulting order appears here if it rests on the book rather than filling immediately; algo orders execute as IOC slices and so never rest here. Read-only query.
# private/get_order
Source: https://docs.derive.xyz/api-reference/orderbook/privateget_order
/openapi.json post /private/get_order
Returns one order (active or completed) by order_id and subaccount_id; the subaccount filter enforces ownership. Returns the order or an order-does-not-exist error.
# private/get_trigger_orders
Source: https://docs.derive.xyz/api-reference/orderbook/privateget_trigger_orders
/openapi.json post /private/get_trigger_orders
Returns all pending trigger (conditional) orders for the given subaccount that have not yet fired. Read-only query.
# private/order
Source: https://docs.derive.xyz/api-reference/orderbook/privateorder
/openapi.json post /private/order
Submits a signed limit or market order for a subaccount, specifying instrument, direction, amount, limit price and max fee, plus optional flags like time-in-force, reduce-only, post-only and MMP. The same endpoint also creates trigger orders (via trigger_type/trigger_price) and algo orders (via algo_type), though the two cannot be combined in one request. Requires a signed order payload and Orderbook trade scope for the instrument's asset; returns the created order with its assigned id and current status.
# private/order_debug
Source: https://docs.derive.xyz/api-reference/orderbook/privateorder_debug
/openapi.json post /private/order_debug
Takes the same params as private/order and rebuilds the order Action without executing anything, returning the EIP-712 encoded_data, encoded_data_hashed, action_hash, typed_data_hash, domain_separator, action_typehash, module, owner and expected_signer, plus the decoded order action data. Byte-compare these against your local computation to find why a signature is rejected. Requires a logged-in session; no trade scope needed.
# private/order_quote
Source: https://docs.derive.xyz/api-reference/orderbook/privateorder_quote
/openapi.json post /private/order_quote
Dry-run pricing helper that estimates the outcome of a prospective order without placing it, returning projected fill price and amount, fees, pre/post initial margin, realized PnL, resulting order status and (where relevant) liquidation price and max tradable amount. All monetary values are returned as decimal strings. The private variant requires account-level authentication.
# private/replace
Source: https://docs.derive.xyz/api-reference/orderbook/privatereplace
/openapi.json post /private/replace
Atomically cancels a resting order (identified by order_id_to_cancel or nonce_to_cancel) and submits a replacement order in a single request. The payload is a full new-order specification plus the cancel target and an optional expected_filled_amount guard. Requires a signed order payload and Orderbook trade scope; returns both the cancelled and the newly created order.
# public/order_quote
Source: https://docs.derive.xyz/api-reference/orderbook/publicorder_quote
/openapi.json post /public/order_quote
Public dry-run variant of order_quote that estimates fill price, fees, margin impact, projected order status and realized PnL for a prospective order without placing it, returning all values as decimal strings. Unlike private/order_quote it bypasses session scope checks, so it can be used to preview costs without account authentication.
# private/get_subaccount_value_history
Source: https://docs.derive.xyz/api-reference/other/privateget_subaccount_value_history
/openapi.json post /private/get_subaccount_value_history
# private/liquidate
Source: https://docs.derive.xyz/api-reference/other/privateliquidate
/openapi.json post /private/liquidate
# public/get_margin
Source: https://docs.derive.xyz/api-reference/other/publicget_margin
/openapi.json post /public/get_margin
# public/set_socialization_feed_data
Source: https://docs.derive.xyz/api-reference/other/publicset_socialization_feed_data
/openapi.json post /public/set_socialization_feed_data
# public/start_auction
Source: https://docs.derive.xyz/api-reference/other/publicstart_auction
/openapi.json post /public/start_auction
# public/get_all_referral_codes
Source: https://docs.derive.xyz/api-reference/referrals/publicget_all_referral_codes
/openapi.json post /public/get_all_referral_codes
Returns every registered referral code along with the owner's wallet address and, when configured, the wallet that receives referral rewards. Takes no parameters.
# public/get_referral_performance
Source: https://docs.derive.xyz/api-reference/referrals/publicget_referral_performance
/openapi.json post /public/get_referral_performance
Returns broker-program referral performance for a referral code, identified either by the code itself or by the referrer's wallet, over a requested time window (start_ms to end_ms, capped at 28 days). The response includes total notional volume, referred fees, fee rewards, builder fees collected, the applicable fee-share percentage, and a per-role/currency/instrument-type breakdown, all as decimal strings.
# private/cancel_batch_quotes
Source: https://docs.derive.xyz/api-reference/rfq/privatecancel_batch_quotes
/openapi.json post /private/cancel_batch_quotes
Cancels all open quotes on a subaccount that match the supplied filters (any combination of quote_id, rfq_id, label, and nonce, applied together) in a single atomic operation. Omitting the optional filters targets all of the subaccount's open quotes. Returns the list of cancelled quote ids. Requires an RFQ trade scope.
# private/cancel_batch_rfqs
Source: https://docs.derive.xyz/api-reference/rfq/privatecancel_batch_rfqs
/openapi.json post /private/cancel_batch_rfqs
Cancels all open RFQs on a subaccount that match the supplied filters (any combination of rfq_id, label, and nonce, applied together), cascade-cancelling their quotes. Omitting the optional filters targets all of the subaccount's open RFQs. Returns the set of cancelled RFQs. Requires an RFQ trade scope.
# private/cancel_quote
Source: https://docs.derive.xyz/api-reference/rfq/privatecancel_quote
/openapi.json post /private/cancel_quote
Cancels one open quote owned by the subaccount, identified by quote_id (optionally further constrained by rfq_id, label, or nonce). Returns the cancelled quote, or a quote-not-found error if it does not exist or is no longer open. Requires an RFQ trade scope.
# private/cancel_rfq
Source: https://docs.derive.xyz/api-reference/rfq/privatecancel_rfq
/openapi.json post /private/cancel_rfq
Cancels one open RFQ owned by the given subaccount and cascade-cancels any quotes makers have submitted against it, notifying those makers. Identify the RFQ by its rfq_id. Returns "ok" on success. Requires an RFQ trade scope.
# private/execute_quote
Source: https://docs.derive.xyz/api-reference/rfq/privateexecute_quote
/openapi.json post /private/execute_quote
Taker-side call that accepts a specific maker quote and settles the trade atomically. You reference the rfq_id and quote_id and supply the taker's priced legs, direction, max fee, and EIP-712 signature (signer, signature, nonce, expiry), with an optional taker-protection flag. The signature is verified and the taker fill, maker quote, and RFQ are updated together in one atomic step. Requires trade scope for every instrument in the legs.
# private/get_quotes
Source: https://docs.derive.xyz/api-reference/rfq/privateget_quotes
/openapi.json post /private/get_quotes
Returns a paginated, merged view of a subaccount's quotes, combining currently open quotes with the archived history of filled, cancelled, and expired ones. Supports filtering by quote_id, rfq_id, status, and a from/to timestamp window, with page and page_size controls. Each entry includes priced legs, direction, fees, liquidity role, and fill percentage.
# private/get_rfqs
Source: https://docs.derive.xyz/api-reference/rfq/privateget_rfqs
/openapi.json post /private/get_rfqs
Returns a paginated, merged view of a subaccount's RFQs, combining currently open RFQs with the archived history of filled, cancelled, and expired ones. Supports filtering by rfq_id, status, and a from/to timestamp window, with page and page_size controls. Each entry includes legs, status, timestamps, cost bounds, and fill percentage.
# private/poll_quotes
Source: https://docs.derive.xyz/api-reference/rfq/privatepoll_quotes
/openapi.json post /private/poll_quotes
Taker-side call that lists the maker quotes received against RFQs owned by the subaccount. Supports filtering by quote_id, rfq_id, status, and a timestamp window, with pagination. Returns public quote views (including the maker's wallet) without any signing material.
# private/poll_rfqs
Source: https://docs.derive.xyz/api-reference/rfq/privatepoll_rfqs
/openapi.json post /private/poll_rfqs
Maker-side call that lists the RFQs a subaccount is eligible to quote, i.e. RFQs that are open to all makers or that name the maker's wallet as a counterparty. Supports filtering by rfq_id, status, an originating RFQ subaccount, and a timestamp window, with pagination. Returns public RFQ views without taker-private signing material.
# private/replace_quote
Source: https://docs.derive.xyz/api-reference/rfq/privatereplace_quote
/openapi.json post /private/replace_quote
Maker-side call that atomically cancels an existing quote and submits a new signed quote for the same RFQ in one operation. You provide the new priced legs, direction, max fee, and EIP-712 signature, plus the quote to cancel (by quote_id or nonce_to_cancel). Returns the cancellation result together with the newly created quote. Requires trade scope for every instrument quoted.
# private/rfq_get_best_quote
Source: https://docs.derive.xyz/api-reference/rfq/privaterfq_get_best_quote
/openapi.json post /private/rfq_get_best_quote
Taker-side dry run that evaluates a prospective RFQ (its legs and intended direction) without creating anything. Returns the best available maker quote if one exists, along with the expected fee, validity, projected liquidation prices, and realized-PnL estimate for taking it.
# private/send_quote
Source: https://docs.derive.xyz/api-reference/rfq/privatesend_quote
/openapi.json post /private/send_quote
Maker-side call that submits a priced, signed quote in response to an existing RFQ. You provide the priced legs, direction, max fee, an EIP-712 signature (signer, signature, nonce, expiry), and optional label or MMP flag. The quote's signature is verified and validated against the referenced RFQ before it is stored. Returns the created quote. Requires trade scope for every instrument quoted.
# private/send_rfq
Source: https://docs.derive.xyz/api-reference/rfq/privatesend_rfq
/openapi.json post /private/send_rfq
Taker-side call that opens a new RFQ for a multi-leg structure on a subaccount, inviting makers to quote it. You supply the legs (instrument, amount, and direction per leg) plus optional limits such as min/max total cost, a partial-fill step, a label, and a list of specific counterparties to restrict who can see it. Returns the created RFQ including its assigned id and validity window. Requires trade scope for every instrument in the legs.
# public/execute_quote_debug
Source: https://docs.derive.xyz/api-reference/rfq/publicexecute_quote_debug
/openapi.json post /public/execute_quote_debug
Public signing-preview helper that returns the taker's EIP-712 encoding artifacts (encoded data and hashes) plus the maker-side encoded legs and legs hash for a quote execution a client is about to sign. It does not verify signatures, persist anything, or execute the trade; use it to construct and check the exact payload before calling private/execute_quote.
# public/send_quote_debug
Source: https://docs.derive.xyz/api-reference/rfq/publicsend_quote_debug
/openapi.json post /public/send_quote_debug
Public signing-preview helper that returns the EIP-712 encoding artifacts (encoded data, its hash, the action hash, and the typed-data hash) for a maker quote a client is about to sign. It does not verify signatures, persist anything, or place a quote; use it to construct and check the exact payload to sign before calling private/send_quote.
# private/edit_session_key
Source: https://docs.derive.xyz/api-reference/session-keys/privateedit_session_key
/openapi.json post /private/edit_session_key
Updates an existing session key's label, IP allowlist, and/or off-chain scopes for a given wallet; it cannot change on-chain (protocol) scopes, for which you must re-register with private/set_session_key. Editing only the label needs account-info permission, while changing the IP allowlist or off-chain scopes requires admin/owner authorization. Returns the updated session key details.
# private/session_keys
Source: https://docs.derive.xyz/api-reference/session-keys/privatesession_keys
/openapi.json post /private/session_keys
Returns every session key registered to the given wallet, including expired and not-yet-activated keys, each with its public address, scopes, expiry, label, IP allowlist, and permitted subaccounts.
# private/set_session_key
Source: https://docs.derive.xyz/api-reference/session-keys/privateset_session_key
/openapi.json post /private/set_session_key
Authorizes a new session key for a wallet from a signed action, granting it a set of on-chain (protocol) scopes and off-chain scopes with an expiry, an optional label, an optional IP allowlist, and an optional list of subaccounts it may act on (defaults to all of the wallet's subaccounts). Send the signed action fields (nonce, signer, signature, signature expiry, and module) alongside the requested scopes; the endpoint returns the registered key's public address and its granted scopes, expiry, allowlist, label, and subaccounts.
# public/get_wallets_from_session_key
Source: https://docs.derive.xyz/api-reference/session-keys/publicget_wallets_from_session_key
/openapi.json post /public/get_wallets_from_session_key
Public lookup that returns the wallet addresses a given session key is registered to, sorted by expiry, with expired keys omitted. An optional scope filter narrows results to keys holding that off-chain scope; returns an error if no matching, unexpired keys exist.
# private/change_subaccount_label
Source: https://docs.derive.xyz/api-reference/subaccounts/privatechange_subaccount_label
/openapi.json post /private/change_subaccount_label
Updates the human-readable label for a subaccount. The label must be at most 16 characters, otherwise an invalid-params error is returned. Requires a signed session key with account-info scope; on success it echoes back the subaccount ID and the new label.
# private/delete_subaccount
Source: https://docs.derive.xyz/api-reference/subaccounts/privatedelete_subaccount
/openapi.json post /private/delete_subaccount
Permanently deletes one of the caller's subaccounts via a signed action. The subaccount must be fully wound down: no balances or debt, no open orders, RFQs, or quotes, not a vault, not under auction, and not the wallet's fallback subaccount. Deletion never moves value; subaccount ids are never reused. This is an owner-or-admin operation (session keys need the admin scope).
# private/get_all_portfolios
Source: https://docs.derive.xyz/api-reference/subaccounts/privateget_all_portfolios
/openapi.json post /private/get_all_portfolios
Returns the complete portfolio (same shape as get_subaccount) for every subaccount owned by the given wallet, including valuations, margin figures, open orders, positions, and collaterals. If an individual subaccount's portfolio cannot be built, it is returned as a placeholder entry with failed_to_fetch set to true rather than failing the whole request.
# private/get_collaterals
Source: https://docs.derive.xyz/api-reference/subaccounts/privateget_collaterals
/openapi.json post /private/get_collaterals
Returns the subaccount ID and the list of its collateral balances, including each asset's amount and current value valued against live feed data. A lighter alternative to get_subaccount when only collateral holdings are needed.
# private/get_margin
Source: https://docs.derive.xyz/api-reference/subaccounts/privateget_margin
/openapi.json post /private/get_margin
Calculates net initial and maintenance margin for a subaccount before and after an optional simulated state change (position and/or collateral deltas), and whether that change would pass the margin requirement. Values are net margin — mark-to-market value minus the requirement — so positive means healthy. Does not take open-order margin into account.
# private/get_positions
Source: https://docs.derive.xyz/api-reference/subaccounts/privateget_positions
/openapi.json post /private/get_positions
Returns the subaccount ID and the list of its active positions, including size, mark price, average price, unrealized and realized PnL, and Greeks where applicable, valued against live feed data at request time. A lighter alternative to get_subaccount when only positions are needed.
# private/get_subaccount
Source: https://docs.derive.xyz/api-reference/subaccounts/privateget_subaccount
/openapi.json post /private/get_subaccount
Returns the complete portfolio for a single subaccount: its label, manager and risk-universe identifiers, margin type and liquidation status, aggregate valuation and margin figures (positions, collaterals, initial and maintenance margin, open-order margin) as decimal strings, plus the full lists of open orders, positions, and collateral balances. Margin, mark price, and Greek values are computed from live feed data at request time.
# private/get_subaccounts
Source: https://docs.derive.xyz/api-reference/subaccounts/privateget_subaccounts
/openapi.json post /private/get_subaccounts
Returns the wallet address and the sorted list of subaccount IDs owned by that wallet. Yields an empty list if the wallet has no subaccounts.
# public/margin_watch
Source: https://docs.derive.xyz/api-reference/subaccounts/publicmargin_watch
/openapi.json post /public/margin_watch
Calculates the mark-to-market value and initial/maintenance margin for a given subaccount, with per-position and per-collateral breakdowns, computed from live feed data at request time. Margins are reported on the margin basis actually in effect: when a delayed-liquidation override is active the response's is_delayed_liquidation flag is true and the reported margins use the temporarily lowered requirements. A maintenance margin below zero means the subaccount is flagged for liquidation.
# public/decode_action
Source: https://docs.derive.xyz/api-reference/system/publicdecode_action
/openapi.json post /public/decode_action
Runs a hex-encoded Action.data payload through the exchange's own ABI decoder and returns either the decoded fields or the exact reason the bytes were rejected, naming the 32-byte word and the field that broke. Reach for this first when a request fails signature verification (14014): every other route encodes Action.data server-side, so a client whose encoder emits a bad word never sees a decode error - the hashes simply disagree. This is the only route a client's own bytes reach the decoder. Needs no auth and executes nothing.
# public/get_time
Source: https://docs.derive.xyz/api-reference/system/publicget_time
/openapi.json post /public/get_time
Returns the current server time in milliseconds since the UNIX epoch. Takes no parameters and requires no authentication. Use it to align the timestamps and nonces bound into signed actions with the server clock.
# public/get_transaction
Source: https://docs.derive.xyz/api-reference/system/publicget_transaction
/openapi.json post /public/get_transaction
Looks up the settlement lifecycle of a previously submitted operation by its operation UUID (op_uuid). Returns the operation's serialized data along with its batch status, settlement transaction hash, and any error log; the status is null until the operation has been picked up for settlement.
# public/getRateLimits
Source: https://docs.derive.xyz/api-reference/system/publicgetratelimits
/openapi.json post /public/getRateLimits
Returns the caller's current remaining rate-limit allowances for the active connection, broken out into matching-request, non-matching-request, and per-endpoint buckets. On WebSocket connections it also includes the remaining connection allowance.
# private/transfer_positions
Source: https://docs.derive.xyz/api-reference/transfers-&-withdrawals/privatetransfer_positions
/openapi.json post /private/transfer_positions
Atomically transfers one or more derivative positions between a maker and a taker subaccount using matched, signed transfer quotes. Each side supplies its subaccount, signer, nonce, signature, expiry, max fee, direction, and priced legs (instrument, amount, price); the two quotes must mirror each other. Requires a session key with transfer permission and returns the resulting operation details.
# private/transfer_spot
Source: https://docs.derive.xyz/api-reference/transfers-&-withdrawals/privatetransfer_spot
/openapi.json post /private/transfer_spot
Submits a signed transfer of a single spot asset from one subaccount to another subaccount you own. You specify the source and destination subaccounts (or set new_subaccount_manager to create a new destination subaccount under a manager), the asset and its sub_id, the amount, a nonce, signer, signature with expiry, and the maximum USD sequencer fee. Requires a session key with a transfer permission and returns the operation id and uuid.
# private/transfer_spot_external
Source: https://docs.derive.xyz/api-reference/transfers-&-withdrawals/privatetransfer_spot_external
/openapi.json post /private/transfer_spot_external
Submits a signed transfer of a single spot asset to a subaccount belonging to a different owner. Alongside the standard transfer fields (asset, sub_id, amount, nonce, signer, signature, expiry, max USD fee) you give the recipient's wallet address and either an existing destination subaccount or 0 to create a new one under new_subaccount_manager; the max fee must cover both the transfer and any subaccount-creation cost. Requires a session key permitted to transfer to a different owner and returns the operation id and uuid.
# private/update_whitelisted_recipients
Source: https://docs.derive.xyz/api-reference/transfers-&-withdrawals/privateupdate_whitelisted_recipients
/openapi.json post /private/update_whitelisted_recipients
Adds and/or removes recipient wallet addresses on an account's transfer whitelist via a signed, wallet-level action. Provide the owner wallet, signer, nonce, signature with expiry, and the add and remove address lists. This is an owner-or-admin operation (session keys need the admin scope); it returns the operation id, uuid, and the full whitelist after the update is applied.
# private/withdraw
Source: https://docs.derive.xyz/api-reference/transfers-&-withdrawals/privatewithdraw
/openapi.json post /private/withdraw
Submits a signed request to withdraw a spot asset out of a subaccount. You provide the subaccount id, asset name, amount in underlying units, a nonce, the signer, an EIP-712 signature with its expiry, and the maximum sequencer fee (in USD) you authorise; setting force_batch controls whether the withdrawal is batched. Requires a session key with withdraw permission, and returns the accepted operation id and its uuid for tracking.
# public/withdraw_debug
Source: https://docs.derive.xyz/api-reference/transfers-&-withdrawals/publicwithdraw_debug
/openapi.json post /public/withdraw_debug
Dry-run helper that returns the EIP-712 typed data and hashes that would be computed for the given withdrawal parameters, so clients can verify their own signing and hashing before submitting. Takes the same inputs as private/withdraw (subaccount, signer, nonce, amount, max fee, expiry, asset) but performs no state change and needs no signature.
# private/burn_vault_shares
Source: https://docs.derive.xyz/api-reference/vault-curators/privateburn_vault_shares
/openapi.json post /private/burn_vault_shares
Curator-only endpoint that settles a pending withdraw request by signing a burn approval at a quoted share price (USD per share). Takes the request id and the user's withdraw-action hash, burns the shares, and returns the settlement result. Requires the curator mint-and-burn permission.
# private/create_vault
Source: https://docs.derive.xyz/api-reference/vault-curators/privatecreate_vault
/openapi.json post /private/create_vault
Registers a new vault on-chain from a signed action; the signing wallet becomes the vault's curator and seeds the initial deposit from its funding subaccount. Inputs set the deposit asset, initial deposit amount, initial share price, management/performance fee rates (in basis points), max slippage, redemption cooldown, an optional benchmark asset for the high-water mark, and the max sequencer fee authorized. Requires the vault curator-create permission.
# private/force_burn
Source: https://docs.derive.xyz/api-reference/vault-curators/privateforce_burn
/openapi.json post /private/force_burn
Curator-signed endpoint that builds an on-chain action to forcibly redeem a given holder's entire share balance at the current mark-to-market share price. Inputs are the vault subaccount the curator signs on, the EIP-712 envelope (nonce, signer, signature, expiry), and the holder's wallet address. Requires the curator mint-and-burn permission; the protocol re-verifies the curator signature.
# private/get_curated_vaults
Source: https://docs.derive.xyz/api-reference/vault-curators/privateget_curated_vaults
/openapi.json post /private/get_curated_vaults
Returns the subaccount ids of the vaults curated by the given wallet. The wallet parameter must match the authenticated connection. Read-only.
# private/get_live_burn_requests
Source: https://docs.derive.xyz/api-reference/vault-curators/privateget_live_burn_requests
/openapi.json post /private/get_live_burn_requests
Curator-only endpoint that returns a FIFO page of a vault's pending withdraw (burn) requests. Inputs are the vault subaccount and a page limit. Requires the curator mint-and-burn permission.
# private/get_live_mint_requests
Source: https://docs.derive.xyz/api-reference/vault-curators/privateget_live_mint_requests
/openapi.json post /private/get_live_mint_requests
Curator-only endpoint that returns a FIFO page of a vault's pending deposit (mint) requests. Inputs are the vault subaccount and a page limit. Requires the curator mint-and-burn permission.
# private/mint_vault_shares
Source: https://docs.derive.xyz/api-reference/vault-curators/privatemint_vault_shares
/openapi.json post /private/mint_vault_shares
Curator-only endpoint that settles a pending deposit request by signing a mint approval at a quoted share price (USD per share). Takes the request id and the user's deposit-action hash, mints the corresponding shares, and returns the settlement result. Requires the curator mint-and-burn permission.
# private/reject_deposit_request
Source: https://docs.derive.xyz/api-reference/vault-curators/privatereject_deposit_request
/openapi.json post /private/reject_deposit_request
Curator-only endpoint that removes a queued deposit request off-chain (no on-chain settlement), recording the rejection with an optional short reason. Takes the request id and returns an acknowledgement. Requires the curator mint-and-burn permission.
# private/update_vault_info
Source: https://docs.derive.xyz/api-reference/vault-curators/privateupdate_vault_info
/openapi.json post /private/update_vault_info
Applies an off-chain patch to a vault the caller curates, updating any of its display name, description, advisory mark-to-market cap (a USD decimal), or whitelist-only flag. Only the fields supplied are changed. The caller must own the vault's subaccount (i.e. be its curator); description length is capped.
# private/cancel_all_vault_requests
Source: https://docs.derive.xyz/api-reference/vault-shareholders/privatecancel_all_vault_requests
/openapi.json post /private/cancel_all_vault_requests
Submits a signed cancel action that drains all of the caller's pending deposit and withdraw requests for a given vault and posts the corresponding on-chain operation. Input is the vault subaccount; any subaccount the caller owns may sign. Requires the user-cancel permission.
# private/get_live_vault_requests
Source: https://docs.derive.xyz/api-reference/vault-shareholders/privateget_live_vault_requests
/openapi.json post /private/get_live_vault_requests
Returns the caller's currently-pending vault deposit and withdraw requests, read live from the vault queue. Not paginated (the live queue is bounded); settled and terminal history is served by get_vault_request_history. The wallet parameter must match the authenticated connection.
# private/get_shareholder_vaults
Source: https://docs.derive.xyz/api-reference/vault-shareholders/privateget_shareholder_vaults
/openapi.json post /private/get_shareholder_vaults
Returns the subaccount ids of the vaults in which the given wallet holds shares. The wallet parameter must match the authenticated connection. Read-only.
# private/get_vault_request_history
Source: https://docs.derive.xyz/api-reference/vault-shareholders/privateget_vault_request_history
/openapi.json post /private/get_vault_request_history
Returns the caller's full vault action history (deposits, withdrawals, force-withdrawals, and cancels) across every status — enqueued, requested, applied, cancelled, rejected, or expired — with one row per action at its latest state. Paginated by page and page_size; monetary amounts, prices, and share counts are decimal strings. The wallet parameter must match the authenticated connection.
# private/get_vault_shares
Source: https://docs.derive.xyz/api-reference/vault-shareholders/privateget_vault_shares
/openapi.json post /private/get_vault_shares
Returns the caller's share balance for every vault it holds shares in, each paired with the full enriched vault row (the same shape as public/get_vault). The wallet parameter must match the authenticated connection. Read-only.
# private/request_vault_deposit
Source: https://docs.derive.xyz/api-reference/vault-shareholders/privaterequest_vault_deposit
/openapi.json post /private/request_vault_deposit
Submits a signed deposit action from the user's source subaccount and enqueues it in the vault's pending-deposit queue for the curator to settle. Inputs are the target vault subaccount, the deposit asset, and the amount as a decimal string. Returns an acknowledgement with the queued request; requires the user-deposit permission.
# private/request_vault_withdraw
Source: https://docs.derive.xyz/api-reference/vault-shareholders/privaterequest_vault_withdraw
/openapi.json post /private/request_vault_withdraw
Submits a signed withdraw action that enqueues a request to burn a given number of vault shares and redeem the proceeds to the user's subaccount. Inputs are the vault subaccount and the share quantity to burn (decimal string). Returns an acknowledgement with the queued request; requires the user-withdraw permission.
# public/get_vault
Source: https://docs.derive.xyz/api-reference/vault-shareholders/publicget_vault
/openapi.json post /public/get_vault
Returns the full vault record — on-chain state plus curator metadata — for one vault subaccount id. Unauthenticated and read-only.
# public/get_vault_action_history
Source: https://docs.derive.xyz/api-reference/vault-shareholders/publicget_vault_action_history
/openapi.json post /public/get_vault_action_history
Returns a vault's finalized deposit, withdrawal, fee-accrual, and cancel events — including NAV, share price, high-water mark, and the fee-share split across management, performance, curator, and protocol — at the vault level (per-holder position details are omitted). Inputs are the vault subaccount, an optional event_type filter, and pagination. Amounts and prices are decimal strings. Unauthenticated.
# public/get_vault_performance_history
Source: https://docs.derive.xyz/api-reference/vault-shareholders/publicget_vault_performance_history
/openapi.json post /public/get_vault_performance_history
Returns a time series of a vault's mark-to-market performance (NAV, share price, total and curator shares, high-water mark, optional benchmark) sampled hourly and downsampled to the requested resolution (1h, 8h, 24h, or 1wk). Supports optional from/to unix-second bounds and a limit (newest first, default 1000, capped at 10000). Values are decimal strings, with live prices nullable. Unauthenticated.
# public/get_vaults
Source: https://docs.derive.xyz/api-reference/vault-shareholders/publicget_vaults
/openapi.json post /public/get_vaults
Returns every vault in the system, each paired with its subaccount id, paginated by page and page_size. Unauthenticated and read-only.
# Access Scopes
Source: https://docs.derive.xyz/authentication/access-scopes
The two scope sets that bound what a session key may do.
Every [session key](/authentication/session-keys) carries **two independent scope sets**, enforced by two different layers:
* **Protocol scopes** — on-chain authority. Signed into each action and re-validated by the protocol state machine. These decide what state-changing actions the key can authorize.
* **Off-chain scopes** — server-side capabilities. Never signed, never seen by the protocol; enforced only by the server before a request is processed.
All of the steps in this guide can be done through the UX or SDKs.
## Protocol scopes
Protocol scopes form a **tree**. A grant for a branch covers everything under it, so `trade:all` covers `trade:orderbook:all` and `trade:rfq:option` alike, and `all` at any level covers its children. A request is checked by asking whether one of the key's grants *allows* the specific scope the action requires.
## `admin` and `owner` special powers
These are actions only the `admin` or `owner` wallet can do:
* can call `private/update_whitelisted_recipients` to add or remove whitelisted recipients for external transfers and withdrawals.
* can withdraw and transfer to any recipient without setting a whitelist recipient.
* can modify the ip whitelist for a session key
## Off-chain scopes
Off-chain scopes are exact-match only — no tree, no hierarchy.
| Wire string | Grants |
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `account_info` | Read-tier account capability. Gates `private/change_subaccount_label` and a label-only `private/edit_session_key`. |
## How scopes are set
Creator must specify the exact scopes for a session key. A session key with no scopes is a purely read-only session key.
The "owner" wallet by default has all scopes.
See [Session keys](/authentication/session-keys) for the full create / edit / list lifecycle and
[Authentication](/authentication/session-login) for how a session resolves to protocol and off-chain scopes. Amounts
and constants referenced by signed actions live on [Action signing](/authentication/action-signing).
```typescript TypeScript (SDK) theme={null}
import { Wallet } from 'ethers';
import {
DeriveClient,
OffchainScope,
ProtocolScopeCode,
} from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'testnet',
wallet: process.env.PRIVATE_KEY!,
});
// set_session_key is an action-signed private call: open and authenticate first.
await client.connect();
await client.login();
// Generate the key locally — only its address is registered.
const sessionKey = Wallet.createRandom();
const created = await client.sessionKeys.set({
publicSessionKey: sessionKey,
expirySec: Math.floor(Date.now() / 1000) + 30 * 24 * 3600, // the KEY's lifetime: 30 days
protocolScopes: [
ProtocolScopeCode.TradeOrderbookAll,
ProtocolScopeCode.TradeRfqOption,
],
offchainScopes: [OffchainScope.AccountInfo],
label: 'trading-bot',
});
// The ack echoes exactly what the key holds — there is no implicit default:
console.log(created.protocol_scopes); // ['trade:orderbook:all', 'trade:rfq:option']
console.log(created.offchain_scopes); // ['account_info']
```
```python Python (SDK) theme={null}
import asyncio
import time
from eth_account import Account
from derive_py import WebSocketClient
from derive_py.data_types import OffchainScope, ProtocolScope
async def main():
client = WebSocketClient.from_env()
# set_session_key is an action-signed private call: connect first.
await client.connect()
# Generate the key locally — only its address is registered.
session_wallet = Account.create()
created = await client.account.set_session_key(
public_session_key=session_wallet.address,
expiry_sec=int(time.time()) + 30 * 24 * 3600, # the KEY's lifetime: 30 days
protocol_scopes=[
ProtocolScope.TRADE_ORDERBOOK_ALL,
ProtocolScope.TRADE_RFQ_OPTION,
],
offchain_scopes=[OffchainScope.ACCOUNT_INFO],
label="trading-bot",
)
# The ack echoes exactly what the key holds — there is no implicit default:
print(created.protocol_scopes) # ['trade:orderbook:all', 'trade:rfq:option']
print(created.offchain_scopes) # ['account_info']
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use alloy::signers::local::PrivateKeySigner;
use derive_rs::{
Environment, WsClient,
actions::session_key::{OffChainScope, ProtocolScope, SetSessionKeyArgs},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Testnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
// set_session_key is an action-signed private call: authenticate first.
client.login().await?;
// Generate the key locally — only its address is registered.
let session_signer = PrivateKeySigner::random();
let expiry_sec = chrono::Utc::now().timestamp() as u64 + 30 * 24 * 3600;
let args = SetSessionKeyArgs::builder()
.public_session_key(session_signer.address().to_string())
.expiry_second(expiry_sec) // the KEY's lifetime: 30 days
.protocol_scopes(vec![
ProtocolScope::TradeOrderbookAll,
ProtocolScope::TradeRfqOption,
])
.off_chain_scopes(vec![OffChainScope::AccountInfo])
.label("trading-bot".to_string())
.subaccount_ids(vec![9])
.build();
let created = client.session_keys().create(args).await?;
// The ack echoes exactly what the key holds — there is no implicit default:
println!("{:?}", created.protocol_scopes); // ["trade:orderbook:all", "trade:rfq:option"]
println!("{:?}", created.offchain_scopes); // ["account_info"]
Ok(())
}
```
```bash cURL theme={null}
# Body nonce/signature: EIP-712 action signing under the set-session-key
# module (see /action-signing). X-Derive* headers: session auth (see /json-rpc).
curl -X POST https://api.derive.xyz/v3/private/set_session_key \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"wallet": "0xYourWallet",
"public_session_key": "0xSessionKeyAddress",
"expiry_sec": 1733592000,
"subaccount_ids": null,
"nonce": "1730999700000123000",
"signature_expiry_sec": 1731000600,
"signer": "0xYourWallet",
"signature": "0x…",
"protocol_scopes": ["trade:orderbook:all", "trade:rfq:option"],
"offchain_scopes": ["account_info"],
"label": "trading-bot"
}'
```
# Action Signing
Source: https://docs.derive.xyz/authentication/action-signing
The EIP-712 scheme every state-changing action must carry.
The easiest way to begin signing is to either use the SDKs directly.
* [TypeScript SDK](github.com/derivexyz/derive-ts)
* [Rust SDK](github.com/derivexyz/derive-rs)
* [Python SDK](github.com/derivexyz/derive-py)
Continue reading for a deep dive into the signing scheme, its parameters, and how to debug signature issues.
### Why sign actions?
Every state-changing action — placing an order, transferring
spot, withdrawing, creating a session key — carries its own **EIP-712** signature
over an `Action` struct. This ensures the protocol is self-custodial.
Action signing is **not** [session login](/authentication/session-login).
Login (EIP-191) authenticates a connection; it does not authorize actions.
Each action is signed and verified on its own. Keep the two layers distinct.
```jsonc theme={null}
{
"subaccount_id": 9,
"nonce": 1720512000000123456, // ms timestamp * 1e6 + random suffix
"module": "0xB8D2...5e7b", // TRADE_MODULE
"expiry": 1720512300, // unix seconds
"owner": "0xYourWallet",
"signer": "0xYourWallet", // or a session key
"data": { // ABI-encoded into `encoded_data`
"asset": "0xETHOptionAsset", // base_asset_address of the instrument
"sub_id": "39614082545622357598554435968", // packed expiry/strike/is_call; 0 for perps
"limit_price": "310e18",
"amount": "1e18",
"max_fee": "0.01e18",
"recipient_id": 9,
"is_bid": true
}
}
```
The seven `data` words are hashed into the `Action` struct, which is hashed
with the domain separator into the digest `signer` signs.
## Worked example: a buy order
An option buy order signed by the wallet directly (`owner == signer`). The SDK
ABI-encodes the payload, builds and signs the EIP-712 digest, and submits it in a
single call — amounts and prices are plain decimals, with e18 scaling handled
internally. The last tab does the same thing without an SDK, one step at a time:
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// Signed by the wallet key held in the client (owner == signer). The nonce,
// signature, and signature_expiry_sec are generated and signed for you.
const { order } = await client.orders.place({
subaccountId: 9,
instrumentName: 'ETH-20260626-3000-C',
direction: 'buy',
amount: '1',
limitPrice: '310',
maxFee: '0.01',
});
console.log(order.order_id);
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
from derive_py.data_types import Direction
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Signed by the key held in the client. The nonce, signature, and
# signature_expiry_sec are generated and signed for you.
response = await client.orders.create(
instrument_name="ETH-20260626-3000-C",
direction=Direction.buy,
amount=Decimal("1"),
limit_price=Decimal("310"),
max_fee=Decimal("0.01"),
)
print(response.order.order_id)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::OrderArgs,
models::{Direction, OrderType, TimeInForce},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
client.login().await?;
// Signed by the wallet key held in the client (owner == signer). The nonce,
// signature, and signature_expiry_sec are generated and signed for you.
let order = OrderArgs::builder()
.instrument_name("ETH-20260626-3000-C".to_string())
.direction(Direction::Buy)
.order_type(OrderType::Limit)
.time_in_force(TimeInForce::Gtc)
.amount(BigDecimal::from(1))
.limit_price(BigDecimal::from(310))
.build();
let response = client.orders().place(order).await?;
println!("{}", response.order.order_id);
Ok(())
}
```
```python Python (manual) theme={null}
# No SDK: every step done by hand. pip install eth-abi eth-account requests
import os, random, time
from decimal import Decimal
import requests
from eth_abi import encode as abi_encode
from eth_account import Account
from eth_account.messages import encode_defunct
from eth_utils import keccak, to_checksum_address
BASE_URL = "https://api.derive.xyz/v3"
CHAIN_ID = 1 # Sepolia (11155111) on testnet
MATCHING = "0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD"
TRADE_MODULE = "0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b"
# keccak("Action(uint256 subaccountId,uint256 nonce,address module,bytes data,
# uint256 expiry,address owner,address signer)") — same on every deployment.
ACTION_TYPEHASH = bytes.fromhex("4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17")
session_key = Account.from_key(os.environ["PRIVATE_KEY"]) # signer
owner = to_checksum_address(os.environ["OWNER"])
subaccount_id = 9
def e18(value: str) -> int:
"""The exchange rejects, not truncates, sub-1e12 precision."""
scaled = int(Decimal(value) * 10**18)
if scaled % 10**6:
raise ValueError("more than 12 decimal places can never verify")
return scaled
# 1. The action signs the asset, not the instrument name.
instrument = requests.post(
f"{BASE_URL}/public/get_instrument",
json={"instrument_name": "ETH-20260626-3000-C"},
).json()["result"]
# 2. Action data: seven static words, decimals e18-scaled.
encoded_data = abi_encode(
["address", "uint256", "int256", "int256", "uint256", "uint256", "bool"],
[
to_checksum_address(instrument["base_asset_address"]),
int(instrument["base_asset_sub_id"]),
e18("310"), # limit_price
e18("1"), # amount
e18("0.01"), # max_fee
subaccount_id, # recipient_id
True, # is_bid
],
)
# 3. EIP-712 struct hash of the Action envelope. Step 5 resends these two
# values verbatim, so they have to be bound before signing.
nonce = int(time.time() * 1000) * 10**6 + random.randrange(10**6) # nonce
expiry_sec = int(time.time()) + 300 # expiry_sec
action_hash = keccak(
abi_encode(
["bytes32", "uint256", "uint256", "address", "bytes32", "uint256", "address", "address"],
[
ACTION_TYPEHASH,
subaccount_id,
nonce,
to_checksum_address(TRADE_MODULE),
keccak(encoded_data),
expiry_sec,
owner,
session_key.address, # signer; pass owner here if the wallet signs directly
],
)
)
# 4. Domain separator, then the digest to sign.
domain_separator = keccak(
abi_encode(
["bytes32", "bytes32", "bytes32", "uint256", "address"],
[
keccak(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak(b"Matching"),
keccak(b"1.0"),
CHAIN_ID,
to_checksum_address(MATCHING),
],
)
)
digest = keccak(b"\x19\x01" + domain_separator + action_hash)
signature = "0x" + Account.unsafe_sign_hash(digest, session_key.key).signature.hex()
# 5. Submit. Wire decimals are formatted from the signed e18 words so the
# exchange re-derives the exact bytes the signature commits to.
login_ts = str(int(time.time() * 1000))
login_sig = Account.sign_message(encode_defunct(text=login_ts), session_key.key).signature
response = requests.post(
f"{BASE_URL}/private/order",
headers={
"X-DeriveWallet": owner,
"X-DeriveTimestamp": login_ts,
"X-DeriveSignature": "0x" + login_sig.hex(),
},
json={
"subaccount_id": subaccount_id,
"instrument_name": "ETH-20260626-3000-C",
"direction": "buy",
"limit_price": "310",
"amount": "1",
"max_fee": "0.01",
"nonce": str(nonce), # decimal string on the wire
"signer": session_key.address,
"signature": signature,
"signature_expiry_sec": expiry_sec,
"order_type": "limit",
"time_in_force": "gtc",
},
).json()
print(response["result"]["order"]["order_id"])
```
The instrument name above is an option, named `---` (perps are `-PERP`).
See [Instrument names](/trading/instrument-names) for the full grammar.
## Nonce and expiry
The `nonce` param (decoded as a UTC timestamp in nanoseconds \* 6-digit suffix) has some special rules depending on the action.
| Action type | Nonce window (relative to server clock) | Must increase? |
| ------------------------------------------------------------------------ | --------------------------------------- | -------------- |
| Signed actions — withdraw, transfer, session key, whitelist, liquidation | ± 1 hour | Yes |
| Orders | 120 days before → 1 hour after | No |
| RFQs | ± 1 hour | No |
| Vault actions | 60 days before → 1 hour after | Yes |
The Action's `expiry`, in unix seconds; rejected once `now > expiry`. The exchange enforces certain minimums depending on the action type to ensure the action is valid long enough.
| Action type | Minimum expiry (from server clock) | Maximum expiry (from server clock) |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------- |
| Orders | 10 seconds | 120 days — or 15 minutes if the order is MMP |
| RFQ quotes — maker quote and taker execute | 60 seconds past the RFQ's expiry (an RFQ is valid for 10 minutes from creation) | 1 day |
| Position transfers | 60 seconds (both sides; the taker's expiry must also cover the maker's) | — |
| Withdraw, spot transfer, external transfer, liquidation, whitelist | None — must simply be unexpired when verified | — |
| Vault actions | None — must simply be unexpired when verified | 30 days |
| Set session key | None on the signature — but the key's own `expiry_sec` must be ≥ 5 minutes | — |
## Debugging signature issues
A rejected signature is almost always one of three things:
* **Bad action data encoding** — your ABI-encoded `encoded_data` bytes are
wrong. Check them with
[`public/decode_action`](/api-reference/system/publicdecode_action).
* **Bad action envelope encoding** — the action data is fine, but something the
signature also commits to (module, nonce, expiry, owner, signer, domain
separator) differs from what the exchange rebuilt. Byte-compare each stage
against the relevant `private/*_debug` routes
(e.g. [`private/order_debug`](/api-reference/orderbook/privateorder_debug)).
* **Wallet issues** — the encoding is right on both counts, but the wrong key
signed, or `signer` does not match the recovered address. This is often because
you passed in the `owner` instead of the `signer`.
## Constants
The `module` field names the contract whose ABI layout `data` follows. Module
addresses are fixed protocol constants — identical across every deployment, safe
to hardcode:
| Action | Module | Address | Purpose |
| ----------------------- | ------------------------------ | -------------------------------------------- | ------------------------------------------------- |
| Order / trade | `TRADE_MODULE` | `0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b` | Place an order |
| Spot transfer | `TRANSFER_MODULE` | `0x01259207A40925b794C8ac320456F7F6c8FE2636` | Move a spot ERC-20 between subaccounts |
| Withdraw | `WITHDRAW_MODULE` | `0x9d0E8f5b25384C7310CB8C6aE32C8fbeb645d083` | Withdraw to L1 |
| RFQ / position transfer | `RFQ_MODULE` | `0x9371352CCef6f5b36EfDFE90942fFE622Ab77F1D` | RFQ quotes; position transfers book as RFQ trades |
| External transfer | `EXTERNAL_TRANSFER_MODULE` | `0x8F9B8f12ddA05FB1F0DDDDe8f5af8cECF54f8aC9` | External spot transfer |
| Whitelisted recipient | `WHITELISTED_RECIPIENT_MODULE` | `0xB86D6DE1b76c9839e4BA860848CD98A1dABd6B54` | Recipient allow-list |
| Liquidation | `LIQUIDATION_MODULE` | `0x66d23e59DaEEF13904eFA2D4B8658aeD05f59a92` | Bid into a Dutch auction |
| Vault | `VAULT_MODULE` | `0x2885c174ebf5524aED9c721d60c12b1537685186` | Vault actions |
| Set session key | `SET_SESSION_KEY_MODULE` | `0xe330CF64ff6EbF41699aad344Cb21d78db1D2bb6` | Register a delegated session key |
### owner vs signer
`owner` is always the wallet that owns the subaccount. `signer` is whoever
produced the signature — the wallet itself (`owner == signer`) or a delegated
[session key](/authentication/session-keys), in which case `signer` is the key's address and
`owner` stays the wallet. The protocol recovers the ECDSA signer and requires it
to equal `signer`; if `signer != owner`, it loads the session key and checks its
scopes cover the action.
# Smart Contracts & Multi-sigs
Source: https://docs.derive.xyz/authentication/contract-owned-accounts
Own and operate a Derive account from a multi-sig or smart contract on the Ethereum L1.
A Derive account is owned by a single Ethereum L1 address — and that address can be a
**multi-sig or any smart contract**, not just an EOA. The contract keeps custody and control
on L1, while a delegated [session key](/authentication/session-keys) does the day-to-day
trading.
All of the steps in this guide can be done through the UX or programmatically.
## Why it works differently
Most of the API is driven by **signed actions**: your wallet produces a single-key EIP-712
signature and the protocol verifies it (see [Action signing](/authentication/action-signing)).
A multi-sig or smart contract has no single private key, so it cannot produce that signature
directly.
Instead, a contract owner operates entirely through **L1 onchain actions** — transactions it
sends to the `OnchainActionManager` contract. The exchange's L1 listener picks each one up and
applies it, authorized by the transaction's `msg.sender`. Two building blocks are enough to run
an account end to end:
Creating and funding the account is already an L1 call — the same for every owner. See
[Programmatic Onboarding](/getting-started/depositing).
Authorize an EOA session key from L1, then let that key sign trades, transfers, and vault
operations offchain.
The account owner is whatever address you pass as `owner` when depositing, and the L1 listener
authorizes onchain actions by the transaction's `msg.sender`. So the **same contract that owns
the account must send the `OnchainActionManager` transactions** — set the session key from the
multi-sig itself, not from an unrelated EOA.
## Set up and operate
The first deposit creates the account and its subaccounts automatically.
Make sure that either
* `owner` address you pass into
`OnchainActionMAnager.depositToNewSubaccount()` is the multi-sig / smart contract, or
* the `wallet` you send to `public/register_deposit_address` is the multi-sig / smart contract.
Deposit through the UX or see [Programmatic Onboarding](/getting-started/depositing) for more information.
An onchain Set Session Key is ignored if the account does not exist yet.
Submit a Set Session Key onchain action from the owner contract. This registers an EOA
session key with the protocol scopes you choose (see [Access scopes](/authentication/access-scopes)).
```solidity Solidity theme={null}
interface IOnchainActionManager {
function submit(uint256 actionType, bytes calldata data) external payable returns (uint256 actionId);
}
uint256 constant SET_SESSION_KEY = 51;
// Protocol scope codes — see /authentication/access-scopes.
uint256 constant SCOPE_TRADE_ALL = 2;
uint256 constant SCOPE_TRANSFER_EXISTING_SUBACCOUNT = 12;
/// Called by the owner contract itself, so msg.sender is the account owner.
function authorizeSessionKey(IOnchainActionManager manager, address sessionKey, uint256 expirySec) external {
// SetSessionKeyActionData — standard `abi.encode(address, uint256,
// uint256[] scopes, uint256[] subaccountIds)`.
uint256[] memory scopes = new uint256[](2);
scopes[0] = SCOPE_TRADE_ALL;
scopes[1] = SCOPE_TRANSFER_EXISTING_SUBACCOUNT;
uint256[] memory subaccountIds = new uint256[](0); // empty = all subaccounts
bytes memory data = abi.encode(sessionKey, expirySec, scopes, subaccountIds);
manager.submit(SET_SESSION_KEY, data);
}
```
```typescript TypeScript (SDK) theme={null}
import { DeriveClient, ProtocolScopeCode } from '@derivexyz/derive-ts';
import { Wallet } from 'ethers';
const client = new DeriveClient({ network: 'mainnet', ownerAddress: MULTISIG_ADDRESS });
// A signer connected to the chain RPC that executes the OnchainActionManager
// transaction as the account owner (an owner EOA, a Safe module, etc.).
const signer = new Wallet(process.env.OWNER_KEY!, provider);
// Register a trading session key via L1.
await client.onchainActions.setSessionKey({
signer,
sessionKey: SESSION_KEY_ADDRESS,
expirySec: Math.floor(Date.now() / 1000) + 30 * 24 * 3600,
scopes: [ProtocolScopeCode.TradeAll, ProtocolScopeCode.TransferExistingSubaccount],
});
```
```python Python (SDK) theme={null}
# Open an issue in derivexyz/derive-py if you'd like this implemented in the SDK.
```
```rust Rust (SDK) theme={null}
// Open an issue in derivexyz/derive-rs if you'd like this implemented in the SDK.
```
From here everything is normal: the session key [logs in](/authentication/session-login) and
signs actions — trading, transfers, and [creating or curating a vault](/vaults/create-a-vault)
— with no further L1 transactions.
When connecting to the UX with the session key you will be prompted to choose between signing in
as the session key or the smart contract.
Remove a key by sending the same action with expiry `0` — the only way to delete a key (the
offchain API has no revoke).
```typescript TypeScript (SDK) theme={null}
// Revoke: a Set Session Key with expiry 0.
await client.onchainActions.revokeSessionKey({ signer, sessionKey: SESSION_KEY_ADDRESS });
```
```solidity Solidity theme={null}
// Revoke = Set Session Key with expiry 0 (deletes the key).
// IOnchainActionManager as declared in the previous step.
function revokeSessionKey(IOnchainActionManager manager, address sessionKey) external {
uint256[] memory empty = new uint256[](0);
bytes memory data = abi.encode(sessionKey, uint256(0), empty, empty); // expiry 0 = delete
manager.submit(51, data);
}
```
## Related
What a session key is, its scopes, and its lifecycle.
The full protocol and off-chain scope catalog to scope a key tightly.
Create and fund the account with an L1 deposit.
Run a vault from the account with the session key.
# MPC Wallets
Source: https://docs.derive.xyz/authentication/mpc-wallets
Restrict what an MPC wallet can do on Derive
Most MPC services (Fordefi, Fireblocks, and similar) can permission over three types of actions:
* **Smart contract calls** — which contract, which function, and often which argument values.
Fordefi expresses these as
[ABI conditions](https://docs.fordefi.com/user-guide/policies/policy-rules-conditions-and-actions#abi),
matching on `address`, integer, boolean, `bytes`, array, and tuple arguments.
* **Personal messages** (`personal_sign`, e.g. EIP-191) — see
[personal messages and messages on non-EVM chains](https://docs.fordefi.com/user-guide/policies/message-policy#personal-messages-and-messages-on-non-evm-chains).
* **Typed data** (EIP-712) — see
[typed data messages](https://docs.fordefi.com/user-guide/policies/message-policy#typed-data-messages), where you can deny signatures that match certain fields of the EIP-712 domain or message.
Derive uses all three paths for different purposes:
| Path | Medium | Scheme | Reaches |
| ---------------------------------------------------- | --------- | ----------- | --------------------------------------------------------------------- |
| [`OnchainActionManager`](/getting-started/contracts) | Onchain | transaction | deposit, withdraw, add/remove session keys |
| [Signed actions](/authentication/action-signing) | HTTP / WS | EIP-712 | all of the above, **and trading** |
| [Session login](/authentication/session-login) | HTTP / WS | EIP-191 | **read-only** — authenticates an HTTP request or WebSocket connection |
Because the EIP-712 path can do strictly more than the contract-call path, the tightest setup is
to close it entirely and operate through contract calls only.
## Recommended policy
Every state-changing Derive action taken over the API is an EIP-712 signed action. Denying
EIP-712 outright means the MPC wallet can never trade, transfer, or withdraw through the
API — no allow-list to maintain and no new action type can slip through later.
In Fordefi this is a [typed data message](https://docs.fordefi.com/user-guide/policies/message-policy#typed-data-messages)
rule. Fordefi surfaces an EIP-712 message's **recipient as its `verifyingContract`**, so target
the rule at Derive's action domain:
| Domain field | Value |
| ------------------- | ----------------------------------------------------------------- |
| `name` | `Matching` |
| `version` | `1.0` |
| `verifyingContract` | `0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD` |
| `chainId` | the settlement chain's id — the only field that varies by network |
Every Derive signed action uses this same `verifyingContract` on every network, so one deny
rule on that address covers mainnet and testnet alike.
On `OnchainActionManager`, allow the two deposit entrypoints:
```solidity theme={null}
function deposit(address asset, uint256 amount, uint64 subaccountId, address fallbackRecipient);
function depositToNewSubaccount(address asset, uint256 amount, uint32 managerId, address owner);
```
Pin `owner` and `fallbackRecipient` to the MPC wallet address. `owner` is what makes the MPC
wallet the account owner; `fallbackRecipient` is where a deposit lands if it cannot be applied
to its intended subaccount.
Session keys are registered from L1 through the generic entrypoint:
```solidity theme={null}
function submit(uint256 actionType, bytes calldata data)
```
### Example (trade and withdraw only session key)
In this example we're going to create a policy that will only allow your MPC wallet to create Session keys with the `trade:orderbook:perp` and `withdraw` protocol scopes. Because only `admin` scoped keys can withdraw to any recipient, this policy will only allow withdrawals to the MPC wallet's own subaccounts.
Use the TypeScript SDK's
[session-key codec](https://github.com/derivexyz/derive-ts/blob/master/src/codecs/sessionKey.ts),
to produce the bytes you'll use to create an MPC policy.
```typescript theme={null}
import { encodeSetSessionKeyActionData } from '@derivexyz/derive-ts/codecs';
import { ProtocolScopeCode } from '@derivexyz/derive-ts';
import { Interface } from 'ethers';
const data = encodeSetSessionKeyActionData({
sessionKey: '0x9f000000000000000000000000000000000000be',
expirySec: 1793491200, // the KEY's lifetime; 0 deletes it
// Exactly two scopes, in this order — the policy pins both.
scopes: [ProtocolScopeCode.Withdraw, ProtocolScopeCode.TradeOrderbookPerp], // [1, 5]
subaccountIds: [], // empty = all current and future subaccounts
});
const manager = new Interface([
'function submit(uint256 actionType, bytes data) payable returns (uint256)',
]);
// 51 = Set Session Key. `data` is the 256 bytes broken down below.
console.log(manager.encodeFunctionData('submit', [51, data]));
```
### Which bytes to pin in your MPC policy
| Param | Contents | Pin to |
| --------------- | ------------------------------------ | ---------------- |
| `actionType` | Set Session Key action type | `51` |
| `data[0:32]` | `sessionKey` address | free — rotates |
| `data[32:64]` | `expirySec` | free — rotates |
| `data[64:96]` | offset → `scopes` | from console.log |
| `data[96:128]` | offset → `subaccountIds` | from console.log |
| `data[128:160]` | `scopes.length` | from console.log |
| `data[160:192]` | `scopes[0]` = `withdraw` | from console.log |
| `data[192:224]` | `scopes[1]` = `trade:orderbook:perp` | from console.log |
| `data[224:256]` | `subaccountIds.length` = all | from console.log |
You can modify what you pin depending on your needs.
Leave [personal messages (EIP191)](https://docs.fordefi.com/user-guide/policies/message-policy#personal-messages-and-messages-on-non-evm-chains)
allowed so [session login](/authentication/session-login) keeps working and the MPC wallet retains
read-only account access through UX and API.
## Related
The full protocol and off-chain scope catalog.
What a session key is and how its lifecycle works.
The withdrawal action and its recipient semantics.
# Session Keys
Source: https://docs.derive.xyz/authentication/session-keys
Delegated signing keys: create, edit, list, and their lifecycle.
A **session key** is a delegated EVM address (an EOA your client controls) that your wallet authorizes to sign [actions](/authentication/action-signing) on its behalf. Instead of exposing your wallet's private key to a trading service, you mint a scoped, expiring key that can sign only the actions you permit, on only the subaccounts you allow.
Every session key carries **two independent scope sets** on one record:
* **Protocol scopes** — on-chain authority (trade, transfer, withdraw, …). Signed into each action and re-validated by the protocol.
* **Off-chain scopes** — server-side capabilities (`account_info`). Never signed; enforced by the server.
See [Access scopes](/authentication/access-scopes) for the full scope catalog and per-route gating.
## How session keys are used
## Delegated attenuation
The key you create can never exceed the authority of the key that signs the creation action. A child key's **scopes, expiry, and subaccounts must each be a subset of its parent's**. A key without `admin` cannot mint an `admin` child; a key expiring next week cannot mint a child expiring next month. The protocol enforces this when the create action is applied.
The signing parent must itself hold the `set_session_key` protocol scope.
## Create a session key
1. API -> call `private/set_session_key` with the owner or any session key with stronger scopes.
2. Onchain -> see [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts).
```typescript TypeScript (SDK) theme={null}
import {
DeriveClient,
ProtocolScopeCode,
OffchainScope,
} from '@derivexyz/derive-ts';
import { Wallet } from 'ethers';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// A fresh keypair for the delegated key; persist its private key for the bot.
const sessionWallet = Wallet.createRandom();
// The SDK builds and EIP-712-signs the create action for you (nonce, signature,
// signature_expiry_sec, module) using the client's signer.
const created = await client.sessionKeys.set({
publicSessionKey: sessionWallet,
expirySec: 1793491200,
protocolScopes: [
ProtocolScopeCode.TradeOrderbookAll,
ProtocolScopeCode.TransferExistingSubaccount,
],
offchainScopes: [OffchainScope.AccountInfo],
subaccountIds: [9],
label: 'market-maker-1',
});
console.log(created.public_session_key, created.subaccount_ids);
await client.close();
```
```python Python (SDK) theme={null}
import asyncio
from eth_account import Account
from derive_py import WebSocketClient
from derive_py.data_types import OffchainScope, ProtocolScope
async def main():
client = WebSocketClient.from_env()
await client.connect()
# A fresh keypair for the delegated key; persist its private key for the bot.
session_wallet = Account.create()
# The SDK builds and EIP-712-signs the create action for you (nonce,
# signature, signature_expiry_sec, module) using the client's signer.
created = await client.account.set_session_key(
public_session_key=session_wallet.address,
expiry_sec=1793491200,
protocol_scopes=[
ProtocolScope.TRADE_ORDERBOOK_ALL,
ProtocolScope.TRANSFER_EXISTING_SUBACCOUNT,
],
offchain_scopes=[OffchainScope.ACCOUNT_INFO],
subaccount_ids=[9],
label="market-maker-1",
)
print(created.public_session_key, created.subaccount_ids)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use alloy::signers::local::PrivateKeySigner;
use derive_rs::{
Environment, WsClient,
actions::session_key::{OffChainScope, ProtocolScope, SetSessionKeyArgs},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
client.login().await?;
// A fresh keypair for the delegated key; persist its private key for the bot.
let session_signer = PrivateKeySigner::random();
// The SDK builds and EIP-712-signs the create action for you (nonce,
// signature, signature_expiry_sec, module) using the client's signer.
let args = SetSessionKeyArgs::builder()
.public_session_key(session_signer.address().to_string())
.expiry_second(1793491200)
.protocol_scopes(vec![
ProtocolScope::TradeOrderbookAll,
ProtocolScope::TransferExistingSubaccount,
])
.off_chain_scopes(vec![OffChainScope::AccountInfo])
.subaccount_ids(vec![9])
.label("market-maker-1".to_string())
.build();
let created = client.session_keys().create(args).await?;
println!("{} {:?}", created.public_session_key, created.subaccount_ids);
Ok(())
}
```
```bash cURL theme={null}
# Signed action: build nonce/signature/signature_expiry_sec per /authentication/action-signing.
# X-Derive* headers authenticate the HTTP session (see /json-rpc).
curl -X POST https://api.derive.xyz/v3/private/set_session_key \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"wallet": "0x1234...abcd",
"public_session_key": "0x9f00...beef",
"expiry_sec": 1793491200,
"subaccount_ids": [9],
"protocol_scopes": ["trade:orderbook:all", "transfer:existing_subaccount"],
"offchain_scopes": ["account_info"],
"label": "market-maker-1",
"ip_whitelist": [],
"nonce": "1710000000000123000",
"signer": "0x1234...abcd",
"signature": "0xabcd...1b",
"signature_expiry_sec": 1710000600,
"module": "0x0000000000000000000000000000000000000000"
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/set_session_key",
"params": {
"wallet": "0x1234...abcd",
"public_session_key": "0x9f00...beef",
"expiry_sec": 1793491200,
"subaccount_ids": [9],
"protocol_scopes": ["trade:orderbook:all", "transfer:existing_subaccount"],
"offchain_scopes": ["account_info"],
"label": "market-maker-1",
"ip_whitelist": [],
"nonce": "1710000000000123000",
"signer": "0x1234...abcd",
"signature": "0xabcd...1b",
"signature_expiry_sec": 1710000600,
"module": "0x0000000000000000000000000000000000000000"
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"public_session_key": "0x9f00...beef",
"protocol_scopes": ["trade:orderbook:all", "transfer:existing_subaccount"],
"offchain_scopes": ["account_info"],
"label": "market-maker-1",
"ip_whitelist": [],
"expiry_sec": 1793491200,
"subaccount_ids": [9]
}
}
```
## IP whitelist
If a key's whitelist is non-empty, [login](/authentication/session-login) from any other IP is rejected. An empty whitelist imposes no IP restriction.
Adding or removing IP Whitelists requires the owner or an admin-scoped key.
## Editing a session key
* Protocol Scopes: call the same `private/set_session_key` endpoint or onchain `Set Session Key` (see [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts)) action again for the session key you want to modify.
* Offchain Scopes and metadata: call `private/edit_session_key` to update the label, IP whitelist, or off-chain scopes. This does not require a signed action. **IP whitelist or off-chain scopes** requires protocol `admin` scope or the owner.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// Off-chain patch: omitted fields are left unchanged. Editing ip_whitelist or
// offchain_scopes requires the owner or an admin-scoped key.
const updated = await client.sessionKeys.edit({
publicSessionKey: '0x9f00...beef',
label: 'market-maker-primary',
ipWhitelist: ['203.0.113.7'],
});
console.log(updated.label, updated.ip_whitelist);
await client.close();
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Off-chain patch: omitted fields are left unchanged. Editing ip_whitelist or
# offchain_scopes requires the owner or an admin-scoped key.
updated = await client.account.edit_session_key(
public_session_key="0x9f00...beef",
label="market-maker-primary",
ip_whitelist=["203.0.113.7"],
)
print(updated.label, updated.ip_whitelist)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::EditSessionKeyRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
client.login().await?;
// Off-chain patch, no signing: omitted fields are left unchanged. Editing
// ip_whitelist or offchain_scopes requires the owner or an admin-scoped key.
let params = EditSessionKeyRequest::builder()
.wallet(std::env::var("DERIVE_WALLET")?)
.public_session_key("0x9f00...beef")
.label("market-maker-primary".to_string())
.ip_whitelist(vec!["203.0.113.7".to_string()])
.try_into()?;
let updated = client.rpc().session_keys().edit_session_key(params).await?;
println!("{:?} {:?}", updated.label, updated.ip_whitelist);
Ok(())
}
```
```bash cURL theme={null}
# Off-chain patch (no signed action); X-Derive* headers: session auth (see /json-rpc).
curl -X POST https://api.derive.xyz/v3/private/edit_session_key \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"wallet": "0x1234...abcd",
"public_session_key": "0x9f00...beef",
"label": "market-maker-primary",
"ip_whitelist": ["203.0.113.7"]
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 2,
"method": "private/edit_session_key",
"params": {
"wallet": "0x1234...abcd",
"public_session_key": "0x9f00...beef",
"label": "market-maker-primary",
"ip_whitelist": ["203.0.113.7"]
}
}
```
## Lifecycle at a glance
Wallet (or a parent key with `set_session_key` scope) signs a create
action granting a subset of its own scopes, expiry, and subaccounts.
Can be done both through API via `private/set_session_key` or on-chain
via the `Set Session Key` action (see [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts)).
The session key logs in via EIP-191 [session
login](/authentication/session-login); its IP whitelist and expiry are
enforced at this step.
The key EIP-712-signs each action; the protocol re-checks that the key's
scopes cover the action (see [Action
signing](/authentication/action-signing)).
Adjust label, IP whitelist, or off-chain scopes with `edit_session_key`.
Protocol scopes require a fresh `set_session_key`.
The key stops working once `expiry_sec` passes. You can "revoke" session keys by bringing the expiry closer to now.
However, there is a minimum cooldown of 5-15min.
## Related
The full protocol and off-chain scope catalog and per-route gating.
How the create action (and every trading action) is signed with EIP-712.
Authorize a session key from L1 when the account owner is a contract or multi-sig.
# Session Login
Source: https://docs.derive.xyz/authentication/session-login
Authenticate a connection or request on behalf of a wallet.
Session login proves that a connection (WebSocket) or a request (HTTP) may act on
behalf of a wallet. It works over both transports and returns the list of
subaccount IDs the caller is authenticated for.
Logging in does **not** authorize trading. Every state-changing action carries
its own EIP-712 signature that the protocol re-verifies independently — see
[Action signing](/authentication/action-signing).
## What you sign
The signed message is the **current time in milliseconds as a decimal string**
(`Date.now().toString()`), signed with [EIP-191](https://eips.ethereum.org/EIPS/eip-191)
`personal_sign`. There is no separate challenge or login nonce — the timestamp is
the anti-replay token. The signer may be the wallet itself or a delegated
[session key](/authentication/session-keys).
The three values — `wallet`, `timestamp`, `signature` — are the same on both
transports. WebSocket sends them as `public/login` params; HTTP sends them as
`X-Derive*` headers. The server reads each value from the header **or** the
request params interchangeably.
## WebSocket
Open the socket, then call `public/login` with `{wallet, timestamp, signature}`.
The `result` is the array of subaccount IDs the connection is now authenticated
for (an empty session-key subaccount list resolves to all of the wallet's current
subaccounts).
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect(); // open the websocket
const subaccountIds = await client.login(); // EIP-191 login over public/login
// The client re-logs in automatically after a reconnect.
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
# connect() opens the websocket AND performs the EIP-191 public/login;
# it re-authenticates automatically after a reconnect.
await client.connect()
subaccounts = await client.account.get_subaccounts()
print(subaccounts.subaccount_ids)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
#[tokio::main]
async fn main() -> Result<(), Box> {
// new() opens the websocket.
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
// EIP-191 login over public/login; returns the wallet's subaccount ids.
let subaccount_ids = client.login().await?;
println!("{:?}", subaccount_ids);
Ok(())
}
```
The raw frame and response:
```json Request theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"method": "public/login",
"params": {
"wallet": "0xYourWalletAddress",
"timestamp": "1720512000000",
"signature": "0x…65-byte personal_sign over the timestamp string"
}
}
```
```json Response theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"result": [9, 42]
}
```
## HTTP
REST requests authenticate **per request** — there is no separate login call.
Attach the same three values as headers to each private request:
`X-DeriveWallet`, `X-DeriveTimestamp`, `X-DeriveSignature`.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
// No connect()/login(): the client signs and attaches X-Derive\* headers on
// every private REST call automatically.
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
const subaccountIds = await client.subaccounts.list(); // authenticated over HTTP
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import AsyncHTTPClient
async def main():
# No login step: the client signs and attaches X-Derive* headers on
# every private REST call automatically.
client = AsyncHTTPClient.from_env()
await client.connect()
subaccounts = await client.account.get_subaccounts() # authenticated over HTTP
print(subaccounts.subaccount_ids)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
// Only WebSocket as it is lower latency.
// Leave an issue in derivexyz/derive-rs if you'd like this feature in the SDK.
```
```bash cURL theme={null}
# timestamp = current time in milliseconds
TS=$(($(date +%s) * 1000))
# SIG = EIP-191 personal_sign over "$TS" (wallet or session key)
SIG="0x…65-byte signature"
curl -X POST https://api.derive.xyz/v3/private/get_subaccounts \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWalletAddress" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{ "wallet": "0xYourWalletAddress" }'
```
## Who may sign the login
The `signature` may come from the wallet itself **or** from a
[session key](/authentication/session-keys) the wallet has delegated:
The recovered signer equals `wallet`. The connection receives full
authority, and the **timestamp freshness window is enforced** — sign
immediately before connecting.
The recovered signer is a registered session-key address whose `wallet`
matches and which is not expired. The key's authority is bounded by its
[access scopes](/authentication/access-scopes), and if it declares an IP
whitelist the request IP must be on it. **Freshness is not enforced** on
this path.
The exact timestamp validity window for direct-wallet auth is
deployment-specific; allow for it (and a little clock skew) when signing the
login timestamp.
## Re-authentication
Long-lived WebSocket sessions periodically re-verify the stored login signature
as part of the heartbeat, so a connection stays authenticated without a fresh
`public/login`. See [Connecting over WebSocket](/connecting) for heartbeat and
reconnection guidance.
# Changelog
Source: https://docs.derive.xyz/changelog
Release notes for the Derive API.
Release notes for the Derive v3 API, newest first. Subscribe to the [RSS feed](/changelog/rss.xml)
to be notified of new entries.
Migrating from v2? See [Migrating from V2](/migrating/v3-improvements).
## Proven operations share one per-wallet rate-limit budget
Every method that submits a proven operation outside order matching now carries the `proven`
rate-limit class and decrements a single per-wallet budget of **1 TPS** by default (5 points per
5s window on mainnet). Previously most of these methods drew from the far larger `non_matching`
budget, and a few had their own per-method budget.
Affected methods: `private/transfer_spot`, `private/transfer_spot_external`,
`private/transfer_positions`, `private/withdraw`, `private/liquidate`,
`private/set_session_key`, `private/delete_subaccount`, `private/update_whitelisted_recipients`,
`private/create_vault`, `private/mint_vault_shares`, `private/burn_vault_shares`,
`private/request_vault_deposit`, `private/request_vault_withdraw`,
`private/cancel_all_vault_requests`, `private/force_burn`.
* `public/getRateLimits` reports the budget under `remaining_per_endpoint.proven`.
* Rejections use the usual `-32000` code with message `Rate limit exceeded: {wallet}-proven`.
* Order, quote, and RFQ execution methods are unaffected.
See [Rate Limits](/rate-limits).
## `private/create_session_key` renamed to `private/set_session_key`
The route now matches what the action does (register, refresh, **or** retire a key by writing a
nearer expiry) and its L1 counterpart, the `SetSessionKey` onchain action. Everything about the
rename is mechanical:
* Route: `private/create_session_key` → **`private/set_session_key`** (and the debug variant
`private/create_session_key_debug` → `private/set_session_key_debug`). The old route names
are gone.
* Protocol scope: `create_session_key` → **`set_session_key`** in `protocol_scopes` on
registration requests and in `private/session_keys` responses.
* TypeScript SDK: `client.sessionKeys.create(...)` → **`client.sessionKeys.set(...)`**;
`ProtocolScopeCode.CreateSessionKey` → `ProtocolScopeCode.SetSessionKey`.
## Signed action payloads now use canonical Solidity ABI encoding
Every signed action's `data` bytes are now exactly what Solidity `abi.encode` produces — if you
sign with a standard ABI coder (ethers `AbiCoder`, viem `encodeAbiParameters`, `abi.encode` in a
contract), nothing changes for you. Hand-rolled encoders must drop the previous non-standard
layouts, which are **no longer accepted**:
**`private/set_session_key` and the L1 `SetSessionKey` action (type 51)**
The payload is canonical `abi.encode(address sessionKey, uint256 expirySec, uint256[] scopes,
uint256[] subaccountIds)` — dynamic arrays sit behind standard offset words, each prefixed by its
length word. The old hand-packed layout (`[key][expiry][scopeCount][subaccountCount][scopes…] [subaccounts…]`, no offsets) is rejected. In Solidity, build the L1 payload with a plain
`abi.encode` call instead of packing words manually.
**`private/update_whitelisted_recipients`**
The payload is canonical `abi.encode(address[] add, address[] remove)`. The old hand-packed
layout (`[addCount][removeCount][adds…][removes…]`, no offsets) is rejected.
**Orders (`private/order`) and liquidations (`private/liquidate`)**
Negative `int256` values (`limit_price`, `amount`, `price_limit`) are now full 32-byte
two's-complement words, sign-extended through the high 16 bytes — standard ABI coder output.
Previously the server produced and expected a non-standard form with the value packed into the
low 16 bytes only, which broke standard-signing clients on negative liquidation price limits.
Non-negative values encode identically, so most existing signatures are unaffected.
Decoding is also strict across these payloads: offset words must point at their canonical
positions, lengths must match the byte count, and value words must fit their field's range —
dirty high bytes are rejected instead of silently truncated, matching Solidity `abi.decode`.
See [Action signing](/authentication/action-signing) for the per-module payload layouts.
## Settlement status unified under `batch_status`
The `tx_status` field and the `TxStatus` type are removed. Every route and channel that reported
settlement status now uses **`batch_status`**, typed as the shared `BatchStatus` enum — `Batching`,
`Executing`, `Proving`, `Settling`, `Settled` and their `…Error` counterparts — and `null` until
the operation is picked up into a batch.
This replaces the old granular transaction states (`applied`, `in_batch`, `proving`, `submitted`)
**and** the lowercase matching-engine values (`requested`, `pending`, `settled`, `reverted`,
`ignored`, `timed_out`), which are all **gone** across the entire API. There is now one status
field, one set of values.
**REST methods**
* `public/get_trade_history` — optional `batch_status` request filter (unset returns trades in every
batch state) and on each trade
* `private/get_trade_history` — `batch_status` on each trade
* `private/order`, `private/replace` — `batch_status` on each `trades[]` entry (always `null` at placement)
* `private/send_quote`, `private/cancel_quote`, `private/replace_quote`, `private/get_quotes`,
`private/transfer_positions` — `batch_status` on each quote (present only for executed quotes)
* `private/get_deposit_history`, `private/get_withdrawal_history`,
`private/get_erc20_transfer_history`, `private/get_funding_history` — `batch_status` on each row
* `public/get_transaction` — `status` uses the same `BatchStatus` values
**WebSocket channels**
* `trades.{instrument_type}.{currency}.{tx_status}` and `{subaccount_id}.trades.{tx_status}` — removed;
settlement is no longer streamed. Poll `public/get_trade_history` for `batch_status` / `tx_hash` instead
* `trades.{instrument_name}`, `trades.{instrument_type}.{currency}` — each event now carries the full
trade detail (`wallet`, `subaccount_id`, `liquidity_role`, fees, realized PnL)
* `{subaccount_id}.trades`, `{subaccount_id}.quotes` — `batch_status` on each payload item
## `instrument_type` is now a typed enum
On the trade-history schemas, `instrument_type` is typed as the `AssetType` enum
(`erc20` / `option` / `perp`) instead of a free-form string.
See [Breaking changes → Settlement status](/migrating/breaking-changes) for the full route table
and value reference.
# Connecting over WebSocket
Source: https://docs.derive.xyz/connecting
Open a session, send JSON-RPC frames, and keep the connection alive with heartbeats.
The Derive v3 API is **JSON-RPC 2.0** served over WebSocket. Every method also works
over [HTTP POST](/getting-started/introduction#endpoints), but real-time streams and
[cancel-on-disconnect](/trading/cancel-on-disconnect) are WebSocket-only.
## Connect
Open a WebSocket to the `/v3/ws` endpoint for your environment:
```text Production theme={null}
wss://api.derive.xyz/v3/ws
```
```text Testnet theme={null}
wss://testnet.api.derive.xyz/v3/ws
```
Authentication is a separate layer from the transport. You can either log in
in-band after connecting (`public/login`) or attach a read-only JWT to the URL.
See [Authentication](/authentication/session-login) for the full model.
Connect unauthenticated, then send `public/login` with an EIP-191 signature.
This is the only path that can authorize trading actions.
Pass a bearer token as a query parameter:
```text theme={null}
wss://api.derive.xyz/v3/ws?token=
```
A JWT session carries only the off-chain `account_info` capability — it can
read account data but cannot authorize actions. Action signing is still
required for anything state-changing.
## Send JSON-RPC frames
Send each request as a JSON **text frame**. Responses correlate by `id`;
subscription updates arrive as separate notification frames (see
[Subscriptions](/subscriptions)).
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
// public/get_time takes no params and returns the server time in ms.
const client = new DeriveClient({ network: 'mainnet' });
const serverTime = await client.send('public/get_time', null);
console.log(serverTime); // 1720483200000
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
# public/get_time takes no params and returns the server time in ms.
client = WebSocketClient.from_env()
await client.connect()
server_time = await client.system.get_time()
print(server_time) # 1720483200000
# public_api.rpc is the escape hatch for any method without a namespace
# wrapper; every method there takes its generated params struct.
raw = await client.public_api.rpc.get_time(None)
print(raw)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
#[tokio::main]
async fn main() -> Result<(), Box> {
// public/get_time takes no params and returns the server time in ms.
let client = WsClient::new_public(Environment::Mainnet).await?;
let server_time = client.rpc().system().get_time().await?;
println!("{server_time}"); // 1720483200000
// send_rpc is the escape hatch for any method without a typed wrapper.
let raw: serde_json::Value = client
.send_rpc("public/get_time", serde_json::json!({}))
.await?;
println!("{raw}");
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/get_time \
-H "Content-Type: application/json" \
-d '{}'
```
```json Request theme={null}
{
"id": 1,
"method": "public/get_time",
"params": {}
}
```
```json Response theme={null}
{
"id": 1,
"result": 1720483200000
}
```
Assign a unique `id` per in-flight request and match responses back to it —
frames are not guaranteed to return in send order.
## Heartbeat
The server drives a heartbeat loop and expects the client to stay responsive.
On each heartbeat interval the server sends a WebSocket **Ping** frame. Your
client library normally answers with a **Pong** automatically. Any inbound
text frame from you also counts as liveness.
You may send the text frame `ping`; the server replies with `pong`.
If no activity is seen within the heartbeat window, the server closes the
connection.
The heartbeat interval is deployment-configured (currently \~180 seconds).
### Heartbeat re-authentication
On each heartbeat tick, a **logged-in** connection is silently
**re-authenticated** — the server re-runs your stored login signature check
against current account state. If the session key has expired, been revoked, or
falls outside its IP allow-list, the connection is closed. Long-lived
connections therefore pick up permission changes without you reconnecting.
## Connection limits
Concurrent WebSocket connections are capped **per source IP** (deployment-configured,
currently 4 per IP). When the cap is exceeded the server sends a text frame and
then closes the socket:
```text theme={null}
connectionLimitExceeded:
```
This maps to error code **`-32100` "Number of concurrent websocket clients limit exceeded"**.
The exact heartbeat window and per-IP connection cap are deployment-specific
config values. The values above reflect current deployments; see [Rate
limits](/rate-limits) for the per-IP connection cap and read your live budget
from [`public/getRateLimits`](/rate-limits#runtime-introspection).
Per-request rate limits are separate and shared across all of a wallet's
connections — see [Rate limits](/rate-limits).
## Reconnection guidance
Sessions do not survive a dropped socket. On reconnect:
Dial the same `/v3/ws` endpoint, with backoff (exponential + jitter) so a
server restart doesn't produce a reconnect storm that trips the per-IP cap.
Re-send `public/login` (or reconnect with a fresh `?token=`). Nothing from
the previous session — auth, subscriptions — is retained.
Re-establish every channel from [Subscriptions](/subscriptions), then reload
state (open orders, positions) over REST or private reads, since you may
have missed updates while disconnected.
## Cancel on disconnect
Cancel-on-disconnect automatically cancels your resting orders, quotes, and
trigger orders when a connection drops. It is a persisted account setting — see
[Cancel on disconnect](/trading/cancel-on-disconnect) for the full behaviour and
the `private/set_cancel_on_disconnect` request.
# Error Codes
Source: https://docs.derive.xyz/error-codes
The JSON-RPC error shape and the full Derive v3 application error catalog.
Every failed request returns a JSON-RPC 2.0 error object, whether the call was made
over WebSocket or HTTP POST. The `error` object always has three fields:
```json theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": "expiry: value out of range"
}
}
```
The numeric error code. Negative codes are JSON-RPC / transport-level; positive codes are Derive application errors
(see the catalog below).
A short, stable human-readable summary.
Optional detail (e.g. the offending field or protocol reason). Always present on the wire — it is `null` when there is
no detail.
## JSON-RPC base codes
The standard JSON-RPC 2.0 codes cover malformed or unroutable requests before any
application logic runs.
| Code | Name | Message | When it fires |
| ------ | ------------------ | --------------------- | --------------------------------------- |
| −32700 | `parse_error` | Error parsing message | Request body is not valid JSON |
| −32603 | `internal_error` | Internal error | Any uncaught or internal server failure |
| −32602 | `invalid_params` | Invalid params | Params fail validation |
| −32601 | `method_not_found` | Method not found | Unknown RPC method |
| −32600 | `invalid_request` | Invalid Request | Not a valid JSON-RPC request object |
**Internal errors are redacted.** For code **−32603**, the server always returns `message: "Internal error"` and
`data: null` on the wire — any underlying detail is kept server-side and never leaked to clients. Treat −32603 as
"retry or contact support," not as a description of what went wrong.
## Transport & session codes
| Code | Name | Message | When it fires |
| ------ | ------------------------------- | ------------------------------------------------------------------ | ------------------------------------------- |
| −32000 | `rate_limit_exceeded` | Rate limit exceeded | Per-wallet or per-instrument limit hit |
| −32000 | `custom_rate_limit_error` | *(caller-supplied)* | A method-specific rate-limit rejection |
| −32100 | `concurrent_ws_client_exceeded` | Number of concurrent websocket clients limit exceeded | Too many concurrent WebSocket connections |
| 401 | `unauthorized` | Unauthorized | Missing or invalid authentication |
| 403 | `forbidden` | Forbidden | Authenticated but not permitted |
| 403 | `restricted_region` | You are in a restricted region that violates our terms of service. | Request originates from a geoblocked region |
See [Rate limits](/rate-limits) for how the two-tier limiter works and how to read
your live limits, and [Authentication](/authentication/session-login) for the session-login and
action-signing layers behind the 401/403 and 14xxx codes.
## Application error catalog
Application errors use positive codes grouped by numeric range. Codes are stable;
gaps within a range are reserved.
Internal **8xxx** codes (e.g. counterparty/feed diagnostics) are intentionally omitted from this reference — they are
server-side signals, not client-actionable, and are excluded from public docs.
### 9xxx — Confirmation timeouts
The order was accepted but a downstream confirmation did not arrive in time. Query
order state before resubmitting.
| Code | Name | Message | When it fires |
| ---- | ----------------------------- | --------------------------- | ----------------------------------------------------- |
| 9000 | `order_confirmation_timeout` | Order confirmation timeout | Order confirmation timed out |
| 9001 | `engine_confirmation_timeout` | Engine confirmation timeout | Matching-engine confirmation timed out; order dropped |
### 10xxx — Accounts, transfers & margin
Rejections from account, session-key, and subaccount-level constraints before an
order reaches the book.
| Code | Name | Message | When it fires |
| ----- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| 10001 | `asset_not_erc20` | Asset is not an ERC20 token | Deposit/withdraw/transfer of a non-ERC20 asset |
| 10003 | `same_account_transfer` | Sender and recipient subaccount IDs are the same | Self-transfer |
| 10004 | `multiple_currencies_not_supported` | Multiple currencies not supported | Mixed-currency request rejected |
| 10006 | `max_session_keys_per_wallet` | Maximum number of session keys per wallet reached | Session-key cap reached |
| 10007 | `max_assets_per_subaccount_exceeded` | Maximum number of assets per subaccount reached | Per-subaccount asset cap reached |
| 10010 | `pmrm_only_supports_quote_asset` | PMRM only supports USDC asset collateral. Cannot trade spot markets. | PMRM subaccount tried to trade spot |
| 10015 | `pm2_only_support_single_currency_options_and_perps` | PortfolioMargin2 supports multiple collaterals but only options and perps of the same currency | PM2 cross-currency options/perps |
| 10016 | `pm2_does_not_support_this_collateral` | PortfolioMargin2 does not support this collateral | PM2 unsupported collateral |
### 11000–11039 — Orders, balances & matching
Order-lifecycle rejections from the matching engine, plus the balance-level guards
that reject a debit before it is applied.
| Code | Name | Message | When it fires |
| ----- | --------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------- |
| 11000 | `insufficient_funds` | Insufficient funds | Insufficient margin/funds for the order |
| 11006 | `order_does_not_exist` | Does not exist | Cancel/query of an unknown order |
| 11007 | `self_crossing_disallowed` | Self-crossing disallowed | Order would cross your own resting order |
| 11008 | `post_only_reject` | Post only order cannot cross the market | Post-only order would match immediately |
| 11009 | `zero_liquidity_for_taker_order` | Zero liquidity for market or IOC/FOK order | No liquidity within the limit price |
| 11011 | `order_invalid_signature_expiry` | Invalid signature expiry | Expiry outside the allowed validity window |
| 11012 | `invalid_amount` | Invalid amount | Order amount invalid |
| 11013 | `order_invalid_limit_price` | Invalid limit price | Limit price invalid |
| 11014 | `fok_not_filled` | Fill-or-kill not filled | FOK order could not be fully filled |
| 11015 | `mmp_frozen` | MMP frozen | Market-maker protection frozen for the subaccount/currency |
| 11017 | `non_unique_nonce` | Non unique nonce | Nonce already used |
| 11018 | `order_invalid_nonce` | Invalid nonce date | Leading nonce digits are not a valid UTC-second timestamp |
| 11019 | `too_many_orders` | Open orders limit exceeded | Open-order cap per subaccount reached |
| 11020 | `negative_erc20_balance` | Negative ERC20 balance | Wrapped ERC20 balance would go negative |
| 11021 | `instrument_not_live` | Instrument is not live | Instrument deactivated or expired |
| 11022 | `reject_timestamp_exceeded` | Reject timestamp exceeded | Order arrived after its `reject_timestamp` |
| 11023 | `max_fee_too_low` | Max fee order param is too low | `max_fee` below the required minimum |
| 11024 | `reduce_only_not_supported` | Reduce only not supported with this time in force | Reduce-only requires market/IOC/FOK |
| 11025 | `reduce_only_reject` | Reduce only reject | Reduce-only order would increase the position |
| 11027 | `undergoing_liquidation` | Subaccount undergoing liquidation | Subaccount is being liquidated |
| 11028 | `replace_order_filled_amount_mismatch` | Replaced order filled amount does not match expected state. | Replace saw an unexpected filled amount |
| 11029 | `oi_cap_exceeded` | Trade or transfer rejected: open interest cap would be exceeded | Open-interest cap for the manager exceeded |
| 11030 | `fee_consumes_amount` | Fee consumes entire amount | Fee is ≥ the transfer/withdraw amount |
| 11031 | `withdrawal_rounds_to_zero` | Withdrawal rounds to zero | Net withdrawal rounds to zero in native decimals |
| 11032 | `transfer_below_min` | Transfer below minimum | New-subaccount funding below the minimum |
| 11033 | `transfer_not_whitelisted` | Transfer recipient not whitelisted | Recipient not on the sender's whitelist |
| 11034 | `withdrawals_blocked_insolvent_auction` | Withdrawals blocked by insolvent auction | Withdrawals paused during an insolvent auction |
| 11035 | `non_increasing_nonce` | Non increasing nonce | Signed-action nonce is not strictly increasing |
| 11039 | `lending_borrows_exceed_supply` | Lending pool borrows exceed supply | Pool has lent out more than it holds; debit uncoverable |
### 11050–11055 — Trigger orders
Stop / trigger-order specific rejections.
| Code | Name | Message | When it fires |
| ----- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
| 11050 | `trigger_order_already_cancelled_or_expired` | Trigger order was cancelled between the time worker sent order and engine processed order | Trigger cancelled before it could be processed |
| 11051 | `invalid_trigger_price` | Trigger price must be higher than current price for stop orders and vice versa | Trigger price on the wrong side |
| 11052 | `too_many_trigger_orders` | Trigger order limit exceeded (separate limit from regular orders) | Trigger-order cap reached |
| 11053 | `trigger_price_type_not_supported` | Index and last-trade trigger price types not supported yet | Only mark-price triggers are supported |
| 11054 | `cannot_replace_or_be_replaced_by_trigger_orders` | Trigger orders cannot replace or be replaced | Replace involving a trigger order |
| 11055 | `unfillable_market_trigger_order` | Market order limit\_price is unfillable at the given trigger price | Market trigger would not be crossable |
A parallel **11150–11155** code range for trigger-order rejects appears in the reject-message mapping and may surface
for some persisted trigger rejections. Handle trigger rejects by `name`/`message` rather than hard-coding `11050` vs
`11150`.
### 11100–11113 — RFQs & quotes
Request-for-quote and quote-matching rejections.
| Code | Name | Message | When it fires |
| ----- | ---------------------------------------- | ----------------------------------------------- | ------------------------------------------------- |
| 11100 | `leg_instruments_not_unique` | Leg instruments are not unique | Repeated instrument across RFQ/quote legs |
| 11101 | `rfq_not_found` | RFQ not found | RFQ query/cancel matched nothing |
| 11102 | `quote_not_found` | Quote not found | Quote query/cancel matched nothing |
| 11103 | `quote_leg_mismatch_vs_rfq` | Quote leg does not match RFQ leg | Quote legs differ from RFQ legs |
| 11104 | `quote_rfq_not_open` | Requested quote or RFQ is not open | RFQ expired, filled, or cancelled |
| 11105 | `quote_rfq_id_mismatch` | Quote references a different RFQ than requested | Quote's RFQ id differs from the one provided |
| 11106 | `rfq_invalid_counterparty` | Invalid RFQ counterparty | Counterparty unauthorized or nonexistent |
| 11107 | `quote_maker_cost_too_high` | Quote maker total cost too high | Maker cost exceeded the price bandwidth |
| 11108 | `rfq_partial_fill_pct_too_high` | RFQ partial fill percentage too high | Fill exceeds the requested total size |
| 11109 | `rfq_filled_direction_cannot_be_changed` | RFQ filled direction cannot be changed | A later fill would flip the direction |
| 11110 | `quote_taker_cost_too_high` | Quote taker total cost too high | Taker cost exceeded the orderbook execution price |
| 11111 | `rfq_disabled_for_account` | RFQ functionality is disabled for this account | RFQ disabled for the account |
| 11112 | `rfq_too_many_legs` | RFQ has too many legs | Leg count over the maximum |
| 11113 | `rfq_too_many_counterparties` | RFQ has too many counterparties | Counterparty count over the maximum |
### 12xxx — Market data
| Code | Name | Message | When it fires |
| ----- | ---------------------- | -------------------- | ------------------ |
| 12001 | `instrument_not_found` | Instrument not found | Unknown instrument |
| 12002 | `currency_not_found` | Currency not found | Unknown currency |
| 12003 | `asset_not_found` | Asset not found | Unknown asset |
### 13000 — Subscriptions
| Code | Name | Message | When it fires |
| ----- | ------------------ | ---------------- | ----------------------------------------- |
| 13000 | `invalid_channels` | Invalid channels | Unknown or malformed subscription channel |
### 14xxx — Authentication & authorization
Session-login, signature, and session-key scope failures. See
[Authentication](/authentication/session-login) for the underlying scheme.
| Code | Name | Message | When it fires |
| ----- | ---------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------- |
| 14000 | `account_not_found` | Account not found | Account does not exist |
| 14001 | `subaccount_not_found` | Subaccount not found | Subaccount not owned by a registered wallet |
| 14013 | `string_is_not_ethereum_address` | String is not a valid ethereum address | Malformed address |
| 14014 | `invalid_signature` | Signature invalid for message or transaction | Recovered signer does not match the expected signer |
| 14020 | `auth_header_mismatch` | The X-DeriveWallet header does not match the requested subaccount\_id or wallet | Auth header subject mismatch |
| 14021 | `ip_not_whitelisted` | IP not whitelisted | Request IP not on the whitelist |
| 14023 | `invalid_signer` | Signer in on-chain related request is not wallet owner or registered session key | Signer is neither the owner nor a registered session key |
| 14026 | `session_key_not_found` | Session key not found | Unknown session key |
| 14027 | `unauthorized_as_rfq_maker` | Unauthorized as RFQ maker | Account is not an RFQ maker |
| 14028 | `cross_currency_rfq_not_supported` | Cross currency RFQ not supported | RFQ legs span multiple currencies |
| 14030 | `session_key_expired` | Session key expired | Session key is past its expiry |
| 14031 | `unauthorized_key_scope` | Unauthorized Key Scope | Session-key scope insufficient for the action |
| 14033 | `account_not_atomic_whitelisted` | Account not whitelisted for atomic orders | Atomic order from a non-whitelisted account |
| 14035 | `session_key_already_registered` | Session key already registered | Session key already belongs to another wallet |
**`14014` with signing that looks right is usually bad ABI encoding, not a bad
signature.** Routes encode `Action.data` server-side, so your bytes never reach
the decoder — the hashes simply disagree, naming no field.
Send them to [`public/decode_action`](/api-reference/system/publicdecode_action)
(no auth, executes nothing) for the decoded fields or the exact fault:
```
word 2 (limit_price) is not e12-representable: downscale discards a non-zero remainder
```
SDK: `await client.decodeAction('order', encodedData)`. Once the bytes decode,
the `*_debug` routes compare EIP-712 hashes.
### 16xxx — Compliance
| Code | Name | Message | When it fires |
| ----- | ------------------ | ----------------------------------------------------- | ------------------------------------ |
| 16001 | `account_disabled` | Account is disabled due to compliance violations | Account disabled by compliance |
| 16002 | `ofac_blocked` | Account is blocked due to OFAC compliance violations. | Account blocked under OFAC screening |
### 17xxx — Risk universes
| Code | Name | Message | When it fires |
| ----- | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------- |
| 17000 | `cross_universe_trade` | Instrument not supported by this manager / risk universe | Participants belong to different risk universes |
| 17001 | `unknown_risk_universe` | Unknown risk universe | Referenced risk universe does not exist |
| 17002 | `manager_cannot_risk_currency` | Risk manager has no configuration for this currency | Trading or holding a currency the subaccount's manager cannot price |
| 17003 | `asset_not_in_risk_universe` | Asset is not registered in the destination risk universe | Depositing or transferring an asset the destination cannot hold |
### 18xxx — Vaults
| Code | Name | Message | When it fires |
| ----- | -------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------- |
| 18007 | `vault_not_found` | Vault not found | Subaccount is not a vault |
| 18008 | `exceeded_max_user_requests` | Exceeded maximum number of pending vault requests | Too many pending deposit/withdraw requests |
| 18009 | `max_shareholder_vaults_reached` | Exceeded maximum number of shareholder vaults | Shareholder-vault cap reached |
| 18010 | `vault_closed` | Vault is closed | Vault has been fully withdrawn/closed |
| 18011 | `vault_cooldown_active` | Vault withdrawal cooldown active | Deposit→withdraw cooldown not elapsed |
| 18012 | `vault_creation_deposit_below_min` | Vault creation deposit below minimum | Curator's initial deposit below the minimum |
| 18013 | `vault_curator_stake_below_min` | Curator stake below minimum | Withdrawal would drop curator stake below the floor |
| 18014 | `vault_slippage_exceeded` | Vault quote outside slippage band | Share price outside the accepted band |
| 18015 | `vault_amount_below_min` | Vault amount below minimum | Deposit/withdraw below the per-asset minimum |
| 18016 | `vault_signature_expiry_too_long` | Vault signature expiry too far out | Expiry more than 30 days out |
| 18017 | `vault_benchmark_price_unavailable` | Vault HWM benchmark price unavailable | High-water-mark benchmark has no feed |
| 18018 | `vault_initial_share_price_too_far_from_benchmark` | Vault initial share price too far from benchmark | Initial price too far from the benchmark |
| 18019 | `vault_deposit_exceeds_margin` | Insufficient margin for vault deposit | Source subaccount cannot back the deposit |
### 20000 — Protocol
| Code | Name | Message | When it fires |
| ----- | ----------------- | --------------------------- | ------------------------------------------------------------------ |
| 20000 | `protocol_reject` | Protocol rejected operation | The protocol layer rejected the operation; the reason is in `data` |
## Handling errors
Branch on `code` for programmatic handling and surface `message` (and `data` when present) to users. Never parse
`message` text — it is stable but treat `code` as the contract. For −32603, retry with backoff rather than inspecting
the (redacted) body.
# Contracts
Source: https://docs.derive.xyz/getting-started/contracts
On-chain contract addresses for the Derive v3 testnet (Sepolia) deployment.
The on-chain contracts for the Derive v3 **testnet** deployment on **Ethereum
Sepolia**. These are the settlement and custody contracts your own wallet
interacts with for deposits, withdrawals, and the L1 escape hatch.
**Chain ID:** `11155111`
For the **signing** constants — `ACTION_TYPEHASH`, the EIP-712 domain, and the
per-action module addresses — see [Action
signing](/authentication/action-signing).
## `ACTION_MANAGER`
```text theme={null}
0x5e6D8BE71FD6838f045E08c60663C9cC36590c33
```
Direct L1 calls to the Derive exchange can be made for
[deposits](/getting-started/depositing), or for setting session keys from
[MPC wallets](/authentication/mpc-wallets) and
[multi-sigs or contracts](/authentication/contract-owned-accounts).
## `VAPP`
```text theme={null}
0x3012BA2755B2fB921230407A89c91e86914Bfc53
```
The zk-vApp settlement contract. It anchors the protocol's on-chain state root
and verifies the validity (zero-knowledge) proofs that settle every state
transition — the root of custody and settlement for the deployment.
## `WITHDRAWAL_OUTBOX`
```text theme={null}
0xf0894158A309c7B11A0b858B8c62eB88fB65F87f
```
The L1 withdrawal outbox. Finalized withdrawals — and the **escape-hatch** exit
if the operator goes offline — are claimed here; payouts are pulled from the spot
vault through this contract.
## `SPOT_VAULT`
```text theme={null}
0x08bb6253302967ABca91541B0707f5DBc045D271
```
Mainnet contract addresses are not published in this repository — read them
from the mainnet deployment.
# Programmatic Onboarding
Source: https://docs.derive.xyz/getting-started/depositing
Integrate your app within minutes.
User onboarding on Derive is **fully programmatic** - no humans in the loop.
You can integrate the Derive Exchange for your users within minutes.
Your account and first subaccount come into existence the moment a deposit is
credited to your wallet on-chain.
## The account model
Three terms show up throughout the API. They nest:
* **Wallet** — your Ethereum EOA (the address that owns everything). One wallet ↔ one account.
* **Subaccount** — the unit you actually trade from. It holds collateral and positions and is identified by a numeric `subaccount_id`. A wallet can own many.
* **Manager** — the margin/risk model a subaccount runs under: **standard (cross) margin** or **portfolio margin**. You pick a manager when the subaccount is created; it also fixes the subaccount's risk universe.
A brand-new wallet's first deposit creates **two** subaccounts: the funded one you asked for, plus a **fallback
subaccount** (under manager id `0`) that catches any deposit that can't be applied to its intended target. Expect two
ids to appear the first time — the fallback is normal.
v3 has no `private/create_subaccount` method — subaccounts are created **on-chain by depositing**. See the
[changelog](/changelog) for the v2 → v3 method changes.
## Step 1 — Choose deposit params
When depositing to a new subaccount you must chose a **risk universe / manager**.
Use `public/get_risk_universes` to list every
universe with its managers and their accepted collaterals / instruments.
See [Managers & risk universes](/trading/managers-and-risk-universes) for how to choose.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
// Public read — no login required.
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
const universes = await client.marketData.getRiskUniverses();
// Pick the manager that trades what you want (ETH options)
// and accepts the collateral you'll post (USDC).
const manager = universes
.flatMap((u) => u.managers)
.find((m) => m.instruments.includes('ETH-OPTION') && m.collaterals.some((c) => c.name === 'USDC'))!;
const usdc = manager.collaterals.find((c) => c.name === 'USDC')!;
console.log(manager.manager_id, usdc.address, usdc.erc20);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
universes = await client.markets.get_risk_universes()
# Pick the manager that trades what you want (ETH options)
# and accepts the collateral you'll post (USDC).
manager = next(
m
for u in universes
for m in u.managers
if "ETH-OPTION" in m.instruments and any(c.name == "USDC" for c in m.collaterals)
)
usdc = next(c for c in manager.collaterals if c.name == "USDC")
print(manager.manager_id, usdc.address, usdc.erc20)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
#[tokio::main]
async fn main() -> Result<(), Box> {
// Public read — no login required.
let client = WsClient::new_public(Environment::Mainnet).await?;
let universes = client.rpc().market_data().get_risk_universes().await?;
// Pick the manager that trades what you want (ETH options)
// and accepts the collateral you'll post (USDC).
let manager = universes
.iter()
.flat_map(|u| &u.managers)
.find(|m| {
m.instruments.contains(&"ETH-OPTION".to_string())
&& m.collaterals.iter().any(|c| c.name == "USDC")
})
.expect("no manager trades ETH options against USDC");
let usdc = manager
.collaterals
.iter()
.find(|c| c.name == "USDC")
.expect("USDC collateral missing");
println!("{} {} {:?}", manager.manager_id, usdc.address, usdc.erc20);
Ok(())
}
```
### What each asset field means
| Value | Where it comes from | Used as |
| ------------------------------ | ------------------------------- | ------------------------------------------------------------------------------- |
| Protocol spot asset address | `collaterals[].address` | the `asset` in the on-chain deposit call |
| Underlying ERC-20 + `decimals` | `collaterals[].erc20` | the token you approve/send, and how to scale the amount |
| Minimum deposit | `collaterals[].min_deposit_usd` | deposits below this amount will be donated to the security module |
| Margin credit | `collaterals[].im_discount` | how much margin the asset earns under this manager (`"0"` = held but no credit) |
## Step 2 — Deposit
You can deposit 3 different ways - each ideal for different types of users:
1. Direct: call the `Deposit()` or `DepositNewSubaccount()` calls on the `OnchainActionManager.sol`. (\~2 min)
2. Standard: less integration work as simply requires user to send funds to a custom ETH address from where a keeper will auto deposit funds into the exchange. (\~2 min)
3. Instant: fastest deposit times at \~15 seconds, with same integration simplicity to `standard`. Currently only supported for USDC (reach out if you'd like other collaterals) and for amounts \<\$1k. There also may be a small
fee depending on network conditions.
Your own wallet calls the settlement contract (`ACTION_MANAGER`, address per [Contracts](/getting-started/contracts)). First
`approve` the contract to pull your ERC-20, then call one of:
* **`depositToNewSubaccount(asset, amount, managerId, owner)`** — create a new subaccount under `managerId`, owned by `owner`.
* **`deposit(asset, amount, subaccountId, fallbackRecipient)`** — fund an existing `subaccountId`; `fallbackRecipient` receives the funds into its fallback subaccount if the deposit cannot be applied.
`asset` is the **protocol spot asset address** (`collaterals[].address` from Step 1), **not** the ERC-20 token. `amount` is in the token's **native ERC-20 decimals**
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
import { ethers } from 'ethers';
// The SDK holds no provider — supply an ethers signer connected to the
// settlement-chain RPC; it signs the approve + deposit for you.
const provider = new ethers.JsonRpcProvider(SETTLEMENT_CHAIN_RPC_URL);
const signer = new ethers.Wallet(OWNER_PRIVATE_KEY, provider);
// No login needed — a Direct deposit is a pure on-chain call; the network
// config supplies the ActionManager and USDC addresses.
const client = new DeriveClient({ network: 'mainnet' });
// ASSET_ADDRESS = collaterals[].address (protocol asset, NOT the ERC-20) and
// MANAGER_ID = manager_id, both from Step 1. Approves the ActionManager to
// pull the ERC-20 (defaults to the network's USDC), then deposits into a NEW
// subaccount under MANAGER_ID, owned by the signer.
const { txHash } = await client.deposits.contractCall.depositToNewSubaccount({
signer,
asset: ASSET_ADDRESS,
amount: '100', // human units — scaled by the ERC-20's on-chain decimals()
managerId: MANAGER_ID,
});
// Mined on-chain; crediting still happens asynchronously (Step 3).
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
from derive_py.data_types import D, MarginType, RiskUniverseID
async def main():
client = WebSocketClient.from_env()
await client.connect()
# No login needed for the on-chain part — the client holds the wallet key
# and the network config supplies the ActionManager and USDC addresses.
# amount is in HUMAN units; the SDK scales by the ERC-20's decimals().
steps = client.plan_deposit_to_new_subaccount(
risk_universe_id=RiskUniverseID.PRIME,
margin_type=MarginType.SM,
asset_name="USDC",
amount=D("100"),
)
# One step per tx: the ERC-20 approve (skipped if already approved),
# then the deposit into a NEW subaccount owned by the signer.
async for step in steps:
print(f"[{step.kind}] {step.description}")
tx_hash = await step.submit()
receipt = await step.wait_for_finality()
print(f" {tx_hash} in block {receipt.blockNumber}")
# Mined on-chain; crediting still happens asynchronously (Step 3).
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use std::str::FromStr;
use alloy::primitives::Address;
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::{DepositArgs, DepositTypes, DirectDepositType, SupportDepositAssets},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let wallet: Address = std::env::var("DERIVE_WALLET")?.parse()?;
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(wallet.to_string()),
None, // no subaccount yet — this deposit creates one
)
.await?;
// MANAGER_ID comes from Step 1. amount is in HUMAN units; the SDK scales it
// by the ERC-20's decimals() and approves the ActionManager first if needed.
let args = DepositArgs::builder()
.asset(SupportDepositAssets::USDC)
.amount(BigDecimal::from_str("100")?)
.recepient_address(wallet) // owner of the new subaccount
.manager_id(2)
.deposit_type(DepositTypes::Direct(
DirectDepositType::DepositToNewSubaccount,
))
.build();
// One hash per tx sent: the ERC-20 approve (if required), then the deposit.
for hash in client.fund_movements().deposit(args).await? {
println!("{hash:?}");
}
// Mined on-chain; crediting still happens asynchronously (Step 3).
Ok(())
}
```
```solidity Solidity theme={null}
// Raw calls to OnchainActionManager.sol — from your own contract, a Foundry
// script, or `cast send`.
// ASSET_ADDRESS = collaterals[].address — protocol asset, NOT the ERC-20
// ERC20_ADDRESS = collaterals[].erc20.underlying_erc20
// MANAGER_ID = manager_id of the SM-margin manager
// ACTION_MANAGER = settlement contract — see /getting-started/contracts
interface IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
}
interface IOnchainActionManager {
function depositToNewSubaccount(
address asset,
uint256 amount,
uint32 managerId,
address owner
) external returns (uint256 actionId);
}
function depositToDerive() external {
uint256 amount = 100e6; // native ERC-20 units (USDC = 6 decimals)
// 1. Approve the ActionManager to pull the underlying ERC-20.
IERC20(ERC20_ADDRESS).approve(ACTION_MANAGER, amount);
// 2. Create a new subaccount under MANAGER_ID, owned by this wallet;
// crediting still happens asynchronously (Step 3).
IOnchainActionManager(ACTION_MANAGER).depositToNewSubaccount(
ASSET_ADDRESS,
amount,
MANAGER_ID,
msg.sender
);
}
```
Mainnet settlement-contract addresses are deployment-specific — do not hardcode them from the docs. Confirm the
`ACTION_MANAGER` address for your target deployment on [Contracts](/getting-started/contracts) before mainnet use.
`public/register_deposit_address` returns a **deterministic deposit address** for
`(wallet, subaccount, manager, deposit_type)`. Send the token there from anywhere; an off-chain sweeper forwards it
into the protocol and credits your subaccount. It is a plain public call — **no signature** — and idempotent: calling
it again returns the same address.
Wallet to credit. EIP-55 checksummed.
Existing subaccount to route the deposit into. Omit (or `0`) to create a new subaccount instead.
Manager the new subaccount is created under. **Required and non-zero** when `subaccount_id` is omitted or `0`; ignored
when routing into an existing subaccount.
`"standard"` for Standard, `"instant"` for the **Instant** tab. Each type gets its **own distinct address**
for the same `(wallet, subaccount, manager)`.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
// Public call — no login required. The address is deterministic per
// (wallet, subaccount, manager, depositType).
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
const registration = await client.deposits.depositAddress.register({
wallet: '0xYourWallet',
managerId: 2,
depositType: 'standard',
});
console.log(registration.deposit_address);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
from derive_py.data_types.generated_models import DepositType, RegisterDepositAddressParams
async def main():
# Public call — no login required. The address is deterministic per
# (wallet, subaccount, manager, deposit_type).
client = WebSocketClient.from_env()
await client.connect()
registration = await client.public_api.rpc.register_deposit_address(
RegisterDepositAddressParams(
wallet="0xYourWallet",
manager_id=2,
deposit_type=DepositType.standard,
)
)
print(registration.deposit_address)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::RegisterDepositAddressParams};
#[tokio::main]
async fn main() -> Result<(), Box> {
// Public call — no login required. The address is deterministic per
// (wallet, subaccount, manager, depositType).
let client = WsClient::new_public(Environment::Mainnet).await?;
let params = RegisterDepositAddressParams::builder()
.wallet("0xYourWallet")
.manager_id(2)
.deposit_type("standard")
.try_into()?;
let registration = client
.rpc()
.onchain_actions()
.register_deposit_address(params)
.await?;
println!("{}", registration.deposit_address);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/register_deposit_address \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xYourWallet",
"manager_id": 2,
"deposit_type": "standard"
}'
```
Only send the **registered token** to a deposit address, on the **correct chain** (see [Contracts](/getting-started/contracts)).
The address is bound to the `(wallet, subaccount, manager, deposit_type)` you registered — routing is fixed at
registration time.
The same mechanism as Standard — register with `public/register_deposit_address`, this time with
`deposit_type: "instant"`, and send the token to the returned address — but instead of waiting for L1 finality, the
keeper **fronts** the credit into the exchange within **1-3 minutes**. This is the fastest path to a funded
subaccount.
Instant deposits are currently **USDC only**, for amounts **under \$1,000**, and carry a small fee. An amount above
the instant cap is still credited through this path, in cap-sized chunks: the first chunk lands near-instantly and
the remainder follows chunk by chunk, each with more confirmation depth.
The Instant address is **different** from the Standard address for the same `(wallet, subaccount, manager)` — the
deposit type salts the escrow. Register with the type you intend to use.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
const registration = await client.deposits.depositAddress.register({
wallet: '0xYourWallet',
managerId: 2,
depositType: 'instant',
});
console.log(registration.deposit_address);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
from derive_py.data_types.generated_models import DepositType, RegisterDepositAddressParams
async def main():
# Public call — no login required. The address is deterministic per
# (wallet, subaccount, manager, deposit_type).
client = WebSocketClient.from_env()
await client.connect()
registration = await client.public_api.rpc.register_deposit_address(
RegisterDepositAddressParams(
wallet="0xYourWallet",
manager_id=2,
deposit_type=DepositType.instant,
)
)
print(registration.deposit_address)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::RegisterDepositAddressParams};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new_public(Environment::Mainnet).await?;
let params = RegisterDepositAddressParams::builder()
.wallet("0xYourWallet")
.manager_id(2)
.deposit_type("instant")
.try_into()?;
let registration = client
.rpc()
.onchain_actions()
.register_deposit_address(params)
.await?;
println!("{}", registration.deposit_address);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/register_deposit_address \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xYourWallet",
"manager_id": 2,
"deposit_type": "instant"
}'
```
#### Tracking an Instant deposit
Instant deposits are credited **off-chain as transfers**, so they never appear in `public/get_onchain_action_history`
or `private/get_deposit_history`. Their lifecycle lives in `public/get_pending_deposits` — a public call, so it works
before your account exists. Each entry moves through:
* `pending` — the deposit was observed on-chain and is awaiting payout.
* `crediting` — a credit transfer is in flight.
* `credited` — paid out.
A deposit above the instant cap shows **one entry per credit chunk** (`credit_nonce` disambiguates them), summing to
the on-chain amount. The deposit is fully paid once **every** entry reads `credited`. `reverted` marks a deposit
reorged out before its block became safe.
```typescript TypeScript (SDK) theme={null}
// Snapshot the lifecycle...
const { pending_deposits } = await client.deposits.getPending();
// ...or block until the deposit funded by your transfer is fully credited.
const credited = await client.deposits.awaitFastDeposit({ txHash: '0xYourTransferTx' });
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/get_pending_deposits \
-H "Content-Type: application/json" \
-d '{ "wallet": "0xYourWallet" }'
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"wallet": "0xYourWallet",
"pending_deposits": [
{
"action_id": 0,
"action_type": "FastDeposit",
"asset": "USDC",
"amount": "400000000",
"subaccount_id": 10,
"manager_id": 0,
"tx_hash": "0xYourTransferTx",
"log_index": 3,
"block_number": 123456,
"status": "credited",
"deposit_type": "instant",
"credit_nonce": "1730000000001",
"timestamp": 1731000000000,
"updated_at_ms": 1731000012000
},
{
"action_id": 0,
"action_type": "FastDeposit",
"asset": "USDC",
"amount": "200000000",
"subaccount_id": 10,
"manager_id": 0,
"tx_hash": "0xYourTransferTx",
"log_index": 3,
"block_number": 123456,
"status": "pending",
"deposit_type": "instant",
"timestamp": 1731000000000,
"updated_at_ms": 1731000012000
}
]
}
}
```
## Step 3 — Confirm the deposit was credited
A mined deposit transaction is **not** the end of the story: the exchange credits the funds asynchronously, and the new
`subaccount_id` is **not** carried in the transaction receipt. Two endpoints cover the gap.
### First feedback — `public/get_pending_deposits`
Whichever method you used, an entry appears here the moment the exchange picks the deposit up: **Direct** and
**Standard** deposits as soon as the action lands in the `OnchainActionManager` (for Standard, when the keeper sweeps
the deposit address); **Instant** deposits as soon as the keeper indexes the transfer. It is a public call, so it works
before your account exists. From there the paths diverge:
* **Instant** deposits are processed straight from this feed — keep polling it until every entry reads `credited`
(the [lifecycle above](#tracking-an-instant-deposit)); the SDK's `client.deposits.awaitFastDeposit` does exactly
that.
* **Direct / Standard** deposits sit here (`pending` → `confirmed`) while the exchange waits roughly **two minutes**
of confirmations before including them in state — once your entry shows up, switch to polling
`private/get_subaccounts` below.
### Crediting — `private/get_subaccounts`
Discover a **new** subaccount by snapshotting your subaccount ids **before** depositing and polling
`private/get_subaccounts` afterward for the id that appears — the SDK's `client.deposits.awaitNewSubaccount` does this.
For a deposit into an **existing** subaccount, poll `private/get_subaccount` and watch its collateral balance increase.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
const subaccountIds = await client.subaccounts.list();
console.log(subaccountIds);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
subaccounts = await client.account.get_subaccounts()
print(subaccounts.subaccount_ids)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::GetSubaccountsRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let wallet = std::env::var("DERIVE_WALLET")?;
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(wallet.clone()),
Some(1),
)
.await?;
client.login().await?;
let params = GetSubaccountsRequest::builder().wallet(wallet).try_into()?;
let subaccounts = client.rpc().subaccounts().get_subaccounts(params).await?;
println!("{:?}", subaccounts.subaccount_ids);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/get_subaccounts \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWallet" \
-H "X-DeriveTimestamp: 1695836058725" \
-H "X-DeriveSignature: 0x…" \
-d '{ "wallet": "0xYourWallet" }'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/get_subaccounts",
"params": { "wallet": "0xYourWallet" }
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"wallet": "0xYourWallet",
"subaccount_ids": [9, 10]
}
}
```
For **Direct** and **Standard** deposits (both observed on-chain), you can track whether the exchange has picked up your
on-chain action with `public/get_onchain_action_history`. It lists each `OnchainActionManager` action the sequencer
scraped and its `status` — applied (with an `op_uuid`), consumed as a fallback no-op, or still retrying. (Instant
deposits are keeper-fronted and do not appear here.)
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
// Public read — no login required.
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
const { actions } = await client.marketData.getOnchainActionHistory({
wallet: '0xYourWallet',
});
for (const a of actions)
console.log(a.action_type_label, a.status, a.tx_hash, a.op_uuid);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
from derive_py.data_types.generated_models import GetOnchainActionHistoryParams
async def main():
# Public read — no login required.
client = WebSocketClient.from_env()
await client.connect()
history = await client.public_api.rpc.get_onchain_action_history(
GetOnchainActionHistoryParams(wallet="0xYourWallet")
)
for action in history.actions:
print(action.action_type_label, action.status, action.tx_hash, action.op_uuid)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::GetOnchainActionHistoryParams};
#[tokio::main]
async fn main() -> Result<(), Box> {
// Public read — no login required.
let client = WsClient::new_public(Environment::Mainnet).await?;
let params = GetOnchainActionHistoryParams::builder()
.wallet("0xYourWallet".to_string())
.try_into()?;
let history = client
.rpc()
.onchain_actions()
.get_onchain_action_history(params)
.await?;
for action in history.actions {
println!(
"{:?} {:?} {:?} {:?}",
action.action_type_label, action.status, action.tx_hash, action.op_uuid
);
}
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/get_onchain_action_history \
-H "Content-Type: application/json" \
-d '{ "wallet": "0xYourWallet" }'
```
For a full record of credited **Direct** and **Standard** deposits use `private/get_deposit_history`, scoped to the
whole `wallet` or a single `subaccount_id`. Amounts and `fee` are decimal strings; the net credited amount is
`amount - fee`. **Instant** deposits are credited as transfers, so they do not appear here — they show up as incoming
rows in `private/get_erc20_transfer_history` (see [Transfers & Withdrawals](/trading/transfers-withdrawals)), with their
crediting lifecycle in `public/get_pending_deposits`.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// Whole wallet (omit subaccountId). Amounts and fee are decimal strings.
const { deposits } = await client.deposits.getHistory();
for (const d of deposits)
console.log(d.subaccount_id, d.asset, d.amount, d.fee);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Whole wallet. client.history is scoped to the ACTIVE subaccount;
# client.account.history is wallet-wide. Amounts and fee are decimals.
history = await client.account.history.deposits()
for deposit in history.deposits:
print(deposit.subaccount_id, deposit.asset, deposit.amount, deposit.fee)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::GetDepositHistoryRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let wallet = std::env::var("DERIVE_WALLET")?;
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(wallet.clone()),
Some(1),
)
.await?;
client.login().await?;
// Whole wallet (omit subaccount_id). Amounts and fee are decimal strings.
let params = GetDepositHistoryRequest::builder()
.wallet(wallet)
.try_into()?;
let history = client.rpc().history().get_deposit_history(params).await?;
for deposit in history.deposits {
println!(
"{} {} {} {}",
deposit.subaccount_id, deposit.asset, deposit.amount, deposit.fee
);
}
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/get_deposit_history \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWallet" \
-H "X-DeriveTimestamp: 1695836058725" \
-H "X-DeriveSignature: 0x…" \
-d '{ "wallet": "0xYourWallet" }'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/get_deposit_history",
"params": { "wallet": "0xYourWallet" }
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"deposits": [
{
"operation_id": "a1b2...",
"new_subaccount": true,
"subaccount_id": 10,
"wallet": "0xYourWallet",
"asset": "USDC",
"amount": "100",
"fee": "0",
"timestamp": 1731000000000,
"batch_uuid": "c3d4...",
"batch_status": "Settled",
"tx_hash": "0x..."
}
]
}
}
```
`private/get_subaccounts` and `private/get_deposit_history` are private methods — authenticate the connection with
[session login](/authentication/session-login) first. Once a subaccount is funded, you can log in and start trading.
## Next steps
Now that your subaccount is funded, log in, sign an order, and stream your fills.
Session login and the per-action signing model.
Move collateral between subaccounts and back on-chain.
# Introduction
Source: https://docs.derive.xyz/getting-started/introduction
How to interact with the Derive Exchange.
The Derive Exchange is a fully-featured, low-latency exchange that settles trades and manages risk on-chain via Zero Knowledge proofs.
There are two ways users and builders can interact with the exchange:
* HTTP & WebSocket JSON-RPC API: send state-changing requests with self-custodial signed actions or non-sensitive market-data requests.
* L1 Transactions: deposit to the exchange via on-chain contract calls or interact with the exchange with a multi-sig. This method also allows users to withdraw funds using the "escape hatch".
## Agents
* Point your agent at our SKILL.MD at `https://docs.derive.xyz/skill.md`
* Install the skill with `npx skills add https://docs.derive.xyz`
## SDKs
`derive-ts`
`derive-rs`
`derive-py`
## OpenAPI / AsyncAPI
Detailed API specifications can be downloaded here:
* OpenAPI: [`https://docs.derive.xyz/openapi.json`](https://docs.derive.xyz/openapi.json) (applies to both WS and HTTP)
* AsyncAPI for WS-only routes: [`https://docs.derive.xyz/websocket.asyncapi.json`](https://docs.derive.xyz/websocket.asyncapi.json)
* AsyncAPI for WS subscriptions: [`https://docs.derive.xyz/subscriptions.asyncapi.json`](https://docs.derive.xyz/subscriptions.asyncapi.json)
## Endpoints
| Environment | UI | WebSocket | HTTP POST base |
| --------------------- | -------------------------------- | ------------------------------------ | ----------------------------------- |
| Mainnet (coming soon) | `https://app.derive.xyz` | `wss://api.derive.xyz/v3/ws` | `https://api.derive.xyz/v3` |
| Testnet | `https://testnet.app.derive.xyz` | `wss://testnet.api.derive.xyz/v3/ws` | `https://testnet.api.derive.xyz/v3` |
## Next steps
Connect, log in, and place your first order end-to-end.
Session login and per-action EIP-712 signing, step by step.
WebSocket channels for order books, tickers, trades, and account updates.
Typehashes, modules, and per-deployment signing parameters.
# Quickstart
Source: https://docs.derive.xyz/getting-started/quickstart
Make your first trade.
Go from an empty wallet to a resting ETH-PERP order on **testnet** in five steps.
If you're using an **agent**, point it at our [SKILL.MD](https://docs.derive.xyz/skill.md).
```bash TypeScript (SDK) theme={null}
npm install @derivexyz/derive-ts ethers ws
```
```bash Python (SDK) theme={null}
pip install derive-py
```
```bash Rust (SDK) theme={null}
cargo add derive-rs tokio bigdecimal
```
Create an Ethereum wallet and set `PRIVATE_KEY` in your environment before running
the snippets below. The examples use a public Sepolia RPC endpoint, so no RPC
setup is needed.
If you don't have a wallet, you can use the Foundry CLI for this.
`curl -L https://foundry.paradigm.xyz | bash`
`foundryup`
`cast wallet new`
which outputs something like:
```
Address: 0x...
Private Key: 0x...
```
Your wallet pays gas for the on-chain deposit. Grab testnet ETH from the
[Google Cloud Sepolia faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia).
Open [testnet.app.derive.xyz/developers](https://testnet.app.derive.xyz/developers),
connect your wallet, and click **Mint** to receive testnet USDC — the collateral
you'll deposit in the next step.
Every subaccount lives under a **manager** in a **risk universe** — together they
set the margin model and which instruments you can trade. See
[Managers & risk universes](/trading/managers-and-risk-universes) for more.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
import { JsonRpcProvider, Wallet } from 'ethers';
const client = new DeriveClient({
network: 'testnet',
wallet: process.env.PRIVATE_KEY!,
});
// Pick the manager that trades what you want (ETH perps) and accepts the
// collateral you'll post (USDC).
const universes = await client.marketData.getRiskUniverses();
const manager = universes
.flatMap((u) => u.managers)
.find((m) => m.instruments.includes('ETH-PERP') && m.collaterals.some((c) => c.name === 'USDC'))!;
const usdc = manager.collaterals.find((c) => c.name === 'USDC')!;
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Pick the manager that trades what you want (ETH perps) and accepts the
# collateral you'll post (USDC).
universes = await client.markets.get_risk_universes()
manager = next(
m
for u in universes
for m in u.managers
if "ETH-PERP" in m.instruments and any(c.name == "USDC" for c in m.collaterals)
)
usdc = next(c for c in manager.collaterals if c.name == "USDC")
print(manager.manager_id, usdc.address)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new_public(Environment::Testnet).await?;
// Pick the manager that trades what you want (ETH perps) and accepts the
// collateral you'll post (USDC).
let universes = client.rpc().market_data().get_risk_universes().await?;
let manager = universes
.iter()
.flat_map(|u| &u.managers)
.find(|m| {
m.instruments.contains(&"ETH-PERP".to_string())
&& m.collaterals.iter().any(|c| c.name == "USDC")
})
.expect("no manager trades ETH perps against USDC");
let usdc = manager
.collaterals
.iter()
.find(|c| c.name == "USDC")
.expect("USDC collateral missing");
println!("{} {}", manager.manager_id, usdc.address);
Ok(())
}
```
Your first deposit creates your Derive account and its first subaccount under the
manager you chose — there is no separate "create account" call. Your wallet
submits the on-chain deposit.
```typescript TypeScript (SDK) theme={null}
// Snapshot subaccounts so we can spot the new one after depositing.
const knownSubaccountIds = await client.subaccounts.list();
// Your wallet signs the on-chain ActionManager tx (ERC-20 approve + deposit).
const rpcUrl = 'https://ethereum-sepolia-rpc.publicnode.com';
const signer = new Wallet(
process.env.PRIVATE_KEY!,
new JsonRpcProvider(rpcUrl),
);
await client.deposits.contractCall.depositToNewSubaccount({
signer,
asset: usdc.address,
amount: '100',
managerId: manager.manager_id,
});
// The exchange assigns the subaccount id once it sees the deposit (~2 min).
const subaccountId = await client.deposits.awaitNewSubaccount({
knownSubaccountIds,
});
console.log(`account ready: subaccount ${subaccountId}`);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
from derive_py.data_types import D, MarginType, RiskUniverseID
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Snapshot subaccounts so we can spot the new one after depositing.
known = {s.id for s in await client.fetch_subaccounts()}
# plan_deposit_to_new_subaccount yields one step per on-chain tx (an ERC-20
# approve, if needed, then the ActionManager deposit). Your wallet signs each.
steps = client.plan_deposit_to_new_subaccount(
risk_universe_id=RiskUniverseID.PRIME,
margin_type=MarginType.SM,
asset_name="USDC",
amount=D("100"),
)
async for step in steps:
print(f"[{step.kind}] {step.description}")
await step.submit()
await step.wait_for_finality()
# The exchange assigns the subaccount id once it sees the deposit (~2 min).
subaccount_id = None
while subaccount_id is None:
await asyncio.sleep(5)
new = {s.id for s in await client.fetch_subaccounts()} - known
subaccount_id = next(iter(new), None)
print(f"account ready: subaccount {subaccount_id}")
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use std::str::FromStr;
use alloy::primitives::Address;
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::{DepositArgs, DepositTypes, DirectDepositType, SupportDepositAssets},
models::GetSubaccountsRequest,
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let manager_id: u32 = 2; // manager.manager_id from the previous step
let wallet: Address = std::env::var("DERIVE_WALLET")?.parse()?;
let client = WsClient::new(
Environment::Testnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(wallet.to_string()),
None, // no subaccount yet — this deposit creates one
)
.await?;
// Snapshot subaccounts so we can spot the new one after depositing.
let params = GetSubaccountsRequest::builder().wallet(wallet.to_string()).try_into()?;
let known = client.rpc().subaccounts().get_subaccounts(params).await?.subaccount_ids;
// Your wallet signs the on-chain ActionManager txs (ERC-20 approve + deposit).
let args = DepositArgs::builder()
.asset(SupportDepositAssets::USDC)
.amount(BigDecimal::from_str("100")?)
.recepient_address(wallet)
.manager_id(manager_id)
.deposit_type(DepositTypes::Direct(
DirectDepositType::DepositToNewSubaccount,
))
.build();
client.fund_movements().deposit(args).await?;
// The exchange assigns the subaccount id once it sees the deposit (~2 min).
let subaccount_id = loop {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let params = GetSubaccountsRequest::builder().wallet(wallet.to_string()).try_into()?;
let current = client.rpc().subaccounts().get_subaccounts(params).await?.subaccount_ids;
if let Some(id) = current.into_iter().find(|id| !known.contains(id)) {
break id;
}
};
println!("account ready: subaccount {subaccount_id}");
Ok(())
}
```
Open the WebSocket, log in, and place a limit order. `place()` encodes the trade
action, EIP-712-signs it locally with your key, and submits `private/order` — the
exchange only ever settles what you signed.
```typescript TypeScript (SDK) theme={null}
await client.connect(); // open the websocket
await client.login(); // authenticate the session — required for private/*
const { order } = await client.orders.place({
subaccountId,
instrumentName: 'ETH-PERP',
direction: 'buy',
amount: '1',
limitPrice: '3100', // rests below market
});
console.log(`${order.order_id}: ${order.order_status} @ ${order.limit_price}`);
await client.close();
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
from derive_py.data_types import Direction
async def main():
client = WebSocketClient.from_env()
# connect() opens the websocket and authenticates the session,
# which private/* calls require.
await client.connect()
response = await client.orders.create(
instrument_name="ETH-PERP",
direction=Direction.buy,
amount=Decimal("1"),
limit_price=Decimal("3100"), # rests below market
)
order = response.order
print(f"{order.order_id}: {order.order_status} @ {order.limit_price}")
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::OrderArgs,
models::{Direction, OrderType, TimeInForce},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Testnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(std::env::var("DERIVE_SUBACCOUNT_ID")?.parse()?),
)
.await?;
client.login().await?; // authenticate the session — required for private/*
let order = OrderArgs::builder()
.instrument_name("ETH-PERP".to_string())
.direction(Direction::Buy)
.order_type(OrderType::Limit)
.time_in_force(TimeInForce::Gtc)
.amount(BigDecimal::from(1))
.limit_price(BigDecimal::from(3100)) // rests below market
.build();
let response = client.orders().place(order).await?;
let order = response.order;
println!("{}: {:?} @ {}", order.order_id, order.order_status, order.limit_price);
Ok(())
}
```
`maxFee` caps the fee baked into the signature; omit it and the SDK defaults
to 3× the current taker cost. To stream your fills as they happen, subscribe
to the `{subaccountId}.trades` channel — see [Subscriptions](/subscriptions).
## Next steps
Session login, JWT, and the wallet-vs-session-key auth paths.
The full EIP-712 `Action` envelope, per-module `data` layouts, and nonces.
Delegate a signing key so you never hot-wire your wallet into a bot.
On-chain contract addresses per deployment (action manager, vApp, outbox,
spot vault).
# JSON-RPC Envelope
Source: https://docs.derive.xyz/json-rpc
The JSON-RPC 2.0 request/response envelope and the WebSocket and HTTP transports shared by every method.
The Derive v3 API is **JSON-RPC 2.0**. Every method — market data, trading, account, vaults — shares one
request/response envelope and is reachable over **two transports**: a persistent **WebSocket** connection or
a **single HTTP POST**. The method name and its `params` are identical across both; only the framing differs.
## The envelope
A request carries an `id`, the protocol version, the `method` path, and a `params` object.
```json theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"method": "private/cancel",
"params": {
"subaccount_id": 12345,
"instrument_name": "ETH-PERP",
"order_id": "b7e6...c2a1"
}
}
```
A response is **either** a `result` **or** an `error` — never both — and echoes the request `id`.
```json Success theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"result": {
"order_id": "b7e6...c2a1",
"instrument_name": "ETH-PERP",
"order_status": "cancelled"
}
}
```
```json Error theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": "order_id not found"
}
}
```
# Migration skill for your coding agent
Source: https://docs.derive.xyz/migrating/breaking-changes
Use this doc as a SKILL.MD for your coding agent to migration your current code to the v3 API.
This page covers changes in the v3 API in detail. We recommend using the drop down button in the top right to directly copy this page into your LLM / coding agent as a `SKILL.MD`.
If at any step there is not enough detail, refer to the [Guides](/guides) section or the relevant public SDK repos:
* [TypeScript SDK](https://github.com/derivexyz/derive-ts)
* [Python SDK](https://github.com/derivexyz/derive-py)
* [Rust SDK](https://github.com/derivexyz/derive-rs)
Lastly, for granular API details, refer either to the **API Reference** tab or the machine-readable specs — append the path to the base URL:
* [`/openapi.json`](https://docs.derive.xyz/openapi.json) — REST methods
* [`/websocket.asyncapi.json`](https://docs.derive.xyz/websocket.asyncapi.json) — WebSocket-only RPC methods
* [`/subscriptions.asyncapi.json`](https://docs.derive.xyz/subscriptions.asyncapi.json) — subscription channels and their payloads
## No more intermediate "Derive Wallet"
There is no longer a distinction between your "wallet" and a separate "Derive Wallet" or "Smart Contract Wallet".
Every `wallet` / `owner` field is your own EOA or multisig.
Note that during the v2 -> v3 state migration all "Derive Wallets" will be automatically transferred to the user's EOA or multisig.
## Signed-action changes
Porting a v2 encoder? v3 rejects payloads v2 accepted — sub-1e12 precision,
dirty address padding, non-canonical `int256`. If a ported action returns
`14014`, send its `data` to
[`public/decode_action`](/api-reference/system/publicdecode_action): it names
the word and field that broke instead of failing as a bad signature.
### Nonces
In v3, a nonce is a **UTC timestamp in nanoseconds** (\~19 digits for 2025,
e.g. `1751558400000000000`) instead of ms in v2. A millisecond- or microsecond-scale nonce will be rejected.
Beyond the unit change, nonces now have varying validitiy windows depending on action type:
| Action type | Nonce window (relative to server clock / creation) | Must increase? |
| ------------------------------------------------------- | -------------------------------------------------- | -------------- |
| withdraw, transfer, session key, whitelist, liquidation | ± 1 hour | Yes |
| order | 90 days before → 1 hour after | No |
| rfq | ± 1 hour | No |
| vaults | 30 days before → 1 hour after | Yes |
### Deposits
Deposits now happen directly on the ETH L1 instead of a `private/deposit` RPC call.
Refer to the [Depositing](/getting-started/depositing) guide or SDKs for more information.
### Transfers
* `private/withdraw` - gains `max_fee_usd`, `force_batch`, and `recipient` fields to allow for withdrawals to non owner accounts.
* `private/transfer_spot` - replaces `private/transfer_erc20`, and adds a `max_fee_usd` field to allow for automatic creation of new subaccounts.
* \[deprecated] `private/transfer_position` - same effect can be achieved with `private/transfer_positions` (single-leg).
* \[new] `private/transfer_spot_external` — transfers spot to other wallets.
Refer to [Transfers & Withdrawals](/trading/transfers-withdrawals#withdrawals) or SDKs for more information.
### Session keys
* `private/set_session_key` — replaces onchain session key registeration calls and `private/register_scoped_session_key`
* `private/edit_session_key` — existed in v2, but now edits only off-chain fields
(`label`, `ip_whitelist`, `offchain_scopes`). Its v2 `disable` flag was removed.
* To retire a key, re-register the same key via `private/set_session_key` with a past or zero
expiry. Replaces the `deregister_session_key` endpoint.
Refer to [Access scopes](/authentication/access-scopes) or SDKs for more information.
### RFQ quote signatures
Maker and taker quote (`send_quote`) `signature_expiry_sec` must satisfy roughly:
```
rfq_creation + ~11 min ≤ signature_expiry_sec ≤ now + 1 day
```
The lower bound is \~11 minutes after RFQ creation (a 600s validity window plus a 60s buffer);
the upper bound is the current server time plus 1 day. Exact bounds may vary by environment.
## Constants
The hosts and signing constants that changed between v2 and v3.
### Endpoints
| v2 | v3 |
| ------------------------------- | ----------------------------------- |
| `https://api.lyra.finance` | `https://api.derive.xyz/v3` |
| `https://api-demo.lyra.finance` | `https://testnet.api.derive.xyz/v3` |
Note, for users with dedicated connections, there is no change in hostnames (the 'v3' path prefix can also be ignored).
See [Endpoints](/getting-started/introduction#endpoints) for the canonical host/path list and [Connecting](/connecting)
for the WebSocket handshake.
### Module addresses are now shared across deployments
The per-action module contracts — the `module` field selected inside a signed `Action` — are
**identical on testnet and mainnet** in v3. They are fixed protocol constants; hardcoding them
is safe.
| Module | Address | Purpose |
| ------------------------------ | -------------------------------------------- | ---------------------------------------------- |
| `TRADE_MODULE` | `0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b` | Orders and trades |
| `TRANSFER_MODULE` | `0x01259207A40925b794C8ac320456F7F6c8FE2636` | Spot transfers between subaccounts |
| `WITHDRAW_MODULE` | `0x9d0E8f5b25384C7310CB8C6aE32C8fbeb645d083` | Withdrawals to L1 |
| `RFQ_MODULE` | `0x9371352CCef6f5b36EfDFE90942fFE622Ab77F1D` | RFQ execution and position transfers |
| `EXTERNAL_TRANSFER_MODULE` | `0x8F9B8f12ddA05FB1F0DDDDe8f5af8cECF54f8aC9` | Spot transfers to an external subaccount |
| `WHITELISTED_RECIPIENT_MODULE` | `0xB86D6DE1b76c9839e4BA860848CD98A1dABd6B54` | Managing the recipient allow-list |
| `VAULT_MODULE` | `0x2885c174ebf5524aED9c721d60c12b1537685186` | Vault actions (deposit, withdraw, curator ops) |
### Domain separator is recomputed per chain
Unlike the module addresses, the EIP-712 domain separator is **not** shared: v3 recomputes it
per chain. Note, since v3 is now on Ethereum Sepolia & Mainnet, the domain separators must be recomputer.
The `verifyingContract` is constant across every network (the v2 mainnet `Matching`,
`0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD`); only `chainId` differs, so testnet and mainnet
resolve to different separators.
```text theme={null}
domainTypeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
domainSeparator = keccak256(abi.encode(
domainTypeHash,
keccak256("Matching"), // name
keccak256("1.0"), // version
chainId, // 1 mainnet, 11155111 testnet
0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD // verifyingContract (constant across networks)
))
```
Resultant values:
| Environment | `chainId` | `domainSeparator` |
| ----------------- | ---------- | -------------------------------------------------------------------- |
| Mainnet | `1` | `0xda616dfabb88681b08e1592820a41d55ddc62d68de110e327ae99d734506fe19` |
| Testnet (Sepolia) | `11155111` | `0x24d674cd5f2b9d564691c51e9d88f649b99246a2244dd74ce27b96578d773e85` |
### Auth headers: `X-Lyra*` → `X-Derive*`
| v2 header | v3 header |
| ----------------- | ------------------- |
| `X-LyraWallet` | `X-DeriveWallet` |
| `X-LyraTimestamp` | `X-DeriveTimestamp` |
| `X-LyraSignature` | `X-DeriveSignature` |
| `X-LyraScope` | deprecated |
See [Action signing](/authentication/action-signing) or SDKs for more information.
## Settlement changes
In v2, every action was a transaction on the Derive chain. In v3, every action is a zkVM op which is batched and settled together as one Ethereum L1 transaction.
* `transaction_id` -> `op_uuid`
* `tx_hash` on Derive Chain -> `tx_hash` on Ethereum L1 for whole batch
* `tx_status` -> `batch_status`
The full lineage is `op_uuid → operation → batch → L1 tx`, and `tx_hash` (plus `l1_block`) is populated from the batch once it settles.
### `BatchStatus`
| Stage | Healthy | Errored | Explanation |
| --------- | ----------- | ---------------- | --------------------------------------------- |
| Batching | `Batching` | `BatchingError` | Ops processed by the sequencer |
| Executing | `Executing` | `ExecutingError` | Batch run in settler to produce the new state |
| Proving | `Proving` | `ProvingError` | zkVM proof being generated for the batch |
| Settling | `Settling` | `SettlingError` | Proof submitted to L1, awaiting confirmation |
| Settled | `Settled` | `SettledError` | L1 tx confirmed, `tx_hash` and `l1_block` set |
Note, all routes that return `tx_status` in v2 now return `batch_status` in v3.
## Other changes
Note for all of these, refer to the [API Reference](https://docs.derive.xyz/api-reference) and json specs listed at the beginning of this document.
### `public/get_currency` & `public/get_all_currencies`
v3 introduces **risk universes**. A universe is a set of assets, managers and lending pools
that share one security module. Everything risk-related is keyed by `(asset, risk_universe_id)`,
so the same asset can carry different collateral discounts, OI caps and lending terms in
different universes. An insolvency is full contained to one universe.
Start with [Managers & Risk Universes](/trading/managers-and-risk-universes) — it covers
manager ids, universes, and how they decide what a subaccount can trade and hold.
Both routes keep their names and params. The per-currency object is restructured around that
model:
* **Risk data is per universe, not per manager address.** `managers[]` now carries a numeric
`risk_universe_id` plus `sm` / `pm` manager ids. Discounts, OI caps and lending sit under
`option`/`perp`/`spot[]` → `universes[]`. Match on the `risk_universe_id` returned by
`private/get_subaccount`.
* **`spot` is a list.** A currency can have several collateral assets, each with its own
address, `erc20` details, discounts and lending. This is the most likely break for clients
that assumed one spot asset per currency.
* **Lending is no longer USDC-only**, and `instrument_types` and
`srm_perp_margin_requirements` are gone. Infer instrument types from which of `option`,
`perp` and `spot` are present.
### `private/get_subaccount`
New fields: **`manager_id`** (`u32`) and **`risk_universe_id`** (`u32`, the key into the
per-universe currency data above). Also:
* `currency` changed from a single string to a **`string[]`** — a breaking shape change.
* `margin_type` widened from a fixed enum (`PM` | `SM` | `PM2`) to an open string.
* New `failed_to_fetch` (bool) and `vault_deposit_holds` (array) fields.
* `projected_margin_change` is currently always `"0"` (not yet implemented in v3, unlike v2
which computed a real value).
### History routes
* `public/get_trade_history` no longer accepts a `tx_hash` query param.
* **Status field:** deposit / withdrawal / erc20-transfer history **and** trade history (public
and private) expose the settlement status as **`batch_status`** (v2's `tx_status` is renamed).
All use the same set of status values (PascalCase strings) — success stages
`Batching`, `Executing`, `Proving`, `Settling`, `Settled`, and their errored counterparts
`BatchingError`, `ExecutingError`, `ProvingError`, `SettlingError`, `SettledError`. The
value is `null` until the operation is picked up for settlement.
* **Deposit / withdrawal / erc20-transfer history:** each row now carries `operation_id`
(string UUID), `batch_uuid`, and `batch_status`. `tx_hash` is **still present** (nullable)
— it is not removed, but it now resolves to the settling L1 transaction.
### Channel changes
The subscription surface went from 16 channels in v2 to 13 in v3. The full ticker feed and
the settlement-filtered trade channels were removed; every other channel carried over.
| Change | Channel |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Removed | `ticker.{instrument_name}.{interval}` (full ticker feed) |
| Retained | `ticker_slim.{instrument_name}.{interval}` (slim ticker feed) |
| Removed | `trades.{instrument_type}.{currency}.{tx_status}` — settlement is no longer streamed. The live `trades.*` channels now carry the full trade detail (wallet, subaccount, fees, realized PnL); poll `public/get_trade_history` for `batch_status` / `tx_hash` |
| Removed | `{subaccount_id}.trades.{tx_status}` — use `{subaccount_id}.trades` for fills and `private/get_trade_history` for settlement status |
### Deprecated endpoints
| Area | v2 methods not currently exposed | |
| ------------------- | ---------------------------------------------------------------------------------------------------------------- | - |
| Subaccount creation | `private/create_subaccount`, `public/create_subaccount_debug` | |
| Deposits | `private/deposit`, `public/deposit_debug` (deposits are now on-chain — see below) | |
| Settlement history | `public/get_option_settlement_history` (public variant dropped; `private/get_option_settlement_history` remains) | |
| Feeds | `public/get_spot_feed_history`, `public/get_spot_feed_history_candles`, `public/get_perp_impact_twap` | |
| Instruments | `public/get_instruments` (use `get_all_instruments` / `get_all_live_instruments`) | |
| Assets / misc | `public/get_asset` (singular; `get_assets` remains), `public/get_bridge_balances` | |
| Order creation | `private/algo_order`, `private/trigger_order` (use `private/order` with algo/trigger fields) | |
| Order queries | `private/get_orders` (use `get_open_orders` / `get_order_history`) | |
# Coming soon
Source: https://docs.derive.xyz/migrating/coming-soon
v2 methods that are not yet in testnet but will be added before v3 mainnet launch.
| Area | v2 methods |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Liquidations | `private/liquidate`, `private/get_liquidator_history`, `private/get_liquidation_history`, `public/get_liquidation_history` |
| Margin snapshots | `private/get_margin`, `public/get_margin` |
| Statistics | `public/statistics`, `public/all_statistics`, `public/user_statistics`, `public/all_user_statistics` |
# New features
Source: https://docs.derive.xyz/migrating/new-features
Methods and capabilities that v3 adds on top of the v2 surface.
### Permissionless vaults
v3 bakes vaults natively into the protocol - which means you can launch a sophisticated vault strategy within minutes with no-humans-in-the-loop. Vaults can be created, managed, and used entirely through the UX or programmatically.
See the [Vaults](/vaults) guide for more information.
### Risk universes
v3 introduces **risk universes** — isolated risk boundaries, each with its own managers, collateral, and open-interest and lending rules. Losses in one universe are contained to that universe and can never socialize onto another, so a new (and potentially riskier) listing lives walled off from the core book. This lets us **significantly ramp up new collateral, perp, and option listings** — new assets can go live in their own universe without adding risk to existing markets.
`public/get_risk_universes` returns this breakdown directly — one entry per universe with its
managers, tradeable instruments, accepted collaterals, and Security Module. See
[Managers & Risk Universes](/trading/managers-and-risk-universes) for how to read what's supported where.
### 2-sided lending on all collaterals
v2 lending was USDC-only. In v3 **every collateral has a 2-sided lending pool** — you earn supply APY on what you post and can borrow against it, per asset and per risk universe. Live borrow/supply APYs, utilisation, and borrow caps are exposed on each spot asset via `public/get_all_currencies` (`spot[].universes[].lending`).
See [Managers & Risk Universes](/trading/managers-and-risk-universes#reading-public-get-risk-universes) for how universes shape lending per asset.
### Granular session key scopes
In v3, account admins can choose to granular session keys for each type of team member - trading spot vs managing vaults vs purely read-only.
See [Access scopes](/authentication/access-scopes) for more information on session key scopes.
### External transfers & whitelists
v3 adds the ability to transfer spot to a **different owner's** subaccount. Admins can now set allowed recipients for external transfers and withdrawals using the `private/update_whitelisted_recipients` signed action.
See [Transfers & Withdrawals](/trading/transfers-withdrawals).
# DevEx improvements
Source: https://docs.derive.xyz/migrating/v3-improvements
A unified stack - simpler integration
v3 is a re-architecture of the Derive Exchange aimed at improving developer experience and adding powerful new features. The Derive Exchange and the Derive Chain is now combined into a single Zero Knowledge application that settles directly to the Ethereum L1.
### A single wallet on the Ethereum L1
In v3, users can sign actions and move funds directly from their EOAs or multisigs - there is no longer a "Derive Wallet" / "Smart Contract Wallet" in the middle. Furthermore, funds no longer need to be moved to a separate "Derive Chain". Builders can move funds directly from user wallets to the exchange by interacting with Ethereum L1 contracts only.
The v2 -> v3 migration (more information coming soon) will automatically move ownership of user subaccounts from SCWs to the user's EOA or multisig.
### Multi-sig & smart-contract account ownership
In v3 a Derive account can be owned by a **multi-sig or any smart contract** on the Ethereum L1, not just an EOA. Because a contract can't produce the single-key signature that offchain actions need, it operates through **L1 onchain actions**: it deposits to create the account and authorizes an EOA session key straight from L1, and that key then trades, transfers, and runs vaults offchain like any other. Custody and control stay on-chain with the contract.
See [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts) for the full flow.
### Programmatic account creation
Builders of front-ends and products on top of the Derive Exchange no longer need to reach out for access to account creation. Users also do not need to create accounts in the UX before they can use the API or any 3rd party apps. Subaccounts get created automatically when the first deposit happens. This means builders can launch applications within minutes with no humans in the loop.
See the [Programmatic Onboarding](/getting-started/depositing) guide for more information.
### Bridging flexibility
The Derive Exchange now has contracts deployed directly on the Ethereum L1. This allows builders and users to use any bridge they want to move funds to the ETH L1 before depositing to the Derive Exchange.
Furthermore, there are both "Direct" contract-call and "CEX-style" deposit address flows available.
See the [Depositing](/getting-started/depositing) guide for more information.
## Next
* [New features](/migrating/new-features) — what v3 adds on top of v2.
* [Migration skill for your coding agent](/migrating/breaking-changes) — renamed methods, signed-action changes, reshaped responses, and a migration checklist.
# Rate Limits
Source: https://docs.derive.xyz/rate-limits
The two-tier rate-limit model: per-wallet request-budget classes and the per-instrument order-rate limit.
Rate limiting is **two-tier**. Every request first passes a **per-wallet (or per-IP) request budget**, and order-flow requests additionally pass a **per-instrument order-rate limit**. A request that exceeds either tier is rejected with a JSON-RPC error — nothing is queued.
Read your live budgets at any time with [`public/getRateLimits`](#runtime-introspection).
## Tier 1 — Request budget (per wallet)
Every method carries a rate-limit **class** (shown on each method's page in the **API Reference** tab). The class selects which per-wallet budget the request decrements. Authenticated traffic is keyed by **wallet**; unauthenticated/public traffic is keyed by **IP**.
| Class | Applies to | Budget it decrements |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `non_matching` | Read/query methods and non-order mutations (the default) | Per-wallet non-matching budget |
| `matching` | Order-placing / matching methods | Per-wallet matching budget |
| `proven` | Methods that submit a proven operation outside order matching (transfers, withdrawals, session keys, vault ops, liquidations) | Per-wallet proven budget, shared by every `proven` method |
| `endpoint` | A method with its own dedicated per-method budget | Per-`(wallet, method)` budget |
| `custom` | Methods that apply their own limiting logic | Method-specific |
Across the 113 JSON-RPC methods the classes are distributed:
**91** methods
**5** methods
**15** methods
**1** method
**1** method
`order`, `replace`, `cancel`, `cancel_by_instrument`, `cancel_by_nonce`. All
order-flow writes share one per-wallet matching budget, regardless of how
many connections or session keys the wallet has open.
`private/transfer_spot`, `private/transfer_spot_external`,
`private/transfer_positions`, `private/withdraw`, `private/liquidate`,
`private/set_session_key`,
`private/delete_subaccount`, `private/update_whitelisted_recipients`,
`private/create_vault`, `private/mint_vault_shares`,
`private/burn_vault_shares`, `private/request_vault_deposit`,
`private/request_vault_withdraw`, `private/cancel_all_vault_requests`,
`private/force_burn`. Each call is an operation the prover must execute,
so all of them share one small per-wallet budget (`proven` in
`remaining_per_endpoint`), independent of the matching and non-matching
budgets.
`cancel_all` — decrements its own per-`(wallet, method)` budget so a bulk
cancel doesn't consume the shared matching budget.
`cancel_by_label` opts out of the uniform dispatch: it decrements the
matching budget when scoped to a single instrument, otherwise a dedicated
per-method budget.
Budgets are wallet-scoped, **not** per-subaccount or per-session-key. All
concurrent connections and session keys belonging to one wallet share a single
budget. Public, pre-authentication traffic is limited per source IP, along
with a cap on simultaneous WebSocket connections per IP.
### Budget & window semantics
A request budget is a **fixed window**. The number of points available in each window is:
```text theme={null}
points_per_window = TPS × window_seconds
```
Every request costs **one point**; there is no per-method weight table. When the window
elapses the budget resets. Because it is a fixed window (not a sliding one), a client can
briefly emit up to two windows' worth of requests across a window boundary.
## Tier 2 — Per-instrument order-rate limit
Order-flow requests (`order`, `replace`, `cancel`, …) pass a second limiter: a
**per-instrument token bucket**, keyed by `(wallet, instrument)`.
* The **refill rate** is a per-second TPS chosen by asset type — one value for perps and
spot, another for options.
* The bucket's **capacity** (its burst allowance) is `refill_rate × burst_multiplier`.
* Unlike the request budget's fixed window, the bucket **refills continuously**, so it does not
permit a window-boundary double-burst.
An order-placement request is limited **twice**: once by the `matching`
request budget and once by the per-instrument bucket. A rejection can come
from either tier, and both surface the same error code (see below), so a
rejection alone does not tell you which tier tripped. The per-instrument
limits are **not** reported by `public/getRateLimits`.
## Concrete limits
All limits are **deployment-specific** and read from configuration at startup
— code defaults, testnet, and mainnet differ. Treat the values below as a
**mainnet reference snapshot**, not a contract, and always read your live
budget with [`public/getRateLimits`](#runtime-introspection) — the
authoritative, live source for your budget.
| Limiter | Scope | Mainnet reference |
| ------------------------------------ | -------------------------- | ----------------------------- |
| Non-matching budget | Per wallet | 5 TPS → 25 points / 5s window |
| Matching budget | Per wallet | 1 TPS → 5 points / 5s window |
| Proven budget | Per wallet | 1 TPS → 5 points / 5s window |
| `cancel_all` budget | Per `(wallet, method)` | 1 TPS → 5 points / 5s window |
| Public request budget | Per IP | 5 TPS → 25 points / 5s window |
| Concurrent WebSocket clients | Per IP | 4 |
| Per-instrument bucket — perps / spot | Per `(wallet, instrument)` | refill 1 token/s, capacity 5 |
| Per-instrument bucket — options | Per `(wallet, instrument)` | refill 1 token/s, capacity 5 |
## Runtime introspection
`public/getRateLimits` returns your live request budgets. It is itself a `non_matching`
method, so calling it costs one non-matching point. It takes no parameters.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({ network: 'mainnet' });
// No SDK wrapper: use the typed send() escape hatch. Takes no params.
const limits = await client.send('public/getRateLimits', null);
console.log(limits.remaining_matching, limits.remaining_non_matching);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
limits = await client.system.get_rate_limits()
print(limits)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new_public(Environment::Mainnet).await?;
let limits = client.rpc().system().get_rate_limits().await?;
println!("{:?}", limits);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/getRateLimits \
-H "Content-Type: application/json" \
-d '{}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "public/getRateLimits",
"params": {}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"remaining_matching": {
"remainingPoints": 5,
"msBeforeNext": 0,
"consumedPoints": 0,
"isFirstInDuration": true
},
"remaining_non_matching": {
"remainingPoints": 24,
"msBeforeNext": 3120,
"consumedPoints": 1,
"isFirstInDuration": false
},
"remaining_per_endpoint": {
"cancel_all": {
"remainingPoints": 5,
"msBeforeNext": 0,
"consumedPoints": 0,
"isFirstInDuration": true
},
"proven": {
"remainingPoints": 4,
"msBeforeNext": 1870,
"consumedPoints": 1,
"isFirstInDuration": false
}
},
"remaining_connections": {
"remainingPoints": 3,
"msBeforeNext": 0,
"consumedPoints": 1,
"isFirstInDuration": false
}
}
}
```
Live state of the per-wallet matching budget.
Live state of the per-wallet non-matching budget.
Per-method budgets, keyed by method name (e.g. `cancel_all`), plus the
shared `proven` budget. A budget appears once the wallet has used it.
Live state of the per-IP concurrent-connection limiter. Present on WebSocket
only.
Each budget object carries:
Points (tokens) left in the current window.
Milliseconds until the next request is allowed; `0` when a request may go
through now.
Points consumed in the current window.
Whether this is the first request in the current window.
`getRateLimits` exposes only the **request-budget** tier. The per-instrument
token buckets are not reported, so perp/option instrument limits cannot be
discovered at runtime.
## Rejection error codes
When a request exceeds a limit it is rejected — never queued — with a JSON-RPC error.
Codes `-32000` through `-32099` are reserved for rate-limit errors. See
[Error codes](/error-codes) for the full catalogue.
| Code | Message | Raised by |
| -------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `-32000` | `Rate limit exceeded` | Per-wallet / per-IP request budget **or** the per-instrument bucket. The `data` field carries a `Retry after {ms} ms` hint. |
| `-32100` | `Number of concurrent websocket clients limit exceeded` | Per-IP connection-count limiter. The WebSocket receives a `connectionLimitExceeded:` text frame and is then closed. |
```json Rejection theme={null}
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32000,
"message": "Rate limit exceeded",
"data": "Retry after 3120 ms"
}
}
```
Back off using `msBeforeNext` from `getRateLimits` or the `Retry after {ms}{" "}
ms` hint on a `-32000` rejection. Because order flow is limited by both the
`matching` request budget and the per-instrument bucket, spread order flow
across instruments and keep a margin below your reported budget.
# Subscriptions
Source: https://docs.derive.xyz/subscriptions
Real-time WebSocket channels: subscribe to a list of channel names and receive server-pushed notification frames.
Subscriptions deliver real-time updates over WebSocket. You call `subscribe` with a
list of channel names, and the server streams an update frame each time the
underlying data changes.
Subscriptions are **WebSocket-only**. The `subscribe` / `unsubscribe` methods
and the notification frames they produce are not available over HTTP POST. See
[Endpoints](/getting-started/introduction#endpoints) for the WebSocket endpoints.
## Notification model
A subscription update is a JSON-RPC-style **notification frame**: it has a `method` of
`"subscription"` and **no `id`** (it is server-pushed, not a reply to any request). The
payload carries the `channel` that produced it and the `data`:
```json theme={null}
{
"method": "subscription",
"params": {
"channel": "trades.ETH-PERP",
"data": {}
}
}
```
The shape of `data` depends on the channel. For the exact per-channel payload schema,
see the generated **Subscriptions reference** under the API Reference tab.
## Subscribing
Send a `subscribe` request with a `channels` array. The response reports a per-channel
`status` and the connection's full `current_subscriptions` set.
```typescript TypeScript (SDK) theme={null}
import { channel, DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({ network: 'mainnet' });
// Subscriptions live on the websocket; connect() must come first.
await client.connect();
// channel() builds the concrete channel name from its template and
// carries the payload type, so each handler below is fully typed.
const trades = channel('trades.{instrument_name}', {
instrument_name: 'ETH-PERP',
});
const book = channel('orderbook.{instrument_name}.{group}.{depth}', {
instrument_name: 'ETH-PERP',
group: '10',
depth: '20',
});
const tradesSub = await client.subscriptions.subscribe(trades, (batch) => {
for (const trade of batch) {
console.log(
`trade: ${trade.direction} ${trade.trade_amount} @ ${trade.trade_price}`
);
}
});
const bookSub = await client.subscriptions.subscribe(book, (snapshot) => {
console.log(
`book #${snapshot.publish_id}: ${snapshot.bids.length} bids / ${snapshot.asks.length} asks`
);
});
// Later, drop the subscriptions (the last handler on a channel sends unsubscribe):
await tradesSub.unsubscribe();
await bookSub.unsubscribe();
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
# Subscriptions live on the websocket; connect() must come first.
await client.connect()
# One method per channel shape, each with a typed payload.
def on_trades(batch):
for trade in batch:
print(f"trade: {trade.direction} {trade.trade_amount} @ {trade.trade_price}")
def on_book(snapshot):
print(f"book #{snapshot.publish_id}: {len(snapshot.bids)} bids / {len(snapshot.asks)} asks")
await client.public_channels.trades_by_instrument_name(
instrument_name="ETH-PERP", callback=on_trades
)
await client.public_channels.orderbook_group_depth_by_instrument_name(
instrument_name="ETH-PERP", group=10, depth=20, callback=on_book
)
await asyncio.sleep(30)
# There is no per-channel unsubscribe yet; disconnect() ends every
# subscription on the socket.
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
use tokio_stream::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new_public(Environment::Mainnet).await?;
// Each subscribe returns a typed stream of that channel's payload.
let mut trades = client
.subscriptions()
.market_data()
.trades_by_instrument("ETH-PERP")
.await?;
let mut book = client
.subscriptions()
.market_data()
.orderbook("ETH-PERP", "10", "20")
.await?;
loop {
tokio::select! {
Some(Ok(trade)) = trades.next() => {
println!(
"trade: {:?} {} @ {}",
trade.direction, trade.trade_amount, trade.trade_price
);
}
Some(Ok(snapshot)) = book.next() => {
println!(
"book #{}: {} bids / {} asks",
snapshot.publish_id, snapshot.bids.len(), snapshot.asks.len()
);
}
else => break,
}
}
// Later, drop a subscription by channel name:
client.unsubscribe("orderbook.ETH-PERP.10.20").await?;
Ok(())
}
```
```json Request theme={null}
{
"id": 1,
"method": "subscribe",
"params": {
"channels": ["trades.ETH-PERP", "orderbook.ETH-PERP.10.20"]
}
}
```
```json Response theme={null}
{
"id": 1,
"result": {
"status": {
"trades.ETH-PERP": "ok",
"orderbook.ETH-PERP.10.20": "ok"
},
"current_subscriptions": ["trades.ETH-PERP", "orderbook.ETH-PERP.10.20"]
}
}
```
Each `status` entry is `"ok"`, `"already subscribed"`, or an error string for that
channel. If **every** requested channel is invalid, the whole call fails with
[error code 13000 — Invalid channels](/error-codes).
Private channels require an authenticated connection whose session key is authorized
for the target subaccount. See [Authentication](/authentication/session-login) for session login.
## Unsubscribing
Send `unsubscribe` with a `channels` array to drop specific channels. Omit `channels`
(or pass `null`) to unsubscribe from **all** channels on the connection. The response
returns the per-channel `status` and the `remaining_subscriptions` set.
```json theme={null}
{
"id": 2,
"method": "unsubscribe",
"params": { "channels": ["trades.ETH-PERP"] }
}
```
## Channel-name template glossary
Channel names are dot-delimited. Segments in `{braces}` are parameters you fill in.
| Parameter | Meaning | Values |
| ------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `{instrument_name}` | An instrument identifier (e.g. `ETH-PERP`) | See [Instrument names](/trading/instrument-names) |
| `{instrument_type}` | Instrument class | `erc20`, `option`, `perp` |
| `{currency}` | Settlement/collateral currency (e.g. `ETH`) | See [`public/get_risk_universes`](/trading/managers-and-risk-universes#reading-public-get-risk-universes) |
| `{group}` | Orderbook price grouping | `1`, `10`, `100` |
| `{depth}` | Orderbook levels per side | `1`, `10`, `20`, `100` |
| `{interval}` | Slim-ticker publish interval (ms) | `100`, `1000` |
| `{subaccount_id}` | Numeric subaccount id | Your authenticated subaccount |
| `{wallet}` | Wallet address | Your authenticated wallet |
The legacy `ticker.{instrument_name}.{interval}` channel is deprecated —
subscribing to it is rejected. Use `ticker_slim.*` instead.
## Public channels
No authentication required.
| Channel | Address template |
| --------------------------- | --------------------------------------------- |
| Orderbook | `orderbook.{instrument_name}.{group}.{depth}` |
| Slim ticker | `ticker_slim.{instrument_name}.{interval}` |
| Spot feed | `spot_feed.{currency}` |
| Trades (by instrument) | `trades.{instrument_name}` |
| Trades (by type + currency) | `trades.{instrument_type}.{currency}` |
| Auctions | `auctions.watch` |
| Margin | `margin.watch` |
`margin.watch` is an all-users firehose of margin and mark-to-market state. It
is intended for administrative/monitoring use, not per-account data.
Trade events are published at match time and carry the full trade detail
(counterparty wallet and subaccount, fees, realized PnL). Settlement
lifecycle (`batch_status`, `tx_hash`) is not streamed — poll
[`public/get_trade_history`](/api-reference/market-data/publicget_trade_history),
which returns trades in every batch state by default.
### Slim ticker payload
Both the `ticker_slim.*` channel and `public/get_ticker` return the same compact snapshot, keyed by single
letters to keep the feed small:
| Key | Meaning |
| ---------------- | ------------------------------------------------------------------------------- |
| `t` | Snapshot timestamp (ms since epoch) |
| `a` / `A` | Best ask price / amount available at the best ask |
| `b` / `B` | Best bid price / amount available at the best bid |
| `M` | Mark price |
| `I` | Index price |
| `f` | Current hourly funding rate (perps; `null` otherwise) |
| `minp` / `maxp` | Min / max matchable price (the order price band) |
| `stats` | 24h trading statistics (`c` change, `v` volume, `oi` open interest, …) |
| `option_pricing` | Greeks and implied vols for options (`d` delta, `g` gamma, `v` vega, `i` IV, …) |
The `public/get_ticker` response schema in the **API Reference** tab carries every field's full
description.
## Private channels
Require an [authenticated](/authentication/session-login) connection. `{subaccount_id}` channels
stream data for one of your subaccounts; `{wallet}` channels stream data for your wallet.
| Channel | Address template |
| ------------------ | ----------------------------- |
| Balance updates | `{subaccount_id}.balances` |
| Order updates | `{subaccount_id}.orders` |
| Trade updates | `{subaccount_id}.trades` |
| Quote updates | `{subaccount_id}.quotes` |
| Best-quote updates | `{subaccount_id}.best.quotes` |
| RFQ updates | `{wallet}.rfqs` |
The **Subscriptions reference** in the API Reference tab is generated from the
AsyncAPI spec and documents the exact `data` payload for every channel above.
# Cancel on Disconnect
Source: https://docs.derive.xyz/trading/cancel-on-disconnect
Automatically cancel a wallet's resting orders when its WebSocket connection drops.
Cancel-on-disconnect (COD) automatically cancels a wallet's resting orders,
quotes, and trigger orders when its connection drops — protecting you from stale
orders resting on the book after a network failure. On graceful server shutdown,
COD-enabled connections are cancelled as well.
COD is a **persisted account setting**, not a per-frame flag. Enable or disable
it with `private/set_cancel_on_disconnect`:
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// Persisted account setting; requires the Trade(All) scope on the session key.
const result = await client.send('private/set_cancel_on_disconnect', {
enabled: true,
});
console.log(result); // "ok"
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Persisted account setting; requires the Trade(All) scope on the session key.
result = await client.set_cancel_on_disconnect(True)
print(result) # "ok"
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(1234),
)
.await?;
client.login().await?;
// Persisted account setting; requires the Trade(All) scope on the session key.
let result = client.set_cancel_on_disconnect(true).await?;
println!("{:?}", result);
Ok(())
}
```
```bash cURL theme={null}
# Session auth: TS is the current time in ms; SIG is personal_sign(TS) by the wallet.
curl -X POST https://api.derive.xyz/v3/private/set_cancel_on_disconnect \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{ "enabled": true }'
```
```json Request theme={null}
{
"id": 2,
"method": "private/set_cancel_on_disconnect",
"params": { "enabled": true }
}
```
```json Response theme={null}
{
"id": 2,
"result": "ok"
}
```
* The setting is stored against the wallet's account settings and **applied to
every new connection at login** — so once enabled it persists across
reconnects until you disable it.
* Calling `private/set_cancel_on_disconnect` requires the `Trade(All)` protocol
scope on the signing session key (see [Authentication](/authentication/session-login)). A
read-only JWT session cannot toggle it.
* The current value is reported on your account (`cancel_on_disconnect`) via the
account read methods.
In v2 this was a family of `enable`/`disable`/`set` RPCs. In v3 there is a
single `private/set_cancel_on_disconnect` method taking an `enabled` boolean,
gated by the `Trade(All)` scope. If your integration references the older
method names, migrate to this one.
# Instrument Names
Source: https://docs.derive.xyz/trading/instrument-names
How spot, perp, and option instruments are named on the Derive v3 API.
Every tradeable market on the Derive v3 API is identified by a human-readable
`instrument_name` string. The same string is used everywhere an instrument is
referenced: order parameters, RFQ legs, ticker and orderbook queries, and
real-time subscription channels.
Names are **case-sensitive and uppercase**. Each name encodes the market's
currency and, for options, its expiry, strike, and type.
## Formats
`-`
`-PERP`
`---`
| Type | Format | Example |
| ------------- | ---------------------------------- | --------------------- |
| Spot | `-` | `ETH-USDC` |
| Perpetual | `-PERP` | `ETH-PERP` |
| Option (call) | `---C` | `ETH-20240914-2400-C` |
| Option (put) | `---P` | `ETH-20240914-2400-P` |
### Spot
A spot instrument names its base and quote currency, separated by a hyphen —
for example `ETH-USDC`. The quote currency must match the settlement (cash)
currency of the market's risk universe.
### Perpetual
A perpetual is the currency ticker followed by the literal suffix `-PERP`, for
example `BTC-PERP`.
### Option
An option name has four segments:
The underlying ticker, e.g. `ETH`.
The expiry date as an 8-digit `YYYYMMDD`. Options settle at **08:00 UTC** on
that date by default; some markets configure a different settlement time
(e.g. 4pm New York time for US-equity underlyings, which follows US
daylight saving). The exact timestamp is in the instrument's
`option_details.expiry`. `20240914` is 14 September 2024.
The strike price. Integer strikes have no decimal (`2400`). Fractional
strikes use an underscore as the decimal point with trailing zeros trimmed
(`2400_5` = 2400.5). Strikes finer than 1e-6 are rejected.
`C` for a call, `P` for a put.
So `ETH-20240914-2400-C` is an ETH call struck at 2400 expiring 08:00 UTC
(ETH's settlement hour) on 14 September 2024.
## Risk universe suffix
An instrument name may carry an optional trailing `-` segment,
where the id is an unsigned integer — for example `ETH-USDC-42` or
`ETH-PERP-42`. This disambiguates markets on the same currency that belong to
different risk universes.
For each name shape, exactly one instrument may omit the suffix (assigned
first-come, first-served). All others must include it. When present, the id is
validated against the instrument's configured risk universe.
## Querying instruments
Instrument names are assigned by the exchange, not constructed by clients — the
authoritative list comes from the market-data methods. Filter by type using the
`instrument_type` enum, whose values are:
| `instrument_type` | Market type |
| ----------------- | ----------- |
| `erc20` | Spot |
| `perp` | Perpetual |
| `option` | Option |
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
// Public market data needs no wallet, connect, or login.
const client = new DeriveClient({ network: 'mainnet' });
const { instruments } = await client.marketData.getInstruments({
instrumentType: 'option',
currency: 'ETH',
expired: false,
});
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
from derive_py.data_types import AssetType
async def main():
client = WebSocketClient.from_env()
await client.connect()
response = await client.markets.get_all_instruments(
instrument_type=AssetType.option,
currency="ETH",
expired=False,
)
for instrument in response.instruments:
print(instrument.instrument_name)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{
Environment, WsClient,
models::{AssetType, GetAllInstrumentsRequest},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
// Public market data needs no wallet and no login.
let client = WsClient::new_public(Environment::Mainnet).await?;
let params = GetAllInstrumentsRequest::builder()
.instrument_type(AssetType::Option)
.currency("ETH".to_string())
.expired(false)
.try_into()?;
let response = client.rpc().market_data().get_all_instruments(params).await?;
for instrument in response.instruments {
println!("{}", instrument.instrument_name);
}
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/get_all_instruments \
-H "Content-Type: application/json" \
-d '{
"instrument_type": "option",
"currency": "ETH",
"expired": false
}'
```
```json Request theme={null}
{
"id": 1,
"method": "public/get_all_instruments",
"params": {
"instrument_type": "option",
"currency": "ETH",
"expired": false
}
}
```
```json Response theme={null}
{
"id": 1,
"result": {
"instruments": [
{
"instrument_name": "ETH-20240914-2400-C",
"instrument_type": "option",
"is_active": true
}
]
}
}
```
Use `public/get_all_live_instruments` for only currently tradeable markets, or
`public/get_instrument` for the full spec of a single name. See the **API
Reference** tab for the complete response shapes.
## Where instrument names are used
Once you have a name, use it verbatim wherever an instrument is referenced:
* **Order and RFQ parameters** — `instrument_name` on `private/order`,
`private/order_quote`, and each RFQ leg.
* **Market data** — `public/get_ticker`, `public/get_instrument`, and related
lookups.
* **Real-time channels** — the name is a path segment, e.g.
`orderbook.{instrument_name}.{group}.{depth}`,
`ticker_slim.{instrument_name}.{interval}`, and
`trades.{instrument_name}`. Trade channels can also filter by
`instrument_type` and `currency`.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// The instrument_name is used verbatim in the signed order.
const { order } = await client.orders.place({
subaccountId: 1234,
instrumentName: 'ETH-PERP',
direction: 'buy',
amount: '1.5',
limitPrice: '3200',
});
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
from derive_py.data_types import Direction
async def main():
client = WebSocketClient.from_env()
await client.connect()
# The instrument_name is used verbatim in the signed order.
response = await client.orders.create(
instrument_name="ETH-PERP",
direction=Direction.buy,
amount=Decimal("1.5"),
limit_price=Decimal("3200"),
)
print(response.order.instrument_name)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::OrderArgs,
models::{Direction, OrderType, TimeInForce},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(1234),
)
.await?;
client.login().await?;
// The instrument_name is used verbatim in the signed order.
let order = OrderArgs::builder()
.instrument_name("ETH-PERP".to_string())
.direction(Direction::Buy)
.order_type(OrderType::Limit)
.time_in_force(TimeInForce::Gtc)
.amount(BigDecimal::from(1))
.limit_price(BigDecimal::from(3200))
.build();
let response = client.orders().place(order).await?;
println!("{}", response.order.instrument_name);
Ok(())
}
```
```bash cURL theme={null}
# Body nonce/signature: EIP-712 action signing (see /action-signing).
# X-Derive* headers: session auth (see /json-rpc).
curl -X POST https://api.derive.xyz/v3/private/order \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 1234,
"instrument_name": "ETH-PERP",
"direction": "buy",
"amount": "1.5",
"limit_price": "3200"
}'
```
```json Request theme={null}
{
"id": 2,
"method": "private/order",
"params": {
"subaccount_id": 1234,
"instrument_name": "ETH-PERP",
"direction": "buy",
"amount": "1.5",
"limit_price": "3200"
}
}
```
Instrument names are case-sensitive and must be uppercase. A lowercase or
malformed name is rejected — see [Error codes](/error-codes).
For the list of currencies and settlement assets, see
[`public/get_risk_universes`](/trading/managers-and-risk-universes#reading-public-get-risk-universes).
# Managers & Risk Universes
Source: https://docs.derive.xyz/trading/managers-and-risk-universes
How margin managers and risk universes shape what a subaccount can trade and hold.
Every subaccount is bound to a **manager** and, through it, to a **risk universe**. Together they
decide the margin model you get, which instruments you can trade, and which assets you can post as
collateral. You never set the universe directly — you pick a `manager_id`, and everything else
follows.
The margin model applied to your subaccount — **Standard** (cross collateral margin) or
**Portfolio** (scenario-based netting).
Restricted set of instruments, collaterals and lending rules, aimed at containing risk. Losses are socialized only **within** it.
## How they relate
* A subaccount can only have **one** manager.
* A manager belongs to **exactly one** universe.
* A universe exposes **at most one Standard and one Portfolio** manager.
* You choose a `manager_id`; that fixes both your **margin model** and your **universe**.
This breakdown is exactly what `public/get_risk_universes` returns — see
[Reading `public/get_risk_universes`](#reading-public-get-risk-universes) below.
## Managers
A manager is the margin engine that risk-prices your subaccount. There are two:
| Manager | Wire label | Margin model | Best for |
| ------------- | ---------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| **Standard** | `SM` | Cross-collateral: collaterals contribute margin but options margined separately. | Simple/directional positions and collateral-style holding. |
| **Portfolio** | `PM2` | Scenario-based: a whole book netted across a grid of shocks. | Complex option/perp books that benefit from cross-position offsets. |
Each manager has a numeric **`manager_id`**. A subaccount stores its `manager_id` and derives its
universe, tradeable instruments, and accepted collateral from it. Picking Standard vs Portfolio for
the same book changes your margin requirement, not what you can hold.
## Risk universes
A risk universe is a set of assets and managers that share a single risk boundary. It exists so
that an insolvency in one universe can only ever be absorbed by that universe's own Security Module
and, if needed, socialized to solvent accounts **inside the same universe** — never across the
whole exchange. As a direct consequence, **a trade, RFQ, or liquidation is rejected if the two
sides sit in different universes.**
Everything risk-related is keyed by `(asset, risk_universe_id)`: collateral discounts, OI caps, and
lending pools can all differ per universe for the very same asset.
## Two ways to find a manager
* **By risk universe — `public/get_risk_universes`**. Break down all trade-able instruments and supported collaterals by universe and manager.
* **By currency — `public/get_all_currencies`**. Similar to above but broken down by currency. This route also includes market summary data such as APYs, borrow headroom, OI details and caps.
## Reading `public/get_risk_universes`
```json Response (abridged, one universe) theme={null}
{
"risk_universe_id": 1,
"name": "PRIME",
"description": "Blue chip universe",
"managers": [
{
"manager_id": 3,
"margin_type": "SM",
"instruments": ["BTC-OPTION", "BTC-PERP", "ETH-OPTION", "ETH-PERP"],
"collaterals": [
{
"name": "USDC",
"address": "0x…",
"erc20": { "decimals": 6, "underlying_erc20": "0x…" },
"min_deposit_usd": "1",
"im_discount": "1",
"mm_discount": "1"
},
{
"name": "ETH",
"address": "0x…",
"erc20": { "decimals": 18, "underlying_erc20": "0x…" },
"min_deposit_usd": "1",
"im_discount": "0.72",
"mm_discount": "0.8"
}
]
},
{ "manager_id": 12, "margin_type": "PM2", "instruments": ["…"], "collaterals": ["…"] }
],
"security_module": { "subaccount_id": 48291, "cash_asset": "0x…", "cash_currency": "USDC" }
}
```
| To determine… | Read… |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Which universes exist, and what they're called | one entry per universe; `name`/`description` are display metadata (absent until set) |
| Which `manager_id` to use | pick a manager by `margin_type` (`SM` = Standard, `PM2` = Portfolio); its `manager_id` is what you pass |
| What you can trade under a manager | `managers[].instruments[]` — the live perp/option asset names (an option entry names the family) |
| What you can deposit as **collateral** | `managers[].collaterals[]` — `address` is the deposit `asset`, `erc20` the token to send + decimals |
| How much margin a collateral earns | `collaterals[].im_discount` / `mm_discount` under that manager (`"1"` = par credit — the cash asset) |
| Who absorbs losses | `security_module` — the universe's SM subaccount and its cash asset |
For app builders, `public/get_all_currencies` carries useful market data that can be useful for onboarding such as open interest caps, lending limits, APYs.
### Picking your manager
Settle on the instruments you want to trade (e.g. `ETH-OPTION`) and the collateral you'll post
(e.g. `USDC`). If you care about the margin model, decide `SM` (Standard cross-collateral) or
`PM2` (Portfolio) too.
Call `public/get_risk_universes` and scan its managers for the one whose `instruments[]` include
what you want to trade and whose `collaterals[]` include what you'll post (add a `margin_type`
filter if you want a specific model). Its `manager_id` is the id you pass everywhere.
Check your collateral's `im_discount` under that manager is non-zero — `"0"` means the manager
holds it but grants no margin against it.
Pass the chosen id as `manager_id` when depositing to a new subaccount (see [Depositing](/getting-started/depositing)) —
the collateral entry's `address` is the deposit `asset`. Universe, instruments, and collateral
set all follow from it. You can read them back on `private/get_subaccount` via its `manager_id`
and `risk_universe_id`.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
// Public read — no login required.
const client = new DeriveClient({ network: 'mainnet', wallet: process.env.PRIVATE_KEY! });
// Universe-first: pick the manager that trades ETH options and accepts USDC.
const universes = await client.marketData.getRiskUniverses();
const manager = universes
.flatMap((u) => u.managers)
.find((m) => m.instruments.includes('ETH-OPTION') && m.collaterals.some((c) => c.name === 'USDC'))!;
const usdc = manager.collaterals.find((c) => c.name === 'USDC')!;
console.log(manager.manager_id, manager.margin_type, usdc.address);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Universe-first: pick the manager that trades ETH options and accepts USDC.
universes = await client.markets.get_risk_universes()
manager = next(
m
for u in universes
for m in u.managers
if "ETH-OPTION" in m.instruments and any(c.name == "USDC" for c in m.collaterals)
)
usdc = next(c for c in manager.collaterals if c.name == "USDC")
print(manager.manager_id, manager.margin_type, usdc.address)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient};
#[tokio::main]
async fn main() -> Result<(), Box> {
// Public read — no login required.
let client = WsClient::new_public(Environment::Mainnet).await?;
let universes = client.rpc().market_data().get_risk_universes().await?;
// Universe-first: pick the manager that trades ETH options and accepts USDC.
let manager = universes
.iter()
.flat_map(|u| &u.managers)
.find(|m| {
m.instruments.contains(&"ETH-OPTION".to_string())
&& m.collaterals.iter().any(|c| c.name == "USDC")
})
.expect("no manager trades ETH options against USDC");
let usdc = manager
.collaterals
.iter()
.find(|c| c.name == "USDC")
.expect("USDC collateral missing");
println!("{} {:?} {}", manager.manager_id, manager.margin_type, usdc.address);
Ok(())
}
```
Choosing the wrong id has consequences at deposit and trade time:
* **Fallback routing** — depositing to a non-existant manager or one that doesn't support your asset sends the funds to the fallback subaccount (universe `0`).
* **Cross-universe trade** — a subaccount can't trade against a counterparty in a different universe; the order is
rejected.
## The fallback universe
Universe **`0`** is a special **fallback** ("lost-and-found") universe: a no-margin holding area whose
only job is to safely custody collateral that has nowhere else to go. It registers every spot asset
but supports **no trading, borrowing, options, or perps**.
Every wallet is given a single fallback subaccount when its account is created. A deposit lands there —
instead of the subaccount you intended — whenever it can't be honoured as requested:
* it targets the fallback manager (`manager_id` `0`),
* the asset isn't registered in the target manager's universe, or
* the amount is below the subaccount-creation fee.
Funds in the fallback subaccount are safe but idle — you can't trade against them. To put them to work,
move the spot out to a real subaccount with `private/transfer_spot`.
The fallback appears in `public/get_risk_universes` like any other universe (as id `0`, listed
first) — with no tradeable instruments. Skip it when choosing where to deposit.
# Market Maker Protection
Source: https://docs.derive.xyz/trading/market-maker-protection
Auto-cancel a maker's quotes and freeze trading when fills breach configurable thresholds within a rolling window.
Market Maker Protection (MMP) is a safety mechanism for makers. It watches the fills a
subaccount takes within a rolling time window and, when cumulative fills breach a
configured threshold, **freezes** that subaccount for a currency: its resting MMP orders
and quotes are cancelled and further trades in that currency are rejected until the freeze
expires or is manually reset.
MMP protects against rapid adverse selection — for example a stale quote getting swept
across many strikes faster than a maker can react.
MMP is configured **per subaccount, per currency** (e.g. `ETH`, `BTC`). Freezes are also scoped to a single currency:
a freeze on `ETH` does not block trading in `BTC`.
## How it works
Only fills from orders and quotes submitted with the [per-order `mmp` flag](/trading/order-types) set to `true` count toward
MMP limits. Non-MMP orders are ignored by the tracker.
Each MMP fill is recorded with its absolute traded amount and its signed delta. The engine keeps a sliding window of
length `mmp_interval` and sums fills inside it.
If cumulative traded amount exceeds `mmp_amount_limit`, **or** the absolute cumulative delta exceeds
`mmp_delta_limit`, the subaccount is frozen for that currency. Its open MMP orders and quotes are cancelled.
The freeze lasts `mmp_frozen_time`. When it elapses the subaccount can trade again. A freeze of `0` duration is
permanent until you call [`private/reset_mmp`](#reset-a-tripped-state).
A limit of `0` disables that check — set only `mmp_amount_limit` or only `mmp_delta_limit` if you want a single
trigger. An `mmp_interval` of `0` disables MMP entirely for that (subaccount, currency).
## Configuration fields
| Field | Type | Meaning |
| ------------------ | -------------- | ---------------------------------------------------------------------------------------- |
| `subaccount_id` | integer | Subaccount the config applies to. |
| `currency` | string | Settlement currency the config applies to (e.g. `ETH`). |
| `mmp_interval` | integer (ms) | Length of the rolling window over which fills are summed. |
| `mmp_frozen_time` | integer (ms) | How long the freeze lasts once tripped. `0` = frozen until manually reset. |
| `mmp_amount_limit` | decimal string | Cumulative **absolute traded amount** in the window that trips the freeze. `0` disables. |
| `mmp_delta_limit` | decimal string | Cumulative **absolute net delta** in the window that trips the freeze. `0` disables. |
`mmp_interval` and `mmp_frozen_time` are milliseconds. Amount and delta limits are plain decimal strings in the
instrument's units. Each `set_mmp_config` call is a full upsert: every field is written, and an omitted
`mmp_amount_limit` / `mmp_delta_limit` defaults to `0`.
Setting or resetting MMP requires a session key with the `trade:all`
[scope](/authentication/access-scopes); reading the config requires only an authenticated session.
## Configure MMP — `private/set_mmp_config`
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
const config = await client.orders.setMmpConfig({
subaccountId: 12345,
currency: 'ETH',
mmpInterval: 5000,
mmpFrozenTime: 60000,
mmpAmountLimit: '50',
mmpDeltaLimit: '10',
});
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
config = await client.mmp.set_config(
currency="ETH",
mmp_interval=5000,
mmp_frozen_time=60000,
mmp_amount_limit=Decimal("50"),
mmp_delta_limit=Decimal("10"),
)
print(config)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::SetMmpConfigRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(12345),
)
.await?;
client.login().await?;
let params = SetMmpConfigRequest::builder()
.subaccount_id(12345)
.currency("ETH")
.mmp_interval(5000)
.mmp_frozen_time(60000)
.mmp_amount_limit(50)
.mmp_delta_limit(10)
.try_into()?;
let config = client
.rpc()
.market_maker_protection()
.set_mmp_config(params)
.await?;
println!("{:?}", config);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/set_mmp_config \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 12345,
"currency": "ETH",
"mmp_interval": 5000,
"mmp_frozen_time": 60000,
"mmp_amount_limit": "50",
"mmp_delta_limit": "10"
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/set_mmp_config",
"params": {
"subaccount_id": 12345,
"currency": "ETH",
"mmp_interval": 5000,
"mmp_frozen_time": 60000,
"mmp_amount_limit": "50",
"mmp_delta_limit": "10"
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"subaccount_id": 12345,
"currency": "ETH",
"mmp_interval": 5000,
"mmp_frozen_time": 60000,
"mmp_amount_limit": "50",
"mmp_delta_limit": "10"
}
}
```
The example freezes `ETH` trading for 60 seconds if this subaccount's MMP fills accumulate
more than 50 units of absolute amount, or more than 10 units of net delta, within any
5-second window. The response echoes the stored config back.
## Read MMP config — `private/get_mmp_config`
Returns one row per configured `(subaccount_id, currency)`. Pass `currency` to fetch a
single row, or omit it to return every currency for the subaccount.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
const configs = await client.send('private/get_mmp_config', {
subaccount_id: 12345,
currency: 'ETH',
});
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
configs = await client.mmp.get_config(currency="ETH")
for config in configs:
print(config.currency, config.mmp_interval, config.mmp_frozen_time)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::MmpScopeRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(12345),
)
.await?;
client.login().await?;
let params = MmpScopeRequest::builder()
.subaccount_id(12345)
.currency("ETH".to_string())
.try_into()?;
let configs = client
.rpc()
.market_maker_protection()
.get_mmp_config(params)
.await?;
for config in configs {
println!("{} {} {}", config.currency, config.mmp_interval, config.mmp_frozen_time);
}
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/get_mmp_config \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 12345,
"currency": "ETH"
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 2,
"method": "private/get_mmp_config",
"params": {
"subaccount_id": 12345,
"currency": "ETH"
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 2,
"result": [
{
"subaccount_id": 12345,
"currency": "ETH",
"mmp_interval": 5000,
"mmp_frozen_time": 60000,
"mmp_amount_limit": "50",
"mmp_delta_limit": "10",
"is_frozen": false,
"mmp_unfreeze_time": 0
}
]
}
```
`true` while the subaccount is currently frozen for this currency.
Unix epoch milliseconds at which the freeze lifts. `0` when not frozen; a very large value indicates a freeze that
lasts until a manual reset.
## Reset a tripped state — `private/reset_mmp`
Clears the freeze and the rolling window so the subaccount can trade again immediately.
Pass `currency` to reset one currency, or omit it to reset every currency for the
subaccount.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
await client.orders.resetMmp({ subaccountId: 12345, currency: 'ETH' });
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
await client.mmp.reset(currency="ETH")
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::MmpScopeRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(12345),
)
.await?;
client.login().await?;
let params = MmpScopeRequest::builder()
.subaccount_id(12345)
.currency("ETH".to_string())
.try_into()?;
client.rpc().market_maker_protection().reset_mmp(params).await?;
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/reset_mmp \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 12345,
"currency": "ETH"
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 3,
"method": "private/reset_mmp",
"params": {
"subaccount_id": 12345,
"currency": "ETH"
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 3,
"result": "ok"
}
```
While frozen, any new trade in the frozen currency is rejected before settlement. Watch your subaccount order stream
for cancellations tagged with an MMP trigger, then call `private/reset_mmp` once you have re-priced.
## Cancel on disconnect
MMP guards against adverse fills; **cancel-on-disconnect (COD)** guards against a
dropped connection. When a COD-enabled connection drops, the exchange
automatically cancels that wallet's resting orders, quotes, and trigger orders —
so a network failure never leaves stale quotes on the book. Graceful server
shutdowns cancel COD-enabled connections too.
For a market maker the two are complementary: MMP reacts to fills breaching your
thresholds; COD reacts to losing the session entirely.
COD is a **persisted account setting** (not a per-message flag), toggled with
`private/set_cancel_on_disconnect` and gated by the `Trade(All)` scope — once
enabled it applies to every new connection until you disable it.
Full behaviour, the `private/set_cancel_on_disconnect` request, and scope requirements.
## Related
Set the per-order `mmp` flag to include an order in MMP tracking.
The `trade:all` scope required to set and reset MMP.
# Orderbook Trading
Source: https://docs.derive.xyz/trading/order-types
Limit and market orders, time-in-force, direction, order flags, trigger and algo orders, replace, and cancellation.
Every order is placed with `private/order`, a signed [action](/authentication/action-signing) against the
`TRADE_MODULE`. The same payload shape describes limit and market orders, resting and
immediate orders, and (with a few extra fields) trigger and algo orders. This page covers the
enums and flags that shape order behaviour; see the **API Reference** tab for exact
field types and the full request/response schemas.
Prices, amounts, and fees (`limit_price`, `amount`, `max_fee`, `trigger_price`, `extra_fee`) are decimal strings. The
signer signs the fixed-point encoding of these values as part of the EIP-712 action — see [Action
signing](/authentication/action-signing).
## Order type
`order_type` selects how the order interacts with the book. Default `limit`.
| Value | Behaviour |
| -------- | ----------------------------------------------------------------------------------------- |
| `limit` | Rests on the book at `limit_price` (subject to time-in-force). |
| `market` | Crosses the book immediately; any unfilled portion is cancelled rather than left resting. |
`limit_price` is **required even for market orders** — it is a component of the order signature. For a market order it
acts as a worst-acceptable price bound; the unfilled remainder is cancelled instead of resting.
## Time in force
`time_in_force` controls resting vs. immediate execution. Default `gtc`.
| Value | Behaviour |
| ----------- | ---------------------------------------------------------------------------------------------- |
| `gtc` | Good til cancelled — rests until filled, cancelled, or the signature expires. |
| `post_only` | Maker-only limit order; see [`reject_post_only`](#post-only) for cross handling. |
| `fok` | Fill or kill — rejected unless fully filled immediately. |
| `ioc` | Immediate or cancel — fills what it can at the limit (or market) price; the rest is cancelled. |
Orders always expire at `signature_expiry_sec` regardless of time-in-force. `market`, `ioc`,
and `fok` orders never leave a resting order in the book.
## Direction
`direction` is `buy` or `sell`.
## Order flags
Applies to `post_only` orders. When `true`, a post-only order that would cross the book is rejected. When `false`, its
limit price is instead adjusted to one tick away from the best bid/offer so it rests as a maker order.
When `true`, the order can only reduce an existing position — never increase it. If the amount exceeds the current
position size, the order fills up to that size and cancels the remainder. Supported only for market orders and
non-resting limit orders (`ioc` or `fok`).
Tags the order for [Market Maker Protection](/trading/market-maker-protection). Tagged orders count toward the MMP counters
and are cancelled when protection trips.
Optional client-defined tag (max 64 chars). Enables bulk cancel via `private/cancel_by_label`.
`max_fee` is the max fee per unit of volume the signer accepts; the order is rejected if the
estimated fee exceeds it. Optional advanced fields include `referral_code`, `client`,
`reject_timestamp`, `extra_fee`, and `is_atomic_signing` (EIP-1271 atomic-signing orders).
### Place an order
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/sdk';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
const { order, trades } = await client.orders.place({
subaccountId: 1234,
instrumentName: 'ETH-PERP',
direction: 'buy',
orderType: 'limit',
timeInForce: 'gtc',
amount: '1.5',
limitPrice: '3000',
maxFee: '10',
reduceOnly: false,
mmp: false,
});
console.log(order.order_id, order.order_status);
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
from derive_py.data_types import Direction, OrderType
from derive_py.data_types.generated_models import TimeInForce
async def main():
client = WebSocketClient.from_env()
await client.connect()
response = await client.orders.create(
instrument_name="ETH-PERP",
direction=Direction.buy,
order_type=OrderType.limit,
time_in_force=TimeInForce.gtc,
amount=Decimal("1.5"),
limit_price=Decimal("3000"),
max_fee=Decimal("10"),
reduce_only=False,
mmp=False,
)
print(response.order.order_id, response.order.order_status)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::OrderArgs,
models::{Direction, OrderType, TimeInForce},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(1234),
)
.await?;
client.login().await?;
// max_fee and the nonce/signature/expiry are filled in by the SDK.
let order = OrderArgs::builder()
.instrument_name("ETH-PERP".to_string())
.direction(Direction::Buy)
.order_type(OrderType::Limit)
.time_in_force(TimeInForce::Gtc)
.amount(BigDecimal::from(1))
.limit_price(BigDecimal::from(3000))
.reduce_only(false)
.mmp(false)
.build();
let response = client.orders().place(order).await?;
println!("{} {:?}", response.order.order_id, response.order.order_status);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/order \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWallet" \
-H "X-DeriveTimestamp: 1695836058725" \
-H "X-DeriveSignature: 0x…" \
-d '{
"subaccount_id": 1234,
"instrument_name": "ETH-PERP",
"direction": "buy",
"order_type": "limit",
"time_in_force": "gtc",
"amount": "1.5",
"limit_price": "3000",
"max_fee": "10",
"reduce_only": false,
"mmp": false,
"nonce": "1695836058725001000",
"signature_expiry_sec": 1695836358,
"signer": "0xYourWalletOrSessionKey",
"signature": "0x…"
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/order",
"params": {
"subaccount_id": 1234,
"instrument_name": "ETH-PERP",
"direction": "buy",
"order_type": "limit",
"time_in_force": "gtc",
"amount": "1.5",
"limit_price": "3000",
"max_fee": "10",
"reduce_only": false,
"mmp": false,
"nonce": "1695836058725001000",
"signature_expiry_sec": 1695836358,
"signer": "0xYourWalletOrSessionKey",
"signature": "0x…"
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"order": {
"order_id": "8f7e…",
"subaccount_id": 1234,
"instrument_name": "ETH-PERP",
"direction": "buy",
"order_type": "limit",
"time_in_force": "gtc",
"order_status": "open",
"amount": "1.5",
"filled_amount": "0",
"limit_price": "3000",
"average_price": "0",
"mmp": false,
"nonce": "1695836058725001000"
},
"trades": []
}
}
```
Preview an order's price and fee without placing it using `public/order_quote` (unauthenticated) or
`private/order_quote` (authenticated). Both take the **same fully-signed order payload** as `private/order` (including
`nonce`, `signer`, `signature`, and `signature_expiry_sec`) — they simply return the quote instead of resting the
order.
## Trigger (conditional) orders
Supply the trigger fields to submit a stop-loss or take-profit that stays dormant
(`order_status: untriggered`) until its trigger price is reached, then enters the book.
| Field | Values | Notes |
| -------------------- | ------------------------ | ------------------------------------ |
| `trigger_type` | `stoploss`, `takeprofit` | Required for a trigger order. |
| `trigger_price_type` | `mark`, `index` | Reference price the trigger watches. |
| `trigger_price` | decimal string | Price at which the order activates. |
`trigger_price_type: index` is defined in the schema but not yet supported by the matching engine — use `mark`.
Query and cancel trigger orders with `private/get_trigger_orders`,
`private/cancel_trigger_order` (by `order_id`), and `private/cancel_all_trigger_orders`.
## Algo orders
An algo order (e.g. slice-based execution) is submitted by supplying `algo_type` alongside
`algo_duration_sec` and `algo_num_slices`. Its lifecycle status is `algo_active`. Trigger and
algo fields are **mutually exclusive** — an order carrying both is rejected.
`twap` is currently the only supported `algo_type`; any other value is rejected.
Query and cancel algo orders with `private/get_algo_orders`, `private/cancel_algo_order`
(by `order_id`), and `private/cancel_all_algo_orders`.
## Replace (atomic cancel + replace)
`private/replace` atomically cancels an existing order and places a new one in a single call —
avoiding the race where a separate cancel-then-place leaves you briefly out of the book. The
payload is a `private/order` params object plus the cancel-target fields:
| Field | Notes |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| `order_id_to_cancel` | The order to replace. |
| `nonce_to_cancel` | Cancel target by nonce (alternative to `order_id_to_cancel`). |
| `expected_filled_amount` | Optional guard — the replace is rejected if the target's filled amount has moved past this value. |
## Cancelling orders
`private/cancel` — by `order_id` (plus `instrument_name`, `subaccount_id`).
`private/cancel_by_nonce` — cancels the order(s) for a `nonce` on an `instrument_name`.
`private/cancel_by_instrument` — all open orders on one `instrument_name`.
`private/cancel_by_label` — all orders carrying a `label` (optionally scoped to one instrument).
`private/cancel_all` cancels every open order on a subaccount. Set `cancel_trigger_orders` and
`cancel_algo_orders` to also clear untriggered trigger orders and active algo orders.
For multi-leg block trades, use the [RFQ](/trading/rfq) workflow rather than individual orders.
Order-placing and cancel methods are rate-limited under the `matching` class (with
`cancel_all` under `endpoint` and `cancel_by_label` under `custom`); see
[Rate limits](/rate-limits).
# RFQ Trading
Source: https://docs.derive.xyz/trading/rfq
Request a quote on a multi-leg package, collect maker quotes, and execute the block atomically.
The RFQ (request-for-quote) workflow lets a **taker** request a price on a package of one
or more instruments and execute it as a single atomic block trade against a **maker**'s
quote. It is the venue for large or multi-leg structures that would be hard to fill on the
public orderbook.
Every RFQ method is a `private/*` call and works over both WebSocket and HTTP POST. See
[Authentication](/authentication/session-login) for session login and [Endpoints](/getting-started/introduction#endpoints) for
endpoints.
Initiates the RFQ, collects incoming quotes, and signs the execution that fills the block.
Discovers open RFQs, prices them, and submits signed quotes that the taker can execute.
## Legs and packages
An RFQ is defined by its **legs**. Each leg names an instrument, an amount, and a side:
Instrument the leg trades (e.g. `ETH-PERP`).
Contract quantity as a decimal string.
`buy` or `sell` — the taker's side of this leg.
A single-leg RFQ is a simple block; multi-leg RFQs let you price spreads, straddles, and
delta-hedged option structures as one package that fills all-or-nothing at a single
`total_cost`.
Instrument naming conventions are covered on the market-data reference. Resolve exact, currently-tradeable names with
`public/get_all_live_instruments` before submitting an RFQ.
## Signed actions and scopes
The RFQ **request itself is an unsigned intent** — it does not move funds. The state-changing
steps are **EIP-712 signed actions** under the RFQ module:
* The maker signs each **quote** (`private/send_quote`, `private/replace_quote`).
* The taker signs the **execution** (`private/execute_quote`).
These signatures carry a `signer`, `signature`, `nonce`, `signature_expiry_sec`, and `max_fee`,
and are re-verified by the protocol. See [Action signing](/authentication/action-signing) for the `Action`
struct, the RFQ module address, and the nonce/expiry conventions.
Each RFQ method requires the `trade:rfq:` protocol scope for **every** asset type its
legs touch (`trade:rfq:option`, `trade:rfq:perp`, `trade:rfq:spot`, or `trade:rfq:all`).
Cancel methods only require **any one** RFQ trade scope. See [Access scopes](/authentication/access-scopes).
| Method | Signed | Scope requirement |
| ------------------------------------------------------------- | ----------- | --------------------------------- |
| `private/send_rfq` | No | `trade:rfq:` for every leg |
| `private/send_quote` / `private/replace_quote` | Yes (maker) | `trade:rfq:` for every leg |
| `private/execute_quote` | Yes (taker) | `trade:rfq:` for every leg |
| `private/cancel_rfq` / `private/cancel_quote` / batch cancels | No | any one `trade:rfq:*` |
## Taker flow
Call `private/send_rfq` with the package legs. Leave `counterparties` empty to open the RFQ to all makers, or list
wallet addresses to direct it privately.
Poll `private/poll_quotes` for quotes received on your RFQs, or subscribe to the `{subaccount_id}.quotes` and ` {subaccount_id}.best.quotes` channels for live updates. Track your RFQ's own status on `{wallet}.rfqs`.
Call `private/rfq_get_best_quote` with your legs and direction to get the best executable quote for the package.
Sign and submit `private/execute_quote` with the chosen `quote_id`. The engine re-reads the RFQ and maker quote and
settles the block atomically.
### Example: `private/send_rfq`
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login(); // RFQ methods are all private
// Unsigned intent: request quotes for a two-leg covered-call package.
const rfq = await client.rfq.sendRfq({
subaccountId: 1234,
legs: [
{ instrumentName: 'ETH-PERP', amount: '10', direction: 'buy' },
{ instrumentName: 'ETH-20260925-3000-C', amount: '10', direction: 'sell' },
],
label: 'eth-covered-call',
maxTotalCost: '500',
partialFillStep: '1',
});
console.log(
`RFQ ${rfq.rfq_id} open until ${new Date(rfq.valid_until).toISOString()}`
);
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
from derive_py.data_types import Direction
from derive_py.data_types.generated_models import LegUnpricedParams
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Unsigned intent: request quotes for a two-leg covered-call package.
rfq = await client.rfq.send_rfq(
legs=[
LegUnpricedParams(
instrument_name="ETH-PERP", amount=Decimal("10"), direction=Direction.buy
),
LegUnpricedParams(
instrument_name="ETH-20260925-3000-C",
amount=Decimal("10"),
direction=Direction.sell,
),
],
label="eth-covered-call",
max_total_cost=Decimal("500"),
partial_fill_step=Decimal("1"),
)
print(f"RFQ {rfq.rfq_id} open until {rfq.valid_until}")
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
models::{Direction, LegUnpricedParams, SendRfqRequest},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(1234),
)
.await?;
client.login().await?; // RFQ methods are all private
// Unsigned intent: request quotes for a two-leg covered-call package.
let perp: LegUnpricedParams = LegUnpricedParams::builder()
.instrument_name("ETH-PERP")
.amount(BigDecimal::from(10))
.direction(Direction::Buy)
.try_into()?;
let call: LegUnpricedParams = LegUnpricedParams::builder()
.instrument_name("ETH-20260925-3000-C")
.amount(BigDecimal::from(10))
.direction(Direction::Sell)
.try_into()?;
let request = SendRfqRequest::builder()
.subaccount_id(1234)
.legs(vec![perp, call])
.label("eth-covered-call")
.max_total_cost(BigDecimal::from(500))
.partial_fill_step(BigDecimal::from(1))
.try_into()?;
let rfq = client.rfqs().send_rfq(request).await?;
println!("RFQ {} open until {}", rfq.rfq_id, rfq.valid_until);
Ok(())
}
```
```bash cURL theme={null}
# X-Derive* headers: session auth (see /authentication/session-login).
# send_rfq is an unsigned intent, so the body carries no EIP-712 signature.
curl -X POST https://api.derive.xyz/v3/private/send_rfq \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 1234,
"legs": [
{ "instrument_name": "ETH-PERP", "amount": "10", "direction": "buy" },
{ "instrument_name": "ETH-20260925-3000-C", "amount": "10", "direction": "sell" }
],
"label": "eth-covered-call",
"max_total_cost": "500",
"min_total_cost": null,
"partial_fill_step": "1",
"counterparties": null
}'
```
```json Request theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"method": "private/send_rfq",
"params": {
"subaccount_id": 1234,
"legs": [
{ "instrument_name": "ETH-PERP", "amount": "10", "direction": "buy" },
{
"instrument_name": "ETH-20260925-3000-C",
"amount": "10",
"direction": "sell"
}
],
"label": "eth-covered-call",
"max_total_cost": "500",
"min_total_cost": null,
"partial_fill_step": "1",
"counterparties": null
}
}
```
```json Response theme={null}
{
"id": "1",
"jsonrpc": "2.0",
"result": {
"rfq_id": "f2b1c0de-1234-4a56-8b90-abcdef012345",
"subaccount_id": 1234,
"wallet": "0x1a2b...",
"label": "eth-covered-call",
"status": "open",
"valid_until": 1758801000000,
"legs": [
{ "instrument_name": "ETH-PERP", "amount": "10", "direction": "buy" },
{
"instrument_name": "ETH-20260925-3000-C",
"amount": "10",
"direction": "sell"
}
],
"max_total_cost": "500",
"min_total_cost": null,
"filled_pct": "0"
}
}
```
Identifier used to reference this RFQ in quotes, polls, and execution.
One of `open`, `filled`, `cancelled`, `expired`.
Millisecond timestamp after which the RFQ expires and no longer accepts quotes.
An RFQ is quotable only for a bounded window after creation (`valid_until`). The default window is 10 minutes.
The exact window is deployment-specific and may differ per environment; confirm it for your target environment.
Optional `send_rfq` params: `label`, `counterparties`, `min_total_cost`, `max_total_cost`
(decimal strings bounding the acceptable package cost), `partial_fill_step` (minimum fill
increment, default `1`), `client`, `extra_fee`, and `referral_code`.
## Maker flow
Poll `private/poll_rfqs` for RFQs visible to you (open-to-all, or those naming your wallet in `counterparties`), or
subscribe to the `{wallet}.rfqs` channel.
Sign and submit `private/send_quote` with priced legs (each leg adds a `price` to the RFQ's `instrument_name` /
`amount` / `direction`), a `direction`, and `max_fee`. Set `mmp` to opt the quote into [market-maker
protection](/trading/market-maker-protection).
Use `private/replace_quote` to atomically cancel and re-submit a quote (provide one of `quote_id_to_cancel` or
`nonce_to_cancel`), or `private/cancel_quote` / `private/cancel_batch_quotes` to withdraw quotes. Track your quotes
on `{subaccount_id}.quotes`.
Preview a quote or execution signature before sending with the public `public/send_quote_debug` and
`public/execute_quote_debug` helpers — they return the EIP-712 encoded payload and hashes so you can byte-compare a
rejected signature. They are a debugging aid, not a required step.
## Subscription channels
RFQ workflows are event-driven; prefer the WebSocket channels over repeated polling.
| Channel | Address | Audience |
| ------------- | ----------------------------- | --------------------------------- |
| RFQ updates | `{wallet}.rfqs` | Maker discovery; taker RFQ status |
| Quote updates | `{subaccount_id}.quotes` | Maker's own quotes |
| Best quotes | `{subaccount_id}.best.quotes` | Taker's best incoming quote |
See [Subscriptions](/subscriptions) for the channel envelope and payload formats.
## Related methods
The full RFQ surface — including `private/get_rfqs`, `private/get_quotes`,
`private/cancel_rfq`, and `private/cancel_batch_rfqs` — is documented in the
**API Reference** tab.
# Transfers & Withdrawals
Source: https://docs.derive.xyz/trading/transfers-withdrawals
Move collateral between subaccounts, to other wallets, and on-chain, plus transferring positions.
Moving value in the Derive v3 API is always a **[signed action](/authentication/action-signing)**: your wallet (or a scoped [session key](/authentication/session-keys)) EIP-712-signs an `Action` envelope, and the protocol re-verifies the signature before applying it. Four methods cover the distinct destinations:
Move collateral between two subaccounts you own.
Send collateral to a subaccount owned by a **different** wallet, bounded by your recipient allow-list.
Withdraw collateral on-chain to an L1 recipient address.
Move open positions between subaccounts, booked as an RFQ trade.
Each method needs a specific [protocol scope](/authentication/access-scopes) on the signing key, and each carries a `nonce`, `signer`, `signature`, and `signature_expiry_sec` built exactly as described in [Action signing](/authentication/action-signing). The action `module` for each flow is filled in by the server and does not appear on the wire — it is part of the signed struct hash and must match the deployment's module address in [Action signing](/authentication/action-signing#per-action-modules-and-data).
Amounts, prices, and fees are human decimals on the wire — decimal strings (e.g. `"100.5"`) or JSON numbers. The
exception is `private/withdraw`, whose on-chain amount uses the asset's **native ERC-20 decimals** (see below).
## Transfer collateral between your subaccounts
`private/transfer_spot` moves a spot balance from one of your subaccounts to another subaccount **you own** (existing, or a new one created in the same call). Positions are not moved by this method — use [`private/transfer_positions`](#transfer-positions-between-subaccounts) for that.
**Required scope:** `transfer:existing_subaccount` **or** `transfer:new_subaccount` (see [Access scopes](/authentication/access-scopes)).
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect(); // open the websocket
await client.login(); // authenticate the session — required for private/\*
// transferInternal resolves 'USDC' to its protocol asset, encodes and
// EIP-712-signs the transfer action locally, then submits private/transfer_spot.
const result = await client.spotTransfers.transferInternal({
subaccountId: 9,
toSubaccountId: 12,
asset: 'USDC',
amount: '250',
maxFeeUsd: '1.5',
});
console.log(result.op_uuid, result.operation_id);
await client.close();
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# transfer_spot resolves "USDC" to its protocol asset, encodes and
# EIP-712-signs the transfer action locally, then submits private/transfer_spot.
result = await client.collateral.transfer_spot(
to_subaccount_id=12,
asset_name="USDC",
amount=Decimal("250"),
max_fee_usd=Decimal("1.5"),
)
print(result.op_uuid, result.operation_id)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{Environment, WsClient, actions::SpotTransferArgs};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
client.login().await?;
// The SDK resolves "USDC" to its protocol asset from the client's cache,
// EIP-712-signs the transfer action, then submits private/transfer_spot.
let args = SpotTransferArgs::builder()
.subaccount_id(9)
.to_subaccount_id(12)
.new_subaccount_manager(0) // 0 = transfer into an existing subaccount
.asset("USDC".to_string())
.amount(BigDecimal::from(250))
.max_fee_usd("1.5".parse()?)
.build();
let result = client.fund_movements().transfer_spot(args).await?;
println!("{} {}", result.op_uuid, result.operation_id);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/transfer_spot \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWalletAddress" \
-H "X-DeriveTimestamp: 1731000000000" \
-H "X-DeriveSignature: 0x…session-signature" \
-d '{
"subaccount_id": 9,
"to_subaccount_id": 12,
"new_subaccount_manager": 0,
"asset_name": "USDC",
"sub_id": 0,
"amount": "250",
"max_fee_usd": "1.5",
"nonce": "1731000000000123000",
"signer": "0xYourWalletOrSessionKey",
"signature": "0x...",
"signature_expiry_sec": 1731000300
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/transfer_spot",
"params": {
"subaccount_id": 9,
"to_subaccount_id": 12,
"new_subaccount_manager": 0,
"asset_name": "USDC",
"sub_id": 0,
"amount": "250",
"max_fee_usd": "1.5",
"nonce": "1731000000000123000",
"signer": "0xYourWalletOrSessionKey",
"signature": "0x...",
"signature_expiry_sec": 1731000300
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"op_uuid": "b2c3...",
"operation_id": 84213
}
}
```
## Transfer positions between subaccounts
`private/transfer_positions` moves open positions from one subaccount to another. It is **booked as an RFQ-module trade**: both sides sign a transfer quote over the same legs, and the transfer clears at the agreed prices. The signing key needs a `transfer:*` scope covering the destination (`transfer:existing_subaccount`, `transfer:new_subaccount`, or `transfer:different_owner_subaccount`).
Both quotes sign the same legs hash; each side authorizes its own `max_fee`. The response returns the resulting `maker_quote` and `taker_quote`.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// The SDK resolves each leg's instrument, signs the maker quote and the
// matching opposite-direction taker execute (both zero-fee), then submits
// private/transfer_positions. The taker's 'sell' direction is derived here.
const result = await client.positionTransfers.transferPositions({
makerSubaccountId: 9,
takerSubaccountId: 12,
makerDirection: 'buy',
legs: [
{ instrumentName: 'ETH-PERP', amount: '1', price: '0', direction: 'buy' },
],
});
console.log(result.maker_quote, result.taker_quote);
await client.close();
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
from derive_py.data_types import Direction, PositionTransfer
async def main():
client = WebSocketClient.from_env()
await client.connect()
# The SDK signs the maker quote and the matching opposite-direction taker
# execute (both zero-fee), then submits private/transfer_positions.
result = await client.positions.transfer(
positions=[PositionTransfer("ETH-PERP", Decimal("1"))],
direction=Direction.buy,
to_subaccount=12,
)
print(result.maker_quote, result.taker_quote)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::TransferPositionsArgs,
models::{Direction, PricedLegParamsAndResponse},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
client.login().await?;
let leg: PricedLegParamsAndResponse = PricedLegParamsAndResponse::builder()
.instrument_name("ETH-PERP")
.amount(BigDecimal::from(1))
.price(BigDecimal::from(0))
.direction(Direction::Buy)
.try_into()?;
// The SDK signs the maker quote and the matching opposite-direction taker
// execute (both zero-fee), then submits private/transfer_positions.
let args = TransferPositionsArgs::builder()
.legs(vec![leg])
.from_subaccount_id(9)
.to_subaccount_id(12)
.maker_direction(Direction::Buy)
.max_fee(BigDecimal::from(0))
.build();
let result = client.fund_movements().transfer_positions(args).await?;
println!("{:?} {:?}", result.maker_quote, result.taker_quote);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/transfer_positions \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWalletAddress" \
-H "X-DeriveTimestamp: 1731000000000" \
-H "X-DeriveSignature: 0x…session-signature" \
-d '{
"wallet": "0xYourWallet",
"maker_params": {
"direction": "buy",
"legs": [{ "instrument_name": "ETH-PERP", "amount": "1", "price": "0", "direction": "buy" }],
"max_fee": "1.5",
"subaccount_id": 9,
"nonce": "1731000000000321000",
"signer": "0xYourWallet",
"signature": "0x...",
"signature_expiry_sec": 1731000300
},
"taker_params": {
"direction": "sell",
"legs": [{ "instrument_name": "ETH-PERP", "amount": "1", "price": "0", "direction": "buy" }],
"max_fee": "1.5",
"subaccount_id": 12,
"nonce": "1731000000000322000",
"signer": "0xYourWallet",
"signature": "0x...",
"signature_expiry_sec": 1731000300
}
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/transfer_positions",
"params": {
"wallet": "0xYourWallet",
"maker_params": {
"direction": "buy",
"legs": [
{
"instrument_name": "ETH-PERP",
"amount": "1",
"price": "0",
"direction": "buy"
}
],
"max_fee": "1.5",
"subaccount_id": 9,
"nonce": "1731000000000321000",
"signer": "0xYourWallet",
"signature": "0x...",
"signature_expiry_sec": 1731000300
},
"taker_params": {
"direction": "sell",
"legs": [
{
"instrument_name": "ETH-PERP",
"amount": "1",
"price": "0",
"direction": "buy"
}
],
"max_fee": "1.5",
"subaccount_id": 12,
"nonce": "1731000000000322000",
"signer": "0xYourWallet",
"signature": "0x...",
"signature_expiry_sec": 1731000300
}
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"maker_quote": { "...": "..." },
"taker_quote": { "...": "..." }
}
}
```
## Transfer collateral to another wallet
`private/transfer_spot_external` sends collateral to a subaccount owned by a **different** wallet. The destination owner must be on the sender's **whitelisted-recipient allow-list**, and the signing key needs `transfer:different_owner_subaccount`.
### Managing the recipient allow-list
Withdrawals or transfers to external subaccounts can only reach wallets you have explicitly whitelisted.
Manage the list with `private/update_whitelisted_recipients`. Permissions that can modify this list are:
* owner
* `admin` scoped session key
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// The resulting allow-list is (current ∪ add) \ remove; the wallet is taken
// from the signing credentials. This is itself an admin-scoped signed action.
const whitelist = await client.spotTransfers.updateWhitelistedRecipients({
add: ['0xRecipientA', '0xRecipientB'],
remove: ['0xOldRecipient'],
});
console.log(whitelist.whitelisted_recipients);
await client.close();
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# The resulting allow-list is (current | add) - remove; the wallet is taken
# from the signing credentials. This is itself an admin-scoped signed action.
whitelist = await client.account.update_whitelisted_recipients(
add=["0xRecipientA", "0xRecipientB"],
remove=["0xOldRecipient"],
)
print(whitelist.whitelisted_recipients)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::UpdateWhitelistedRecipientsRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
client.login().await?;
// The resulting allow-list is (current | add) \ remove. This is an
// admin-scoped signed action, so nonce/signature must be supplied.
let params = UpdateWhitelistedRecipientsRequest::builder()
.wallet(std::env::var("DERIVE_WALLET")?)
.add(vec!["0xRecipientA".to_string(), "0xRecipientB".to_string()])
.remove(vec!["0xOldRecipient".to_string()])
.try_into()?;
let whitelist = client
.rpc()
.transfers_withdrawals()
.update_whitelisted_recipients(params)
.await?;
println!("{:?}", whitelist.whitelisted_recipients);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/update_whitelisted_recipients \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWalletAddress" \
-H "X-DeriveTimestamp: 1731000000000" \
-H "X-DeriveSignature: 0x…session-signature" \
-d '{
"wallet": "0xYourWallet",
"add": ["0xRecipientA", "0xRecipientB"],
"remove": ["0xOldRecipient"],
"nonce": "1731000000000456000",
"signer": "0xYourWallet",
"signature": "0x...",
"signature_expiry_sec": 1731000600
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/update_whitelisted_recipients",
"params": {
"wallet": "0xYourWallet",
"add": ["0xRecipientA", "0xRecipientB"],
"remove": ["0xOldRecipient"],
"nonce": "1731000000000456000",
"signer": "0xYourWallet",
"signature": "0x...",
"signature_expiry_sec": 1731000600
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"op_uuid": "c4d5...",
"operation_id": 84220,
"whitelisted_recipients": ["0xRecipientA", "0xRecipientB"]
}
}
```
## Withdraw
`private/withdraw` removes collateral from a subaccount and settles it to an **Ethereum L1 recipient**. It is a signed action requiring the `withdraw` scope.
Withdrawals signed by a session key must go to an address on the owner's **whitelisted-recipient allow-list**, including the owner wallet itself. See [Managing the recipient allow-list](#managing-the-recipient-allow-list) above.
`amount_in_underlying` is denominated in the asset's **native ERC-20 decimals** (e.g. 6 for USDC), **not** the decimal
convention used by the transfer methods above. Match the on-chain token's decimals exactly.
`recipient` is optional and **defaults to the account's owner wallet** — not the signer, and not the subaccount. The
signature commits to it, so the server cannot redirect a payout: a `recipient` disagreeing with what you signed fails
signature verification rather than being honored.
A session key may only pay out to an address on the owner's whitelist — **including the owner wallet itself**, which is
not whitelisted implicitly. Whitelist it before granting a key the `withdraw` scope, or the withdrawal is rejected with
`RecipientNotWhitelisted`. The owner and `admin`-scoped keys skip the whitelist entirely.
```typescript TypeScript (SDK) theme={null}
import { DeriveClient } from '@derivexyz/derive-ts';
const client = new DeriveClient({
network: 'mainnet',
wallet: process.env.PRIVATE_KEY!,
});
await client.connect();
await client.login();
// Pass the amount in HUMAN units ("1000" = 1000 USDC); the SDK looks up the
// token's native ERC-20 decimals and scales the signed amount_in_underlying.
const result = await client.withdrawals.withdraw({
subaccountId: 9,
asset: 'USDC',
amount: '1000',
maxFeeUsd: '1.5',
forceBatch: false,
// Optional. Defaults to the owner wallet; any other address must be
// whitelisted unless the owner or an admin-scoped key signs.
recipient: '0xYourL1Address',
});
console.log(result.op_uuid, result.operation_id);
await client.close();
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Pass the amount in HUMAN units ("1000" = 1000 USDC); the SDK looks up the
# token's native ERC-20 decimals and scales the signed amount.
# The payout goes to the owner wallet unless a recipient is given.
result = await client.active_subaccount.withdraw(
asset_name="USDC",
amount=Decimal("1000"),
max_fee_usd=Decimal("1.5"),
force_batch=False,
)
print(result.op_uuid, result.operation_id)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{Environment, WsClient, actions::WithdrawArgs};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(9),
)
.await?;
client.login().await?;
// amount is in HUMAN units; the SDK scales it by the token's native
// ERC-20 decimals when signing. The payout goes to recepient_address.
let args = WithdrawArgs::builder()
.asset("USDC".to_string())
.amount(BigDecimal::from(1000))
.max_fee_usd("1.5".parse()?)
.recepient_address(std::env::var("DERIVE_WALLET")?.parse()?)
.force_batch(false)
.build();
let result = client.fund_movements().withdraw(args).await?;
println!("{} {}", result.op_uuid, result.operation_id);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/withdraw \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: 0xYourWalletAddress" \
-H "X-DeriveTimestamp: 1731000000000" \
-H "X-DeriveSignature: 0x…session-signature" \
-d '{
"subaccount_id": 9,
"asset_name": "USDC",
"amount_in_underlying": "1000000000",
"force_batch": false,
"max_fee_usd": "1.5",
"recipient": "0xYourL1Address",
"nonce": "1731000000000789000",
"signer": "0xYourWalletOrSessionKey",
"signature": "0x...",
"signature_expiry_sec": 1731000300
}'
```
```json Request theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"method": "private/withdraw",
"params": {
"subaccount_id": 9,
"asset_name": "USDC",
"amount_in_underlying": "1000000000",
"force_batch": false,
"max_fee_usd": "1.5",
"recipient": "0xYourL1Address",
"nonce": "1731000000000789000",
"signer": "0xYourWalletOrSessionKey",
"signature": "0x...",
"signature_expiry_sec": 1731000300
}
}
```
```json Response theme={null}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"op_uuid": "d6e7...",
"operation_id": 84231
}
}
```
`public/withdraw_debug` returns the EIP-712-encoded data and hashes for a withdraw action, so you can byte-compare
against your local signing to diagnose rejected signatures. It is a debugging aid, not a required step — see [Action
signing](/authentication/action-signing) for the full set of `*_debug` signing-preview helpers.
# Create a Vault
Source: https://docs.derive.xyz/vaults/create-a-vault
Launch a vault in 3 minutes via the UX or SDK.
A **vault** is a managed subaccount that outside depositors ("shareholders")
buy into. Shareholders deposit a spot asset and receive **shares**; you — the
**curator** — trade the vault's balance like any other subaccount. You earn
management and performance fees on what you run.
A vault can be managed end-to-end via both UX or programmatically.
Vaults can trade any instrument and on any venue that subaccounts can:
* Orderbook, RFQ, lending
* Spot, perpetuals, options
You can fund and launch a vault that will show up in the Derive Vaults page instantly
without manual approval!
## How a vault works
A vault is one subaccount with extra protocol state on top:
* **Shares** — `total_shares` outstanding across all holders (you included). A deposit
mints shares; a withdrawal burns them.
* **NAV** — the vault subaccount's live mark-to-market value in USD, from the risk
engine. `share price ≈ NAV / total_shares`.
* **High-water mark (HWM)** — the highest share price the vault has reached.
Performance fees only accrue on new gains above it — see [Fees](/vaults/fees).
Running a vault breaks into four jobs, each with its own page:
How management and performance fees accrue, the high-water mark, and the protocol's share.
The settle loop: poll queued shareholder intents and settle each at a quoted price.
Run your strategy on the vault subaccount with the normal trading API.
Exit every holder, take total shares to zero, and close the vault.
## Prerequisites
1. Create and fund an account on Derive.
2. Create a session key with proper scopes.
Running the vault from a multi-sig or smart contract? See [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts) to setup an account and session key from the L1. You can continue to next steps once that step is complete.
### Required Session Key Scopes
| Role | SDK methods | Scope |
| ------- | --------------------------------------------------------------------------------------------------- | ----------------------------- |
| Curator | `createVault`, `updateInfo` | `vault:curator_create` |
| Curator | `mintShares`, `burnShares`, `forceBurn`, `rejectDepositRequest`, and the live mint/burn queue reads | `vault:curator_mint_and_burn` |
## Choose your immutable parameters
You fix these when you create the vault. They shape what shareholders pay and what you
earn.
Choose between Standard vs Portfolio Manager. Managers also determine the risk universe — see [Managers & Risk Universes](/trading/managers-and-risk-universes).
Spot asset shareholders deposit and withdraw in.
Initial share price. See [Choosing the Initial Share
Price](#choosing-the-initial-share-price).
Annualised management fee, in basis points, accrued pro-rata over time on NAV regardless of performance.
Performance fee, in basis points, charged only on gains above the high-water mark relative to a benchmark.
Performance fees charge only on **outperformance versus that currency**. Default benchmarks against USD.
Maximum deviation of the share price during deposits / withdrawals from the oracle mark-to-market value of the vault.
Minimum seconds between a holder's last deposit and a withdrawal (prevents excessive churn).
Maximum sequencer fee you authorise for this action, in USD. This fee covers the vault-creation fee (\$1000).
### Choosing the Initial Share Price
For most vaults, `initialSharePriceUsd: '1'` is the natural choice: shares start at
\$1.00, and the share price thereafter reads directly as the vault's cumulative USD
performance.
If your vault tracks a benchmark, consider seeding the share price at the benchmark's
**current spot price** instead. One share then starts worth exactly one unit of the
benchmark, and the share-price-to-benchmark ratio becomes a live measure of relative
performance that shareholders can read at a glance. For example, launch an
ETH-benchmarked vault while ETH trades at \$1,950 with `initialSharePriceUsd: '1950'`:
a share is worth 1.0 ETH at inception, and if the vault outperforms ETH by 10%, a
share is worth 1.1 ETH — visible directly in the price, with no reference to when
anyone entered.
The seed price must lie between 0.01 and 1,000,000. When `benchmarkAsset` is set, it must additionally be within 1000×
of the benchmark's spot price in either direction.
### Global limits
Your config is bounds-checked at creation against a protocol-wide, **deployment-set**
global config:
| Global cap | Value | Constrains |
| ---------------------------- | ---------------- | ------------------------------------------------------------------------------- |
| `min_creation_deposit_usd` | \$10,000 | Minimum `initialDeposit` (USD) to open a vault |
| `min_curator_stake_usd` | \$10,000 | Flat floor on curator skin-in-the-game, checked on your own withdrawals |
| `min_curator_stake_frac_bps` | 100 (1%) | Fractional stake floor on vault value — the **greater** of the two floors binds |
| `global_max_slippage_bps` | 300 (3%) | Ceiling on `maxSlippageBps` |
| `max_management_fee_bps` | 300 (3%) | Ceiling on `managementFeeBps` |
| `max_performance_fee_bps` | 5,000 (50%) | Ceiling on `performanceFeeBps` |
| `min_cooldown_sec` | 60 (1 min) | Floor on `cooldownSec` |
| `max_cooldown_sec` | 604,800 (7 days) | Ceiling on `cooldownSec` |
| `vault_creation_fee` | \$1,000 | One-time fee paid from depositing account. Reach out for custom arrangements. |
## Create the vault and resolve its id
```typescript TypeScript (SDK) theme={null}
const curator = client.vaults.curator;
// Snapshot the vaults you already curate, to diff against below.
const before = new Set((await curator.getCuratedVaults()).subaccount_ids);
await curator.createVault({
subaccountId: 1234, // funding subaccount — the seed deposit leaves here
managerId: 1,
depositSpotAsset: '0x…', // your environment's USDC spot-asset address
initialDeposit: '15000',
initialSharePriceUsd: '1',
managementFeeBps: 100, // 1% p.a.
performanceFeeBps: 1000, // 10% of gains above the high-water mark
maxSlippageBps: 50,
cooldownSec: 86400, // 24h between a holder's deposit and their withdrawal
maxFeeUsd: '1000', // must cover the deployment-set creation fee
// benchmarkAsset: '0x…', // optional: denominate the HWM in a spot asset
});
let vaultId: number | undefined;
while (vaultId === undefined) {
await new Promise((resolve) => setTimeout(resolve, 500));
const { subaccount_ids } = await curator.getCuratedVaults();
vaultId = subaccount_ids.find((id) => !before.has(id));
}
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# The funding subaccount you act as — the seed deposit leaves here.
curator = await client.fetch_subaccount(1234)
# Snapshot the vaults you already curate, to diff against below.
before = set((await client.vaults.list_curated()).subaccount_ids)
await curator.vaults.create(
manager_id=1,
deposit_spot_asset="0x…", # your environment's USDC spot-asset address
initial_deposit=Decimal("15000"),
initial_share_price_usd=Decimal("1"),
management_fee_bps=100, # 1% p.a.
performance_fee_bps=1000, # 10% of gains above the high-water mark (the global cap)
max_slippage_bps=50,
cooldown_sec=86400, # 24h between a holder's deposit and their withdrawal
max_fee_usd=Decimal("1000"), # must cover the deployment-set creation fee
# benchmark_asset="0x…", # optional: denominate the HWM in a spot asset
)
vault_id = None
while vault_id is None:
await asyncio.sleep(0.5)
after = set((await client.vaults.list_curated()).subaccount_ids)
vault_id = next(iter(after - before), None)
print(vault_id)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use std::collections::HashSet;
use bigdecimal::BigDecimal;
use derive_rs::{Environment, WsClient, actions::CreateVaultArgs, models::GetCuratedVaultsRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let wallet = std::env::var("DERIVE_WALLET")?;
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(wallet.clone()),
Some(1234), // funding subaccount — the seed deposit leaves here
)
.await?;
client.login().await?;
let curated = || GetCuratedVaultsRequest::builder().wallet(&wallet).try_into();
// Snapshot the vaults you already curate, to diff against below.
let before: HashSet = client
.rpc()
.vault_curators()
.get_curated_vaults(curated()?)
.await?
.subaccount_ids
.into_iter()
.collect();
client
.vaults()
.create(
CreateVaultArgs::builder()
.subaccount_id(1234)
.manager_id(1)
.deposit_spot_asset("0x…".to_string()) // your environment's USDC spot-asset address
.initial_deposit(BigDecimal::from(15_000))
.initial_share_price_usd(BigDecimal::from(1))
.management_fee_bps(100) // 1% p.a.
.performance_fee_bps(1000) // 10% of gains above the high-water mark (the global cap)
.max_slippage_bps(50)
.cooldown_sec(86_400) // 24h between a holder's deposit and their withdrawal
.max_fee_usd(BigDecimal::from(1000)) // must cover the deployment-set creation fee
// .benchmark_asset("0x…".to_string()) // optional: denominate the HWM in a spot asset
.build(),
)
.await?;
let vault_id = loop {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let after = client.rpc().vault_curators().get_curated_vaults(curated()?).await?;
if let Some(id) = after.subaccount_ids.into_iter().find(|id| !before.contains(id)) {
break id;
}
};
println!("{vault_id}");
Ok(())
}
```
That `vaultId` is the `vaultSubaccountId` every other vault call takes.
## Read it back
`getVault` is public — anyone can read any vault's config and live pricing:
```typescript TypeScript (SDK) theme={null}
const vault = await client.vaults.getVault(vaultId);
console.log(vault.protocol.config); // the immutable economics you set above
// nav_usd / simulated_share_price_usd are null when the vault cannot be
// priced right now, so never assume they are set.
console.log(
`NAV $${vault.nav_usd ?? '?'}, share price $${
vault.simulated_share_price_usd ?? '?'
}`
);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
vault = await client.vaults.get(vault_subaccount_id=vault_id)
print(vault.protocol.config) # the immutable economics you set above
# nav_usd / simulated_share_price_usd are unset when the vault cannot be
# priced right now, so never assume they are set.
print(f"NAV ${vault.nav_usd or '?'}, share price ${vault.simulated_share_price_usd or '?'}")
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::GetVaultRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new_public(Environment::Mainnet).await?;
let vault_id = 5678;
let params = GetVaultRequest::builder().subaccount_id(vault_id).try_into()?;
let vault = client.rpc().vault_shareholders().get_vault(params).await?;
println!("{:?}", vault.protocol.config); // the immutable economics
// nav_usd / simulated_share_price_usd are null when the vault cannot be
// priced right now, so never assume they are set.
println!("{:?} {:?}", vault.nav_usd, vault.simulated_share_price_usd);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/get_vault \
-H "Content-Type: application/json" \
-d '{ "subaccount_id": 42 }'
```
`client.vaults.listVaults({ page, pageSize })` pages every vault on the exchange —
that is how shareholders will discover yours.
## Set the metadata
`updateInfo` patches only the **off-chain** fields — the economics stay immutable.
Supply just the fields you want to change; the caller must be the vault's curator.
```typescript TypeScript (SDK) theme={null}
await client.vaults.curator.updateInfo({
vaultSubaccountId: vaultId,
name: 'Delta-Neutral ETH',
description: 'Short vol, delta-hedged hourly.',
mtmCap: '1000000', // advisory NAV soft-cap in USD — a signal to shareholders
whitelistOnly: false, // restrict who may deposit
});
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
await client.vaults.update_info(
vault_subaccount_id=vault_id,
name="Delta-Neutral ETH",
description="Short vol, delta-hedged hourly.",
mtm_cap=Decimal("1000000"), # advisory NAV soft-cap in USD — a signal to shareholders
whitelist_only=False, # restrict who may deposit
)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{Environment, WsClient, models::UpdateVaultInfoRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(1234),
)
.await?;
client.login().await?;
// Off-chain metadata, so no action signing.
let params = UpdateVaultInfoRequest::builder()
.subaccount_id(5678)
.name("Delta-Neutral ETH".to_string())
.description("Short vol, delta-hedged hourly.".to_string())
.mtm_cap(Some(BigDecimal::from(1_000_000))) // advisory NAV soft-cap in USD
.whitelist_only(true)
.try_into()?;
client.rpc().vault_curators().update_vault_info(params).await?;
Ok(())
}
```
```bash cURL theme={null}
# Unsigned (ownership-checked); X-Derive* headers: session auth (see /json-rpc).
curl -X POST https://api.derive.xyz/v3/private/update_vault_info \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 42,
"name": "Delta-Neutral ETH",
"description": "Short vol, delta-hedged hourly.",
"mtm_cap": "1000000",
"whitelist_only": false
}'
```
## Next steps
Your first shareholder is waiting: run the settle loop.
When and how the fees you just configured are actually paid.
Run the settle loop from a scoped key, not the raw wallet.
# Process Deposits & Withdrawals
Source: https://docs.derive.xyz/vaults/deposits-withdrawals
The request-and-settle flow: shareholders queue signed intents; the curator settles each by minting or burning shares at a quoted price.
You can choose when to process user deposit & withdrawal requests. The protocol intentionally
gives this flexibility to the curator to allow for a wide variety of vault strategies.
For example, a covered call vault may not be able to provide exit liquidity at all times.
However, curators must process withdrawals within a 14 days, failure to do so may put the vault
at the risk of being frozen and delisted.
All of the steps in this guide can be done through the UX or programmatically.
A signed deposit or withdrawal request lands in the vault's queue. Deposits hold the funds on the shareholder's
source subaccount until settled or cancelled.
You poll the queue, pick a share price, and sign a mint (deposit) or burn (withdrawal) approval bound to that exact
request.
The sequencer re-verifies your approval, checks your quoted price against the vault's own mark-to-market price,
mints/burns shares, and moves the funds.
## The shareholder side
You'll want this to test your vault end-to-end, and to build a deposit UI. From a
shareholder's wallet:
```typescript TypeScript (SDK) theme={null}
const mySubaccountId = 5678;
// The deposit asset must equal the vault's — read it off the vault row
// instead of hardcoding an address.
const vault = await client.vaults.getVault(vaultId);
await client.vaults.shareholder.requestDeposit({
subaccountId: mySubaccountId, // the funds' source (the intent is signed on it)
vaultSubaccountId: vaultId,
depositSpotAsset: vault.protocol.config.deposit_spot_asset,
amount: '5000',
});
await client.vaults.shareholder.requestWithdraw({
subaccountId: mySubaccountId, // destination for the redeemed funds
vaultSubaccountId: vaultId,
sharesToBurn: '1000',
});
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# The subaccount you act AS: the funds' source, and where redemptions land.
me = await client.fetch_subaccount(5678)
# The deposit asset must equal the vault's — read it off the vault row
# instead of hardcoding an address.
vault = await client.vaults.get(vault_subaccount_id=vault_id)
await me.vaults.request_deposit(
vault_subaccount_id=vault_id,
deposit_spot_asset=vault.protocol.config.deposit_spot_asset,
amount=Decimal("5000"),
)
await me.vaults.request_withdraw(
vault_subaccount_id=vault_id,
shares_to_burn=Decimal("1000"),
)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::{DepositVaultArgs, WithdrawVaultArgs},
models::GetVaultRequest,
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let (vault_id, my_subaccount_id) = (42, 5678);
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(my_subaccount_id),
)
.await?;
client.login().await?;
// The deposit asset must equal the vault's — read it off the vault row
// instead of hardcoding an address.
let vault = client
.rpc()
.vault_shareholders()
.get_vault(GetVaultRequest::builder().subaccount_id(vault_id).try_into()?)
.await?;
client
.vaults()
.deposit(
DepositVaultArgs::builder()
.subaccount_id(my_subaccount_id) // the funds' source (the intent is signed on it)
.vault_id(vault_id)
.deposit_spot_asset(vault.protocol.config.deposit_spot_asset.to_string())
.deposit_amount(BigDecimal::from(5000))
.build(),
)
.await?;
client
.vaults()
.withdraw(
WithdrawVaultArgs::builder()
.subaccount_id(my_subaccount_id) // destination for the redeemed funds
.vault_id(vault_id)
.shares_to_burn(BigDecimal::from(1000))
.build(),
)
.await?;
Ok(())
}
```
```typescript TypeScript (SDK) theme={null}
// Intents wait in the queue until the curator settles them; they can be
// cancelled at any point before that (this drains ALL of the wallet's
// pending requests for the vault, deposits and withdrawals alike).
const live = await client.vaults.shareholder.getLiveRequests();
await client.vaults.shareholder.cancelAllRequests({
subaccountId: mySubaccountId,
vaultSubaccountId: vaultId,
});
// Share balances only change once the curator settles.
const holdings = await client.vaults.shareholder.getShares();
const history = await client.vaults.shareholder.getRequestHistory({
pageSize: 20,
});
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
me = await client.fetch_subaccount(5678)
# Intents wait in the queue until the curator settles them; they can be
# cancelled at any point before that (this drains ALL of the wallet's
# pending requests for the vault, deposits and withdrawals alike).
live = await client.vaults.list_live_requests()
await me.vaults.cancel_all_requests(vault_subaccount_id=vault_id)
# Share balances only change once the curator settles.
holdings = await client.vaults.shares()
history = await client.vaults.request_history(page_size=20)
print(live.total, holdings.vaults, history)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{
Environment, WsClient,
actions::CancelAllVaultRequestsArgs,
models::{GetLiveVaultRequestsRequest, GetVaultRequestHistoryRequest, GetVaultSharesRequest},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let (vault_id, my_subaccount_id) = (42, 5678);
let wallet = std::env::var("DERIVE_WALLET")?;
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(wallet.clone()),
Some(my_subaccount_id),
)
.await?;
client.login().await?;
let shareholder = || client.rpc().vault_shareholders();
// Intents wait in the queue until the curator settles them.
let live = shareholder()
.get_live_vault_requests(
GetLiveVaultRequestsRequest::builder().wallet(&wallet).try_into()?,
)
.await?;
// They can be cancelled at any point before that (this drains ALL of the
// wallet's pending requests for the vault, deposits and withdrawals alike).
client
.vaults()
.cancel_all_vault_requests(
CancelAllVaultRequestsArgs::builder()
.subaccount_id(my_subaccount_id)
.vault_id(vault_id)
.build(),
)
.await?;
// Share balances only change once the curator settles.
let holdings = shareholder()
.get_vault_shares(GetVaultSharesRequest::builder().wallet(&wallet).try_into()?)
.await?;
let history = shareholder()
.get_vault_request_history(
GetVaultRequestHistoryRequest::builder()
.wallet(&wallet)
.page_size(20)
.try_into()?,
)
.await?;
println!("{live:?}\n{holdings:?}\n{history:?}");
Ok(())
}
```
```bash cURL theme={null}
# Reads are keyed by wallet; every private call carries the same X-Derive* session headers.
curl -X POST https://api.derive.xyz/v3/private/get_live_vault_requests \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{ "wallet": "0xHOLDER…" }'
# Cancel is a signed action (nonce/signature via /action-signing):
curl -X POST https://api.derive.xyz/v3/private/cancel_all_vault_requests \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 5678,
"nonce": "1751558400000960000",
"signature_expiry_sec": 1751559000,
"signer": "0xHOLDER…",
"signature": "0x…",
"vault_subaccount_id": 42
}'
curl -X POST https://api.derive.xyz/v3/private/get_vault_shares \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{ "wallet": "0xHOLDER…" }'
curl -X POST https://api.derive.xyz/v3/private/get_vault_request_history \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{ "wallet": "0xHOLDER…", "page_size": 20 }'
```
Withdrawals are blocked until the vault's `cooldownSec` has elapsed since the holder's **last deposit**
(`vault_cooldown_active`, 18011). Intent signatures default to a 10-minute validity in the SDK — pass
`signatureExpirySec` to extend it (the API caps it at 30 days out); an intent whose signature lapses before you settle
it expires.
## The settle loop
Queued intents sit in two per-vault FIFO queues. There is **no WebSocket channel** for
them — poll, then settle. A settle approval is bound to one exact request by its
**user-action hash**, so you cannot pair a price with the wrong request.
Your quote is bounded on both sides by the vault's immutable `maxSlippageBps` against the protocol's mtm-derived
price.
`getLiveMintRequests` (pending deposits) and `getLiveBurnRequests` (pending withdrawals) return a FIFO page of
requests plus the queue's total length. Each request carries a composite `id` and the `user_action_hash` your
approval commits to.
Read the vault and quote at (or near) `simulated_share_price_usd` — the live price a depositor faces with your fees
settled. The protocol rejects a quote outside `maxSlippageBps` of its own mtm price.
`mintShares` settles a deposit; `burnShares` settles a withdrawal. Both are signed on the vault subaccount and bound
to the request's hash.
```typescript TypeScript (SDK) theme={null}
const curator = client.vaults.curator;
const { requests } = await curator.getLiveMintRequests(vaultId);
for (const request of requests) {
// Unset means the vault cannot be priced right now — retry later.
const { simulated_share_price_usd } = await client.vaults.getVault(vaultId);
if (simulated_share_price_usd == null) break;
await curator.mintShares({
vaultSubaccountId: vaultId,
sharePrice: simulated_share_price_usd,
depositHash: request.user_action_hash, // binds the approval to this exact request
requestId: request.id,
});
}
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
# Settle approvals are signed AS the vault subaccount.
vault_account = await client.fetch_subaccount(vault_id)
pending = await client.vaults.list_live_mint_requests(vault_subaccount_id=vault_id)
for request in pending.requests:
# Unset means the vault cannot be priced right now — retry later.
vault = await client.vaults.get(vault_subaccount_id=vault_id)
if vault.simulated_share_price_usd is None:
break
await vault_account.vaults.mint_shares(
share_price=vault.simulated_share_price_usd,
deposit_hash=request.user_action_hash, # binds the approval to this exact request
request_id=request.id,
)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{
Environment, WsClient,
actions::MintVaultSharesArgs,
models::{GetLiveMintRequestsRequest, GetVaultRequest},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let vault_id = 42;
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(vault_id), // approvals are signed on the vault subaccount
)
.await?;
client.login().await?;
let pending = client
.rpc()
.vault_curators()
.get_live_mint_requests(
GetLiveMintRequestsRequest::builder()
.subaccount_id(vault_id)
.limit(100)
.try_into()?,
)
.await?;
for request in pending.requests {
// Unset means the vault cannot be priced right now — retry later.
let vault = client
.rpc()
.vault_shareholders()
.get_vault(GetVaultRequest::builder().subaccount_id(vault_id).try_into()?)
.await?;
let Some(share_price) = vault.simulated_share_price_usd else {
break;
};
client
.vaults()
.mint_shares(
MintVaultSharesArgs::builder()
.vault_id(vault_id)
.share_price(share_price)
.user_action_hash(request.user_action_hash) // binds the approval to this exact request
.request_id(request.id)
.build(),
)
.await?;
}
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/get_live_mint_requests \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{ "subaccount_id": 42, "limit": 100 }'
# Quote from public/get_vault (simulated_share_price_usd), then settle.
# Body nonce/signature: EIP-712 action signing on the vault subaccount.
curl -X POST https://api.derive.xyz/v3/private/mint_vault_shares \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 42,
"nonce": "1751558411000001000",
"signature_expiry_sec": 1751558711,
"signer": "0xCURATOR…",
"signature": "0x…",
"share_price": "1.02",
"deposit_hash": "0xabc123…",
"request_id": { "vault_subaccount_id": 42, "wallet": "0xHOLDER…", "vault_nonce": "1751558400000900000" }
}'
```
```typescript TypeScript (SDK) theme={null}
const burns = await curator.getLiveBurnRequests(vaultId);
for (const request of burns.requests) {
const { simulated_share_price_usd } = await client.vaults.getVault(vaultId);
if (simulated_share_price_usd == null) break;
await curator.burnShares({
vaultSubaccountId: vaultId,
sharePrice: simulated_share_price_usd,
withdrawHash: request.user_action_hash,
requestId: request.id,
});
}
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
vault_account = await client.fetch_subaccount(vault_id)
burns = await client.vaults.list_live_burn_requests(vault_subaccount_id=vault_id)
for request in burns.requests:
vault = await client.vaults.get(vault_subaccount_id=vault_id)
if vault.simulated_share_price_usd is None:
break
await vault_account.vaults.burn_shares(
share_price=vault.simulated_share_price_usd,
withdraw_hash=request.user_action_hash,
request_id=request.id,
)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{
Environment, WsClient,
actions::BurnVaultSharesArgs,
models::{GetLiveBurnRequestsRequest, GetVaultRequest},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let vault_id = 42;
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(vault_id),
)
.await?;
client.login().await?;
let burns = client
.rpc()
.vault_curators()
.get_live_burn_requests(
GetLiveBurnRequestsRequest::builder()
.subaccount_id(vault_id)
.limit(100)
.try_into()?,
)
.await?;
for request in burns.requests {
let vault = client
.rpc()
.vault_shareholders()
.get_vault(GetVaultRequest::builder().subaccount_id(vault_id).try_into()?)
.await?;
let Some(share_price) = vault.simulated_share_price_usd else {
break;
};
client
.vaults()
.burn_shares(
BurnVaultSharesArgs::builder()
.vault_id(vault_id)
.share_price(share_price)
.user_action_hash(request.user_action_hash)
.request_id(request.id)
.build(),
)
.await?;
}
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/private/get_live_burn_requests \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{ "subaccount_id": 42, "limit": 100 }'
curl -X POST https://api.derive.xyz/v3/private/burn_vault_shares \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 42,
"nonce": "1751558412000002000",
"signature_expiry_sec": 1751558712,
"signer": "0xCURATOR…",
"signature": "0x…",
"share_price": "1.02",
"withdraw_hash": "0xdef456…",
"request_id": { "vault_subaccount_id": 42, "wallet": "0xHOLDER…", "vault_nonce": "1751558400000950000" }
}'
```
Before a burn settles, make sure the vault subaccount holds enough of the deposit asset to pay the redemption — you
may need to [unwind positions](/vaults/trade) first. The redemption moves funds out of the vault subaccount at settle
time.
Don't want a deposit? `curator.rejectDepositRequest(request.id, 'reason')` pops it off the queue and releases the
holder's funds with no on-chain settle. The reason is optional and capped at 20 characters.
## Request lifecycle
Every request moves through these statuses:
| Status | Meaning |
| ------------------- | ------------------------------------------------------------------- |
| `enqueued` | Waiting in the FIFO queue for the curator. |
| `sequencer_applied` | Settled — the mint/burn executed. |
| `user_cancel` | The shareholder cancelled it (`cancelAllRequests`). |
| `curator_reject` | You rejected the deposit (`rejectDepositRequest`). |
| `protocol_reject` | The settle failed a protocol check (slippage, margin, cooldown, …). |
| `expired` | The intent's signature expiry passed before it was settled. |
Shareholders see the full status trail via `getRequestHistory`; the settled events are
public in `getActionHistory`.
## Reading vault state
All unauthenticated and read-only, on `client.vaults`:
| Method | Returns |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `getVault` | Full row for one vault: on-chain state, config, live `nav_usd`, `simulated_share_price_usd`, `curator_shares`, benchmark. |
| `listVaults` | Every vault, paginated. |
| `getActionHistory` | Settled deposit / withdrawal / fee-accrual / cancel events with the fee-share split. |
| `getPerformanceHistory` | Sampled NAV / share-price / HWM time series (`1h`, `8h`, `24h`, `1wk`). |
`getVault` is the one you poll while settling — `simulated_share_price_usd` is your
quote anchor, and `nav_usd` is negative if the vault is insolvent.
Keep enough of the deposit asset liquid to pay redemptions.
Every settle here is also when your fees are minted.
# Fees
Source: https://docs.derive.xyz/vaults/fees
How management and performance fees accrue, the high-water mark and benchmark assets, and the protocol fee share.
You set two fee rates when you [create the vault](/vaults/create-a-vault) —
`managementFeeBps` and `performanceFeeBps`. This page covers how they are actually
charged.
## How fees are paid
Fees are **dilutive share mints**: when a fee settles, the protocol mints new vault
shares to you, the curator, diluting every other holder proportionally. Assets never
leave the vault — your fee is a growing claim on it.
There is no separate fee-collection call. Fees settle **atomically inside every
deposit and withdrawal settlement** — each `mintShares`, `burnShares`, and `forceBurn`
first brings fees up to date, then applies the deposit or withdrawal math on the
post-fee share price.
Both fee rates are **immutable** after creation. There is no method to change them — winding down and creating a new
vault is the only way to reprice.
## Management fee
`managementFeeBps` is an annualised rate on NAV, accrued pro-rata over the time elapsed
since the last fee settlement, regardless of performance. A 100 bps vault that settles
a deposit 73 days after its last settlement mints you shares worth ≈ `1% × 73/365` of
NAV.
As a safety valve, a single settlement mints at most **2.5% of NAV** in management fees, however long the gap since
the last one. If your vault can go months without a deposit or withdrawal, settle something periodically or the excess
accrual is forfeited.
## Performance fee
`performanceFeeBps` is charged only on gains **above the high-water mark** — the
highest share price the vault has already paid fees at. Each fee mint ratchets the HWM
up to the new post-fee share price; it never moves down. You are never paid twice for
recovering the same drawdown.
The vault row exposes the current mark as `global_hwm`.
## Benchmark assets
By default the HWM is denominated in USD. Setting `benchmarkAsset` at creation
denominates it in a spot asset instead, so the performance fee charges only on
**outperformance versus that asset** — e.g. an ETH vault that charges fees on beating
ETH, not on an ETH rally. Omit it for the feed-less USD default.
## Protocol fee share
A deployment-set `protocol_fee_share_bps` slice of **every** fee mint is diverted to
the protocol fee recipient instead of you. The total shares minted are unchanged — the
split only decides how they are divided between you and the protocol. The vault row
exposes the rate.
## Withdrawing fees
Fees arrive as vault shares, so realising them is just a withdrawal: from your own
wallet, request a withdrawal for the shares you want to redeem
(`shareholder.requestWithdraw`), then settle your own burn in the
[settle loop](/vaults/deposits-withdrawals#the-settle-loop) like any other request.
The one constraint is the **curator stake floor**. To keep your incentives aligned
with your shareholders', you must hold the **greater** of:
* **\$10,000**, or
* **1% of the vault's value**
in the vault at all times. A withdrawal that would leave your stake below the floor is
rejected (`vault_curator_stake_below_min`, 18013). Everything above it — accrued fees
included — is yours to withdraw whenever you like.
The floor applies for as long as the vault is open. It lifts only on the vault's final closing burn — see
[Winddowns](/vaults/winddowns) for taking out your full stake.
## Observe fees in practice
Fee settlements are public — every mint shows up in the vault's action history with
the shares minted and the new high-water mark:
```typescript TypeScript (SDK) theme={null}
const history = await client.vaults.getActionHistory(vaultId, { pageSize: 20 });
console.log(history); // deposits, withdrawals, and fee events with the curator/protocol share split
// Track share price vs the HWM over time ('1h' | '8h' | '24h' | '1wk'):
const perf = await client.vaults.getPerformanceHistory(vaultId, '24h');
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
from derive_py.data_types.generated_models import PerformanceResolution
async def main():
client = WebSocketClient.from_env()
await client.connect()
history = await client.vaults.action_history(vault_subaccount_id=vault_id, page_size=20)
print(history) # deposits, withdrawals, and fee events with the curator/protocol share split
# Track share price vs the HWM over time (1h | 8h | 24h | 1wk):
perf = await client.vaults.performance_history(
vault_subaccount_id=vault_id,
resolution=PerformanceResolution.field_24h,
)
print(perf)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{
Environment, WsClient,
models::{GetVaultActionHistoryRequest, GetVaultPerformanceHistoryRequest},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new_public(Environment::Mainnet).await?;
let vault_id = 5678;
// Deposits, withdrawals, and fee events with the curator/protocol split.
let history = client
.rpc()
.vault_shareholders()
.get_vault_action_history(
GetVaultActionHistoryRequest::builder()
.subaccount_id(vault_id)
.page_size(20)
.try_into()?,
)
.await?;
println!("{history:?}");
// Track share price vs the HWM over time ("1h" | "8h" | "24h" | "1wk"):
let perf = client
.rpc()
.vault_shareholders()
.get_vault_performance_history(
GetVaultPerformanceHistoryRequest::builder()
.subaccount_id(vault_id)
.resolution("24h")
.try_into()?,
)
.await?;
println!("{perf:?}");
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/get_vault_action_history \
-H "Content-Type: application/json" \
-d '{ "subaccount_id": 42, "page_size": 20 }'
curl -X POST https://api.derive.xyz/v3/public/get_vault_performance_history \
-H "Content-Type: application/json" \
-d '{ "subaccount_id": 42, "resolution": "24h" }'
```
`public/get_vault` also folds pending fees into its live pricing:
`simulated_share_price_usd` is the price a depositor would actually face **with your
fees settled**, which is why it is the quote anchor for the
[settle loop](/vaults/deposits-withdrawals#the-settle-loop).
Where the fee rates, benchmark, and the rest of the economics are set.
The settlements your fees piggyback on.
# Trade
Source: https://docs.derive.xyz/vaults/trade
Trade the vault subaccount like any other subaccount.
There is no vault-specific trading & market data API. The vault **is** a subaccount owned by your
wallet, so you run your strategy on it with the normal trading methods — orders, RFQs,
transfers — passing the vault's subaccount id. The `managerId` you chose at
[creation](/vaults/create-a-vault) is the margin manager that governs what it can
trade.
All of the steps in this guide can be done through the UX or programmatically.
```typescript TypeScript (SDK) theme={null}
await client.orders.place({
subaccountId: vaultId, // the vault subaccount — that's the whole trick
instrumentName: 'ETH-PERP',
direction: 'buy',
amount: '2',
limitPrice: '2500',
});
```
```python Python (SDK) theme={null}
import asyncio
from decimal import Decimal
from derive_py import WebSocketClient
from derive_py.data_types import Direction
async def main():
client = WebSocketClient.from_env()
await client.connect()
vault = await client.fetch_subaccount(vault_id)
await vault.orders.create(
instrument_name="ETH-PERP",
direction=Direction.buy,
amount=Decimal("2"),
limit_price=Decimal("2500"),
)
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{
Environment, WsClient,
actions::OrderArgs,
models::{Direction, OrderType, TimeInForce},
};
#[tokio::main]
async fn main() -> Result<(), Box> {
// Bind the client to the VAULT subaccount — that's the whole trick.
// derive-rs takes the subaccount at construction, not per order.
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(5678),
)
.await?;
client.login().await?;
let order = OrderArgs::builder()
.instrument_name("ETH-PERP".to_string())
.direction(Direction::Buy)
.order_type(OrderType::Limit)
.time_in_force(TimeInForce::Gtc)
.amount(BigDecimal::from(2))
.limit_price(BigDecimal::from(2500))
.build();
client.orders().place(order).await?;
Ok(())
}
```
```bash cURL theme={null}
# Body nonce/signature: EIP-712 trade signing (see /action-signing).
curl -X POST https://api.derive.xyz/v3/private/order \
-H "Content-Type: application/json" \
-H "X-DeriveWallet: $WALLET_ADDRESS" \
-H "X-DeriveTimestamp: $TS" \
-H "X-DeriveSignature: $SIG" \
-d '{
"subaccount_id": 42,
"instrument_name": "ETH-PERP",
"direction": "buy",
"order_type": "limit",
"time_in_force": "gtc",
"amount": "2",
"limit_price": "2500",
"max_fee": "10",
"mmp": false,
"nonce": "1751558400000123000",
"signature_expiry_sec": 1751558700,
"signer": "0xCURATOR…",
"signature": "0x…"
}'
```
## How trading moves the share price
NAV is the vault subaccount's live mark-to-market value from the risk engine, and
`share price ≈ NAV / total_shares`. As you trade on behalf of the vault, the vault
subaccount's position values change.
On each withdrawal, a management fee is charged, as well as a performance fee on any
profits since the last withdrawal. The share price is computed after these fees are applied.
The protocol ensures that deposits and withdrawals are never settled with a slippage to the
mtm beyond `maxSlippageBps` set at vault creation.
```typescript TypeScript (SDK) theme={null}
const { nav_usd, simulated_share_price_usd } = await client.vaults.getVault(
vaultId
);
console.log(
`NAV $${nav_usd ?? '?'}, share price $${simulated_share_price_usd ?? '?'}`
);
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
vault = await client.vaults.get(vault_subaccount_id=vault_id)
print(f"NAV ${vault.nav_usd or '?'}, share price ${vault.simulated_share_price_usd or '?'}")
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::GetVaultRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new_public(Environment::Mainnet).await?;
let params = GetVaultRequest::builder().subaccount_id(5678).try_into()?;
let vault = client.rpc().vault_shareholders().get_vault(params).await?;
println!("{:?} {:?}", vault.nav_usd, vault.simulated_share_price_usd);
Ok(())
}
```
```bash cURL theme={null}
curl -X POST https://api.derive.xyz/v3/public/get_vault \
-H "Content-Type: application/json" \
-d '{ "subaccount_id": 42 }'
```
Nothing is stored or settled per-trade — pricing is computed live, and hourly
snapshots accumulate in `getPerformanceHistory` for the track record shareholders see.
## Trading while requests are queued
Your positions and the [settle loop](/vaults/deposits-withdrawals) interact in three
ways worth engineering around:
**Redemption liquidity.** Withdrawals pay out in the vault's deposit asset. A vault that is fully deployed into
positions cannot settle a large burn — unwind first, then settle.
**Curator stake floor.** The protocol requires you to keep minimum skin-in-the-game. A withdrawal that would drop your
own stake below the deployment-set floor is rejected (`vault_curator_stake_below_min`, 18013) — you cannot drain your
seed while shareholders remain. See [Winddowns](/vaults/winddowns) for the exit order this implies.
Everything the normal trading API supports, the vault subaccount supports.
The settle loop your trading has to leave room for.
# Winddowns
Source: https://docs.derive.xyz/vaults/winddowns
Close a vault: settle final withdrawals, force-burn remaining holders, and understand the terminal closed state.
There is no close endpoint. A vault is automatically closed when a burn takes `total_shares` to
zero — a terminal state that rejects every subsequent vault operation with `vault_closed` (18010).
Winding down is therefore just getting every holder out, yourself last.
## Orderly winddown
Reject anything queued with `rejectDepositRequest`, and keep rejecting new intents as they arrive. Optionally set
`whitelistOnly: true` via `updateInfo` to discourage new ones.
Redemptions pay out in the deposit asset, so [close out the vault's positions](/vaults/trade) until it holds only
that asset.
Run the [settle loop](/vaults/deposits-withdrawals#the-settle-loop) until no shareholder requests remain, and
force-burn any holder who never submits one (below).
The curator stake floor rejects a withdrawal that would drop your skin-in-the-game below the minimum while others
still hold shares (`vault_curator_stake_below_min`, 18013) — so your own full exit comes last. Your final burn takes
`total_shares` to zero and closes the vault.
## Force-burning holders
`forceBurn` redeems a holder's **entire** share balance at the current mark-to-market
price — no request from them, no price quote from you. The curator signs on the vault
subaccount; use it to eject holders who never submit their own withdrawal.
```typescript TypeScript (SDK) theme={null}
await client.vaults.curator.forceBurn({ vaultSubaccountId: vaultId, holder: '0xHOLDER…' });
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
await client.vaults.force_burn(vault_subaccount_id=vault_id, holder="0xHOLDER…")
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use derive_rs::{Environment, WsClient, models::ForceBurnRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(1234),
)
.await?;
client.login().await?;
let params = ForceBurnRequest::builder()
.subaccount_id(5678)
.holder("0xHOLDER...")
.try_into()?;
client.rpc().vault_curators().force_burn(params).await?;
Ok(())
}
```
## The curator's final exit
For your own shares you act as a shareholder of your own vault — request the
withdrawal, then settle it yourself:
```typescript TypeScript (SDK) theme={null}
// Your remaining share balance: your seed stake plus every fee mint.
await client.vaults.shareholder.requestWithdraw({
subaccountId: curatorSubaccountId, // where the redeemed funds land
vaultSubaccountId: vaultId,
sharesToBurn: myRemainingShares,
});
// Settle your own burn like any other (see Process Deposits & Withdrawals).
// It is the vault's last: total_shares hits zero and the vault closes.
const { protocol } = await client.vaults.getVault(vaultId);
console.log(protocol.total_shares, protocol.closed); // '0', true
```
```python Python (SDK) theme={null}
import asyncio
from derive_py import WebSocketClient
async def main():
client = WebSocketClient.from_env()
await client.connect()
curator = await client.fetch_subaccount(curator_subaccount_id)
# Your remaining share balance: your seed stake plus every fee mint.
await curator.vaults.request_withdraw(
vault_subaccount_id=vault_id,
shares_to_burn=my_remaining_shares,
)
# Settle your own burn like any other (see Process Deposits & Withdrawals).
# It is the vault's last: total_shares hits zero and the vault closes.
vault = await client.vaults.get(vault_subaccount_id=vault_id)
print(vault.protocol.total_shares, vault.protocol.closed) # 0, True
await client.disconnect()
asyncio.run(main())
```
```rust Rust (SDK) theme={null}
use bigdecimal::BigDecimal;
use derive_rs::{Environment, WsClient, actions::WithdrawVaultArgs, models::GetVaultRequest};
#[tokio::main]
async fn main() -> Result<(), Box> {
let (vault_id, curator_subaccount_id) = (42, 1234);
let my_remaining_shares = BigDecimal::from(15_000);
let client = WsClient::new(
Environment::Mainnet,
Some(std::env::var("DERIVE_PRIVATE_KEY")?),
Some(std::env::var("DERIVE_WALLET")?),
Some(curator_subaccount_id),
)
.await?;
client.login().await?;
// Your remaining share balance: your seed stake plus every fee mint.
client
.vaults()
.withdraw(
WithdrawVaultArgs::builder()
.subaccount_id(curator_subaccount_id) // where the redeemed funds land
.vault_id(vault_id)
.shares_to_burn(my_remaining_shares)
.build(),
)
.await?;
// Settle your own burn like any other (see Process Deposits & Withdrawals).
// It is the vault's last: total_shares hits zero and the vault closes.
let vault = client
.rpc()
.vault_shareholders()
.get_vault(GetVaultRequest::builder().subaccount_id(vault_id).try_into()?)
.await?;
println!("{} {}", vault.protocol.total_shares, vault.protocol.closed); // 0, true
Ok(())
}
```
Accrued fees settle inside these final burns like any other settlement — your last withdrawal includes them. See
[Fees](/vaults/fees).
## After closure
`closed: true` never reverts. Once set:
* New deposit and withdrawal requests are rejected with `vault_closed` (18010) — and so
is settling any request that somehow remained queued.
* `mintShares`, `burnShares`, and `forceBurn` all reject the same way.
* The vault stays **readable**: `getVault`, `getActionHistory`, and
`getPerformanceHistory` keep serving its history and track record.
To run a new strategy — or the same one with different economics —
[create a new vault](/vaults/create-a-vault).