Polymarket Bot Tutorial · Chapter 10 of 32
Polymarket order types explained for bot builders: Fill-or-Kill (FOK), Fill-and-Kill (FAK), Good-til-Cancelled (GTC), and limit-vs-market trade-offs. With production-grade decision rules.
What this chapter covers
Order-type confusion is the single most expensive class of bug for new bot builders. Sending FOK when GTC was needed produces missed entries; sending GTC when FOK was needed leaves resting orders that fill at terrible prices hours later. This chapter is the decision tree and the production defaults that have held up across thousands of orders.
- Quick decision tree
- FOK: when you must fill or skip
- FAK: when partials are acceptable
- GTC: when you want to rest on the book
- GTD: orders that expire on their own
- Post-only: guarantee maker status
- Limit vs market and the spread tax
- Our production defaults (FOK buys, GTC sells)
- Code: place each order type
Quick decision tree
Three questions decide every order placement.
- Do you need a guaranteed fill right now, and not at all if you cannot get it now? → FOK.
- Do you want as much fill as you can get right now, accept partials, no resting order? → FAK.
- Do you want to rest on the book at your price and wait for someone to come to you? → GTC.
That's it. Most bot bugs around order types come from picking #1 when you wanted #3 (a "buy" turns into "no position because the spread was too wide") or picking #3 when you wanted #1 (a "buy" turns into a resting order that fills hours later at the wrong moment).
FOK: when you must fill or skip
Fill-or-Kill matches the entire order at the requested price or better, instantly. If the full size cannot be filled instantly, the order is rejected and nothing happens. No resting, no partial.
Use FOK for: news-arbitrage entries (you only want in at the news price, not at where the market is in 30s); take-profit exits at a specific target where partials would muddy bookkeeping; any time the strategy assumes atomic execution.
The trade-off: FOK rejects more often than other order types, especially on illiquid books. Always have a fallback path - re-evaluate the strategy condition and retry if still valid, or move on.
FAK: when partials are acceptable
Fill-and-Kill (also called "immediate or cancel") matches as much as it can right now, then cancels the unfilled remainder. You may get the full size, a partial, or zero.
Use FAK for: market-buy with a specific price ceiling (lift the ask up to N cents above mid); sweep-the-book sells when reducing inventory urgently; any strategy where "some position is better than none."
Operationally trickier than FOK because the bot has to know whether it got 100% or 30% before deciding the next step. The fill response includes a filled_size field - always read it.
GTC: when you want to rest on the book
Good-til-Cancelled rests on the book at your price until filled or you cancel. No timeout (other order types in the v2 API include GTD with an expiry).
Use GTC for: take-profit sells at +Nc above entry; stop-loss sells at -Nc below entry (with caveats - see below); market-making both-sided quotes; any position where the bot is willing to wait for a better price.
The hard rule: GTC requires ≥ 5 shares. Orders below 5 shares are rejected by the CLOB with Size (X) lower than the minimum: 5. A bot that posts a 4-share GTC sell silently fails to set the exit and rides the position to resolution. Always check inventory ≥ 5 before posting GTC; fall back to FAK or ride-to-resolve if smaller.
GTD: orders that expire on their own
Good-til-Date is a resting order, like GTC, with a built-in expiry. It sits on the book until it fills, you cancel it, or its expiration timestamp passes - whichever comes first. Once the timestamp passes the CLOB drops it for you; you never have to send a cancel.
Use GTD for: a maker quote you only want live until a known event (a data release, a game kickoff, a debate); a take-profit that should not outlive the session; any "post it and forget it, but not forever" order. It is the clean alternative to posting GTC and running your own timer to cancel later - one fewer moving part to get wrong.
The expiration field is a UTC seconds timestamp (Unix epoch seconds, not milliseconds). The CLOB enforces a security buffer of about a minute, so the order must expire at least ~60 seconds in the future: for a real lifetime of N seconds, set now + 60 + N. An expiry in the past is rejected with INVALID_ORDER_EXPIRATION.
Post-only: guarantee maker status
Post-only is a flag on a resting order that says "only add liquidity, never take it." If the order would cross the spread and match immediately - making you the taker - the CLOB rejects it instead of filling. That guarantees you stay the maker on every fill: no taker fee, and you keep the maker rebate.
Use post-only for market-making and passive accumulation, where being the maker is the whole point. It removes the race where the market moves into your price between your read of the book and your post, silently turning a maker order into a taker that pays the spread plus the taker fee.
Two rules. Post-only works only with GTC and GTD (the resting types); combine it with FOK or FAK and the order is rejected with INVALID_POST_ONLY_ORDER_TYPE. And if a post-only order would cross the book at submission, it is rejected with INVALID_POST_ONLY_ORDER - catch that, re-read the book, and re-post behind the spread.
Limit vs market and the spread tax
Every Polymarket order is technically a limit order - even what bots call a "market buy" specifies a price ceiling. The distinction is whether that price is at the best ask (effectively a market order, will fill against the book) or below it (will rest on the book).
The spread tax is the cost of crossing - bid 0.45, ask 0.47, mid 0.46. A round trip that buys ask and sells bid pays 2 cents per share. On a 60% win-rate strategy with +3c/-4c targets, that 2c spread is the difference between profit and loss.
Maker pattern (post GTC at the bid or below, wait to be hit) collects the spread instead of paying it. The cost is uncertain fill - you may never get hit. For high-conviction trades, pay the spread. For passive accumulation, work the book.
Our production defaults (FOK buys, GTC sells)
The pattern most of our production bots converge on:
- Entries: FOK at ask + 0-2 cents. If the bot decided to buy, it should buy now or skip. Resting an entry order is rarely worth it - the situation that triggered the buy decision changes faster than the order will rest.
- Take-profit exits: GTC at target price. Posted immediately after entry fills. We let the market come to us; we don't chase the bid down. With ≥ 5 shares.
- Stop-loss: case-by-case. GTC works for slow strategies where price changes are bounded. For fast-moving markets a GTC stop won't fill if price flies through it; we ride to resolution in option-D fashion (memory: trader-gtc-sell.md).
The pattern is conservative - fewer fills, less slippage. A more aggressive variant uses FAK entries and FAK exits, accepting partial fills. Pick one and stay consistent; mixing per-trade decisions invites confusion.
Code: place each order type
All five in the official V2 clients - py-clob-client-v2 (Python) and @polymarket/clob-client-v2 (Node/TS). Switch tabs for your language. Both follow the same shape: build the order args, pass tick_size in the options, and choose the order type. Post-only is the last argument on create_and_post_order; FOK/FAK go through create_and_post_market_order.
from py_clob_client_v2 import (
ClobClient, ApiCreds, OrderArgs, MarketOrderArgs,
OrderType, PartialCreateOrderOptions, Side,
)
import os, time
# L2 client - creds go in the constructor (no set_api_creds in v2)
creds = ApiCreds(api_key=os.environ["CLOB_API_KEY"],
api_secret=os.environ["CLOB_SECRET"],
api_passphrase=os.environ["CLOB_PASS_PHRASE"])
c = ClobClient(host="https://clob.polymarket.com", chain_id=137,
key=os.environ["PK"], creds=creds)
opts = PartialCreateOrderOptions(tick_size="0.01") # neg_risk=True for multi-outcome
# FOK market buy: spend the amount now in full, or cancel
c.create_and_post_market_order(
order_args=MarketOrderArgs(token_id=TOKEN, amount=100, side=Side.BUY,
order_type=OrderType.FOK),
options=opts, order_type=OrderType.FOK)
# FAK market buy: take what's available now, cancel the rest
c.create_and_post_market_order(
order_args=MarketOrderArgs(token_id=TOKEN, amount=100, side=Side.BUY,
order_type=OrderType.FAK),
options=opts, order_type=OrderType.FAK)
# GTC limit sell: rest 100 shares at 0.85 until filled or cancelled
c.create_and_post_order(
order_args=OrderArgs(token_id=TOKEN, price=0.85, side=Side.SELL, size=100),
options=opts, order_type=OrderType.GTC)
# GTD limit buy: auto-expire. expiration = UTC seconds, must be >= ~60s ahead
c.create_and_post_order(
order_args=OrderArgs(token_id=TOKEN, price=0.40, side=Side.BUY, size=100,
expiration=int(time.time()) + 70),
options=opts, order_type=OrderType.GTD)
# Post-only (maker-only): rejected if it would cross. GTC/GTD only.
c.create_and_post_order(
order_args=OrderArgs(token_id=TOKEN, price=0.46, side=Side.BUY, size=100),
options=opts, order_type=OrderType.GTC, post_only=True)import { ClobClient, OrderType, Side } from "@polymarket/clob-client-v2";
const opts = { tickSize: "0.01" }; // negRisk: true for multi-outcome
// FOK market buy: spend the amount now in full, or cancel
await client.createAndPostMarketOrder(
{ tokenID: TOKEN, amount: 100, side: Side.BUY, orderType: OrderType.FOK },
opts, OrderType.FOK);
// FAK market buy: take what's available now, cancel the rest
await client.createAndPostMarketOrder(
{ tokenID: TOKEN, amount: 100, side: Side.BUY, orderType: OrderType.FAK },
opts, OrderType.FAK);
// GTC limit sell: rest 100 shares at 0.85 until filled or cancelled
await client.createAndPostOrder(
{ tokenID: TOKEN, price: 0.85, side: Side.SELL, size: 100 },
opts, OrderType.GTC);
// GTD limit buy: auto-expire. expiration = UTC seconds, must be >= ~60s ahead
const expiration = Math.floor((Date.now() + 70 * 1000) / 1000);
await client.createAndPostOrder(
{ tokenID: TOKEN, price: 0.40, side: Side.BUY, size: 100, expiration },
opts, OrderType.GTD);
// Post-only (maker-only): 4th arg true. Rejected if it would cross. GTC/GTD only.
await client.createAndPostOrder(
{ tokenID: TOKEN, price: 0.46, side: Side.BUY, size: 100 },
opts, OrderType.GTC, true);The neg_risk / negRisk flag (chapter 11) must be set in the options for multi-outcome markets - missing it routes the order to the wrong exchange contract.


