> ## Documentation Index
> Fetch the complete documentation index at: https://docs.derive.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration skill for your coding agent

> Use this doc as a SKILL.MD for your coding agent to migration your current code to the v3 API.

<Note>
  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`.
</Note>

If at any step there is not enough detail, refer to the [Guides](/getting-started/introduction) 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

<Tip>
  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.
</Tip>

### 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

The maker quote (`send_quote`) and taker execute (`execute_quote`) `signature_expiry_sec` must satisfy:

```
rfq_creation + 31 min  ≤  signature_expiry_sec  ≤  now + 1 day
```

The lower bound is 31 minutes after RFQ creation (the RFQ's 30-minute validity window plus a 1-minute buffer).
The upper bound is the current server time plus 1 day.

## 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 |
| Da | `Da` | `DaError` | State diff being posted to the data-availability layer |
| 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` is gone. Infer instrument types
  from which of `option`, `perp` and `spot` are present.
* **`srm_perp_margin_requirements` moved.** It keeps its v2 fields (`im_perp_req`,
  `mm_perp_req`, `max_leverage`) but is no longer top-level: the rates are per risk universe, so
  it now sits on each entry of `perp.universes[]`. `public/get_instrument` reports the same
  object inside `perp_details` for that instrument's universe.

### `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` projects to a minute past the account's own next option expiry,
  not v2's fixed 08:01 UTC.

### `public/get_ticker` & `public/get_tickers`

`public/get_ticker` now returns the **slim ticker**. It is the same object as one entry of
`public/get_tickers` and the data of a `ticker_slim.{instrument_name}.{interval}` update. v2
returned the full ticker, which v3 dropped along with its channel (see
[Channel changes](#channel-changes)).

* **Live fields use the slim short keys.** `best_bid_price` / `best_ask_price` → `b` / `a`,
  `best_bid_amount` / `best_ask_amount` → `B` / `A`, `index_price` → `I`, `mark_price` → `M`,
  `min_price` / `max_price` → `minp` / `maxp`, `timestamp` → `t`. `option_pricing` uses short
  keys too (`d`, `g`, `v`, `i`, …).
* **Instrument metadata moved to `public/get_instrument`.** Tick and amount steps, fees,
  activation times, `option_details`, `perp_details` and `erc20_details` are no longer on the
  ticker.
* **Dropped:** `five_percent_bid_depth`, `five_percent_ask_depth` and the per-margin-type
  `open_interest` map. Total open interest is `stats.oi`. OI caps are per risk universe in
  `public/get_currency`.
* **New:** 24h `stats`, and `f`, the hourly funding rate (perps only).

`public/get_tickers` is unchanged from v2. It takes the same params and returns the same
map of instrument name to slim ticker.

### `public/get_all_instruments`

v2 described `expired` as "include expired instruments": with `expired: true`, options came back
live and expired together. In v3 the flag selects one or the other. For options, `expired: true`
returns **only expired options**, and `expired: false` returns live options as before. To list
both, make both calls and merge by `instrument_name`.

* An empty listing returns `instrument_not_found` (12001), as in v2. Treat it as empty when
  merging.
* Perps and spot ignore `expired`.
* An option enters the `expired: true` listing up to a minute before it expires, so it does
  not drop out of both listings at expiry.

### 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`, `Da`, `Proving`, `Settling`, `Settled`, and their errored counterparts
  `BatchingError`, `ExecutingError`, `DaError`, `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` | |
| Instruments | `public/get_instruments` (use `get_all_instruments` / `get_all_live_instruments`) | |
| Assets / misc | `public/get_asset`, `public/get_assets` (use `get_currency` / `get_all_instruments`), `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`) | |


## Related topics

- [DevEx improvements](/migrating/v3-improvements.md)
- [Introduction](/getting-started/introduction.md)
- [Quickstart](/getting-started/quickstart.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.