Polymarket Bot Tutorial · Chapitre 12 sur 32
Comment détecter les phantom fills sur Polymarket (des ordres qui semblent exécutés mais ne le sont pas), implémenter des retries idempotent, distinguer status=matched de rested-on-book, et survivre aux failures transitoires.
Ce que couvre ce chapitre
Un phantom fill est un mode de failure spécifique à Polymarket où le CLOB accuse réception d'un match, mais la chain n'a pas encore confirmé le transfert ERC-1155. Un ordre de suivi dans les ~5 secondes est rejeté avec une erreur trompeuse "balance: 0". La solution est l'idempotence et un temps d'attente de settlement. Ce chapitre est le playbook de production que nous avons payé avec de l'argent réel.
- Qu'est-ce qu'un phantom fill
- status=matched vs status=delayed vs status=posted
- Polling pattern: poll status before celebrating
- FOK comme anti-phantom-fill
- Retries idempotent avec client_order_id
- Incident de production réel: comment nous avons corrigé le nôtre
- Code: detect-then-act post-order pattern
Qu'est-ce qu'un phantom fill
Un phantom fill se produit lorsque l'API CLOB répond à votre ordre avec status: "matched" mais que le transfert ERC-1155 on-chain n'a pas encore été settle. Le matcher du CLOB est plus rapide que la production de blocs Polygon (~2s par bloc). Pendant environ 2 à 5 secondes après le match de l'API, votre wallet ne détient pas encore on-chain les tokens que le matcher dit que vous possédez.
Le bug du bot apparaît lorsqu'une action de suivi - généralement une vente GTC pour poster le take-profit - s'exécute pendant cette fenêtre. Le CLOB vérifie le chain balance, voit zéro, et rejette avec not enough balance / allowance: balance: 0, order amount: N. Le message d'erreur accuse l'allowance ; la cause est le settlement lag.
La première fois que cela arrive, on suppose un bug d'allowance et on perd une heure. La solution est simple: attendre, vérifier, puis poster.
status=matched vs status=delayed vs status=posted
La réponse de placement d'ordre inclut un champ status avec trois valeurs importantes.
matched: l'ordre a été matché immédiatement contre le book. L'inventory se settle en 2 à 5 secondes. C'est ce que renvoient FOK/FAK en cas de succès.delayed: le matcher n'a pas pu settle de manière synchrone et a mis le match en queue. Rare ; indique généralement une congestion. Traitez-le commematchedpour le pattern wait + verify.posted(aussi appelélive): l'ordre reste sur le book sans être exécuté. Renvoyé par les ordres GTC qui n'ont pas matché immédiatement. L'inventory n'est pas affecté ; aucune action de suivi n'est nécessaire pour l'instant.
La règle de décision: si le status est matched ou delayed, ne placez aucun follow-up qui nécessite le nouvel inventory tant que vous n'avez pas vérifié le chain transfer.
Polling pattern: poll status before celebrating
Le pattern de vérification: après un match réussi, poll le solde CTF jusqu'à ce qu'il reflète les nouveaux tokens, puis continuez.
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
Settlement typique: 2 à 5 secondes dans de bonnes conditions réseau, jusqu'à 15s en cas de congestion Polygon. Un délai de 5 secondes couvre 95% des cas ; en production, réglez le timeout sur 15s et déclenchez une alerte en cas de timeout.
Pour les bots à haute fréquence qui ne peuvent pas se permettre de bloquer, une alternative est l'event-subscription: surveillez l'événement TransferSingle du CTF pour votre adresse proxy et déclenchez les actions aval à sa réception. Cela déplace l'attente vers une queue au lieu de bloquer la boucle de stratégie.
FOK comme anti-phantom-fill
Choisir FOK plutôt que FAK est une défense partielle contre le chaos des phantom fills. FOK exécute soit l'ordre en entier, soit renvoie cancelled ; FAK peut renvoyer un filled_size partiel. Lorsqu'un fill partiel est suivi d'une vente GTC dimensionnée sur l'ordre d'origine, la vente échoue à cause du settlement lag plus d'un size mismatch - deux bugs qui se cumulent.
Avec FOK, la taille est binaire: soit la taille complète a matché, soit rien du tout. La logique de posting de suivi sait toujours à quoi s'attendre.
Cela n'élimine pas le besoin d'attendre - même un match FOK parfait est soumis à la fenêtre de settlement de 2 à 5 secondes. Mais cela supprime une classe de divergence de bookkeeping.
Retries idempotent avec client_order_id
Les failures réseau pendant le placement d'ordre créent le pire scénario: l'appel HTTP du bot a timeout, mais l'ordre a peut-être été reçu, ou non. Retenter naïvement peut doubler le placement ; ne pas retry peut faire tomber une position.
La solution est le champ client_order_id lors du placement d'ordre. Générez un UUID déterministe par ordre prévu ; si le serveur a déjà vu cet ID, il renvoie le status de l'ordre existant au lieu d'en créer un doublon.
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")
Le pattern: générez l'ID d'abord, retry en cas de failure de transport, jamais en cas de rejection logique. La déduplication côté serveur se fait par API key, pendant environ 5 minutes.
Incident de production réel: comment nous avons corrigé le nôtre
Tiré de notre propre journal de production, mai 2025. Une fenêtre de 60 minutes pendant laquelle le trader bot a placé 22 ordres d'achat, tous matchés, mais seulement 14 ventes GTC ont été acceptées. Huit positions n'avaient aucun exit posté.
Cause racine: le bot a posté la vente GTC dans les 800ms après le match de l'achat, bien avant que la chain confirme le transfert ERC-1155. Le CLOB a rejeté avec le message "balance: 0" ; le bot a loggé l'erreur mais n'a pas retry. Huit positions ont silencieusement été conservées jusqu'à la résolution sans protection take-profit. Trois ont clôturé hors de la monnaie ; une a clôturé à 0.99 par chance.
Le correctif a été déployé sous forme d'un wait bloquant de 5 secondes entre tout fill d'achat et toute publication GTC sur le même token. Vérifié via 30 paper trades plus 30 live trades ; zéro erreur balance-zero depuis.
La leçon: un chemin d'erreur silencieux coûte plus cher qu'un chemin bruyant. Après cela, nous avons fait en sorte que toutes les erreurs de phantom-fill déclenchent une alerte Telegram, afin qu'un futur drift mode soit visible en quelques secondes.
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}
Le pattern survit aux modes de failure courants: phantom-fill, coupure réseau transitoire, taille GTC sous le minimum. Il renvoie suffisamment d'informations pour que la stratégie décide quoi retry, logguer ou alerter.














