Polymarket Bot Tutorial · Bab 12 dari 32
Cara mendeteksi phantom fills Polymarket (order yang terlihat filled tetapi sebenarnya tidak), menerapkan idempotent retries, membedakan status=matched dari rested-on-book, dan bertahan dari transient failures.
Apa yang dibahas bab ini
Phantom fill adalah failure mode khusus Polymarket di mana CLOB mengakui match, tetapi chain belum mengonfirmasi transfer ERC-1155. Order lanjutan dalam ~5 detik akan ditolak dengan error yang menyesatkan, "balance: 0". Solusinya adalah idempotence dan wait untuk settlement. Bab ini adalah playbook produksi yang kami bayar dengan uang sungguhan.
- Apa itu phantom fill
- status=matched vs status=delayed vs status=posted
- Polling pattern: poll status sebelum merayakan
- FOK sebagai anti-phantom-fill
- Idempotent retries dengan client_order_id
- Insiden produksi nyata: bagaimana kami memperbaikinya
- Code: detect-then-act post-order pattern
Apa itu phantom fill
Phantom fill adalah saat CLOB API merespons order Anda dengan status: "matched" tetapi transfer ERC-1155 on-chain belum selesai terselesaikan. Matcher CLOB lebih cepat daripada produksi block Polygon (~2 detik per block). Selama kira-kira 2-5 detik setelah API match, wallet Anda belum secara on-chain memegang token yang menurut matcher sudah Anda miliki.
Bug bot muncul ketika aksi lanjutan - biasanya GTC sell untuk memasang take-profit - dijalankan dalam jendela waktu itu. CLOB memeriksa balance di chain, melihat nol, lalu menolak dengan not enough balance / allowance: balance: 0, order amount: N. Pesan error menyalahkan allowance; penyebabnya adalah settlement lag.
Pertama kali ini terjadi, Anda mengira ada bug allowance dan membuang satu jam. Solusinya sederhana: tunggu, verifikasi, lalu post.
status=matched vs status=delayed vs status=posted
Respons penempatan order mencakup field status dengan tiga nilai yang penting.
matched: order langsung match terhadap book. Inventory akan settle dalam 2-5 detik. Ini yang dikembalikan FOK/FAK saat berhasil.delayed: matcher tidak bisa settle secara sinkron dan menaruh match ke queue. Jarang; biasanya menandakan congestion. Perlakukan sepertimatcheduntuk pola wait + verify.posted(juga disebutlive): order tetap resting di book dan belum terisi. Dikembalikan oleh order GTC yang tidak langsung match. Inventory tidak terpengaruh; belum perlu aksi lanjutan.
Aturan keputusan: jika status adalah matched atau delayed, jangan melakukan follow-up apa pun yang membutuhkan inventory baru sampai Anda memverifikasi transfer di chain.
Polling pattern: poll status sebelum merayakan
Pola verifikasi: setelah match berhasil, poll balance CTF sampai mencerminkan token baru, lalu lanjutkan.
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 tipikal: 2-5 detik dalam kondisi jaringan yang baik, hingga 15 detik saat Polygon congestion. Wait 5 detik menutup 95% kasus; untuk produksi, set timeout ke 15 detik dan beri alert saat timeout.
Untuk bot high-frequency yang tidak bisa menunggu secara blocking, alternatifnya adalah event-subscription: pantau event TransferSingle dari CTF untuk alamat proxy Anda dan trigger aksi downstream saat event diterima. Ini memindahkan wait ke queue alih-alih memblokir strategy loop.
FOK sebagai anti-phantom-fill
Memilih FOK daripada FAK adalah pertahanan sebagian terhadap kekacauan phantom-fill. FOK akan terisi seluruh order atau mengembalikan cancelled; FAK bisa mengembalikan filled_size yang parsial. Saat partial fill diikuti GTC sell dengan ukuran sebesar order asli, sell gagal karena settlement lag ditambah size mismatch - dua bug yang saling memperparah.
Dengan FOK, ukurannya bersifat biner: seluruh ukuran match atau tidak sama sekali. Logika posting lanjutan selalu tahu apa yang harus diharapkan.
Ini tidak menghilangkan kebutuhan untuk menunggu - bahkan match FOK yang sempurna tetap tunduk pada jendela settlement 2-5 detik. Tetapi ini menghilangkan satu kelas divergence dalam bookkeeping.
Idempotent retries dengan client_order_id
Gangguan jaringan saat penempatan order menciptakan skenario terburuk: panggilan HTTP bot Anda timeout, tetapi order mungkin sudah diterima atau belum. Retry secara naif bisa membuat double-place; tidak retry bisa membuat posisi hilang.
Solusinya adalah field client_order_id pada penempatan order. Generate UUID deterministik per order yang dimaksud; jika server sudah pernah melihat ID itu, server akan mengembalikan status order yang sudah ada alih-alih membuat duplikat.
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")
Pola ini: generate ID terlebih dahulu, retry saat transport failure, jangan pernah saat logical rejection. Dedup di sisi server berlaku per API key, bertahan ~5 menit.
Insiden produksi nyata: bagaimana kami memperbaikinya
Dari diary produksi kami sendiri, Mei 2025. Sebuah window 60 menit saat trader bot memasang 22 buy order, semuanya match, tetapi hanya 14 GTC sell yang diterima. Delapan posisi tidak memiliki exit yang diposting.
Akar masalah: bot memposting GTC sell dalam 800ms setelah buy match, jauh sebelum chain mengonfirmasi transfer ERC-1155. CLOB menolak dengan pesan "balance: 0"; bot mencatat error tetapi tidak retry. Delapan posisi diam-diam berjalan sampai resolusi tanpa perlindungan take-profit. Tiga ditutup out of the money; satu ditutup pada 0.99 karena keberuntungan.
Perbaikannya dirilis sebagai wait blocking 5 detik antara buy fill dan GTC post pada token yang sama. Diverifikasi lewat 30 paper trades plus 30 live trades; sejak itu nol balance-zero errors.
Pelajarannya: jalur error yang diam-diam lebih mahal daripada yang berisik. Setelah ini kami membuat semua phantom-fill errors memicu Telegram alert, sehingga future drift mode akan terlihat dalam hitungan detik.
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}
Pola ini bertahan dari failure mode umum: phantom-fill, transient network drop, GTC size di bawah minimum. Mengembalikan cukup informasi agar strategy layer bisa memutuskan mana yang perlu retry, mana yang perlu log, dan mana yang perlu alert.














