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
- A single read is
subaccount_infofor 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 - 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
- A position is
perp_balances[].balance.amount(x18, sign = side) andv_quote_balance. Average entry price ≈|v_quote / amount|, unrealized PnL =amount × oracle + v_quote, and the oracle comes fromperp_products[].oracle_price_x18. → §2 - 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
- Consistent snapshot: positions → orders → positions. If even one
amountchanged between the two reads, a fill occurred in between and the snapshot is inconsistent — do not make decisions from it; read again. → §4 - 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 - 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_ratesquery; product rates come fromsymbols. 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
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:
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
longWeightInitialfor estimating position margin (§5) from this response'sperp_products[].riskwhen it is in (0, 1); otherwise, take it fromsymbols. - 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. entryPricecalculated throughv_quote / amountis 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, anordersCompleteflag 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_onlyinsymbolsand theisolatedbit 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_ratesquery 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 hasmaker_fee_rate_x18/taker_fee_rate_x18(BTC has1e14/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'sfee_ratesfor 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_ratesas 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_signerquery, 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_signermeans 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_ratesresponse 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_balancewere not studied.
Facts verified through 2026-09-16. Nado changes: verify fees and response shapes against the live API.
© markpaper authors. Licensed under CC BY 4.0: when publishing or adapting this work, credit “markpaper — Nado knowledge base” and link to the original and the license.