# Nado — pitfalls: summary table, error classes, anti-patterns

All knowledge-base pitfalls in one place: symptom, cause, and fix. Details are in the file referenced by each row.

## TL;DR

1. **An order response contains only a digest.** “Success” ≠ “executed.” An IOC fill is the position delta; no delta → rejection. → `orders.md` §7
2. **Resting reduceOnly is impossible (2067).** A TP order in the book is a plain limit order that reverses the position after it closes. Protection: keep the sum of resting TPs no larger than the position and cancel the asset’s resting TPs before every IOC that changes the position. This follows from the exchange design. → `orders.md` §3
3. **Market mode is a load-bearing fact.** `post_only` accepts only POST_ONLY (2117); `not_tradable` accepts nothing (2069). A metadata field that was parsed but not consumed turns every resting order into a rejection while the market is in `post_only`. → `orders.md` §4
4. **The $100 minimum applies only to the book.** IOC orders pass well below it. Do not trust documentation about minimums; measure them with orders. → `orders.md` §5
5. **A cache that validates itself against itself freezes forever.** A market rename makes new listings invisible until restart. → `markets-and-numbers.md` §7
6. **“Could not read” ≠ “empty.”** Orders are read by market: a partial read looks like an empty account. Make irreversible decisions only after a complete survey. → `account-and-fees.md` §3
7. **The nonce-tag scheme remains unchanged across deployments**, or the entire book becomes orphaned. Manual orders on the sub-account are “foreign” and prevent the conclusion that the account is empty. → `api-and-signing.md` §7

---

## 1. Pitfall summary

| Symptom | Cause | Correct approach | File |
|---|---|---|---|
| Code concludes that an IOC executed from a successful response | `place_order` returns only a digest | Fill = position delta before/after; 3 attempts at 150 ms; no delta → `REJECTED` | `orders.md` §7 |
| Resting TP with reduceOnly is rejected (2067) | RO is only for IOC/FOK | Use GTC for TP; encode the role in the nonce tag; clamp to the position; cancel the asset’s TP before IOC | `orders.md` §3 |
| A TP opens the opposite position after a close | A plain limit order does not stop at zero | Before every IOC that changes the position, cancel the asset’s resting TPs; replace them only after a fresh position read and never above the position | `orders.md` §3 |
| The $100 minimum “blocks” a dust close | The minimum was taken from documentation | IOC orders pass well below $100; use two minimums; never gate a full close | `orders.md` §5 |
| Size is 0.28 instead of 0.29 with a 0.01 lot | Division by an inexact float lot | Add epsilon to the quotient and construct the result with BigInt | `markets-and-numbers.md` §3 |
| Market cache remains frozen until restart and new listings are invisible | Market rename (CIRCLE → CRCL, 2026-08-10); the stability check rejects every refresh because the comparison baseline is the cache itself | Do not freeze the cache because of an asset outside positions/orders; issue 3 `symbols` requests before restart | `markets-and-numbers.md` §7 |
| “Asset X is not listed on Nado” is wrong | Claimed from memory; Nado is not limited to crypto | Always query `symbols` | `markets-and-numbers.md` §6 |
| Stream of 2069 rejections | Market is `not_tradable`, but metadata exists, so it appears “listed” | Do not enable new markets in `not_tradable` for trading | `orders.md` §4 |
| Resting orders are not placed and every attempt is rejected with 2117 | Market is `post_only`; the executor does not see `trading_status` because the field was parsed and discarded | Use POST_ONLY for resting orders based on a fresh cache; on 2117 for DEFAULT, resend as POST_ONLY; defer partial IOC; never gate a full close | `orders.md` §4 |
| Repeats can exhaust the configured execute budget | Repeated close on a `post_only` market is rejected with 2117 | Retry close only after a fresh market-mode read; calculate the cost before acting | `rate-limits.md` §5 |
| “Empty account” while live orders exist | Orders were read for only some markets | Make irreversible decisions only with proof of a complete all-market survey (`ordersComplete`) | `account-and-fees.md` §3 |
| Cancellation is “confirmed” by the absence of an error | Its digest is absent from `cancelled_orders` | Confirm only by digest; otherwise do not place a replacement | `orders.md` §9 |
| Equity is `UNKNOWN` without any error | Wrong sub-account name (`exists:false`) | Print the name and bytes32 at startup and compare them with the UI | `api-and-signing.md` §6 |
| Nonce from the response is corrupted | Values around 1.87e18 exceed 2^53 | String only → `BigInt` | `markets-and-numbers.md` §2 |
| An `orders` request is rejected locally | Its weight exceeds the configured bucket capacity | Use an explicit chunk size that fits within query burst | `rate-limits.md` §4 |
| Every request returns 403 | Manually set `Accept-Encoding` | Do not set it; undici negotiates it itself | `api-and-signing.md` §2 |
| Every execute goes to the wrong network | Gateway is for one network and chain id for another | Preflight `contracts`; abort startup | `api-and-signing.md` §1.3 |
| After deployment, the process does not recognize its own orders | Nonce-tag scheme changed | Keep the scheme unchanged; migration requires manually canceling the book | `api-and-signing.md` §7 |
| Master key is on the server | Signer was linked with a CLI script | Use a browser popup | `api-and-signing.md` §11 |

---

## 2. Error classes (how to recognize them in new code)

- **A field was read but not consumed.** `trading_status` exists in metadata, but the executor does not see it: the code looks complete while behavior does not change. Check that every metadata field has a consumer and every consumer has a test.
- **A cache validating itself against itself.** The baseline is the previous state of the same cache, so the freeze becomes self-sustaining. The stability check must rely on what is actually in use (positions and orders), not the entire list.
- **Success without evidence.** A digest without a delta, cancellation without the digest in the response, `exists:false` without an error. Record “done” only with evidence; return an uncompleted action upward as a rejection rather than writing it off as covered.
- **Remembered state survives a schema change.** The nonce tag, stored digests of your orders, and metadata cache must either be restored from the exchange after restart or schema change, or be honestly marked “unknown.”
- **A number from documentation instead of a measurement.** The $100 minimum, “there are no stock perpetuals.” Measure exchange constants.
- **A local throttle refusal ≠ an exchange rejection.** Temporary and permanent outcomes need distinct statuses, or instrumentation lies in both directions.

---

## 3. Anti-patterns (and why they do not work)

| Approach | Why not |
|---|---|
| Replace IOC with FOK/POST_ONLY on a `post_only` market for partial position increases and reductions | There is no taker flow; POST_ONLY + RO is forbidden; the rejection must reach instrumentation |
| Convert resting-order 2117 rejections into “SKIPPED due to minimum” | Suppresses real complaints; for orders above the minimum, the minimum message is false |
| Treat a `post_only` market as unlisted | The exchange accepts POST_ONLY; the market is listed |
| Gate a full close on a flag from the symbols cache | The cache can be up to 5 minutes stale; rely only on an exchange rejection in the same tick |
| Hide metadata for a `not_tradable` market that already has a position or orders | There is then no way to manage the position, and “market was delisted” is false; prevent only new markets from being enabled again (`orders.md` §4) |
| Extend the cache stability check to `trading_status` | Opening a market would freeze the cache just like a rename |
| Change the nonce-tag scheme to something “more convenient” | Orphans the entire resting book |
| Link a signer through a CLI script with the master key on the server | The master key must not leave the operator’s browser |
| Retry an entry after timeout | The outcome is unknown → double position |

---

## 4. Open questions (knowledge-base summary)

- Oversized reduceOnly IOC: cap or 2064; semantics of 2064. → `orders.md`
- Exact IOC floor (IOC orders passed well below $100; no binary search was performed, and one estimate contradicts this—the contradiction is unresolved). → `orders.md`
- Full close on a `post_only` market with a position: rejection code and budget cost. → `orders.md`, `rate-limits.md`
- `reduce_only` / `soft_reduce_only`: semantics and codes. → `markets-and-numbers.md`
- Rejection code for a POST_ONLY order that crosses the book. → `orders.md`
- The 2400/600 limits are documented only; which wallet is counted. → `rate-limits.md`
- Fee tier: rolling 30 days or monthly epochs. → `account-and-fees.md`
- Trade history, candles, WebSocket, funding, liquidation, ADL, and isolated margin have not been studied. → `account-and-fees.md`, `markets-and-numbers.md`
- Official SDK: not identified; signing uses `viem`. → `api-and-signing.md`

---

Facts verified through 2026-09-16. Nado changes: recheck market modes, minimums, codes, and limits with live requests.

---

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