Skip to content

Order Lifecycle

An order on GoMarket crosses two trust boundaries: the off-chain matching engine, and the on-chain settlement program. This page traces the whole path.

sign insert match execute confirm
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐
│wallet│ ──▶ │ Order │ ──▶ │ Matching │ ─▶ │ Executor │ ─▶ │ Solana │
│ │ │ Manager │ │ engine │ │ builds & │ │ finalized │
└──────┘ └────┬────┘ └─────┬────┘ │ signs │ └──────┬─────┘
│ │ └─────┬────┘ │
▼ ▼ │ ▼
validation Ledger: order Ledger: Ledger:
(12 checks) + trade rows MINED CONFIRMED

You compute the domain-separated SHA-256 order hash and sign it with your wallet’s ed25519 key. That hash is the order’s identity everywhere — it’s the orderID the API returns, the de-duplication key, and the digest the on-chain program recomputes at settlement.

sha256( b"CTF_EXCHANGE_ORDER_V1" ‖ salt ‖ maker ‖ signer ‖ taker
‖ token_id_len ‖ token_id ‖ maker_amount ‖ taker_amount
‖ expiration ‖ nonce ‖ fee_rate_bps ‖ side ‖ signature_type )

The signature is not part of the hash.

POST /v1/orders runs a fixed pipeline. Any failure returns success: false with a reason, and nothing is persisted.

#StepRejects when
1Resolve token ID—
2Locate the bookNo book exists for that token
3Build ProcessedOrderPrice/size math fails
4Validate on insertBad signature, expired, bad fee rate, off-tick
5Duplicate checkThe order hash is already in the books
6Market readinessMarket isn’t ready to trade
7Rounding configUnknown tick size
8Amount / tick-sizeAmounts don’t land on the grid
9Unique-order registryHash seen before
10Balance / allowanceInsufficient ATA balance (allowance: warning)
11Fee rateMarketable order priced below the taker fee
12GTD registration— (registers expiry on the pending service)
13Match / insert / schedule—

Step 10 also reserves the maker amount, so one balance cannot back two orders simultaneously.

Step 13 forks three ways, and the response’s status tells you which:

statusWhat happened
liveNothing crossed; the order is resting in the book
matchedIt crossed; trades were built and sent to the executor
delayedParked on the pending service inside a game-start window
unmatchedCouldn’t fill — typically a FOK that found no liquidity

Note that success: true with status: "live" is a completely normal outcome. Don’t treat a successful insert as a fill.

TypeBehaviour
GTCRests until filled or cancelled. expiration must be "0".
GTDRests until expiration (unix seconds), then is ejected.
FOKFills completely on arrival or is killed. Never rests.

The taker walks the book against resting maker orders in price-time order. For each maker leg the engine computes the fill amount, the fee, and the match type — COMPLEMENTARY, MINT or MERGE. Self-trades are filtered and the price-distortion guard caps how far the walk may push the price.

→ Match Types

Matched legs go to the Executor over gRPC. The executor:

  1. Re-runs the order validation on every order in the request.
  2. De-dupes by full trade key — taker and maker order hashes, fill amounts, fee rate, bucket. A repeat is rejected as DuplicateTrade.
  3. Builds the transaction: one execute_trade instruction per maker (15 accounts each), the residual mint_tokens/merge_tokens for same-side matches, a top-level ed25519 verify instruction, and ComputeBudget instructions.
  4. Signs with the operator keypair and the taker key, and sends.

The compute budget is simulateTransaction × 1.2, capped. MINT/MERGE transactions overflow the 1232-byte legacy limit, so the message compiles to v0 with an address lookup table when one is configured.

That de-duplication is load-bearing: the on-chain program carries no per-order fill state. There is no nonce PDA and no fill-status PDA. The executor is the single trusted settlement instance, and a caller who bypasses it and sends execute_trade directly can settle the same signed order repeatedly — bounded only by the maker’s standing delegate allowance.

A background worker batches getSignatureStatuses over all pending signatures each tick. Solana has no reorgs, so finalized is the settlement point.

OutcomeAction
FinalizedLedger UpdateTrade(CONFIRMED)
Out of gas (CU ≥ 95% of limit)Retry with CU ×2 and fee ×2, up to the retry limit
FailedLedger UpdateTrade(FAILED) + OrderSizeUpdate to eject affected resting orders
Stuck past the idle timeoutRe-executed through the same path

The pending map and the de-dupe key set are snapshotted to disk on every change, so a restart re-confirms in-flight transactions rather than re-sending them.

When settlement fails on-chain, the resting orders involved are now backed by state that didn’t move. The executor calls the order manager’s internal POST /v1/orders/size-update (authorized by X-Internal-Token, not SIWS) to invalidate them. This is the only route where an arbitrary owner’s orders can be touched, and it fails closed when no token is configured.

StateMeaning
LIVEResting on the book
MATCHEDMatched, settlement in flight
MINEDTransaction landed, not yet finalized
CONFIRMEDFinalized on-chain
FAILEDSettlement failed; orders reconciled
CANCELEDCancelled by the owner or ejected

Every transition is appended to the ledger’s Redis notifications stream. → Real-Time Data

You can cancel any resting order you own at any time. Orders are also ejected without your involvement when:

  • A GTD order reaches its expiration.
  • A market’s game start fires — every order created before it is ejected.
  • Settlement failed and reconciliation invalidates the order.

Cancellation is off-chain only. It removes the order from the book; it does not touch your delegate allowance, which stays standing until you revoke it.