All knowledge-base pitfalls in one place: symptom, cause, and fix. Details are in the file referenced by each row.
TL;DR
- An order response contains only a digest. “Success” ≠ “executed.” An IOC fill is the position delta; no delta → rejection. →
orders.md§7 - 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 - Market mode is a load-bearing fact.
post_onlyaccepts only POST_ONLY (2117);not_tradableaccepts nothing (2069). A metadata field that was parsed but not consumed turns every resting order into a rejection while the market is inpost_only. →orders.md§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 - A cache that validates itself against itself freezes forever. A market rename makes new listings invisible until restart. →
markets-and-numbers.md§7 - “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 - 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_statusexists 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:falsewithout 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_onlymarket 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.
© markpaper authors. Licensed under CC BY 4.0: when publishing or adapting this material, credit “markpaper — Nado knowledge base” and link to the original and the license.