# Migrate from Ethereal

Use this guide to port an Ethereal trading integration to Meridian Perpetuals. The signed-order model is similar, but Meridian is a separate deployment with new configuration, identifiers, authentication headers, WebSockets, and margin accounting.

:::warning
Meridian Perpetuals mainnet is not live. The current integration target is Robinhood Chain testnet. Do not infer or hard-code mainnet configuration.
:::

## Before You Migrate

Treat Meridian as a new environment, not a hostname replacement. Ethereal balances, subaccounts, identifiers, signatures, approvals, and linked signers do not carry over.

For coding agents and repository-wide migrations, start by locating legacy assumptions:

```bash
rg -n -i 'ethereal|x-ethereal|wusde|socket\.io|ws2|range=(DAY|WEEK|MONTH)|uuidv4' .
```

Replace configuration through the migration steps below. Do not replace arbitrary `productId` or `subaccount` fields without checking whether the API expects a UUID, numeric onchain ID, or `bytes32` value.

## Breaking Changes

The following changes require code or configuration updates:

* **Environment:** Replace the Ethereal chain, RPC, API, contract, and token configuration. Meridian Perpetuals currently runs on Robinhood Chain testnet.
* **Signing:** Rebuild EIP-712 signatures using the domain and type strings returned by Meridian.
* **Identifiers:** Rediscover every server-generated ID. Meridian uses UUIDv7 rather than UUIDv4; treat IDs as opaque strings.
* **Authentication:** Replace the complete `X-Ethereal-*` header set with `X-Meridian-*`.
* **WebSockets:** Move to the native `ws` endpoint. Socket.IO has been removed, and existing native `ws2` clients must use the new host.
* **Margin:** Partition balances, equity, and liquidation state by quote-token pool instead of assuming one primary cross-margin pool.
* **Collateral:** Replace Ethereal's native-USDe deposit flow with ERC-20 USDe and MeridianUSD handling, then add internal conversion for isolated markets.
* **Products:** Support crypto perpetuals and mPerps, including `marginMode`, mark-price gaps, position fees, and `DELISTED` status.
* **Funding history:** Replace `range=DAY/WEEK/MONTH` with explicit time bounds and cursor pagination.

## 1. Bootstrap Meridian Configuration

Use the current testnet services from [API Hosts](/protocol-reference/api-hosts):

```bash
export MERIDIAN_API_BASE='https://api.meridiantest.net/v1'
export MERIDIAN_ARCHIVE_BASE='https://archive.meridiantest.net/v1'
export MERIDIAN_WS_URL='wss://ws.meridiantest.net/v1/stream'

curl "$MERIDIAN_API_BASE/rpc/config"
curl "$MERIDIAN_API_BASE/product?limit=100&orderBy=createdAt&order=asc"
curl "$MERIDIAN_API_BASE/token?limit=100&orderBy=createdAt&order=asc"
curl "$MERIDIAN_API_BASE/rate-limit/config"
```

Robinhood Chain testnet uses chain ID `46630`. Read the exchange address from `domain.verifyingContract` in `GET /v1/rpc/config`.

At startup, load:

* The EIP-712 domain and `signatureTypes`.
* Product UUIDs, `onchainId`, symbols, limits, fees, status, `marginMode`, and quote tokens.
* Token UUIDs, addresses, decimals, transfer settings, and `backingTokenId`.
* HTTP and WebSocket rate-limit costs.

Register a Meridian subaccount, fund it, and recreate any linked-signer relationships. Keep Ethereal and Meridian configuration as separate environment objects during migration.

## 2. Update API Schemas and Signing

The main `/v1` resource paths remain familiar, with these breaking changes:

| Ethereal                                                           | Meridian                                                                         |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `GET /v1/funding?productId=...&range=DAY`                          | `GET /v1/funding?productId=...&startTime=...&endTime=...` with cursor pagination |
| Funding field `createdAt`                                          | `chargedAt`; records also include `chargePerUnitUsd`                             |
| `GET /v1/funding/projected?productId=...`                          | `GET /v1/funding/projected-rate?productIds=...`                                  |
| `GET /v1/whitelist`                                                | Removed; no replacement preflight is required                                    |
| `X-Ethereal-Auth`, `-Sender`, `-Signature`, `-Intent`, `-SignedAt` | Corresponding `X-Meridian-*` headers                                             |
| Subaccount-level Archive balances                                  | Quote-token-aware balances with `tokenId`, `conversionIn`, and `conversionOut`   |

Meridian validates server-generated resource IDs as UUIDv7. Do not reuse Ethereal product, order, position, transfer, token, or subaccount IDs. `clientOrderId` remains a client-defined UUID or alphanumeric string of up to 32 characters.

Generate models from the current OpenAPI specification. UUIDv4-only validators and older generated clients will reject valid IDs or omit new fields.

Signed `POST` requests retain the outer `data` and `signature` shape. At runtime:

1. Read the domain and type strings from `GET /v1/rpc/config`.
2. Encode signed decimal values as D9 integers and JSON values as decimal strings.
3. Sign the subaccount's zero-padded `bytes32` name; use its UUID in API queries.
4. Generate `nonce` in nanoseconds and `signedAt` in Unix seconds.
5. Send the signature in the body or `X-Meridian-*` headers required by the endpoint.

`TradeOrder.productId` is the numeric onchain product ID. The JSON order request provides the same value as `onchainId`; neither field uses the product UUID.

Referral reads require the `X-Meridian-*` header set. Orders, cancellations, withdrawals, conversions, linked-signer changes, and referral actions use signed bodies. See [Authentication](/developer-guides/trading-api/authentication) and [Message Signing](/developer-guides/trading-api/message-signing).

## 3. Update Smart Contract Integrations

Use the Meridian exchange ABI. Several function signatures, returned tuples, and events are incompatible with the Ethereal ABI.

### USD, Gas, and Deposits

Ethereal used USDe as both its native gas asset and USD collateral. Meridian uses ERC-20 USDe as the backing asset for the ERC-20 MeridianUSD (`merUSD`) settlement token. Gas requires a separate balance of the network's native asset.

The USD deposit ABI changed:

```solidity
// Ethereal
depositUsd(bytes32 subaccount, bytes32 referralCode) external payable;

// Meridian
depositUsd(bytes32 subaccount, uint256 amount, bytes32 referralCode) external;
```

Approve the exchange to spend ERC-20 USDe, then call the nonpayable Meridian function. The exchange pulls USDe, wraps it 1:1 into MeridianUSD, and credits the subaccount. `depositOnBehalf` is also nonpayable and requires sufficient USDe allowance.

The generic `deposit(bytes32,address,uint256,bytes32)` signature is unchanged. It accepts the configured USDe backing-token address and performs the same wrapping. Synthetic and virtual tokens cannot be deposited directly.

Fetch MeridianUSD through `GET /v1/token`, then read `defaultWrappedToken()` from that contract when the underlying USDe address is required. Do not reuse WUSDe, send USDe through `msg.value`, or treat the gas balance as collateral.

Wrapping and conversion are separate operations. A deposit wraps wallet USDe into MeridianUSD. `POST /v1/token/convert` moves an existing exchange balance between MeridianUSD and related synthetic quote-token pools.

### Withdrawals

The `finalizeWithdraw(address account, bytes32 withdrawDigest)` ABI is unchanged. For a same-chain USD withdrawal, the contract burns MeridianUSD and transfers the configured ERC-20 USDe backing asset to the destination. Sign the withdrawal using the MeridianUSD token returned by the API, not the underlying USDe address.

`msg.value` on withdrawal finalization is reserved for a configured cross-chain messaging fee. It is not part of the USD amount. Meridian testnet does not currently have a cross-chain USD route, so use same-chain withdrawals there.

### Read Calls and Events

Regenerate contract bindings rather than copying Ethereal tuple definitions:

* `getToken` adds `name` and `backingToken`. Because `name` is the first tuple field, positional Ethereal decoders will misread the response.
* `getPerpPosition` adds `lastPositionFeeUsd`.
* `getPerpProduct` adds delisting state and `cumulativePositionFeeUsd`.
* `ProductStatus` adds `DELISTED`.
* `PerpOrderMatched` adds `makerFee` and `takerFee`, changing the event signature and topic used by event filters.
* New events cover token conversion, position-fee updates, and product delisting.

Raw storage readers must also be updated: Meridian uses `xyz.meridian.*` storage namespaces instead of `trade.ethereal.*`.

### Signatures and Validation

The `TradeOrder`, `InitiateWithdraw`, linked-signer, and signer-revocation message layouts remain compatible, but they must be signed with the Meridian EIP-712 domain. `ConvertToken` is a new signed type and accepts an account EOA or active linked signer; EIP-1271 contract-wallet signatures are not supported for conversion.

Meridian also enforces:

* Quote-token-scoped balances, position counts, liquidation, funding, and position fees.
* Conversion signatures, token relationships, amounts, zero fees, nonces, and expiry.
* The configured withdrawal fee and an amount large enough to cover it.
* Available balance after outstanding funding and position fees.
* Valid external prices and terminal `DELISTED` product state.

Signed orders, signer authorization, nonces, expiry, product limits, fills, fees, and settlement accounting remain onchain-validated. Reload configuration before retrying a rejected request.

## 4. Add Isolated Margin and Token Conversion

Meridian calculates equity and liquidation per quote-token pool. Cross-margin products share the concrete USD quote token. An isolated market uses a synthetic quote token backed by that token. Products sharing a quote token also share collateral and liquidation state, so resolve `quoteTokenAddress` through `GET /v1/token` and key risk state by subaccount and token.

To fund an isolated market:

1. Read the product's `marginMode` and `quoteTokenAddress`.
2. Resolve the quote token and its `backingTokenId`.
3. Deposit the concrete backing token.
4. Sign `ConvertToken` and call `POST /v1/token/convert`.
5. Wait for transfer status `COMPLETED` before using the destination balance.

```json
{
  "data": {
    "sender": "0xSIGNER_ADDRESS",
    "subaccount": "0xBYTES32_SUBACCOUNT",
    "fromToken": "0xBACKING_TOKEN_ADDRESS",
    "toToken": "0xISOLATED_QUOTE_TOKEN_ADDRESS",
    "amount": "1000",
    "nonce": "1785811200000000000",
    "signedAt": 1785811200
  },
  "signature": "0xEIP712_SIGNATURE"
}
```

Conversions are 1:1 in D9 units with zero fee. Valid paths are parent-to-child, child-to-parent, or between synthetic tokens with the same backing token.

Key constraints:

* The source balance excludes outstanding funding and position fees; unrealized profit is not convertible.
* Conversion has a separate rolling quota and can return `429 RATE_LIMIT_CONVERSION`.
* The initial status is `SUBMITTED`; track `CONVERT` transfers until `COMPLETED`.
* An account EOA or active linked signer can sign. `ConvertToken` does not support EIP-1271 contract-wallet signatures.
* Synthetic tokens cannot be deposited or withdrawn. Convert back to the concrete token before withdrawal.
* Meridian testnet currently supports same-chain withdrawals only.

See [Token Conversions](/developer-guides/trading-api/token-conversions) and [Token Transfers](/developer-guides/trading-api/token-transfers).

## 5. Account for mPerps and Position Fees

mPerps add isolated collateral, scheduled mark-price gaps, and position fees.

* Load `GET /v1/product/mark-price-gap`; schedules can change and should not be hard-coded.
* During a gap, the mark remains frozen while trading can continue. Model delayed mark-based stops and a possible reopen jump.
* Read `GET /v1/position-fee/projected-rate` when quoting across a gap.
* Use `GET /v1/position-fee` and Archive `GET /v1/subaccount/position-fee` for applied fees.
* Stop trading products that are not `ACTIVE`; `DELISTED` is terminal.

Position fees apply to both long and short positions open at the charge time:

```text
applied fee = abs(position quantity) * chargePerUnitUsd
```

For estimates:

```text
estimated fee = abs(position quantity) * projected positionFeeRate * reference price estimate
```

Use the applied `chargePerUnitUsd` for accounting. Outstanding `positionFeeUsd` reduces equity, available balance, convertible balance, and withdrawable balance. `positionFeeAccruedUsd` is the amount already applied through position settlement.

## 6. Replace the WebSocket Client

```text
wss://ws.meridiantest.net/v1/stream
```

:::warning
Socket.IO has been removed and is no longer supported. There is no Socket.IO compatibility endpoint; all clients must use the native WebSocket protocol.
:::

Clients already using Ethereal's native `ws2` protocol can retain the JSON message model, but must change the host, reload identifiers, and update payload parsing. Socket.IO clients require these changes:

| Removed Socket.IO behavior          | Native WebSocket replacement                        |
| ----------------------------------- | --------------------------------------------------- |
| `socket.emit('subscribe', payload)` | Send `{"event":"subscribe","data":payload}` as JSON |
| Per-event listeners                 | Route messages using the response `e` field         |
| `BookDepth` with `productId`        | `L2Book` with `symbol`                              |
| `MarketPrice` with `productId`      | `Ticker` with `symbol`                              |
| `exception` event                   | Inline `{ "ok": false, "code": "..." }` response    |
| WebSocket dry run                   | HTTP `POST /v1/order/dry-run`                       |

```json
{
  "event": "subscribe",
  "data": { "type": "L2Book", "symbol": "BTCUSD" }
}
```

Connections last approximately four hours. Reconnect, resubscribe, and rebuild state on every disconnect. Protocol pings are handled at the WebSocket layer.

An `L2Book` stream begins with a snapshot. Subsequent messages contain absolute quantities; zero removes a level. After reconnect, discard the old book and verify each `data.pt` matches the preceding `data.t`.

Account channels use the subaccount UUID; market channels use a symbol. Check each `{ "ok": true }` acknowledgement because an open socket does not confirm a successful subscription.

Treat `OrderUpdate` and `OrderFill` as authoritative. The `POST /v1/order` response is only an acknowledgement, and its deprecated `filled` field is not suitable for execution accounting.

`PositionUpdate.pfee` is accrued position fee, not outstanding `positionFeeUsd`. Funding and position-fee changes do not always emit a position message. Reconcile positions and fees over HTTP, and track conversions through `TokenTransfer` events with type `CONVERT`.

## Verify the Migration

Before enabling production order flow:

* No Ethereal host, chain ID, contract, header, signing-domain constant, Socket.IO dependency, or cached resource ID remains in the Meridian environment.
* Native-USDe and WUSDe assumptions are removed; gas, ERC-20 USDe, MeridianUSD, and exchange balances are tracked separately.
* API models accept UUIDv7 and include quote-token, conversion, mPerp, and position-fee fields.
* Contract bindings decode the current tuples and events, including the updated `PerpOrderMatched` signature.
* Owner and linked-signer signatures pass using the live EIP-712 configuration.
* Balances, equity, PnL, and liquidation state are partitioned by quote token.
* Conversions are not credited until `COMPLETED`.
* The WebSocket client handles acknowledgements, reconnects, resubscriptions, fresh snapshots, and sequence gaps.
* HTTP positions, balances, funding, position fees, transfers, and WebSocket state reconcile.
* Order dry runs, small orders, cancels, conversions, and withdrawals pass on testnet.

Common failure signals:

| Symptom                                          | Check                                                                           |
| ------------------------------------------------ | ------------------------------------------------------------------------------- |
| Signed requests return `401`                     | Domain, contract, chain ID, header prefix, nonce, and clock                     |
| UUID validation fails                            | Cached Ethereal ID or UUIDv4-only validator                                     |
| Isolated order lacks margin                      | Balance has not been converted into the product's quote token                   |
| Conversion remains unavailable                   | Transfer is still `SUBMITTED`, or fees reduce the effective source balance      |
| Conversion signature fails for a contract wallet | `ConvertToken` requires an EOA or active linked signer                          |
| Funding history fails                            | Replace `range` with millisecond `startTime` and `endTime`, then follow cursors |
| WebSocket connects without data                  | Subscription failed or the client still uses legacy channel fields              |
| Local order book diverges                        | Rebuild from a snapshot and verify `pt`/`t` continuity                          |
| Isolated risk is understated                     | Equity was aggregated across quote tokens or position fees were omitted         |
| Repeated conversions return `429`                | Apply `Retry-After` and handle the conversion-specific quota                    |

See [Order Placement](/developer-guides/trading-api/order-placement), [Margining](/trading/perpetual-futures/margining), [mPerp and Position-Fee API](/developer-guides/trading-api/rwa-and-position-fees), [WebSockets](/developer-guides/trading-api/websockets), and [System Limits](/developer-guides/trading-api/system-limits) for full schemas.
