DEV Community

Cover image for Why a Polymarket Trading Bot Needs State Reconciliation
Casatrick | Polymrket Bot Dev
Casatrick | Polymrket Bot Dev

Posted on Originally published at casatrick.substack.com

Why a Polymarket Trading Bot Needs State Reconciliation

A trading bot can keep running while its internal state is already wrong.

That is one of the most dangerous failure modes in automated trading.

The obvious problems are easy to notice:

  • the process crashes
  • the WebSocket disconnects
  • an API request fails
  • an order is rejected

The harder problems are silent:

  • a partial fill was missed
  • an order was cancelled remotely but remains open locally
  • a reconnect caused an event gap
  • local position state no longer matches exchange state
  • the bot continues trading using stale exposure data

At that point, the bot may still look healthy.

It just isn't trading from reality anymore.

For a serious Polymarket trading system, state reconciliation needs to be a first-class component.

The basic problem

Imagine the bot submits:

BUY 100 contracts @ 0.57
Enter fullscreen mode Exit fullscreen mode

Its local state becomes:

order_id = 123
status   = OPEN
size     = 100
filled   = 0
Enter fullscreen mode Exit fullscreen mode

Then 40 contracts fill.

The correct state is now:

status    = PARTIALLY_FILLED
filled    = 40
remaining = 60
Enter fullscreen mode Exit fullscreen mode

But suppose the WebSocket disconnects at exactly the wrong time.

Your process reconnects.

The event containing that fill is gone.

Your local state still says:

filled = 0
remaining = 100
Enter fullscreen mode Exit fullscreen mode

Now the strategy can make completely different decisions from the ones it should make.

It might:

  • place another order
  • calculate incorrect exposure
  • report incorrect PnL
  • apply the wrong risk limit
  • cancel an order that isn't actually in the state it expects

Nothing necessarily crashes.

That is what makes stale state dangerous.


WebSocket events are not enough

Real-time events are essential for a trading system.

They let the bot react quickly to order and trade changes without constantly polling.

But there is an important architectural distinction:

Events tell you what happened. Reconciliation tells you what is true now.

A production system needs both.

I think about the architecture like this:

                 REAL-TIME PATH

             Polymarket CLOB
                    |
                WebSocket
                    |
            Event Processor
                    |
             Local State
          orders / fills /
          positions / risk


              RECONCILIATION PATH

             Remote API State
                    |
              Reconciliation
                    |
              Local State
                    |
             State Repair
Enter fullscreen mode Exit fullscreen mode

The real-time path optimizes for speed.

The reconciliation path optimizes for correctness.


1. Turn events into explicit state transitions

One mistake is letting every incoming event directly mutate arbitrary application state.

Instead, model the trading lifecycle explicitly.

For example:

ORDER_PLACED
     ↓
OPEN
     ↓
PARTIALLY_FILLED
     ↓
FILLED
Enter fullscreen mode Exit fullscreen mode

Cancellation has its own lifecycle:

OPEN
  ↓
CANCEL_REQUESTED
  ↓
CANCELLED
Enter fullscreen mode Exit fullscreen mode

And failures should also be explicit.

This gives you a state machine that can be tested and replayed.

Instead of asking:

Why does the bot think it owns 400 contracts?

you can ask:

Which state transition caused the position to become 400?

That makes failures much easier to debug.


2. Make event processing idempotent

Real-time trading systems eventually encounter:

  • duplicate events
  • retries
  • reconnects
  • replayed messages
  • process restarts

Processing the same event twice should not create two fills.

A simplified example:

def process_event(event):
    if already_processed(event.id):
        return

    apply_transition(event)
    mark_processed(event.id)
Enter fullscreen mode Exit fullscreen mode

The exact implementation depends on the system, but the principle is important:

The same event should produce the same state exactly once.

This becomes especially important when rebuilding state after a restart.


3. Reconcile after reconnects

A reconnect shouldn't simply mean:

connect
→ subscribe
→ resume trading
Enter fullscreen mode Exit fullscreen mode

A safer flow is:

disconnect
    ↓
reconnect
    ↓
load persisted state
    ↓
query current remote state
    ↓
compare local vs remote
    ↓
repair differences
    ↓
verify risk
    ↓
resume trading
Enter fullscreen mode Exit fullscreen mode

For example:

LOCAL                     REMOTE

Order 123: OPEN           Order 123: OPEN
Filled: 0                 Filled: 40

Order 456: FILLED         Order 456: FILLED
Position: +100            Position: +100
Enter fullscreen mode Exit fullscreen mode

The reconciliation layer identifies:

Order 123:
local filled  = 0
remote filled = 40

→ local state is stale
→ repair local state
Enter fullscreen mode Exit fullscreen mode

Only after that should the strategy continue.


4. Separate orders, fills, and positions

These are related, but they are not the same thing.

A useful hierarchy is:

Orders
   ↓
Fills
   ↓
Positions
   ↓
Exposure
   ↓
Risk
Enter fullscreen mode Exit fullscreen mode

Consider:

Order A: BUY 100
Order B: BUY 50
Enter fullscreen mode Exit fullscreen mode

Then:

Order A → filled 60
Order B → filled 50
Enter fullscreen mode Exit fullscreen mode

The position isn't 150.

It is based on the actual executed fills.

This distinction becomes critical once you have partial fills, multiple orders, cancellations, and concurrent strategy actions.

A robust trading system should never derive actual exposure from intended order sizes alone.


5. Order lifecycle is not a single state

Another common mistake is treating:

order submitted
Enter fullscreen mode Exit fullscreen mode

as equivalent to:

order executed
Enter fullscreen mode Exit fullscreen mode

They are different stages.

A more realistic lifecycle is:

INTENDED
   ↓
SUBMITTED
   ↓
ACCEPTED
   ↓
MATCHED
   ↓
FILLED
Enter fullscreen mode Exit fullscreen mode

With alternative paths:

SUBMITTED → REJECTED
SUBMITTED → CANCELLED
MATCHED   → RETRYING
Enter fullscreen mode Exit fullscreen mode

The strategy layer needs to know which state it is actually in.

This becomes particularly important in asynchronous execution systems, where an order can be accepted before the full execution lifecycle is known.


6. Persist state

If the process restarts, memory disappears.

A production system should persist enough state to reconstruct what happened.

At minimum, that usually means tracking things like:

order ID
market
side
requested size
filled size
remaining size
status
timestamps
event IDs
trade IDs
Enter fullscreen mode Exit fullscreen mode

For positions:

market
position size
average entry
realized PnL
unrealized PnL
last update
Enter fullscreen mode Exit fullscreen mode

The goal isn't to persist every internal object.

The goal is to preserve enough information that the system can:

  1. restart
  2. inspect the remote state
  3. reconcile
  4. continue safely

7. Detect gaps instead of assuming everything arrived

A WebSocket connection can be alive while your application is still missing information.

That means your system should have some notion of continuity.

For example:

event 1001
event 1002
event 1004
Enter fullscreen mode Exit fullscreen mode

Where is:

event 1003?
Enter fullscreen mode Exit fullscreen mode

A trading system shouldn't silently assume that the missing event doesn't matter.

This is where sequence tracking, timestamps, persisted event IDs, or explicit resynchronization logic can become useful.

The exact mechanism depends on the stream and API.

The architectural principle is the important part:

Don't confuse “socket connected” with “state synchronized.”


8. Reconciliation should be safe

Reconciliation itself can create dangerous behavior if implemented badly.

Suppose local state says:

position = +100
Enter fullscreen mode Exit fullscreen mode

but remote state says:

position = +40
Enter fullscreen mode Exit fullscreen mode

The solution isn't necessarily:

position = 40
Enter fullscreen mode Exit fullscreen mode

You first need to understand why the states differ.

Was there:

  • a missed fill?
  • a delayed event?
  • a duplicate event?
  • an unexpected trade?
  • a stale API response?
  • a bug in local processing?

A useful reconciler should produce a discrepancy such as:

Position mismatch

Local:
  +100

Remote:
  +40

Difference:
  -60

Reason:
  unmatched fill / state gap

Action:
  repair + log + alert
Enter fullscreen mode Exit fullscreen mode

That makes reconciliation observable and debuggable.


9. Add a trading safety gate

One of the most useful patterns is to make reconciliation part of trading permission.

Conceptually:

if not state.is_consistent():
    trading_enabled = False
Enter fullscreen mode Exit fullscreen mode

Then:

NORMAL
  ↓
STATE MISMATCH
  ↓
TRADING PAUSED
  ↓
RECONCILIATION
  ↓
STATE VERIFIED
  ↓
TRADING RESUMED
Enter fullscreen mode Exit fullscreen mode

This is much safer than allowing the strategy to keep trading while its view of exposure is uncertain.

You don't always need to shut down the entire system.

Depending on the risk model, you could:

  • pause new entries
  • allow exits only
  • reduce position size
  • disable a particular market
  • require manual approval

The important part is that strategy execution becomes conditional on trusted state.


10. Recovery should be designed before failure

A common development pattern is:

“We'll deal with reconnects later.”

That usually becomes painful once the system has real positions.

A better architecture defines recovery from the start:

process starts
     ↓
load persisted state
     ↓
connect
     ↓
synchronize
     ↓
reconcile
     ↓
validate risk
     ↓
start strategy
Enter fullscreen mode Exit fullscreen mode

And after a failure:

failure
   ↓
reconnect
   ↓
detect possible gap
   ↓
reconcile
   ↓
repair
   ↓
resume
Enter fullscreen mode Exit fullscreen mode

Recovery is not a special case.

It is part of normal trading-system behavior.


A practical architecture

Putting the pieces together:

                  POLYMARKET CLOB
                         |
              +----------+----------+
              |                     |
          WebSocket             API / Reads
              |                     |
              v                     v
       +--------------+      +--------------+
       | Event        |      | Reconciliation|
       | Processor    |      | Engine        |
       +------+-------+      +------+---------+
              |                     |
              +----------+----------+
                         |
                         v
                +------------------+
                | Persisted State  |
                |                  |
                | Orders           |
                | Fills            |
                | Positions        |
                | Exposure         |
                +--------+---------+
                         |
                         v
                +------------------+
                | Strategy Engine  |
                +--------+---------+
                         |
                         v
                +------------------+
                | Risk Controls    |
                +--------+---------+
                         |
                         v
                +------------------+
                | Execution Engine |
                +------------------+
Enter fullscreen mode Exit fullscreen mode

The strategy is only one component.

The state layer is what allows the strategy to operate safely.


Backtesting should test this too

Most backtests focus on the strategy:

signal
→ order
→ profit
Enter fullscreen mode Exit fullscreen mode

Production systems need more failure scenarios.

For example:

WebSocket disconnect
Partial fill
Duplicate event
Missed event
Delayed API response
Process restart
Order cancellation race
State mismatch
Enter fullscreen mode Exit fullscreen mode

These can be simulated.

A good trading-system test should ask:

What happens if the world becomes inconsistent for 10 seconds?

That's often much more useful than another percentage-point improvement in a strategy backtest.


The bigger lesson

The most dangerous trading-system bug isn't necessarily a crash.

A crashed bot is obvious.

A bot that continues operating with the wrong view of reality is much harder to notice.

That is why state reconciliation deserves to be treated as a first-class subsystem.

The architecture should assume that:

  • events can be delayed
  • connections can fail
  • fills can be partial
  • processes can restart
  • local state can become stale
  • APIs can behave unexpectedly

And the system should know how to recover.

Final takeaway

When building a Polymarket trading bot, it's tempting to focus on the strategy:

signal
→ buy
→ sell
→ profit
Enter fullscreen mode Exit fullscreen mode

Production systems look more like:

market data
→ event processing
→ state
→ reconciliation
→ strategy
→ risk
→ execution
→ persistence
→ recovery
Enter fullscreen mode Exit fullscreen mode

The strategy determines what you want to do.

The infrastructure determines whether you know what is actually happening.

That's why I would treat state reconciliation as a core part of any serious Polymarket trading system.

Fast execution matters.

Correct state matters more.


What I'd build next

The natural next component after reconciliation is a replayable Polymarket event stream.

If every order, fill, position change, and reconciliation event can be persisted and replayed, you can use the same infrastructure for:

  • debugging
  • backtesting
  • incident analysis
  • simulation
  • strategy research
  • production recovery

That's when the trading system starts becoming a reusable piece of infrastructure rather than a single-purpose bot.


This article focuses on production trading-system architecture and reliability. Proprietary strategy logic and parameters are intentionally omitted.

Top comments (1)

Collapse
 
marcusykim profile image
Marcus Kim

Missing the 40-contract fill while the bot still thinks all 100 are open makes the reconnect safety gate pretty concrete. In the idempotency example, I'd persist the state transition and processed-event record in one transaction. A crash after apply_transition but before mark_processed could otherwise count that fill again on replay. I'd inject a crash at that exact point in the process-restart tests: recovery should restore the same position regardless of how many times the event gets replayed.