ASK KNOX
beta
LESSON 264

FOK vs GTC Execution Semantics

Fill-or-Kill attempts first for maker fee savings, Good-Til-Canceled is the fallback. The HE-06 bug: status=live means pending on the book, not filled. How to distinguish resting orders from actual fills.

8 min read

The HE-06 bug looked like a ghost trade. Hermes logged "order placed" and moved on to the next signal. The wallet balance showed a small debit. But the position never appeared in the resolver, and the PnL never materialized.

The root cause was a single line of logic: Hermes was treating status=live as a successful fill.

The Semantics

Polymarket's order creation endpoint returns a JSON blob with a status field. That field has several possible values, and only one of them means the order actually traded:

StatusMeaning
liveOrder is resting on the book. No fill yet. Waiting for counterparty.
matchedOrder has been matched against another order. Settlement is in progress.
filledOrder is fully settled. Position exists.
partialPartially filled. Remainder is still live on the book.
canceledOrder was canceled before any fill.
expiredOrder timed out. No fill.

status=live is the default state after posting a new GTC order. It is a healthy, normal response — but it is not a confirmation of trade. Treating it as one was the HE-06 bug.

FOK vs GTC

Polymarket supports both Fill-or-Kill (FOK) and Good-Til-Canceled (GTC) semantics via the order_type field on order creation.

  • FOK: Attempt to fill the entire order immediately at the specified price. If the book cannot absorb the full size at that price, the order is killed. Either you get a full fill right now, or you get nothing. No resting.
  • GTC: Post the order to the book and let it sit. Matches happen when a counterparty crosses the spread. The order stays live until filled, canceled, or expired.

Hermes's execution strategy is FOK-first:

  1. Query the orderbook depth. Compute the walked price at intended size.
  2. If walked price is acceptable: attempt FOK at the aggressive price. If it fills, done.
  3. If FOK fails (insufficient depth): fall back to GTC at a more conservative maker price. Let the order rest.
  4. Poll the order status until it transitions out of live.

The rationale for FOK-first is fee optimization — a successful immediate fill can save basis points that compound across hundreds of trades. The rationale for GTC fallback is execution reliability — thin books will reject a lot of FOK attempts and you need a path that eventually trades.

Inline Diagram — Decision Tree

EXECUTION DECISION TREE — FOK → GTC → VERIFYsignal fires1. attempt FOKfilled immediately → done2. fallback GTC — poll statusstatus=live means PENDING — never done

The HE-06 Fix

The bug fix was a state machine:

# Buggy
resp = clob.place_order(order)
if resp.get("status") == "live":  # WRONG — this is pending, not filled
    mark_position_as_filled(signal_id)

# Correct
resp = clob.place_order(order)
order_id = resp["id"]
for attempt in range(30):
    status = clob.get_order(order_id)
    if status["status"] in ("matched", "filled"):
        mark_position_as_filled(signal_id, fill_price=status["avg_price"])
        break
    if status["status"] in ("canceled", "expired"):
        mark_signal_as_unexecuted(signal_id)
        break
    time.sleep(2)
else:
    # Still live after 60s — cancel and retry with GTC maker order
    clob.cancel_order(order_id)
    place_gtc_maker(signal_id)

The Rule

Order creation does not equal order execution. FOK attempts first, GTC falls back, polling confirms the fill. Treat every status=live response as "not yet" and build the state machine that resolves it.