DEV Community

Cover image for Polymarket Trading Bot Order States: Handling Pending, Failed, and Unknown Orders

Polymarket Trading Bot Order States: Handling Pending, Failed, and Unknown Orders

One of the harder parts of an automated trading system is not submitting an order.

It is knowing what actually happened after the order was submitted.

A bot can receive an incomplete response.

An order can remain unresolved.

A fill can arrive later.

A connection can disappear between two events.

The application can restart before it finishes processing the result.

That creates a dangerous situation if the bot treats every order response as final.

For a Polymarket trading bot, I prefer to model order execution as an explicit state machine rather than a single boolean such as:

order = successful
Enter fullscreen mode Exit fullscreen mode

A simplified internal lifecycle looks more like:

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

with other paths such as:

REJECTED
CANCELLED
FAILED
RETRYING
UNKNOWN
INCONSISTENT
Enter fullscreen mode Exit fullscreen mode

The exact state model can vary by implementation.

The important part is that unknown should be a real state.


Why a single order status is not enough

A simple trading bot often does something like:

submit order
    ↓
get response
    ↓
if success:
    mark filled
Enter fullscreen mode Exit fullscreen mode

That works only when every step behaves exactly as expected.

A more realistic execution flow looks like:

Strategy
   ↓
Order intent
   ↓
Submission
   ↓
Exchange response
   ↓
Matching
   ↓
Fill
   ↓
Transaction
   ↓
Confirmation
   ↓
Position
Enter fullscreen mode Exit fullscreen mode

There are several different facts here.

The strategy intended to place an order.

The system submitted something.

The venue accepted or rejected it.

The order may or may not have matched.

A fill may or may not have occurred.

The resulting execution may still need to be tracked further.

Collapsing all of that into one status makes recovery much harder.


An internal order state machine

I usually find it easier to reason about execution with explicit states.

For example:

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

Possible failure paths:

SUBMITTED → REJECTED
SUBMITTED → FAILED
ACCEPTED  → CANCELLED
MATCHED   → PARTIAL
Enter fullscreen mode Exit fullscreen mode

And when the system cannot establish what happened:

SUBMITTED
    ↓
UNKNOWN
Enter fullscreen mode Exit fullscreen mode

The state machine is not meant to describe every possible platform implementation.

It is an internal representation that lets the rest of the trading system make decisions based on what is actually known.


Pending does not mean failed

Suppose the strategy submits an order and does not immediately receive a final outcome.

The wrong response is often:

No final response
      ↓
Assume failed
      ↓
Submit again
Enter fullscreen mode Exit fullscreen mode

That can create duplicate exposure.

The order may still be alive.

It may have been accepted but the application has not observed the next event yet.

It may already have matched.

The system simply does not know yet.

So I prefer:

SUBMITTED
    ↓
PENDING
    ↓
WAIT / VERIFY
Enter fullscreen mode Exit fullscreen mode

rather than immediately converting uncertainty into failure.


The dangerous UNKNOWN state

This is the state I think deserves the most attention.

Imagine:

Order submitted
       ↓
Network interruption
       ↓
Response lost
Enter fullscreen mode Exit fullscreen mode

The bot now has:

Local state:
UNKNOWN
Enter fullscreen mode Exit fullscreen mode

That does not mean:

Order failed
Enter fullscreen mode Exit fullscreen mode

It also does not mean:

Order succeeded
Enter fullscreen mode Exit fullscreen mode

It means:

The application does not currently know.
Enter fullscreen mode Exit fullscreen mode

That distinction matters.

If the bot immediately retries, the first order could still exist.

You could end up with:

Original order
      +
Retry order
      =
More exposure than intended
Enter fullscreen mode Exit fullscreen mode

Unknown state therefore needs its own recovery path.


Unknown should block unsafe assumptions

A useful rule is:

Don't convert uncertainty into a successful or failed state just because the strategy needs an answer.

Instead:

UNKNOWN
   ↓
PAUSED / NO RETRY
   ↓
RECONCILE
   ↓
VERIFY
Enter fullscreen mode Exit fullscreen mode

Only after the system establishes enough information should it decide whether the order should be considered complete, failed, cancelled, or retried.

This is one reason execution verification and reconciliation belong close to the trading system rather than being treated as dashboard features.


Retrying an order is not as simple as submitting it again

A common implementation is:

if order failed:
    retry()
Enter fullscreen mode Exit fullscreen mode

The difficult part is determining whether the order actually failed.

Consider:

Order 123
   ↓
Submitted
   ↓
Response timeout
Enter fullscreen mode Exit fullscreen mode

The application might classify that as:

FAILED
Enter fullscreen mode Exit fullscreen mode

But the real state could be:

ACCEPTED
Enter fullscreen mode Exit fullscreen mode

or:

MATCHED
Enter fullscreen mode Exit fullscreen mode

or:

FILLED
Enter fullscreen mode Exit fullscreen mode

The network failure tells you that the application didn't get a response.

It does not necessarily tell you what happened to the order.

So the retry decision should depend on verified state, not just the absence of a response.

A safer flow is:

Request timeout
      ↓
Mark UNKNOWN
      ↓
Query / reconcile
      ↓
Did the order exist?
      ↓
What is its current state?
      ↓
Only then decide:
retry / cancel / continue
Enter fullscreen mode Exit fullscreen mode

Partial fills make the state model more important

Suppose the strategy intends:

100
Enter fullscreen mode Exit fullscreen mode

The actual execution is:

40 filled
60 remaining
Enter fullscreen mode Exit fullscreen mode

That is neither a clean failure nor a clean completion.

The system needs to represent the difference between:

requested quantity = 100
filled quantity    = 40
remaining quantity = 60
Enter fullscreen mode Exit fullscreen mode

The resulting state could be represented internally as:

PARTIALLY_FILLED
Enter fullscreen mode Exit fullscreen mode

The important part is that downstream calculations use the actual fill.

For example:

Order
  ↓
40 Fill
  ↓
Position +40
  ↓
Exposure
  ↓
Risk
Enter fullscreen mode Exit fullscreen mode

not:

Order 100
  ↓
Assume Position +100
Enter fullscreen mode Exit fullscreen mode

This connects directly to position reconciliation.

A trading system cannot maintain reliable risk controls when it confuses order intent with actual execution.


Order state affects position state

The order state machine should not live in isolation.

It feeds other parts of the system.

A useful dependency chain is:

Order
   ↓
Fill
   ↓
Position
   ↓
Exposure
   ↓
Risk
Enter fullscreen mode Exit fullscreen mode

For example:

Order = ACCEPTED
Enter fullscreen mode Exit fullscreen mode

does not automatically mean:

Position increased
Enter fullscreen mode Exit fullscreen mode

And:

Order = INTENDED
Enter fullscreen mode Exit fullscreen mode

definitely does not mean:

Position increased
Enter fullscreen mode Exit fullscreen mode

The position should be based on actual execution.

That sounds obvious, but this distinction becomes important when processing real-time events, partial fills, retries, reconnects, and application restarts.


Execution state and transaction state should stay separate

Another useful separation is between the order execution state and what happens afterward.

For example:

ORDER
  ↓
FILL
  ↓
TRANSACTION
  ↓
CONFIRMATION
  ↓
SETTLEMENT
  ↓
POSITION
Enter fullscreen mode Exit fullscreen mode

I don't want one field called:

status = filled
Enter fullscreen mode Exit fullscreen mode

to represent every stage of that lifecycle.

A system may know that a trade matched while still waiting on a later stage.

That is why my execution-verification work treats these stages separately.

Repository:

https://github.com/casatrickdev/polymarket-execution-verifier

The verifier is designed around checking execution across order, fill, transaction, and settlement states rather than assuming that one event tells the entire story.


What happens after a restart?

Imagine the application crashes here:

SUBMITTED
   ↓
?
Enter fullscreen mode Exit fullscreen mode

The process restarts several seconds later.

The local database might contain:

Order ID: 123
State: SUBMITTED
Enter fullscreen mode Exit fullscreen mode

The application should not simply continue from there as though nothing happened.

A safer startup process is:

Load local state
      ↓
Find unresolved orders
      ↓
Query / reconcile current state
      ↓
Update local state
      ↓
Recalculate position
      ↓
Recalculate exposure
      ↓
Check risk
      ↓
Resume trading
Enter fullscreen mode Exit fullscreen mode

That makes unresolved orders part of startup recovery.

This is closely related to position reconciliation: the system needs to re-establish a trustworthy state before creating additional risk.


WebSocket events should update state, not define reality

Real-time events are useful for keeping local state current.

But I don't want the local state machine to assume:

event received = complete truth forever
Enter fullscreen mode Exit fullscreen mode

Consider:

Order submitted
      ↓
Fill event
      ↓
WebSocket disconnect
      ↓
More execution activity
      ↓
Reconnect
Enter fullscreen mode Exit fullscreen mode

Some events may have been missed.

The application now needs a way to establish current state again.

So I think about real-time processing and reconciliation as two complementary mechanisms:

Real-time events
      ↓
Fast local updates

Reconciliation
      ↓
Recovery and verification
Enter fullscreen mode Exit fullscreen mode

The first keeps the system responsive.

The second gives it a way to recover from uncertainty.


Duplicate events are another problem

The opposite problem can also happen.

Instead of missing an event, the application might process the same logical event more than once.

For example:

Fill event
   ↓
Position +40

Same event processed again
   ↓
Position +80
Enter fullscreen mode Exit fullscreen mode

That creates incorrect state even though the connection was never lost.

This is why event handling should be designed with idempotency and deduplication in mind.

The system needs to know which execution events have already been applied.

That state belongs in the execution layer rather than being left to the strategy.


Order state should be observable

A useful trading dashboard should not simply show:

Orders: 14
Enter fullscreen mode Exit fullscreen mode

It should make unresolved conditions visible.

For example:

SUBMITTED       3
ACCEPTED        2
PARTIAL         1
FILLED         14
REJECTED        1
CANCELLED       2
UNKNOWN         1
Enter fullscreen mode Exit fullscreen mode

The exact numbers are only examples.

The point is that the operator should be able to see where execution is getting stuck.

An UNKNOWN order should be much more visible than a successful order that needs no action.


Unknown orders should have a timeout policy

A system can also define how long an order is allowed to remain unresolved.

Conceptually:

UNKNOWN
   ↓
recheck
   ↓
still unknown
   ↓
alert
   ↓
continue reconciliation
Enter fullscreen mode Exit fullscreen mode

The timeout should not automatically mean:

cancel and retry
Enter fullscreen mode Exit fullscreen mode

because the system still needs to understand whether the original order exists and what exposure it may have created.

A useful policy might distinguish between:

UNKNOWN for 1 second
Enter fullscreen mode Exit fullscreen mode

and:

UNKNOWN for 5 minutes
Enter fullscreen mode Exit fullscreen mode

The longer uncertainty persists, the more aggressively the control plane may need to intervene.


Risk controls should understand execution uncertainty

Risk management often focuses on:

Position limits
Exposure limits
Daily loss
Enter fullscreen mode Exit fullscreen mode

But execution uncertainty can itself become a reason to block trading.

For example:

Open position:
+40

Unresolved order:
+60

Known exposure:
40

Potential exposure:
100
Enter fullscreen mode Exit fullscreen mode

Depending on the strategy, treating the unresolved order as irrelevant could lead to incorrect decisions.

This doesn't necessarily mean counting every unknown order as filled.

It means the system should know that uncertainty exists and apply an appropriate policy.

That is an important distinction.


A control plane can make these decisions explicit

This is where order management connects to the broader trading control plane.

The strategy should not have to understand every operational failure mode.

Instead:

                 TRADING BOT
                      │
       ┌──────────────┼──────────────┐
       ↓              ↓              ↓
   Strategy       Execution        Risk
                      │
                      ↓
                Order State
                      │
          ┌───────────┴───────────┐
          ↓                       ↓
   Verification            Reconciliation
          │                       │
          └───────────┬───────────┘
                      ↓
                 CONTROL PLANE
                      │
              Pause / Resume
              Alert / Recover
Enter fullscreen mode Exit fullscreen mode

The control plane can decide whether the system is allowed to continue trading.

For example:

UNKNOWN ORDER
     ↓
Trading blocked
     ↓
Reconcile
     ↓
Execution verified
     ↓
Risk checked
     ↓
Trading allowed
Enter fullscreen mode Exit fullscreen mode

That keeps operational safety separate from strategy logic.


An example failure flow

Consider this sequence:

1. Strategy decides to buy 100

2. Order is submitted

3. Network response times out

4. Local state becomes UNKNOWN

5. Strategy wants to retry

6. Control Plane blocks retry

7. Execution state is reconciled

8. Original order is found

9. 40 units were filled

10. Position is updated to +40

11. Risk is recalculated

12. Remaining quantity is handled explicitly
Enter fullscreen mode Exit fullscreen mode

Without the state machine, the system might have simply retried the order at step 5.

That is exactly the kind of situation where a small implementation detail can turn into an unnecessary position.


Testing order-state handling

I would test explicit failure scenarios rather than only the happy path.

Order accepted
    → continue

Order rejected
    → record rejection

Order cancelled
    → record cancellation

Partial fill
    → update actual quantity

Response timeout
    → UNKNOWN

WebSocket disconnect
    → preserve unresolved state

Application restart
    → reconcile unresolved orders

Duplicate event
    → do not double-apply

Order state mismatch
    → pause and reconcile

Unknown state persists
    → alert and block unsafe actions
Enter fullscreen mode Exit fullscreen mode

These cases are much closer to the real operational problem than simply testing:

submit()
returns success
Enter fullscreen mode Exit fullscreen mode

What I want the execution layer to answer

At any point, the system should be able to answer questions like:

What did the strategy intend?

Was the order submitted?

Was it accepted?

Did it match?

How much actually filled?

What remains unresolved?

Did we receive the relevant events?

Does local state agree with remote state?

What position resulted?

Is the resulting exposure within risk limits?

Is it safe to place another order?
Enter fullscreen mode Exit fullscreen mode

Those questions are more useful than a single status field.

They also provide much better information when investigating an incident.


Connecting this to execution verification

This is the reason I keep execution verification separate from the strategy.

The strategy should care about:

Should I trade?
Enter fullscreen mode Exit fullscreen mode

The execution system should care about:

What happened to the order?
Enter fullscreen mode Exit fullscreen mode

The reconciliation layer should care about:

Does local state match reality?
Enter fullscreen mode Exit fullscreen mode

The risk system should care about:

Is the resulting state still acceptable?
Enter fullscreen mode Exit fullscreen mode

And the control plane should care about:

Should the system currently be allowed to trade?
Enter fullscreen mode Exit fullscreen mode

Those are related questions, but they are not the same question.


The architecture I'm building around this

My current Polymarket infrastructure projects reflect that separation.

Polymarket Trading Bot

https://github.com/casatrickdev/polymarket-trading-bot

The broader bot handles strategy, execution, risk, monitoring and backtesting.

Polymarket Execution Verifier

https://github.com/casatrickdev/polymarket-execution-verifier

The verifier focuses on execution state across order, fill, transaction and later settlement stages.

Polymarket Trading Control Plane

https://github.com/casatrickdev/polymarket-trading-control-plane

The control plane focuses on state, health, risk, reconciliation, alerts and operational recovery.

The goal is not to make the system more complicated for its own sake.

It's to keep different kinds of uncertainty from being hidden inside one order handler.


Final takeaway

A trading bot should not think in terms of:

order succeeded
Enter fullscreen mode Exit fullscreen mode

It should know where the order is in its lifecycle.

Something like:

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

with explicit paths for:

REJECTED
CANCELLED
FAILED
PARTIAL
UNKNOWN
INCONSISTENT
Enter fullscreen mode Exit fullscreen mode

The particularly important state is:

UNKNOWN
Enter fullscreen mode Exit fullscreen mode

because unknown does not mean failed.

It means the system does not yet know what happened.

That should trigger verification or reconciliation, not an automatic assumption.

For an automated Polymarket trading system, that distinction matters because the next order is going to be based on whatever state the system believes right now.

The execution layer needs to make that belief explicit.

And when it cannot establish the truth, the safest state is sometimes simply:

UNKNOWN
   ↓
STOP
   ↓
VERIFY
   ↓
RESUME
Enter fullscreen mode Exit fullscreen mode

That is a much better foundation for a trading system that needs to keep operating reliably.

Top comments (0)