🇺🇸 English
Dec 24, 2025
📒 Docs · ☎️ Telegram · 𝕏 @ZEITFinance · 𝕏 @autonomous_af
← Previous: Part 3, The Local Mirror
In Part 3, we built the Local Mirror to accurately see the market. Now, we must act on it.
Posting orders is easy—knowing what actually filled is the real work.
If you have ever automated trades on Polymarket, you have likely encountered the "phantom fill" scenario:
Suddenly, your theoretically neutral strategy is carrying naked exposure. This is not an edge case, it is the default reality of trading on a CLOB.
The fix is to stop treating execution as a "fire-and-forget" action and treat it as a strict Confirmation Loop:
Before you write your execution logic, you need to unlearn a standard concept: Polymarket's CLOB doesn't actually have a "Market Order" packet.
Every order sent to the exchange is fundamentally a Limit Order. When you want to execute immediately, you are simply sending an aggressive Limit Order paired with a specific Time-In-Force (TIF) instruction.
Every order is fundamentally a Limit Order with different Time-In-Force instructions
Categorize your orders by their liquidity intent:
Use Fill-Or-Kill (FOK) for immediate execution.
FOK is binary. It fills your entire size immediately or cancels the whole order. This is safest for arbitrage. It prevents you from getting stuck with half a hedge.
Fill-And-Kill (FAK) fills what it can and cancels the rest. This leaves you with partial inventory to manage.
Market makers rely on Good-Til-Cancelled (GTC).
Sophisticated bots prefer Good-Til-Date (GTD). This auto-expires the order after a set time (e.g., 60s). It acts as a dead man's switch. If your bot crashes, orders vanish instead of sitting as stale targets.
One design choice breaks most new bots. Polymarket handles units differently depending on the trade type.
This asymmetry complicates arbitrage logic.
You calculate an edge based on exactly 100 shares. The API forces a dollar input. Price slippage guarantees you end up with 99.8 or 100.2 shares.
You never get exactly 100.
⚠️ Critical Rule: Market orders are dollar-denominated. Always account for floating-point imprecision. Never assume exact share counts.
When you submit an order via the REST API, the response gives you an orderId and a status of sent. This is not a fill. It is merely an acknowledgement that the engine received your request.
"Filled" is a physical fact derived from the matching engine, and you should never guess at it by polling the REST API. You need the User WebSocket Channel.
While the Market Channel gives you public data, the User Channel is your authenticated source of truth. It pushes updates the instant your order state changes. Your bot should essentially ignore its own HTTP responses and listen exclusively for:
The User Stream is your only source of truth for order fills
💡 The Golden Rule: Your bot does not own a position until the User Stream emits a MATCHED event for that specific orderId.
To prevent naked exposure, your execution logic must act as a state machine. It moves from planning to waiting, and finally to accounting.
Before sending bytes, define your constraints. Is a partial fill acceptable? If you are arbitraging, the answer is usually No, so you force FOK. Send the order and capture the orderId from the response. If the API errors out immediately (e.g., "Insufficient Balance"), abort the loop.
Once the order is sent, your bot enters a blocked state. It waits for the User Stream to speak.
The Execution Loop: A state machine that prevents phantom fills
There is a specific production failure mode regarding "Market Buys" that requires defensive coding.
Because Market Buys are denominated in USDC (Dollars), floating-point math ensures you will rarely hit an exact integer share count.
The Scenario: You try to buy $100.00 of Trump=Yes.
The Math: At a price of 33.3 cents, the engine calculates you bought 29.998 shares.
The Bug: Your bot's logic waits for shares_filled == 30. It never happens. The bot hangs, times out, and assumes the trade failed—even though you actually own the shares.
For FOK/FAK orders, do not validate against an exact share target.
Listen for Terminal States.
If status is MATCHED or FAILED, the order is done. Record the reported size. Move on.
Floating-point math means you'll rarely get exact share counts
Once you master single orders, you enter the world of Multi-Leg execution (NegRisk Arbs). You have two choices:
You post Leg 1, wait for confirmation, and only then post Leg 2.
Pros: It is easy to handle failures. If Leg 1 gets Killed, you simply stop. You have zero exposure.
Cons: It is slow. In the 200ms it takes to confirm Leg 1, the price of Leg 2 might move against you ("Legging Risk").
You post Leg 1 and Leg 2 simultaneously.
Pros: Maximizes speed and captures fleeting liquidity.
Cons: Atomic Risk. Leg 1 might fill while Leg 2 gets Killed. You are now holding a naked position in Leg 1 with no hedge. To do this safely, you need an automated Unwinder (which we will cover in Part 5).
Sequential vs Parallel: Speed vs Safety trade-offs
💡 Recommendation: Start with Sequential. Only move to Parallel when you have a robust hedging module built.
Placing an order is a request. A fill is a fact. A production bot is built around the discipline of never confusing the two.
We can now execute reliably. Next we analyze profitable market structures.
Part 5 covers NegRisk Foundations.
We will cover mutually exclusive markets. We will explore why fragmented books create "probability gaps." We will learn the specific arbitrage loops (Minting and Conversion) to profit from these inefficiencies.
This article is Part 4 of the ZEIT Finance series on building Polymarket trading bots. ZEIT Finance turns prediction markets into perpetual assets.