For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

Orderbook precompile address: 0x50d0000000000000000000000000000000000002

Orders are identified by a computed order_id, not the tx hash. A resting order is keyed by

order_id = keccak256(abi.encode(address signer, uint64 nonce, uint32 sequence))

where signer is the order owner, nonce is the submitOrder transaction's nonce, and sequence is the intent's position inside a submitBatch envelope (0 for a standalone submitOrder). Wherever a call references an existing order — cancel(canceledOrder, …) and update(updatedOrder, …) — pass this order_id. You can compute it yourself with the formula above, or read it back from ob_getOrders, which returns it as order_id (the originating submitOrder tx hash is exposed separately as tx_hash).

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.

Bit
Value
Flag
Meaning

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_ONLY is rejected (post-only order cannot be immediate-or-cancel) — IOC demands a fill in the admitting batch, post-only forbids one.

  • POST_ONLY on a Market order 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_ONLY is fine, as is any other combination.

  • Market orders must set IOC (market orders must be immediate-or-cancel).

submitOrder is overloaded, and only the flags form is current. Encode against the exact signature — the selector differs per overload:

Signature
Selector
Status

submitOrder(bytes32,int256,uint256,uint8,uint128,uint128,uint8)

0x1e416275

Current. The flags form; the only one that can request post-only.

submitOrder(bytes32,int256,uint256,uint8,uint128,uint128,bool,bool)

0x435f7e71

Deprecated. The old reduceOnly, ioc pair, still accepted; equivalent to setting bit 0 from reduceOnly and bit 1 from ioc.

The flags overload can only be decoded by nodes that ship it, so a client that must also work against a network running an older build can keep emitting the deprecated bool, bool form — it is accepted unchanged, and it simply cannot request post-only.

submitTrigger still takes bool reduceOnly, bool ioc and has no flags argument, so a trigger's synthetic order cannot be post-only.

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 from canceled so 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:

  • amount is zero.

  • recipient is the signer. It would burn a nonce and move nothing.

  • recipient is the zero address. Burning has to be deliberate, not a typo.

  • recipient is a system precompile.

  • deadline is 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.

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:

Surface
Use

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.

Call
Effect

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.

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