
DexorynA Polymarket trading bot can receive the same event more than once, restart after a crash, or lose...
A Polymarket trading bot can receive the same event more than once, restart after a crash, or lose its connection while an order is being submitted. If the application treats every repeated signal as a new instruction, it can place duplicate orders and expose more capital than intended.
The solution is not simply to add a delay between trades. A reliable bot needs idempotent event processing, persistent state, and order reconciliation.
This tutorial explains how to design those safeguards in Python, using a copy-trading bot as the example. The same principles apply to other automated trading systems.
Why duplicate orders happen
Consider a bot that monitors a target wallet and copies its trades.
The bot detects a buy event and submits a corresponding order. Several failure scenarios can produce an unintended second order.
Repeated events: A reconnect or polling cycle causes the same source activity to be processed again.
Application restarts: The process crashes after detecting a trade but before recording that the event has been handled.
Network timeouts: The order request reaches the trading API, but the response never reaches the bot. The application cannot tell whether the order was accepted.
Concurrent workers: Two workers process the same event before either records it as complete.
Partial failures: The bot saves a trade as processed before submitting its order, then crashes. On restart, it may skip a trade that was never submitted.
These are different failure modes. A single in-memory set of processed events will not solve all of them.
The first safeguard is to distinguish a genuinely new trade from a repeated notification about an existing trade.
Ideally, the source provides a stable event identifier. Depending on the data source, this might be a transaction hash combined with a log index, or another unique activity identifier.
Do not use only the wallet address, market ID, and trade direction as the deduplication key. A trader can legitimately buy the same outcome multiple times.
Here is a simple Python example:
def event_key(event: dict) -> str:
tx_hash = event.get("transaction_hash")
log_index = event.get("log_index")
if tx_hash is None or log_index is None:
raise ValueError("Missing stable event identity")
return f"{tx_hash.lower()}:{log_index}"
This example assumes the source exposes both fields and that the combination uniquely identifies the relevant event. Verify the actual schema before using it.
If your data provider exposes a different stable identifier, use that instead. If no reliable identifier exists, a composite fingerprint may be necessary, but it must be designed around the source's semantics and can accidentally merge legitimate trades.
The principle is simple: deduplicate events by identity, not by similarity.
An in-memory set disappears when the process restarts. A persistent database allows the bot to remember which source events it has already accepted for processing.
SQLite is a practical starting point for a single-process Python bot.
import sqlite3
db = sqlite3.connect("bot_state.db")
db.execute("""
CREATE TABLE IF NOT EXISTS source_events (
event_key TEXT PRIMARY KEY,
status TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
db.commit()
The primary key prevents the database from storing the same event identity twice.
You can attempt to register an event with an insert that ignores an existing key:
def register_event(event_id: str) -> bool:
cursor = db.execute(
"""
INSERT OR IGNORE INTO source_events
(event_key, status)
VALUES (?, ?)
""",
(event_id, "received"),
)
db.commit()
return cursor.rowcount == 1
If the function returns "True", the event was newly registered. If it returns "False", an event with that identity already exists.
This is more reliable than checking a set and inserting later, because the database enforces uniqueness. In a multi-worker system, the claim operation and associated state transitions still need appropriate transaction and concurrency handling.
Most importantly, registering an event does not mean its order has been executed.
That distinction leads to the next safeguard.
A robust bot should represent the lifecycle of an event and its associated order.
A simplified state model might include:
These states should not be treated as interchangeable.
For example, a submitted order might remain open, fill partially, or be cancelled. A rejected order is different from an order whose response timed out.
A useful database design separates the source event from the order attempt. One source event may produce no order because it fails validation, while a valid event may require more than one order attempt under a carefully defined recovery policy.
The system should record the source event identity, intended market and outcome, requested size, order identifier when available, current state, timestamps, and any error details.
That record makes recovery possible without blindly repeating the original action.
This is one of the most important rules in automated execution.
Suppose the bot sends an order request. The server accepts it, but the network connection drops before the response arrives.
The bot sees a timeout. It does not know whether the order was accepted.
If it immediately submits the same order again, the account could end up with two orders.
A safer recovery process is:
The exact reconciliation procedure depends on the current API and the information it exposes. Read the current "Polymarket developer documentation" (https://docs.polymarket.com/) before implementing it.
There is an important limitation here: a local database transaction cannot atomically commit both a database update and a remote trading order. Without a server-supported idempotency mechanism, the application cannot guarantee exactly-once execution across every network failure.
The practical goal is to make duplicate execution unlikely, detect ambiguous outcomes, and recover conservatively.
Do not place the entire trading workflow inside a WebSocket message handler.
If event detection, database writes, risk checks, network requests, and order submission all happen in one callback, a slow API response can interfere with processing incoming events.
A cleaner architecture separates the responsibilities:
Market or wallet activity
|
v
Event normalizer
|
v
Persistent event registry
|
v
Validation and risk
|
v
Order submission
|
v
Order reconciliation
|
v
Persistent state update
The event registry prevents the same source activity from being independently accepted multiple times. The execution component handles order submission. Reconciliation checks whether the external system agrees with the bot's local records.
A queue can connect the components, but the queue alone is not a deduplication mechanism. Events may be delivered more than once, workers may crash, and messages may be retried.
The persistent event identity and state machine remain essential.
A bot that works during a normal test run may still fail when requests time out or the process restarts.
At minimum, test these scenarios:
Duplicate event: Deliver the same source event twice. Confirm that it creates no more than one active processing record.
Crash before submission: Restart the application after registering an event but before sending an order. Confirm that recovery resumes safely.
Timeout after submission: Simulate a request whose result is unknown. Confirm that the bot reconciles before considering another submission.
Concurrent processing: Allow two workers to receive the same event simultaneously. Confirm that database uniqueness and transaction handling prevent duplicate processing.
Partial fill: Confirm that the bot tracks filled and remaining quantities rather than treating any fill as a fully completed order.
Stale state: Restart the bot with existing open orders and positions. Confirm that it reconciles external state before resuming new execution.
These tests should use mocked API responses or a dry-run environment. They should not require risking live funds.
When an order is rejected or an event is skipped, the logs should explain why.
Record the source event identifier, state transition, validation outcome, order identifier, requested size, response status, and reconciliation result. Never log private keys, API secrets, or other credentials.
Useful operational metrics include:
These measurements help distinguish a data-feed problem from an execution problem or a state-management bug.
Conclusion
Preventing duplicate orders in a Polymarket trading bot requires more than checking whether a trade was seen before.
Use stable event identities, persistent uniqueness constraints, explicit order states, conservative timeout recovery, and reconciliation with the external trading system. Then test the system under duplicate delivery, restarts, concurrency, and ambiguous network outcomes.
For developers building more advanced automation, these safeguards should be part of the trading architecture from the beginning.
Dexoryn Labs maintains a public "Polymarket copy-trading bot repository" (https://github.com/dexorynlabs/polymarket-copy-trading-bot) that developers can inspect when evaluating an existing implementation. Teams with different execution, monitoring, or risk requirements may also need custom development.
Reliable execution does not guarantee profitable trading. It helps ensure the bot behaves more predictably when software and network failures occur.
This article is for educational purposes only. Automated trading involves financial risk. Test in dry-run mode before considering live execution, protect credentials, and use only funds you can afford to lose.