# Nado — account, positions, equity, and fees

How to read your own sub-account on Nado: `subaccount_info`, positions and their PnL, account value in USDT0, order-survey completeness, signer-link verification, and fees.

## TL;DR

1. **A single read is `subaccount_info` for the bytes32 sub-account** (weight 2): `exists`, `healths[]`, `perp_balances[]`, `perp_products[]`. Account value = **unweighted health** (`healths[2].health`, x18) — “assets minus liabilities” in USDT0. *Verified 2026-07-24.* → §1
2. **Unified cross-margin: USDT0 collateral is already included in account value.** There are no free stablecoins outside equity: do not add the USDT0 balance separately, or you will count it twice. → §1.3
3. **A position is `perp_balances[].balance.amount`** (x18, sign = side) and `v_quote_balance`. Average entry price ≈ `|v_quote / amount|`, unrealized PnL = `amount × oracle + v_quote`, and the oracle comes from `perp_products[].oracle_price_x18`. → §2
4. **Orders are read by market.** A truncated read is indistinguishable from an empty account. Conclude “there are no orders,” and make irreversible decisions based on that conclusion, only after a complete survey of every market. → §3
5. **Consistent snapshot: positions → orders → positions.** If even one `amount` changed between the two reads, a fill occurred in between and the snapshot is inconsistent — do not make decisions from it; read again. → §4
6. **Leverage is not configurable:** margin is determined by product weights; `maxLeverage ≈ 1/(1 − long_weight_initial)`. There is no separate request for changing leverage. → §5
7. **Fees.** VIP 0 is maker 1.0 / taker 3.5 bps; the tier is based on 30-day volume; maker becomes 0 at $100M and rebates start at $500M. Your rates come from the `fee_rates` query; product rates come from `symbols`. Uncertainty: rolling 30 days or monthly epochs. → §6

---

## 1. `subaccount_info`

Request: `{type:'subaccount_info', subaccount:<bytes32>}` (weight 2; see `api-and-signing.md` §6 for bytes32).

### 1.1. Fields used

| Field | Meaning | How to read it |
|---|---|---|
| `exists` | the sub-account exists (created by a deposit) | `false` → account value 0; most often this means **the wrong sub-account** (name), or the deposit has not arrived yet |
| `healths[]` | an array of ≥ 3 records `{health, …}` | index 2 is **unweighted health** = account value in USDT0 (x18). According to the documentation, indices 0 and 1 are initial and maintenance health; they were not verified against live data |
| `perp_balances[]` | `{product_id, balance:{amount, v_quote_balance}}` | signed `amount` x18 is the position; `v_quote_balance` x18 is the quote leg of the position |
| `perp_products[]` | `{product_id, oracle_price_x18, risk:{long_weight_initial_x18, …}}` | oracle for valuation; weight for margin |

Decode fail-closed: missing `exists`, fewer than 3 `healths` entries, a malformed `product_id`, or a duplicate `product_id` in `perp_balances` makes the entire read invalid, rather than “partially correct.”

### 1.2. Account value

```ts
const accountValue = raw.exists ? x18ToNumber(x18ToBigInt(healths[2].health)) : 0;
```

This number is the account equity. Read it with the same request as the positions (one `subaccount_info`) so that the “equity + positions” pair is captured at one moment.

### 1.3. Unified margin and “free stablecoins”

The deposit is USDT0 on Ink. Collateral is already included in unweighted health, and there is no free collateral outside account value: adding the USDT0 balance to health would count it twice.

---

## 2. Positions

For every `perp_balances` record with `amount ≠ 0`:

```ts
const size = x18ToNumber(amountX18);                // sign = side
const vQuote = x18ToNumber(vQuoteX18);
const entryPrice = size !== 0 ? Math.abs(vQuote / size) : 0;
const notional = Math.abs(size) * oracle;           // oracle from perp_products
const unrealizedPnl = size * oracle + vQuote;       // position quote not yet settled into USDT0
```

- Take `longWeightInitial` for estimating position margin (§5) from this response's `perp_products[].risk` when it is in (0, 1); otherwise, take it from `symbols`.
- A position in a product that is **missing from metadata** (the coin could not be named) → the position read is **degraded** (`ok:false`): it cannot support a conclusion that there is no position in that coin. A missing oracle (`≤ 0`) for a position also makes the read degraded.
- `entryPrice` calculated through `v_quote / amount` is an estimate that includes accrued funding/realization in the leg; it has not been verified as the exact average entry price (open question).

---

## 3. Order-survey completeness

The `orders` query requires an explicit list of `product_ids` (`orders.md` §8). Consequences:

- **Reading only some markets looks like an empty account.** An order resting in a market outside the list (a remnant after a crash, or a manual order) is invisible.
- The **working set** of products is markets with nonzero balances and with your own orders. It is enough to see orders in markets where there is a position or one of your own orders.
- A **complete survey** covers every product from `symbols`. Only a complete read proves “there are no orders”; a partial view is usable for markets in the working set and **never** for concluding “the account is empty.” Store a completeness indicator (for example, an `ordersComplete` flag in the snapshot) alongside the order snapshot.
- A complete survey is expensive: weight 2 × the number of products (~72), so run it periodically rather than on every read.

---

## 4. Stability fence

Read your own account snapshot in the order **positions → orders → positions**: `subaccount_info`, then `orders` for the required products, then `subaccount_info` again. If the product-to-`amount` map changed between the reads (for even one product), `stable=false`: a fill occurred between the reads, and the “positions + orders” pair in that snapshot is inconsistent. Use the positions and account value from the **second** read.

A `subaccount_info` read has weight 2, so there is no reason to skip the fence.

---

## 5. Leverage and margin

- There is no separate account-level leverage setting. The margin requirement follows from the product weight: initial margin fraction = `1 − long_weight_initial` (0.02 for BTC → 50x; 0.1 → 10x; 0.2 → 5x).
- You neither need nor have a way to set leverage before an order; calculate the margin-based size limit from `maxLeverage`, derived from the product weight.
- `isolated_only` in `symbols` and the `isolated` bit in the appendix were not used; the semantics of isolated mode on Nado are not verified.
- Liquidation, ADL, maintenance weights, and funding were not investigated (open questions).

---

## 6. Fees

### 6.1. Your rates

- The `fee_rates` query for a sub-account returns maker/taker rates and a tier. A new sub-account with no volume is at **VIP 0: maker 1.0 / taker 3.5 bps** (tier table in §6.2).
- In `symbols`, every product has `maker_fee_rate_x18` / `taker_fee_rate_x18` (BTC has `1e14` / `3.5e14` = 1.0 / 3.5 bps; pre-listing PONS had 0 / 0 on 2026-09-11). Which source takes precedence when these differ from the account rates is not verified; use the account's `fee_rates` for calculations.
- The actual trade fee is not included in order responses (the response is a digest); accounting through account history was not verified.

### 6.2. Tiers (docs.nado.xyz, Fee Schedule Update, captured 2026-09-10)

Nado calculates the tier from **30-day** volume.

| Tier | 30d threshold | maker/taker, bps |
|---|---|---|
| VIP 0 | $0 | 1.0 / 3.5 |
| VIP 1 | $5M | 0.8 / 3.3 |
| VIP 2 | $25M | 0.5 / 3.0 |
| VIP 3 | $100M | **0.0** / 2.8 |
| VIP 4 | $250M | 0.0 / 2.5 |
| VIP 5 | $500M | **−0.5** / 1.8 |
| VIP 6 | $1B | −0.8 / 1.5 |

- Makers pay 0 from $100M/30d, and rebates start at $500M/30d.
- **Uncertainty:** the documentation says both “prior 30 days” and “epochs resetting on the first of each UTC month.” If the tier is recalculated monthly, the entire first month uses VIP 0. Check the tier field in your account's `fee_rates` as volume accumulates.

### 6.3. Fee estimation

Use the account's current fee rates when estimating costs. IOC is taker; POST_ONLY/DEFAULT resting in the book is maker. Product rates of 0 / 0 during pre-listing are a temporary state, not the norm. An estimate is not proof of the actual fee charged; accounting through exchange history remains unverified (§9).

---

## 7. Key-link verification

- Compare the address derived from the signing key with the sub-account's `linked_signer` (`linked_signer` query, weight 5, `api-and-signing.md` §11) **or** with the master address (self-signing).
- If the verification itself is unavailable → `ok:false`: refuse on the first launch; on a later check, retain the last known state (fail-closed).
- A zero address in `linked_signer` means that no signer is linked.

---

## 8. Pitfalls

| What breaks | Why | Correct approach |
|---|---|---|
| Equity ×2 | the USDT0 balance was added to unweighted health | USDT0 is already included in health; do not add it separately |
| “Account is empty” while live orders exist | orders were read for only some markets | conclude “there are no orders” only from a complete survey of every market (`ordersComplete`) |
| A coin appears flat while a position exists | the product is absent from metadata (truncated `symbols`) | a position in an unknown product means the read is degraded |
| Equity remains `UNKNOWN` forever with no errors | the sub-account name is wrong, `exists:false` | print the name and bytes32 at startup and compare them with the UI |
| Position and orders disagree in the snapshot | a fill occurred between reads | read positions → orders → positions; if `amount` changes, read again |

---

## 9. Open questions / not verified

- **Trade and fill history through the API:** not used; realized PnL, actual fees, and average fill price were not verified.
- **`healths[0]`, `healths[1]`:** initial / maintenance according to the documentation, not verified; the liquidation formula and ADL were not studied.
- The **accuracy of `entryPrice = |v_quote / amount|`** as the average entry price was not verified against the UI.
- **Fee tier:** rolling 30 days or epochs starting on the 1st day of the UTC month; the `fee_rates` response shape and weight were not recorded.
- **Product rates versus account rates** when they differ: which one the matching engine applies.
- **Withdrawals by a signer** “only to the master” are documented but not verified; deposits and withdrawals were not performed through the API (UI only).
- **Multiple sub-accounts under one address:** margin isolation between them follows from the design (different bytes32 values), but was not verified against live behavior.
- **Funding:** frequency, where to read it, and how it enters `v_quote_balance` were not studied.

---

Facts verified through 2026-09-16. Nado changes: verify fees and response shapes against the live API.

---

<!-- license-footer -->
_© markpaper authors. Licensed under [CC BY 4.0](LICENSE.md): when publishing or adapting this work, credit “markpaper — Nado knowledge base” and link to the original and the license._
