Core concepts
The handful of concepts every Lighter integration depends on.
L1 address
The user's Ethereum address. It owns the Lighter account, is used for deposits and secure withdrawals, and signs a few sensitive actions (registering an API key, transfers to other addresses, fast withdrawals, approving an integrator with fees).
Account index
Lighter's integer ID for an account. One L1 address can own several:
- Master account – created automatically on the first deposit.
- Sub-accounts – created with a
CreateSubAccounttransaction. Each has its own index, balances and API keys. - Public pools – a special kind of sub-account that other users can deposit into for shares.
How indexes are assigned:
- Master accounts count up from 0, in order of creation. The lowest indexes (0–2) are system accounts such as the treasury and insurance fund.
- Sub-accounts and pools count down from
2^48 - 2=281474976710654, which is the LLP. Every new sub-account, public pool or staking pool gets the next lower index.
So a small index is a master account and a very large one is a sub-account or pool. Don't assume a sub-account's index is close to its master's; look it up.
Find account indexes for an L1 address with accountsByL1Address. The first entry in sub_accounts is the master account:
curl "https://mainnet.zklighter.elliot.ai/api/v1/accountsByL1Address?l1_address=0xYourAddress"API key
An API key is a keypair (separate from the Ethereum key) that signs Lighter transactions. Each key is bound to one account index, uses an index from 4 to 254 (0–3 are reserved for Lighter's apps), and is registered once with an L1 signature (ChangePubKey). Permissions, auth tokens and maker-only keys are covered in API keys.
Transactions are queued per API key. Each key has its own nonce stream, and a transaction can't execute before the ones sent earlier on the same key.
Use separate API keys for independent workloads
Every transaction waits for a speed bump before it executes. The length depends on the account tier and the transaction type (for example, on Standard, taker orders wait 300 ms and post-only orders 0 ms; see Account Types).
Because the queue is per key, a slow transaction delays everything sent after it on the same key. A post-only order sent right after a taker order on the same key waits for the taker order's 300 ms.
Use a dedicated key per workload (e.g. one for quoting/cancelling, one for taking, one for transfers) so that:
- delays on one workload don't hold up another, and
- each workload manages its own nonces without clashing.
Premium accounts can go further with maker-only API keys, which skip the speed-bump queue entirely.
Nonce
Every transaction carries a nonce, tracked per API key.
- Default rule:
new_nonce = previous_nonce + 1. - Get the next nonce: nextNonce (
GET /api/v1/nextNonce?account_index=…&api_key_index=…). - Skipping nonces: set the
SkipNonceattribute (skip_noncein the Python SDK, 4th inL2TxAttributes) to1. Then anyold_nonce < new_nonce < 2^47 - 1is accepted. Nonces are capped at2^48 - 1. - The Python SDK manages nonces for you. Manage them locally if you run several processes or need the lowest latency.
When a rejected transaction consumes its nonce
| Where it's rejected | Examples | Nonce consumed? |
|---|---|---|
API server (sendTx returns an error code) | Bad signature, malformed tx, wrong nonce, rate limit, ExpiredAt less than ~5 s away, not enough margin for a resting order | No |
| Sequencer, before the tx is applied | ExpiredAt passed before execution, not enough margin or balance for a resting order (margin changed after the API accepted it), market inactive, invalid integrator fee, wrong nonce | No |
| Sequencer, while applying the tx | Invalid size or price, duplicate clientOrderIndex, wrong reduce-only direction, orderExpiry already passed, cancelling or modifying an unknown order | Yes (the tx is included as a no-op) |
"Resting order" means any order that can rest on the book, i.e. not IOC or TWAP. Taker orders (IOC / market) and TWAP orders aren't margin-checked up front. If a fill would leave the account without enough margin, the order is cancelled during matching and the nonce is consumed.
An order that executes normally and is then cancelled by matching rules (e.g. post-only would cross, IOC not filled, not enough margin for a taker fill, self-trade prevention) is a successful transaction and consumes its nonce.
When a transaction fails without consuming its nonce, any transactions you already sent on the same key with higher nonces will fail too. Re-sign them starting from the current nextNonce.
Retrying safely
- Rejected by the API server: fix the issue and resend with the same nonce.
- Accepted (
code: 200), outcome unknown (timeout, dropped connection, no WebSocket confirmation): don't re-sign blindly. First check the tx status by hash (GET /api/v1/tx?by=hash&value=…) or theaccount_txchannel, and comparenextNoncewith the nonce you used.- Executed: done, don't resend.
- Failed and
nextNoncestill equals your nonce: resend with the same nonce. - Failed and the nonce was consumed: re-sign with the new
nextNonce.
- Re-signing with a new nonce while the original may still execute can place the order twice. Reusing the same
clientOrderIndexhelps: a second order with aclientOrderIndexthat belongs to a still-open order is rejected. It doesn't protect you once the first order has filled or been cancelled, so always check status first.
Integer prices and sizes
Prices and amounts are sent as integers. Each market publishes its decimals via orderBookDetails:
{ "symbol": "ETH", "market_id": 0, "size_decimals": 4, "price_decimals": 2,
"min_base_amount": "0.0020", "min_quote_amount": "10.000000" }base_amount = size × 10^size_decimals→ 0.01 ETH =100price = price × 10^price_decimals→ $3,100.00 =310000
Minimum order: the larger of min_base_amount (in the base asset) and min_quote_amount (in USDC). Minimums apply to maker orders.
For taker orders, price is the worst price you accept. If the sequencer can't fill at that price or better, the rest is cancelled.
Transaction lifecycle
- Sign locally (WASM / Go / Python signer) →
tx_type,tx_info,tx_hash. - Send via
POST /api/v1/sendTx(orsendTxBatch, or WebSocketjsonapi/sendtx). - API response
code: 200means accepted, not executed. - Confirm execution via WebSocket account channels or
GET /api/v1/tx.
Transaction status values are listed in Data Structures, Constants and Errors.
Auth tokens and account tiers
- Private REST endpoints and WebSocket channels need an auth token signed by an API key, or a read-only token. See API keys.
- Fees, latency and limits depend on the account tier (Standard, Plus, Premium). See Account Types and Rate Limits.
Updated 2 days ago
