Polymarket Bot Tutorial · Hoofdstuk 12 van 32
Hoe Polymarket phantom fills te detecteren (orders die er gevuld uit lijken te zien maar dat niet zijn), idempotente retries implementeren, status=matched onderscheiden van rested-on-book en transient failures overleven.
Wat dit hoofdstuk behandelt
Een phantom fill is een Polymarket-specifieke failure mode waarin de CLOB een match bevestigt maar de chain de ERC-1155 transfer nog niet heeft bevestigd. Een vervolg-order binnen ~5 seconden wordt afgewezen met een misleidende "balance: 0" fout. De genezing is idempotentie en een settlement-wacht. Dit hoofdstuk is het productie-playbook waarvoor we met echt geld hebben betaald.
Dit is hoofdstuk 12 van onze 32-delige serie over het bouwen van een Polymarket trading bot. We behandelen het onderwerp in detail in de secties hieronder. De body content voor elke sectie wordt geschreven en hoofdstuk-per-hoofdstuk uitgerold; FAQ-antwoorden en referenties zijn al compleet en weerspiegelen production-ervaring van het draaien van onze eigen trader.
- Wat een phantom fill is
- status=matched vs status=delayed vs status=posted
- Polling-patroon: poll status voor je viert
- FOK als anti-phantom-fill
- Idempotente retries met client_order_id
- Echte productie-incident: hoe we de onze fixten
- Code: detect-then-act post-order patroon
Wat een phantom fill is
Een phantom fill is wanneer de CLOB API reageert op je order met status: "matched" maar de on-chain ERC-1155 transfer nog niet is gesettled. De CLOB matcher is sneller dan Polygon block production (~2s per block). Voor ongeveer 2-5 seconden na de API-match houdt je wallet on-chain niet de tokens die de matcher zegt dat je bezit.
De bot-bug emergeert wanneer een vervolg-actie — typisch een GTC sell om take-profit te posten — binnen dat venster draait. De CLOB checkt de chain-balance, ziet nul en weigert met not enough balance / allowance: balance: 0, order amount: N. De foutmelding beschuldigt de allowance; de oorzaak is settlement-lag.
De eerste keer dat dit gebeurt neem je aan dat het een allowance-bug is en verspil je een uur. De genezing is simpel: wacht, verifieer, post dan.
status=matched vs status=delayed vs status=posted
De order-placement respons bevat een status-veld met drie waarden die tellen.
matched: de order matched direct tegen het book. Inventory zal in 2-5 seconden settlen. Dit is wat FOK/FAK teruggeven bij succes.delayed: de matcher kon niet synchroon settlen en zette de match in queue. Zeldzaam; meestal indiceert congestie. Behandel alsmatchedvoor het doel van het wait + verify patroon.posted(ooklivegenoemd): de order rust ongevuld op het book. Teruggegeven door GTC orders die niet direct matchten. Inventory is niet beïnvloed; nog geen vervolg-actie nodig.
De beslis-regel: als status matched of delayed is, plaats geen vervolg dat de nieuwe inventory vereist totdat je de chain-transfer hebt geverifieerd.
Polling-patroon: poll status voor je viert
Het verificatie-patroon: na een succesvolle match, poll de CTF-balance totdat hij de nieuwe tokens reflecteert, ga dan door.
def wait_for_settlement(token_id, expected_size, timeout=15):
"""Block until on-chain balance reaches expected_size or timeout."""
start = time.time()
while time.time() - start < timeout:
bal = ctf_contract.functions.balanceOf(PROXY, token_id).call()
if bal >= expected_size:
return True
time.sleep(0.5)
return False
Typisch settlement: 2-5 seconden in goede netwerk-condities, tot 15s tijdens Polygon-congestie. Een 5-seconden wacht dekt 95% van de gevallen; zet voor productie de timeout op 15s en alert bij timeout.
Voor high-frequency bots die zich het blocken niet kunnen veroorloven, is een alternatief event-subscription: kijk naar het TransferSingle event van de CTF voor je proxy-adres en trigger downstream acties bij ontvangst. Dit duwt de wacht naar een queue in plaats van de strategy-loop te blocken.
FOK als anti-phantom-fill
FOK kiezen boven FAK is een partiële verdediging tegen phantom-fill chaos. FOK vult ofwel de hele order of geeft cancelled terug; FAK kan een partiële filled_size teruggeven. Wanneer een partiële fill wordt gevolgd door een GTC sell gedimensioneerd op de originele order, faalt de sell op settlement-lag plus size-mismatch — twee compounderende bugs.
Met FOK is de size binair: ofwel de volledige size matched of niets. De vervolg-posting logica weet altijd wat te verwachten.
Dit elimineert niet de noodzaak voor de wacht — zelfs een perfecte FOK match is onderhevig aan het 2-5 seconde settlement-venster. Maar het verwijdert één klasse van boekhouding-divergentie.
Idempotente retries met client_order_id
Netwerk-failures tijdens order-plaatsing creëren een worst-case scenario: de HTTP-call van de bot ging timeout, maar de order kan al dan niet ontvangen zijn. Naïef retryen kan dubbel-plaatsen; niet retryen kan een positie missen.
De fix is het client_order_id veld op order-plaatsing. Genereer een deterministische UUID per bedoelde order; als de server die ID eerder heeft gezien, geeft hij de status van de bestaande order terug in plaats van een duplicaat te creëren.
import uuid
oid = str(uuid.uuid4()) # generate once, retry with same value
for attempt in range(3):
try:
resp = c.create_and_post_order(args, OrderType.FOK, client_order_id=oid)
return resp
except (TimeoutError, ConnectionError):
time.sleep(0.5 * (2 ** attempt))
raise RuntimeError("post failed after 3 attempts")
Het patroon: genereer de ID eerst, retry op transport-failure, nooit op logische rejection. De server-side dedup is per-API-key, duurt ~5 minuten.
Echte productie-incident: hoe we de onze fixten
Uit ons eigen productie-diary, mei 2025. Een venster van 60 minuten waarin de trader-bot 22 buy orders plaatste, allemaal matched, maar slechts 14 GTC sells werden geaccepteerd. Acht posities hadden geen exit gepost.
Root cause: de bot postte de GTC sell binnen 800ms van de buy-match, ruim voordat de chain de ERC-1155 transfer bevestigde. De CLOB weigerde met de "balance: 0" boodschap; de bot logde de fout maar deed geen retry. Acht posities reden stil tot resolution zonder take-profit bescherming. Drie sloten out of the money; één sloot op 0,99 door geluk.
De fix shipte als een 5-seconde blocking wacht tussen elke buy-fill en elke GTC post op dezelfde token. Geverifieerd via 30 paper trades plus 30 live trades; nul balance-zero fouten sindsdien.
De les: een stil foutpad is duurder dan een luid pad. Hierna lieten we alle phantom-fill fouten een Telegram alert triggeren, zodat een toekomstige drift-modus binnen seconden zichtbaar zou zijn.
Code: detect-then-act post-order patroon
Productie buy-then-post patroon.
def buy_then_post_tp(token_id, size, buy_price, tp_price):
# 1. Place FOK buy
buy_args = OrderArgs(token_id=token_id, price=buy_price, size=size, side="BUY")
buy_resp = c.create_and_post_order(buy_args, OrderType.FOK)
if buy_resp.status != "matched":
return {"ok": False, "stage": "buy", "reason": buy_resp.status}
# 2. Wait for on-chain settlement (5s sane default; bump for congestion)
if not wait_for_settlement(token_id, expected_size=size, timeout=15):
return {"ok": False, "stage": "settle", "reason": "timeout"}
# 3. Confirm minimum size for GTC
if size < 5:
# GTC won't accept; fall back to ride-to-resolve or FOK sell at TP later
return {"ok": True, "stage": "buy_only", "note": "size<5, no GTC posted"}
# 4. Post GTC sell
sell_args = OrderArgs(token_id=token_id, price=tp_price, size=size, side="SELL")
sell_resp = c.create_and_post_order(sell_args, OrderType.GTC)
return {"ok": sell_resp.status in ("posted","live"), "buy": buy_resp, "sell": sell_resp}
Het patroon overleeft de gangbare failure modes: phantom-fill, transient netwerk-drop, under-minimum GTC size. Geeft genoeg informatie terug voor de strategy-laag om te beslissen wat te retryen vs loggen vs alerten.











