Polymarket Bot Tutorial · Kapitel 12 von 32
Wie man Polymarket phantom fills erkennt (Orders, die ausgeführt aussehen, es aber nicht sind), idempotente Retries implementiert, status=matched von rested-on-book unterscheidet und vor transient failures bestehen bleibt.
Was dieses Kapitel abdeckt
Ein phantom fill ist ein Polymarket-spezifischer failure mode, bei dem das CLOB einen Match bestätigt, die chain aber den ERC-1155 transfer noch nicht bestätigt hat. Eine Folge-Order innerhalb von etwa 5 Sekunden wird mit einem irreführenden "balance: 0"-Fehler abgelehnt. Die Lösung sind Idempotence und eine settlement-Wartezeit. Dieses Kapitel ist das Production-Playbook, für das wir mit echtem Geld bezahlt haben.
- Was ein phantom fill ist
- status=matched vs status=delayed vs status=posted
- Polling pattern: status prüfen, bevor man feiert
- FOK als Anti-phantom-fill
- Idempotente Retries mit client_order_id
- Realer Production-Vorfall: wie wir unseren behoben haben
- Code: detect-then-act post-order pattern
Was ein phantom fill ist
Ein phantom fill liegt vor, wenn die CLOB API auf deine Order mit status: "matched" antwortet, der on-chain ERC-1155 transfer aber noch nicht settled ist. Der CLOB matcher ist schneller als die Polygon block production (ca. 2s pro Block). Für ungefähr 2-5 Sekunden nach dem API-Match hält deine Wallet on-chain die Tokens, die der matcher dir zuschreibt, noch nicht.
Der Bot-Bug entsteht, wenn eine Folgeaktion - typischerweise ein GTC sell, um den take-profit zu platzieren - innerhalb dieses Fensters läuft. Das CLOB prüft den chain balance, sieht null und lehnt mit not enough balance / allowance: balance: 0, order amount: N ab. Die Fehlermeldung schiebt es auf allowance; die Ursache ist settlement lag.
Beim ersten Mal vermutest du einen allowance-Bug und verlierst eine Stunde. Die Lösung ist einfach: warten, verifizieren, dann posten.
status=matched vs status=delayed vs status=posted
Die Order-Platzierungsantwort enthält ein status-Feld mit drei relevanten Werten.
matched: Die Order wurde sofort gegen das book gematcht. Das Inventory wird in 2-5 Sekunden settled. Das ist das, was FOK/FAK bei Erfolg zurückgeben.delayed: Der matcher konnte nicht synchron settlen und hat den Match in eine Queue gestellt. Selten; weist meist auf congestion hin. Für das wait + verify pattern wiematchedbehandeln.posted(auchlivegenannt): Die Order liegt unfilled im book. Wird von GTC orders zurückgegeben, die nicht sofort gematcht wurden. Das Inventory bleibt unverändert; vorerst ist keine Folgeaktion nötig.
Die Entscheidungsregel: Wenn status matched oder delayed ist, keine Folgeaktion platzieren, die das neue Inventory benötigt, bevor du den chain transfer verifiziert hast.
Polling pattern: status prüfen, bevor man feiert
Das Verifikationsmuster: Nach einem erfolgreichen Match die CTF balance pollen, bis sie die neuen Tokens widerspiegelt, und erst dann fortfahren.
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
Typisches Settlement: 2-5 Sekunden bei guten Netzwerkbedingungen, bis zu 15s bei Polygon congestion. Eine 5-Sekunden-Wartezeit deckt 95% der Fälle ab; in Production den Timeout auf 15s setzen und bei Timeout alarmieren.
Für High-Frequency bots, die nicht blockieren können, ist event-subscription eine Alternative: Das TransferSingle-Event der CTF für deine proxy address beobachten und Folgeaktionen beim Empfang auslösen. So wandert das Warten in eine Queue, statt die Strategy-Loop zu blockieren.
FOK als Anti-phantom-fill
FOK statt FAK zu wählen ist ein teilweiser Schutz gegen das Chaos durch phantom fills. FOK füllt entweder die gesamte Order oder gibt cancelled zurück; FAK kann filled_size zurückgeben, das nur teilweise ist. Wenn auf einen Partial Fill ein GTC sell mit der ursprünglichen Ordergröße folgt, scheitert der Sell wegen settlement lag plus Größen-Mismatch - zwei sich verstärkende Bugs.
Mit FOK ist die Größe binär: Entweder wurde die volle Größe gematcht oder gar nichts. Die Folge-Posting-Logik weiß immer, was zu erwarten ist.
Das beseitigt nicht die Notwendigkeit des Wartens - selbst ein perfekter FOK-Match unterliegt dem 2-5-Sekunden-Settlement-Fenster. Aber es entfernt eine Klasse von Buchhaltungs-Abweichungen.
Idempotente Retries mit client_order_id
Netzwerkfehler während der Order-Platzierung erzeugen ein Worst-Case-Szenario: Der HTTP-Call des Bots ist abgelaufen, aber die Order könnte empfangen worden sein oder auch nicht. Blindes Retries kann doppelt platzieren; gar nicht zu retrien kann eine Position verlieren.
Die Lösung ist das Feld client_order_id bei der Order-Platzierung. Generiere pro geplanter Order eine deterministische UUID; wenn der Server diese ID schon gesehen hat, gibt er den Status der existierenden Order zurück, statt eine Duplikat-Order zu erstellen.
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")
Das Muster: Zuerst die ID erzeugen, bei Transportfehlern retryen, nie bei einer logischen Ablehnung. Das serverseitige Dedup gilt pro API-key und hält etwa 5 Minuten.
Realer Production-Vorfall: wie wir unseren behoben haben
Aus unserem eigenen Production-Tagebuch, Mai 2025. Ein 60-Minuten-Fenster, in dem der trader bot 22 Buy Orders platzierte, alle gematcht wurden, aber nur 14 GTC sells akzeptiert wurden. Acht Positionen hatten keinen geposteten Exit.
Ursache: Der Bot postete den GTC sell innerhalb von 800ms nach dem Buy-Match, also lange bevor die chain den ERC-1155 transfer bestätigte. Das CLOB lehnte mit der Meldung "balance: 0" ab; der Bot loggte den Fehler, retried aber nicht. Acht Positionen liefen stillschweigend bis zur Resolution ohne take-profit-Schutz. Drei schlossen out of the money; eine schloss zufällig bei 0.99.
Der Fix wurde als 5-Sekunden-blocking wait zwischen jedem buy fill und jedem GTC post auf demselben Token ausgeliefert. Verifiziert mit 30 paper trades plus 30 live trades; seitdem keine balance-zero errors mehr.
Die Lehre: Ein stiller error path ist teurer als ein lauter. Danach ließen wir alle phantom-fill-Fehler einen Telegram-Alert auslösen, damit ein zukünftiger drift mode innerhalb von Sekunden sichtbar wäre.
Code: detect-then-act post-order pattern
Production buy-then-post pattern.
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}
Das Muster überlebt die häufigsten failure modes: phantom-fill, transient network drop, unter-minimale GTC size. Es liefert genug Informationen für die Strategy-Ebene, um zu entscheiden, was man retried, was man loggt und was man alarmiert.














