All Lighter traps in one place: symptom, cause, fix, plus approaches that do not work (and why). The mechanics are detailed in the topic files; this page is the summary and set of invariants.
TL;DR — ten invariants
- An order’s identity is the
order_idstring.order_indexis rounded byJSON.parse(values around 1e16 > 2^53). Canceling by the number produces a valid transaction for a nonexistent order. - Placement returns only
tx_hash. IoC fill = position delta; no delta → REJECTED. - The book lags behind your own writes. Keep confirmed-write memory for ~45 seconds; do not send a duplicate, and filter out canceled orders.
- 40 writes / 60 seconds per L1 address. Use a sliding window, reserve capacity for cancellations and reduceOnly, seed it after restart, and check an entire multi-write sequence against window capacity before its first write.
- Two minimums: $10 and
min_base_amount(21706). Entries below either minimum are skipped; a full close is raised above both. - Two margin-fraction scales: metadata
/10000, account/100. - The key is not EVM. An address cannot be derived from it; authorization =
check_client+ account owner. - Timeout = unknown outcome. Do not retry placement; an unconfirmed cancellation means the order is still live.
- Resting reduceOnly is capped by the position and does not reverse it; an order larger than the remaining position is either not placed or is truncated.
- Throttling ≠ rejection ≠ minimum. Represent all three states in the result type and in instrumentation.
1. Pitfall summary
| Symptom | Cause | Correct approach | File |
|---|---|---|---|
| A large share of orders is rejected with 21706 | the dollar minimum passes, but lot-based min_base_amount does not | check metadata minSz in the same sizing function as the dollar minimum | markets-and-numbers.md §4 |
Stream of unexplained rejections, exchange returns HTTP 429 | 40 writes/60 seconds per address; the SDK turns 429 into an exception and the signer into an unnamed error | limiter before sending, explicit limit and reserve; recognize code 23000 in the text | rate-limits.md §2 |
| Lighter key fails format validation and the process does not start | format validation expects an EVM key; a Lighter key is 40 bytes | do not validate a Lighter key as EVM; prove authorization with the signer’s check_client and the account’s l1_address | signing-and-sdk.md §1 |
| Entry silently does not fill and is blamed on “minimums” | spread is wider than the IoC cross (1.18% versus 0.5%); the limit does not cross the book | measure the spread; use a 1.5% cross; the fill still occurs at the best price | markets-and-numbers.md §5 |
| A noticeable share of orders will not cancel (cancellation OK, order remains in the book), wasting write budget | cancellation uses rounded order_index; the exchange responds OK | store and match orders by the order_id string; cancel with the string | orders.md §5 |
| Pairs of orders collapse into one identifier → duplicates “fixed” in the wrong place | matching by the rounded number | match by the order_id string, not the number | orders.md §5.3 |
| The same order is canceled repeatedly | the book lags and the confirmed cancellation is forgotten | cancelled memory for 45 seconds | orders.md §7.2 |
| More orders appear in the book than were intentionally sent | the book lags and placement is repeated | confirmed-placement placed memory | orders.md §7.2 |
| Rejection after rejection at one price, thousands of wasted writes per day | code 21734: limit order too far from the mark (for example, after a sharp market drop) | remember the rejected price for 5 minutes; expose the first rejection and do not send repeats | orders.md §7.4 |
| Leverage is 200x instead of 2x, margin /100, ROE ×100; false protective trigger | the account fraction ("50.00" = percent) was divided by 10,000 | /100 for the account, /10000 for metadata; prove with the exchange identity | account-and-leverage.md §3 |
| More collateral than expected (L/2 times more at target leverage L) | leverage was not set; account remains at default 2x | update_leverage; safe under an open cross-margin position | account-and-leverage.md §4 |
| reduceOnly IoC orders do not go out for minutes | RO IoC is measured against the ordinary allowance even though it would fit the critical allowance | reduceOnly IoC is a critical write | rate-limits.md §2 |
| A real failure remains hidden indefinitely | throttling is recorded as an “exchange minimum” | distinguish the three states | rate-limits.md §4 |
| Trading process exits when the signer restarts | exits after the first /health failure | wait for the signer at startup (up to 90 seconds) | ops.md |
2. Error classes behind these pitfalls
2.1 “A number is not a value”
order_index is rounded during JSON parsing; the margin fraction has two scales; 10 ** -n ≠ 0.0001; sign is separate from position. General rule: for every value in an exchange response, ask about its origin and scale, and test precision against its string twin rather than with String(Number(x)) === String(x) (x has already been rounded, so that test reports “0 distorted” even when identifiers are corrupted).
2.2 “Did not read” ≠ “empty,” “success” ≠ “done”
tx_hash is not a fill; cancellation ok for the wrong identifier is not a cancellation; empty accounts[] is not an empty account; one malformed entry makes the entire read untrusted; timeout is not rejection. Every lower-layer gate (lot, minSz, minimums, write window) must be represented for the caller (SKIPPED with a reason, deferred), and the upper layer must not mark an action complete before confirmation.
2.3 zk-rollup echo
A write is confirmed, but reads do not show it yet. Both directions (duplicate placement and repeated cancellation) were observed at the same time. The remedy is confirmed-write memory with a TTL—not synthetic orders inserted into reads (they cannot be canceled) and not a longer tick interval.
2.4 The write window as a first-class resource
Any wasted write is not merely an “inefficiency”: the budget is exhausted and necessary writes (cancellations, closes) starve. Hence the reserve, seeding, critical allowance for reduceOnly, capacity checks for a write sequence before its first write, 21734 rejection memory, echo memory, and leverage cooldown.
2.5 Instrumentation lies first
Throttling recorded as a failure or as an “exchange minimum” distorts the picture. Start incident analysis from exchange primary sources (positions, accountActiveOrders, order history), not alerts: the same alert text can represent different states.
3. Anti-patterns (and why they do not work)
| Approach | Why it does not work |
|---|---|
| Implement Lighter signing in TypeScript instead of a sidecar | rewriting someone else’s cryptography for live funds adds unnecessary risk; the SDK binary already ships in pip |
| Empty signer bearer token means “allow everyone” | one environment mistake exposes the trading-key signer; fail closed |
| Retries inside the signer | a silent order retry can double the position; the calling code that sees the book must decide |
| Substituting size/price in the signer | creates a mismatch with what the calling code believes was placed |
| Write budget “per tick” instead of a sliding window | several ticks fall into the 60-second window, multiplying the excess |
| Hard-block writes for one minute after restart | would delay protective orders when the book is least known; seed gradually |
| Insert a synthetic order into book reads instead of keeping write memory | it has no order_index and cannot be canceled; the first cancellation would go nowhere |
| Permanently ban a price after 21734 | the mark moves, so the price would be lost forever; keep it in memory for 5 minutes |
Precision check String(Number(x)) === String(x) | x is already rounded; the check reports 0 distorted values when identifiers are actually corrupted |
| Treat an asset as “unlisted” from memory | the list changes; query orderBooks on the correct instance |
Exit after the signer’s first /health failure at startup | a signer restart race takes down the trading process |
| Separate “IoC minimum” below $10 | on Lighter both types used the same min_quote_amount; there is no measurement below $10 |
Open questions / not verified
- Summary across files: deduplication by
client_order_index; GTT after 28 days; codes other than 23000/21706/21734; the 21734 threshold; read limits; write window across sub-accounts; fees; isolated margin; liquidation from the maintenance fraction; all of zkLighter mainnet; WebSocket; key registration.
Facts verified through 2026-09-16.
© markpaper authors. Licensed under CC BY 4.0: when publishing or adapting this material, credit “markpaper — Lighter knowledge base” and link to the original and the license.