FOK vs GTC Execution Semantics
Fill-or-Kill attempts first for execution certainty and speed, Good-Til-Canceled is the fallback for resting orders at the maker price. The HE-06 bug: status=live means pending on the book, not filled. How to distinguish resting orders from actual fills.
A fill-detection bug (internally tracked as HE-06) looked like a ghost trade. Agent Framework 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: Agent Framework was treating status=live as a successful fill.
The Semantics
Polymarket's order placement endpoint returns a JSON blob with a status field. The documented placement statuses are live, matched, delayed, and unmatched — and only one of them means the order actually traded:
| Status | Meaning |
|---|---|
live | Order placed and resting on the book. No fill yet. Waiting for counterparty. |
matched | Order was marketable and crossed against resting orders at placement. It traded. |
delayed | Order is marketable, but subject to a matching delay. Not confirmed yet — keep polling. |
unmatched | Order was marketable, but matching failed or was deferred at placement. No fill. |
There is no filled, partial, or expired placement status. Fill quantity lives on the order object itself — the size_matched field you read when you poll the order — and a partially filled order is simply a live order with a non-zero size_matched. Note that a marketable FOK-first flow will routinely see delayed and unmatched, not just live and matched, so the state machine has to handle all four.
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 it fills, is canceled, or hits its expiration.
Agent Framework's execution strategy is FOK-first:
- Query the orderbook depth. Compute the walked price at intended size.
- If walked price is acceptable: attempt FOK at the aggressive price. If it fills, done.
- If FOK fails (insufficient depth): fall back to GTC at a more conservative maker price. Let the order rest.
- Poll the order status until it transitions out of
live.
The rationale for FOK-first is execution certainty and speed — a successful immediate fill removes the uncertainty of a resting order waiting for a counterparty. FOK is a taker order: it crosses the spread and fills right now, or it is killed immediately. It never rests on the book. 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. GTC posts a resting maker order at a more conservative price, accepting slower execution in exchange for a fill once a counterparty crosses the spread. (Fee schedules in CLOBs structurally favor makers, so a resting GTC order would earn the maker side of any fee — though Polymarket's current schedule is 0% on both maker and taker for most markets, so the FOK/GTC choice is driven by execution semantics, not fees.)
Inline Diagram — Decision Tree
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"]
if resp.get("status") == "unmatched":
# Marketable but matching failed at placement — treat as not executed.
clob.cancel_order(order_id)
mark_signal_as_unexecuted(signal_id)
else:
# "live", "delayed", and even "matched" all go through the polling loop —
# confirm the actual fill size before marking any position.
for attempt in range(30):
order = clob.get_order(order_id)
size_matched = float(order.get("size_matched") or 0)
if order["status"] == "matched" or size_matched >= order_size:
# There is no avg_price field on the order. Use the order's price
# field (the limit you signed), or VWAP the associated trades for
# the true average fill price on multi-level walks.
mark_position_as_filled(signal_id, fill_price=float(order["price"]),
size=size_matched)
break
if order["status"] == "canceled":
mark_signal_as_unexecuted(signal_id)
break
time.sleep(2)
else:
# Still resting 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.