Polymarket Bot Tutorial · Chương 12/32
Cách phát hiện phantom fill trên Polymarket (các lệnh trông như đã khớp nhưng thực tế chưa), triển khai idempotent retries, phân biệt status=matched với rested-on-book, và vượt qua các lỗi tạm thời.
Chương này bao gồm những gì
Một phantom fill là một kiểu lỗi đặc thù của Polymarket, trong đó CLOB xác nhận có match nhưng chain vẫn chưa xác nhận việc chuyển ERC-1155. Một lệnh tiếp theo trong khoảng ~5 giây sẽ bị từ chối với lỗi gây hiểu lầm "balance: 0". Cách khắc phục là idempotence và chờ settlement. Chương này là playbook vận hành mà chúng tôi đã trả giá bằng tiền thật để có được.
- Phantom fill là gì
- status=matched vs status=delayed vs status=posted
- Polling pattern: poll status trước khi ăn mừng
- FOK như một biện pháp chống phantom-fill
- Idempotent retries với client_order_id
- Sự cố production thực tế: chúng tôi đã sửa như thế nào
- Code: detect-then-act post-order pattern
Phantom fill là gì
Phantom fill là khi CLOB API phản hồi lệnh của bạn với status: "matched" nhưng giao dịch chuyển ERC-1155 trên-chain vẫn chưa được settlement. CLOB matcher nhanh hơn việc tạo block trên Polygon (~2s mỗi block). Trong khoảng 2-5 giây sau khi API báo match, ví của bạn trên-chain chưa thực sự nắm giữ số token mà matcher nói là bạn sở hữu.
Lỗi bot xuất hiện khi một hành động tiếp theo - thường là một lệnh GTC sell để chốt take-profit - được chạy trong khoảng thời gian đó. CLOB kiểm tra số dư trên chain, thấy bằng 0, và từ chối với not enough balance / allowance: balance: 0, order amount: N. Thông báo lỗi đổ lỗi cho allowance; nguyên nhân thực sự là độ trễ settlement.
Lần đầu gặp tình huống này, bạn sẽ nghĩ đó là lỗi allowance và mất cả tiếng đồng hồ. Cách khắc phục rất đơn giản: chờ, xác minh, rồi mới post.
status=matched vs status=delayed vs status=posted
Phản hồi khi đặt lệnh có một trường status với ba giá trị quan trọng.
matched: lệnh đã khớp ngay với order book. Inventory sẽ được settlement trong 2-5 giây. Đây là trạng thái FOK/FAK trả về khi thành công.delayed: matcher không thể settlement đồng bộ và đã xếp match vào hàng đợi. Hiếm gặp; thường cho thấy có congestion. Hãy xử lý nhưmatchedcho mục đích của pattern chờ + xác minh.posted(còn gọi làlive): lệnh đang nằm chờ trên order book và chưa khớp. Được trả về bởi các lệnh GTC không khớp ngay. Inventory không bị ảnh hưởng; chưa cần hành động tiếp theo.
Quy tắc quyết định: nếu status là matched hoặc delayed, đừng đặt bất kỳ lệnh tiếp theo nào cần inventory mới cho đến khi bạn đã xác minh việc chuyển trên chain.
Polling pattern: poll status trước khi ăn mừng
Pattern xác minh: sau khi khớp thành công, poll số dư CTF cho đến khi nó phản ánh token mới, rồi mới tiếp tục.
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 điển hình: 2-5 giây trong điều kiện mạng tốt, tối đa 15 giây khi Polygon bị congestion. Chờ 5 giây sẽ bao phủ 95% trường hợp; trong production hãy đặt timeout là 15 giây và alert khi quá timeout.
Với bot tần suất cao không thể block, một phương án khác là event-subscription: theo dõi sự kiện TransferSingle của CTF cho địa chỉ proxy của bạn và kích hoạt các hành động tiếp theo khi nhận được sự kiện. Cách này đẩy việc chờ sang queue thay vì chặn strategy loop.
FOK như một biện pháp chống phantom-fill
Chọn FOK thay vì FAK là một lớp phòng thủ một phần chống lại sự hỗn loạn do phantom-fill. FOK либо khớp toàn bộ lệnh hoặc trả về cancelled; FAK có thể trả về filled_size một phần. Khi một partial fill bị theo sau bởi lệnh GTC sell có size bằng lệnh gốc, lệnh sell sẽ thất bại do vừa settlement-lag vừa lệch size - hai lỗi cộng dồn.
Với FOK, size là nhị phân: hoặc toàn bộ size đã khớp, hoặc không gì cả. Logic post lệnh tiếp theo luôn biết chính xác điều gì để mong đợi.
Điều này không loại bỏ nhu cầu chờ - ngay cả một match FOK hoàn hảo cũng vẫn chịu ảnh hưởng của cửa sổ settlement 2-5 giây. Nhưng nó loại bỏ một nhóm divergence trong bookkeeping.
Idempotent retries với client_order_id
Lỗi mạng trong lúc đặt lệnh tạo ra kịch bản tệ nhất: lệnh HTTP của bot bị timeout, nhưng lệnh đó có thể đã hoặc chưa được nhận. Retry một cách ngây thơ có thể tạo lệnh trùng; không retry thì có thể làm mất một vị thế.
Cách khắc phục là trường client_order_id khi đặt lệnh. Hãy tạo một UUID mang tính xác định cho mỗi lệnh dự định gửi; nếu server đã thấy ID đó trước đây, nó sẽ trả về trạng thái của lệnh hiện có thay vì tạo bản sao.
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")
Pattern này là: tạo ID trước, retry khi lỗi transport, không retry khi bị từ chối logic. Cơ chế dedup phía server áp dụng theo từng API key, tồn tại khoảng 5 phút.
Sự cố production thực tế: chúng tôi đã sửa như thế nào
Từ nhật ký production của chính chúng tôi, tháng 5 năm 2025. Trong một khung 60 phút, trader bot đã đặt 22 lệnh mua, tất cả đều matched, nhưng chỉ có 14 lệnh GTC sell được chấp nhận. Tám vị thế không có lệnh thoát nào được post.
Nguyên nhân gốc: bot post lệnh GTC sell chỉ sau 800ms kể từ khi buy match, quá sớm so với thời điểm chain xác nhận chuyển ERC-1155. CLOB từ chối với thông báo "balance: 0"; bot ghi log lỗi nhưng không retry. Tám vị thế đó âm thầm đi tới lúc settlement mà không có lớp bảo vệ take-profit. Ba lệnh đóng ở trạng thái out of the money; một lệnh đóng ở mức 0.99 chỉ nhờ may mắn.
Bản sửa được triển khai bằng một khoảng chờ blocking 5 giây giữa bất kỳ buy fill nào và bất kỳ GTC post nào trên cùng token. Đã xác minh bằng 30 paper trades cộng 30 live trades; từ đó tới nay không còn lỗi balance-zero nào nữa.
Bài học: một đường lỗi im lặng đắt giá hơn nhiều so với một lỗi phát ra tiếng. Sau đó chúng tôi cho mọi lỗi phantom-fill kích hoạt Telegram alert, để một chế độ drift trong tương lai sẽ nhìn thấy trong vài giây.
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}
Pattern này chịu được các failure mode phổ biến: phantom-fill, rớt mạng tạm thời, size GTC dưới mức tối thiểu. Nó trả về đủ thông tin để tầng chiến lược quyết định nên retry, log hay alert.














