Orderbook
The Orderbook precompile is the on-chain execution surface for native markets — both spot and perpetual. The same contract, calls, and balances are shared across both market types; a market's behavior is determined by the MarketType set at creation.
Use it for placing/canceling/updating orders, moving funds between accounts, opening leveraged perpetual positions, arming take-profit / stop-loss triggers, and reading balances and order state.
All timestamps sent to the orderbook are in microseconds, not milliseconds or seconds. This applies to every deadline and ttl on this precompile.
deadline is the latest batch the intent is allowed to be included in — the intent can land in any batch up to and including the one whose end matches deadline. It must be aligned to the market's auction_interval (a multiple of it), or the validator rejects the intent with "CLOB validation failed: Deadline is not aligned to auction interval". Compute it as:
deadline = ceil((now + LAG) / auction_interval) * auction_intervalLAG is the headroom you add to now so the intent reaches enough validators before its target batch. It is capped at 10 minutes; aim for at least 1 minute under normal conditions, smaller when you want to target a specific upcoming batch.
The alignment rule applies to every deadline-bearing call on this precompile — transfer as much as orders, cancels, updates and triggers. All of them pass through the same validator check, so an unaligned transfer deadline is rejected just like an unaligned order deadline.
See Batch Deadline in the protocol reference for the full discussion of deadline semantics and the trade-offs around LAG.
Order flags
submitOrder carries an order's boolean properties in a single uint8 flags bitfield rather than one bool argument per property. OR together the bits you want; 0 is a plain resting limit order.
0
0x01
REDUCE_ONLY
The order may only reduce the submitter's existing position. Perp markets only.
1
0x02
IOC
Immediate-or-cancel: whatever does not match in the order's batch is cancelled at the end of it instead of resting on the book.
2
0x04
POST_ONLY
Add-liquidity-only: the order rests, but may not trade in the batch that admitted it. See Post-only orders.
Combinations are checked when the intent is validated:
IOC | POST_ONLYis rejected (post-only order cannot be immediate-or-cancel) — IOC demands a fill in the admitting batch, post-only forbids one.POST_ONLYon aMarketorder is rejected (post-only is not valid for a market order) — a market order has no resting price, so it has nothing to post at.REDUCE_ONLY | POST_ONLYis fine, as is any other combination.Market orders must set
IOC(market orders must be immediate-or-cancel).
Bits 3–7 must be zero. Calldata carrying a flag bit the network does not recognise is rejected, not masked off — so an intent is never executed with a property silently dropped from it. Future order properties arrive as new bits here rather than as another overload.
Post-only orders
A post-only order is guaranteed to add liquidity: it rests on the book and never takes from it on the way in. Set POST_ONLY (0x04) in flags.
Because pod matches in discrete batch auctions rather than on arrival, "would this order cross the book right now?" is the wrong question — every intent in a batch is matched together, so two orders that arrive in the same batch and match each other are both takers. The guarantee is therefore expressed against the batch:
A post-only order may not trade in the batch that admitted it. From the next batch onwards it is an ordinary resting maker and matches normally.
What that means in practice:
The order enters the book immediately and is reported
active, like any other resting order.If it would have traded in that first batch, it is removed from the book instead. The removal is terminal and never partial — a refusal never leaves a post-only order half-filled — and it is reported with the terminal status
post_only_refused, which is distinct fromcanceledso you can tell a refusal from a cancel you sent yourself.If it would not have traded in that batch, nothing happens to it: it rests, and can be matched from the next batch on.
If another order at the same price with better queue priority absorbs the crossing liquidity first, your post-only order simply rests — it never had the opportunity to take, so there is nothing to refuse.
Two post-only orders admitted in the same batch that cross only each other are both refused. Neither took resting liquidity, but each would have taken from the other.
Amendments re-arm the guarantee. An update that re-queues the order — a price change, or a size increase — makes it a newcomer again, so it may not trade for the rest of that batch and can be refused in it (for example, when you reprice it onto a crossing level). An update that only decreases the size keeps its queue priority and its original admission batch, so it goes on matching normally.
Transfers between accounts
transfer moves amount of token from the signer's balance to another account's balance. Both sides are accounts on Pod, so the funds stay on the network and the recipient can trade them immediately.
The call is global, not orderbook-bound: it names no orderbookId, because the balance it moves is shared across every market. It works standalone and as a submitBatch sub-intent.
What is checked before attestation. The transaction is rejected outright — it never lands — when:
amountis zero.recipientis the signer. It would burn a nonce and move nothing.recipientis the zero address. Burning has to be deliberate, not a typo.recipientis a system precompile.deadlineis unaligned to the auction interval, or points at a batch that has already executed.
The sender's balance is deliberately not checked there: pending fills can raise it before the batch executes. It is checked against the withdrawable balance when the batch executes, and a transfer the balance does not cover is reported as a failed outcome rather than rejected as a transaction.
transfer cannot be delegated. It is refused as the inner call of a delegated wrapper, and refused again when it appears inside a delegated(submitBatch([…])). Pinning the recipient to the master — what makes a delegated call safe elsewhere — would force a self-transfer here, which moves nothing, so the envelope is refused instead. Allowing it would make a leaked delegate key worth as much as a leaked master key for the whole spendable balance.
Following a transfer. Every transfer has an id derived exactly like an order_id, so you can compute it before you submit:
sequence is the intent's position inside a submitBatch envelope, 0 for a standalone transfer. A transfer has no receipt of its own — it settles inside the solver's submitSolutions transaction — so this channel is the only place its fate appears:
eth_subscribe("pod_transfers", { account, since })
Live outcomes, one array per tick. account matches either side, so a recipient hears the credit as readily as a sender hears the debit. Each entry carries transfer_id, from, to, token, amount (18 decimals), error and timestamp_us.
error is absent when the funds moved, and otherwise names why they did not:
insufficient_balance— the sender's withdrawable balance did not cover it when the batch executed.recipient_not_resident— the recipient's account was not resident when the tick ran, so the credit had nowhere to land.not_included— the solver left the intent out of the solution its deadline pointed at.
All three mean nothing moved on either side. The nonce is spent either way, so retrying means a new transaction, which gets a new transfer_id.
Batch envelope
submitBatch packs several single-intent calls (1–64) into a single signed transaction that lands atomically in one auction tick. Each entry in inner is the full ABI-encoded calldata of a single-intent function on this precompile (submitOrder, cancel, update, submitTrigger, transfer, …) — encoded exactly as a standalone call, including its 4-byte selector. Every sub-intent must carry the same deadline (the uniform-deadline invariant), and nested batches are rejected. For the full rules and a worked example, see Submit a batch order.
Delegation envelope
delegated lets a delegate key perform an orderbook call on behalf of a master account. The transaction is signed by the delegate; signature is the master's 65-byte r ‖ s ‖ v EIP-712 signature over DelegationAuth { delegate, validUntil } (domain { name: "pod delegation", version: "1", chainId }), where delegate must equal the transaction's signer, and inner is the full ABI-encoded calldata of the wrapped call, including its 4-byte selector. The certificate is verified statelessly on every transaction — no registration, no on-chain state — and the intent is accepted only while validUntil >= deadline of the inner call (both in microseconds).
The inner intent is owned by the master (balances, resting-order owner, cancel/update target) while its order_id keys on the delegate (the tx signer). Any deadline-bearing call can be wrapped — single intents or a whole submitBatch — but submitSolutions, the market-lifecycle calls (createMarket, disableMarket, settleMarket, updateMarketOracle), and nested delegated are rejected, and so is transfer, both directly and inside a wrapped submitBatch (see Transfers between accounts). Delegated calls are gas-exempt. For the concept and security model see Key Delegation in the protocol reference; for a worked example see Delegate a trading key.
Market lifecycle
Markets are created, halted, settled and re-pointed at their price feeds with four admin-only calls on this precompile. createMarket may be sent by any address on the network's market-admin allowlist and makes the signer the market's owner; the other three are accepted only from that owner. All four carry a deadline like every other intent, ride the solver's solutions, and are applied inside a batch, so every node moves the market through the same states at the same tick.
createMarket(params, deadline, liveAt)
Mints a market from a self-describing MarketParams. The orderbook id is assigned by the protocol (a counter) and returned as bytes32; nothing lets the caller pick it. The market is born pending at deadline and goes active at the first batch at or after liveAt that has seen an oracle price (spot markets flip unconditionally).
disableMarket(orderbookId, deadline, disableAt)
Schedules a halt. disableAt must be tick-aligned and at least the network's minimum notice past deadline. Until it, the book trades normally so users can exit; from that tick the market is disabled and every intent on it is refused. Resting orders and positions stay where they are, and the oracle price of the halt tick is captured as the settlement price.
settleMarket(orderbookId, deadline)
Closes a disabled market: every resting order is refunded and every position closed at the captured settlement price. The market stays listed as settled for history. Disable to settle is one-way; a "revived" market is a new createMarket.
updateMarketOracle(orderbookId, deadline, oracleSpec, midSources)
Rebinds the feeds of an active or pending market without touching orders or positions. oracleSpec is the comma-separated <source>/<asset> list with the first entry primary; empty is legal only for spot. A perp created against a feed that never served a price stays pending until this call points it at one.
ob_getMarkets reports every market with its lifecycle status (pending, active, disabled, settled) plus live_at, disable_at and settlement_price when they apply, so a client can hide a halted book or warn users during a notice window.
Intents due after a scheduled halt are refused up front. Once disable_at is set, any order, cancel or update on that book whose deadline lies past the halt is rejected at submission with market disables at …: an intent with deadline … could never execute. Bots that stamp long deadlines should shorten them, or drop the book, as soon as disable_at appears, and a submitBatch that spans several books fails as a whole when one sub-intent trips this rule.
The lifecycle calls cannot be wrapped in delegated or carried inside submitBatch, and they pay flat gas with no exemption.
Solidity interface (ABI)
Last updated

