> ## Documentation Index
> Fetch the complete documentation index at: https://v3.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 importing
  this directly into your LLM / coding agent as a `SKILL.MD`.
</Note>

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) (coming soon)
* [Rust SDK](https://github.com/derive.xyz/derive-rs) (coming soon)

Lastly, for granular API details, refer either to the **API Reference** tab or the `openapi.json` spec (simply add `/openapi.json` to the base URL).

<Steps>
  <Step title="Collapse to a single 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. A subaccount carries a single
    `wallet` address, and an action is authorised only when the subaccount's `wallet` equals
    the action `owner` (the `signer` is either that owner or one of its session keys).

    Note that during the v2 -> v3 state migration all "Derive Wallets" will be automatically transferred to the user's EOA or multisig.
  </Step>

  <Step title="Update action signing flow">
    Account creation is fully permissionless. Deposits move onchain. Some minor changes to certain actions.
  </Step>

  <Step title="Repoint hosts and constants">
    Switch base URLs to the v3 hosts (`api.derive.xyz/v3` / `testnet.api.derive.xyz/v3`), recompute the EIP-712 domain
    separator for the target chain, and use the shared module addresses. See [Constants](#constants).
  </Step>

  <Step title="API endpoint changes">
    Apply the mapping table. Note that several v3 targets (`get_all_instruments`, `get_open_orders`,
    `get_order_history`, `get_ticker`/`get_tickers`, `order_quote`) already existed in v2 — the corresponding v2 name
    was removed, not aliased.
  </Step>
</Steps>

## Signed-action changes

### Deposits are on-chain

There is no deposit action or `private/deposit` RPC or action in v3. Deposit directly on-chain via one of the three supported flows. Refer to the [Depositing](/getting-started/depositing) guide or SDKs for more information.

### Withdrawals

The withdrawal action gains `max_fee_usd`, `force_batch`, and a `recipient` (in the signed
action data), and the amount stays in the asset's **underlying decimals**:

| Field                  | Type / units                       | Notes                                                                                                                       |
| ---------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `amount_in_underlying` | string, **native ERC-20 decimals** | Use the asset's native ERC-20 decimals (e.g. 6 for USDC, 18 for ETH); apply no additional scaling.                          |
| `max_fee_usd`          | string USD (e.g. `"1.5"`)          | Max sequencer fee the signer authorises.                                                                                    |
| `force_batch`          | bool                               | If true, requests immediate batching/proving for a higher fee but faster withdrawal.                                        |
| `recipient`            | address (signed action only)       | **Not a settable request field.** `recipient` is always forced to the signer, so funds always land at the signer's address. |

`private/withdraw` response: `{ operation_id, op_uuid }` — no `status`.

### Transfers

* `private/transfer_spot` — one spot asset at a time, between subaccounts of the **same
  owner** (existing, or a newly-created sender-owned subaccount via a non-zero
  `new_subaccount_manager`). Request carries `max_fee_usd` (the exchange may charge for
  creating a subaccount). It **cannot** send to a different owner.
* `private/transfer_positions` — replaces `transfer_position`; handles one or many legs
  (position transfers still use the RFQ-style signed-quote envelope).

All transfer responses return `{ operation_id, op_uuid }` (no `status`).

Refer to [Transfers & Withdrawals](/trading/transfers-withdrawals#withdrawals) or SDKs for more information.

### Session keys

v3 splits session-key management into a signed protocol action and an off-chain edit:

* `private/create_session_key` — signs a `CreateSessionKey` action containing
  `{ session_key, expiry_sec, scopes, subaccounts }`. Only **protocol** scopes are part of
  the signed action; off-chain scopes travel as unsigned metadata. Signed against
  `subaccount_id = 0`. This is the replacement for `register_scoped_session_key` and the
  old admin-key register/deregister/build-tx flow.
* `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/create_session_key` with a past or zero
  expiry. v2's `deregister_session_key` and the `disable` flag on `edit_session_key` have no direct replacement in v3.

Two scope kinds: **protocol** scopes are validated in the protocol and are part of the
signed action; **offchain** scopes are validated off-chain and can be changed later
via `edit_session_key`. See [Access scopes](/authentication/access-scopes).

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

### Nonces: nanoseconds, and increasing

In v2, nonces were treated as milliseconds. In v3 a nonce is a **UTC timestamp in nanoseconds** (\~19 digits for 2025,
e.g. `1751558400000000000`). A millisecond- or microsecond-scale nonce will be rejected.

Beyond the unit change, nonces on most signed actions must be **strictly increasing** per
subaccount, and must fall inside a validity window. The window differs by action type:

| Action type                                             | Nonce window (relative to server clock / creation) | Must increase? |
| ------------------------------------------------------- | -------------------------------------------------- | -------------- |
| withdraw, transfer, session key, whitelist, liquidation | ± 1 hour                                           | Yes            |
| Orders                                                  | 90 days before → 1 hour after                      | No             |
| RFQs                                                    | ± 1 hour                                           | No             |
| Vault actions                                           | 30 days before → 1 hour after                      | Yes            |

Orders and RFQs are exempt from the increasing requirement. A coarser ± 5-minute check is also applied
when the request is first received. Exact bounds may vary by environment.

## Constants

The hosts and signing constants that changed between v2 and v3.

### Endpoints

| Host                                | Notes                                                                         |
| ----------------------------------- | ----------------------------------------------------------------------------- |
| `https://api.lyra.finance`          | Legacy host. Remains routable during migration, then retired. Migrate off it. |
| `https://api.derive.xyz/v3`         | v3 mainnet (WebSocket at `wss://api.derive.xyz/v3/ws`).                       |
| `https://testnet.api.derive.xyz/v3` | v3 testnet (WebSocket at `wss://testnet.api.derive.xyz/v3/ws`).               |

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.

### Transaction ids → operation uuids

v2 returned a `transaction_id` (and a `status`) on state-changing calls. v3 removes
`status` and returns an **operation** identifier instead:

* `op_uuid` (string) — the stable UUID of the operation.
* `operation_id` (integer) — a numeric operation id, returned alongside `op_uuid` on
  action responses (withdraw, transfers, whitelist).

`tx_hash` still exists but now points to the **Ethereum L1 transaction** in which the zk
batch settled, not a Derive-chain transaction. The full lineage is
`op_uuid → operation → batch → L1 tx`, and `tx_hash` (plus `l1_block`) is populated from
the batch once it settles.

### Settlement status: `TxStatus` → `BatchStatus`

v2 tracked settlement with a single per-transaction `status`. Because v3 settles operations in
zk-batches, the settlement lifecycle is a **`BatchStatus`** on the batch — the single source of
truth for every operation in it (individual operations carry no status of their own). Each stage
has a healthy value and a matching `…Error` value if that stage fails:

| Stage     | Healthy     | Errored          |
| --------- | ----------- | ---------------- |
| Batching  | `Batching`  | `BatchingError`  |
| Executing | `Executing` | `ExecutingError` |
| Proving   | `Proving`   | `ProvingError`   |
| Settling  | `Settling`  | `SettlingError`  |
| Settled   | `Settled`   | `SettledError`   |

Values serialize as the PascalCase variant name and are **`null`** until the operation is picked
up into a batch. Deposit / withdrawal / erc20-transfer history expose this as **`batch_status`**;
trade history keeps the field name **`tx_status`** but returns these same `BatchStatus` values
(see [History routes](#history-routes)).

<Warning>
  Don't confuse this with the matching-engine `tx_status` on `private/order` and the quote channels
  (`requested` / `pending` / `settled` / `reverted` / `ignored` / `timed_out`). That is a **separate** field, lowercase,
  and unrelated to the batch settlement lifecycle above.
</Warning>

## Renamed / consolidated methods

First, download the full `openapi.json` spec by adding `/openapi.json` to the base URL. Use that as the reference as you migrate the routes.

| v2 method                                                                                                                                                           | v3 method(s)                             | Change                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `register_session_key`, `deregister_session_key`, `build_register_session_key_tx`, `register_scoped_session_key`                                                    | `private/create_session_key`             | Session-key registration consolidated into one scoped-key action. `private/edit_session_key` still exists and now edits only off-chain fields (see below). |
| `private/transfer_erc20`                                                                                                                                            | `private/transfer_spot`                  | Renamed. Same-owner spot transfers. Cross-owner uses the new `private/transfer_spot_external`.                                                             |
| `private/transfer_position`                                                                                                                                         | `private/transfer_positions`             | Singular dropped; plural handles single **and** multiple legs.                                                                                             |
| `public/get_vault_statistics`, `public/get_vault_share`, `public/get_vault_balances`, `public/get_vault_assets`, `public/get_vault_pools`, `public/get_vault_rates` | *(see [Vaults](/vaults/create-a-vault))* | All v2 vault read methods replaced by the reworked v3 vaults API.                                                                                          |

## Response reshapes

### `public/get_currency` & `public/get_all_currencies`

Both keep their names and params and return the same per-currency object shape
(`get_all_currencies` returns an array of it). The object was **completely restructured**:
v2 was flat, keyed risk by manager **contract address**, and assumed USDC collateral. v3
nests everything around two grouping keys — **`risk_universe_id`** and **per-asset**
(`option` / `perp` / `spot`, where **`spot` is now a list**).

| v2 field                                                  | v3 location                                                                                                                                                                             |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `currency`, `market_type`, `spot_price`, `spot_price_24h` | Unchanged (`market_type` same enum).                                                                                                                                                    |
| `instrument_types: [...]`                                 | Removed — infer from which of `option` / `perp` / `spot` are present.                                                                                                                   |
| `protocol_asset_addresses.{option,perp,spot}`             | Inline `option.address` / `perp.address` / `spot[].address`, now **EIP-55 checksummed**.                                                                                                |
| `protocol_asset_addresses.underlying_erc20`               | `spot[].erc20.underlying_erc20`.                                                                                                                                                        |
| `erc20_details` (loose map)                               | `spot[].erc20` (structured; adds `decimals`).                                                                                                                                           |
| `managers: [{address, margin_type, currency}]`            | `managers: [{risk_universe_id, sm?, pm?}]` — **numeric** manager ids per universe (`sm` = SRM, `pm` = PMRM2). Legacy `PM` margin type dropped.                                          |
| `asset_cap_and_supply_per_manager`                        | Inline `option/perp.universes[].oi` and `spot[].universes[].oi` (`{current_open_interest, interest_cap}`).                                                                              |
| `srm_im_discount`, `srm_mm_discount`                      | `spot[].universes[].{srm_im_discount, srm_mm_discount}` (per collateral asset per universe).                                                                                            |
| `pm2_collateral_discounts`                                | `spot[].universes[].{pm2_im_discount, pm2_mm_discount}` (resolved by asset address, not manager currency).                                                                              |
| `borrow_apy`, `supply_apy`, `total_borrow`                | `spot[].universes[].lending = {borrow_apy, supply_apy, total_borrow, total_borrow_cap}` — nullable, per lending asset per universe, **no longer USDC-only**. `total_borrow_cap` is new. |
| `total_supply`                                            | `spot[].universes[].oi.current_open_interest`.                                                                                                                                          |
| `srm_perp_margin_requirements`                            | **Removed** from this response.                                                                                                                                                         |

New fields with no v2 equivalent: `risk_universe_id` on every `managers[]` / `*.universes[]`
entry; `option`/`perp` become `{name, address, universes[]}`; `spot[]` gains `name`,
`min_deposit_usd`, and `erc20.decimals`.

<Note>
  Type notes: `sm` / `pm` / all `risk_universe_id` are **numbers** (`u32`); `erc20.decimals` is a number; all
  money/discount/OI fields are decimal **strings**. In v2 the manager identifier was a hex address string — it is now a
  numeric id. `managers[].pm` is the **PMRM2** manager (v2 `margin_type: "PM2"`), not the legacy v2 `"PM"`.
</Note>

Optional fields are omitted from JSON when absent: `spot_price_24h`, top-level `option`,
top-level `perp`, `managers[].sm`, `managers[].pm`, `spot[].universes[].lending`,
`spot[].erc20.underlying_erc20`. `spot` and every `universes` array are always present (may
be empty).

<Info>
  To resolve a spot collateral's discount/lending, iterate `spot[] → universes[]` and match on `risk_universe_id` (from
  `private/get_subaccount`'s new `risk_universe_id`) instead of reading a single top-level value. **`spot` being an
  array** is the most likely thing to break clients that assumed one spot asset per currency. Perp margin requirements
  are no longer in this response.
</Info>

### `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 now expose
  **`batch_status`**; **trade history (public and private) keeps `tx_status`**. Both 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.

<Note>
  The status field name differs by endpoint: trade history uses `tx_status`; the deposit/withdrawal/erc20-transfer
  history endpoints use `batch_status`. The operation-uuid field is `op_uuid` on the private trade-history row and
  `operation_id` on the deposit/withdrawal/erc20 rows. Public trade history exposes no operation-uuid field.
</Note>

\##\$ Channel changes

The subscription surface went from 16 channels in v2 to 15 in v3. The only removed channel
is the full ticker feed; 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 / not currently exposed

| 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)                                |
| History             | `private/expired_and_cancelled_history`, `private/get_subaccount_value_history`                                  |
| 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`)                                               |


## Related topics

- [DevEx improvements](/migrating/v3-improvements.md)
- [Error Codes](/error-codes.md)
- [Transfer a spot asset between your subaccounts](/api-reference/transfers-&-withdrawals/transfer-a-spot-asset-between-your-subaccounts.md)
