Skip to content

Order Lifecycle

Every trade on Openfish passes through a well-defined sequence: the order is constructed and signed off-chain, matched by the in-memory engine, and then settled on-chain through smart contracts. This hybrid design captures the low-latency benefits of centralized matching without compromising on the verifiability and custody guarantees of blockchain settlement.


Every order on Openfish is a limit order — it specifies both a price and a quantity. There is no separate “market order” concept. To achieve immediate execution, you simply set a price aggressive enough to cross the book and fill against resting orders.

Orders are EIP712-signed messages. By signing, you grant the Exchange contract permission to execute the trade on your behalf. At no point does anyone else take custody of your assets.


Four order types govern how the matching engine handles your submission:

TypeBehaviorUse Case
GTCGood Till Cancelled — stays on the book until filled or explicitly cancelledStandard limit orders; liquidity provision
GTDGood Till Date — automatically expires at a specified Unix timestampTime-boxed strategies; event-triggered trading
FOKFill Or Kill — must fill completely in a single match cycle or is rejected entirelyAll-or-nothing requirements; large block trades
FAKFill And Kill — fills whatever quantity is available immediately, discards the restPartial fills acceptable; sweeping available liquidity

A post-only order is guaranteed to add liquidity rather than remove it. If the order would immediately cross the spread and match against a resting order, the engine rejects it outright. This ensures you remain the maker on every fill and never incur taker fees.

// A post-only buy at 0.49 with asks starting at 0.50 will be placed on the book
// A post-only buy at 0.50 with asks at 0.50 will be REJECTED (would cross)
let order = client
.limit_order()
.token_id(token_id)
.price(dec!(0.49))
.size(dec!(10))
.side(Side::Buy)
.post_only(true)
.build()
.await?;

Your client assembles an order containing:

  • token_id — which outcome token you are trading (Yes or No)
  • side — BUY or SELL
  • price — the limit price (between 0.00 and 1.00)
  • size — the number of shares
  • order_type — GTC, GTD, FOK, or FAK
  • expiration — Unix timestamp for GTD orders (0 for others)
  • nonce — for replay protection

Your private key produces an EIP712 signature over this data, authorizing the Exchange contract to carry out the trade.

The signed order is dispatched to POST /order on the CLOB server (port 3002). Before it reaches the engine, the server runs a series of checks:

  • Signature validity — the EIP712 signature matches the declared maker address
  • L2 authentication — the request carries a valid HMAC signature derived from your API key and secret
  • Balance sufficiency — enough USDC.e (for buys) or tokens (for sells) to cover the order
  • Allowance — the Exchange contract is approved to spend your assets
  • Tick size compliance — the price conforms to the market’s minimum tick size

The order enters the in-memory engine, which enforces price-time priority:

  • Bids are ranked by price descending (highest first), then by arrival time (FIFO)
  • Asks are ranked by price ascending (lowest first), then by arrival time (FIFO)

If marketable (buy price >= lowest ask, or sell price <= highest bid):

The order fills against resting orders. It may consume liquidity across multiple price levels, generating a Fill record for each partial fill — each containing the maker order ID, taker order ID, execution price, and matched size.

If not marketable (GTC/GTD orders only):

The remaining quantity rests on the book until one of the following occurs:

  • An incoming order matches against it
  • You cancel it through the API
  • It expires (GTD orders only — expiration is checked every 10 seconds)

For FOK orders: If the entire quantity cannot be filled, the order is rejected and any partially consumed maker orders are rolled back to their original state.

For FAK orders: The engine fills whatever portion it can, then discards the unfilled remainder.

Matched trades are queued for on-chain settlement. A background process runs approximately every 5 seconds, picking up matched trades and settling them:

  1. Load maker and taker order data (EIP712 fields, signatures)
  2. Construct the settlement transaction
  3. Submit to the Exchange contract on Polygon

The Exchange contract verifies both signatures and then atomically:

  • Transfers outcome tokens from seller to buyer
  • Transfers USDC.e from buyer to seller

Settlement is all-or-nothing — the entire trade either succeeds or is rolled back.

The trade reaches finality on Polygon. Trade statuses advance through these stages:

StatusTerminalDescription
MATCHEDNoTrade matched in the engine, sent to the settlement executor
MINEDNoSettlement transaction mined into the blockchain
CONFIRMEDYesTrade achieved finality, settlement successful
RETRYINGNoTransaction failed, being retried
FAILEDYesTrade failed permanently after retries

StatusDescription
LIVEOrder is resting on the book, waiting to be matched
MATCHEDOrder matched immediately (fully filled)
CANCELEDOrder was cancelled by the user, expired, or rejected (FOK/FAK/post-only)

RoleDescriptionWhen
MakerAdds liquidity to the bookYour order rests on the book and is later filled by an incoming order
TakerRemoves liquidity from the bookYour order fills immediately against resting orders

The taker benefits from price improvement. A buy order at $0.55 that matches a resting sell at $0.52 executes at $0.52 — the maker’s price.


Orders can be withdrawn at any point before they are fully matched:

  • Cancel one: DELETE /order with the order ID
  • Cancel all: DELETE /cancel-all clears every open order for the authenticated user
  • Cancel by market: DELETE /cancel-market-orders removes all orders tied to a specific condition ID

For partially filled orders, only the unfilled portion is cancelled. The already-filled portion settles normally.

Openfish provides a heartbeat mechanism designed for automated trading systems. After registering a heartbeat via GET /auth/heartbeat, your system must continue sending heartbeats at least every 10 seconds. If the heartbeats stop, all of your open orders are automatically cancelled. This acts as a safety net to prevent stale quotes from lingering on the book when a trading bot disconnects or crashes.


All order management endpoints require L2 HMAC authentication. The process works as follows:

  1. Derive API credentials: Sign a message with your private key (L1 auth) to obtain an API key, secret, and passphrase
  2. Sign each request: Every API call includes an HMAC signature computed from the API secret, the request timestamp, method, path, and body
  3. Server verification: The CLOB server validates the HMAC signature before processing the request

The Rust SDK automates this entirely when you call .authenticate() during client initialization.


RequirementDescription
USDC.e balanceSufficient collateral for buy orders
Token balanceSufficient outcome tokens for sell orders
Exchange allowanceApprove the Exchange contract to spend your assets
API credentialsValid L2 API key for authenticated endpoints
Tick sizePrice must be a multiple of the market’s minimum tick size

The maximum order size is determined by your available balance minus the amounts reserved by existing open orders:

max_order_size = balance - sum(open_order_size - filled_amount)

  • Resolution — Learn how markets are resolved and winning tokens redeemed
  • Prices & Orderbook — Understand the in-memory order book structure