DEV Community

Cover image for Building a Polymarket Trading Control Plane: State, Risk, and Recovery
Casatrick | Polymrket Bot Dev
Casatrick | Polymrket Bot Dev

Posted on Originally published at github.com

Building a Polymarket Trading Control Plane: State, Risk, and Recovery

A Polymarket trading bot can be connected, receiving market data, submitting orders, and still be unsafe to operate.

The problem isn't always the strategy.

It can be the system around the strategy.

A production trading system needs to know:

  • what orders are actually open
  • what has actually filled
  • what positions are actually held
  • whether market data is still fresh
  • whether local state matches remote state
  • whether risk limits are still satisfied
  • whether the system should be allowed to place another order

That leads to a different way of thinking about a trading bot.

Instead of:

market data
    ↓
strategy
    ↓
  order
Enter fullscreen mode Exit fullscreen mode

I want the architecture to look more like:

market data
    ↓
event processing
    ↓
  state
    ↓
  risk
    ↓
execution
    ↓
reconciliation
    ↓
monitoring
    ↓
recovery
Enter fullscreen mode Exit fullscreen mode

I'm building a small Polymarket Trading Control Plane around that idea.

The goal is not another trading strategy.

The goal is to build the operational layer that allows a strategy to run with a much clearer understanding of its own state.


Why build a control plane?

When you're prototyping a trading bot, it's easy to keep everything in one process.

Something like:

while True:
    market = get_market_data()
    signal = strategy(market)

    if signal:
        place_order(signal)
Enter fullscreen mode Exit fullscreen mode

This is fine for exploring an idea.

But once the system has real orders and positions, the questions become different.

What happens when the WebSocket disconnects?

What happens when an order partially fills?

What happens when the process restarts?

What happens when a response is delayed?

What happens when local state says one thing and the remote state says another?

These are operational problems, not strategy problems.

And they become more important as the amount of automation increases.

GitHub: Polymarket Trading Control Plane


The first principle: local state is not reality

Suppose a strategy submits:

BUY 100
Enter fullscreen mode Exit fullscreen mode

The local application records:

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

Then 40 contracts fill.

The real state is now:

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

Now imagine the process misses that update.

The process may still believe:

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

Nothing has necessarily crashed.

The program is still running.

The strategy is still calculating.

The system just has the wrong state.

That is much more dangerous than a visible exception because the system can continue making decisions from incorrect information.


WebSockets are useful, but they are not your entire state model

Polymarket's current Rust CLOB client supports WebSocket streams for orderbooks, prices, and authenticated user order/trade events. The current V2 client also exposes Data and Gamma API integrations and other CLOB functionality.

That's exactly what a low-latency trading system needs.

But I don't want my architecture to assume:

WebSocket connected
=
state is correct
Enter fullscreen mode Exit fullscreen mode

Those are two different conditions.

I think about the system as having two paths.

                 POLYMARKET
                     │
          ┌──────────┴──────────┐
          │                     │
      WebSocket              API / Reads
          │                     │
          ▼                     ▼
   Event Processor        Reconciliation
          │                     │
          └──────────┬──────────┘
                     ▼
                Local State
Enter fullscreen mode Exit fullscreen mode

The first path is optimized for speed.

The second path is responsible for validating and repairing state.


State should be explicit

One of the first design decisions in the control plane is to separate the major concepts.

I don't want one giant BotState object that mixes everything together.

Instead:

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

Each layer answers a different question.

Orders

What did the system request?

order_id
market
side
price
requested_size
status
Enter fullscreen mode Exit fullscreen mode

Fills

What actually executed?

trade_id
order_id
executed_size
execution_price
timestamp
Enter fullscreen mode Exit fullscreen mode

Positions

What do we currently hold?

market
size
average_entry
realized_pnl
unrealized_pnl
Enter fullscreen mode Exit fullscreen mode

Exposure

What risk is currently on the book?

market_exposure
total_exposure
concentration
Enter fullscreen mode Exit fullscreen mode

Risk

Should the system be allowed to place another order?

allowed
paused
degraded
killed
Enter fullscreen mode Exit fullscreen mode

Keeping these concepts separate makes the system easier to reason about.


Model order lifecycles explicitly

Another common mistake is treating an order as simply:

OPEN
Enter fullscreen mode Exit fullscreen mode

or:

CLOSED
Enter fullscreen mode Exit fullscreen mode

Real execution is more complicated.

A simplified lifecycle can look like:

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

With other possible paths:

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

The exact lifecycle depends on the interface and execution path, but the architecture should allow for intermediate states.

This is particularly relevant with Polymarket's evolving asynchronous execution model. The current V2 Rust client changelog notes that matched orders can carry tradeIDs rather than immediately returning transaction hashes, and the client can resolve those hashes afterward.

That is an important architectural lesson:

An accepted order isn't necessarily the end of the execution lifecycle.


Event processing should be idempotent

Real-time systems eventually see duplicates, reconnects, retries, and restarts.

So event processing needs to be safe to repeat.

A simplified version:

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

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

The production implementation can use a stronger persistence and transaction model, but the principle remains:

Processing the same event twice should not create two executions.

This becomes even more important if the application needs to replay events after a restart.


Reconciliation is the recovery mechanism

The main job of reconciliation is simple:

Compare what the application believes with what the remote system currently reports.

For example:

LOCAL

Order 123
filled = 0

REMOTE

Order 123
filled = 40
Enter fullscreen mode Exit fullscreen mode

The reconciler identifies:

difference = 40
Enter fullscreen mode Exit fullscreen mode

and creates an explicit discrepancy:

STATE_MISMATCH

order_id: 123
local_filled: 0
remote_filled: 40
action: repair
Enter fullscreen mode Exit fullscreen mode

I don't want this to be a hidden correction somewhere inside the strategy.

It should be its own subsystem.


Reconciliation after a reconnect

A reconnect shouldn't automatically mean:

connected
→ resume trading
Enter fullscreen mode Exit fullscreen mode

I'd rather use:

disconnect
    ↓
reconnect
    ↓
load persisted state
    ↓
read current remote state
    ↓
compare
    ↓
repair
    ↓
verify risk
    ↓
resume
Enter fullscreen mode Exit fullscreen mode

That extra step is where a lot of production safety comes from.

The same pattern applies after a process restart.

process starts
    ↓
load state
    ↓
synchronize
    ↓
reconcile
    ↓
validate
    ↓
trade
Enter fullscreen mode Exit fullscreen mode

Recovery becomes part of normal operation instead of an emergency patch.


The control plane should be able to stop trading

One of the most useful ideas in this project is that trading permission should be separate from the strategy.

For example:

Strategy says:
BUY
Enter fullscreen mode Exit fullscreen mode

doesn't necessarily mean:

System says:
ALLOW
Enter fullscreen mode Exit fullscreen mode

The flow becomes:

Strategy signal
      ↓
Risk check
      ↓
Health check
      ↓
State check
      ↓
Execution permission
Enter fullscreen mode Exit fullscreen mode

If the system detects a critical issue:

state mismatch
market data stale
risk limit exceeded
execution failures
Enter fullscreen mode Exit fullscreen mode

the answer can be:

TRADING_PAUSED
Enter fullscreen mode Exit fullscreen mode

This is much safer than forcing every strategy to implement its own emergency behavior.


A simple risk model

The first version doesn't need a complicated risk engine.

A few explicit limits already create a useful safety layer.

For example:

max_position_size: 500
max_market_exposure: 1000
max_total_exposure: 5000
max_daily_loss: 250
max_consecutive_failures: 5
max_market_data_age_ms: 5000
Enter fullscreen mode Exit fullscreen mode

Then the decision engine can be:

              RISK CHECK
                  │
       ┌──────────┼──────────┐
       │          │          │
 position     exposure     data age
   limit        limit        limit
       │          │          │
       └──────────┼──────────┘
                  │
          ┌───────┴────────┐
          │                │
        SAFE             UNSAFE
          │                │
       ALLOW             PAUSE
Enter fullscreen mode Exit fullscreen mode

The important part is that the rules exist independently from the trading strategy.

That makes them reusable.


Kill switch

Every automated trading system should have a simple way to stop placing new orders.

The control plane should expose something like:

PAUSE
RESUME
KILL
RECONCILE
Enter fullscreen mode Exit fullscreen mode

A kill switch might be triggered by:

  • operator action
  • daily loss threshold
  • abnormal exposure
  • persistent state mismatch
  • repeated execution failure
  • severe market-data staleness

Conceptually:

               KILL SWITCH
                    │
                    ▼
             STOP NEW ORDERS
                    │
                    ▼
             FREEZE STRATEGY
                    │
                    ▼
              ALERT OPERATOR
Enter fullscreen mode Exit fullscreen mode

The exact behavior around existing positions should be configurable.

The important thing is that a runaway strategy shouldn't require the developer to manually terminate the process.


Health is more than uptime

A process being alive doesn't mean a trading system is healthy.

I want health to include things like:

WebSocket connection
Market-data freshness
Order-stream freshness
API availability
Event processing latency
Last reconciliation
Position consistency
Risk state
Enter fullscreen mode Exit fullscreen mode

A useful state might look like:

SYSTEM: DEGRADED

Market data:          OK
Order stream:         OK
API connectivity:     OK
Position state:       MISMATCH
Last reconciliation:  38s ago
Risk state:           WARNING
Trading:              PAUSED
Enter fullscreen mode Exit fullscreen mode

That's much more actionable than:

process = running
Enter fullscreen mode Exit fullscreen mode

Observability should follow the trading lifecycle

The system should eventually be able to answer:

What happened to this order?

A good execution timeline might look like:

10:00:01.120  strategy signal
10:00:01.132  risk approved
10:00:01.145  order submitted
10:00:01.180  order accepted
10:00:01.240  partial fill 40
10:00:01.242  position updated
10:00:01.245  websocket disconnected
10:00:03.101  websocket reconnected
10:00:03.120  reconciliation started
10:00:03.141  state mismatch detected
10:00:03.150  state repaired
10:00:03.170  trading resumed
Enter fullscreen mode Exit fullscreen mode

This is incredibly useful for debugging.

It's also useful for research.

And eventually it can become useful for post-trade analysis.


Why replay matters

There's another reason to persist events.

Once the system stores important state transitions, you can replay them.

historical events
        ↓
event replay
        ↓
rebuild state
        ↓
inspect behavior
Enter fullscreen mode Exit fullscreen mode

That gives the same infrastructure several uses:

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

A trading system that can replay its own decisions is much easier to understand than one that only stores the final PnL.


A small architecture

The first version of the project is intentionally small.

polymarket-trading-control-plane/

crates/
├── ingest/
├── events/
├── state/
├── reconcile/
├── risk/
├── health/
├── alerts/
└── control/

examples/
├── monitor/
├── reconcile/
└── simulated_failure/

tests/
├── state/
├── reconciliation/
├── risk/
└── recovery/
Enter fullscreen mode Exit fullscreen mode

The core flow:

                 POLYMARKET
                     │
          ┌──────────┴──────────┐
          │                     │
      WebSocket               APIs
          │                     │
          ▼                     ▼
   Event Ingestion       Reconciliation
          │                     │
          └──────────┬──────────┘
                     ▼
                 State Store
                     │
           ┌─────────┼─────────┐
           ▼         ▼         ▼
         Risk      Health    Alerts
           │         │         │
           └─────────┼─────────┘
                     ▼
               Control Plane
                     │
                     ▼
                Strategy /
                Execution
Enter fullscreen mode Exit fullscreen mode

This isn't intended to become a full trading platform on day one.

The objective is to make a few critical problems explicit and testable.


Failure scenarios I want to test

A useful test suite should simulate more than successful trades.

For example:

WebSocket disconnect

disconnect
→ reconnect
→ reconcile
→ resume
Enter fullscreen mode Exit fullscreen mode

Partial fill

order size = 100
fill = 40
remaining = 60
Enter fullscreen mode Exit fullscreen mode

Duplicate event

event 1001
event 1001

result:
one state transition
Enter fullscreen mode Exit fullscreen mode

Missed event

1001
1002
1004

detect gap
→ reconcile
Enter fullscreen mode Exit fullscreen mode

Process restart

persisted state
→ process restart
→ reload
→ reconcile
→ continue
Enter fullscreen mode Exit fullscreen mode

Risk breach

exposure > limit
→ trading paused
Enter fullscreen mode Exit fullscreen mode

Stale market data

last update > threshold
→ affected strategy disabled
Enter fullscreen mode Exit fullscreen mode

These scenarios are more valuable to me than simply showing that an order can be submitted successfully.


Where this fits in a trading system

The control plane isn't the strategy.

It's the layer that protects the strategy.

That means different strategies can sit above the same infrastructure:

             STRATEGIES

     momentum   arbitrage   market making
          \        |        /
           \       |       /
            ▼      ▼      ▼
        ┌───────────────────┐
        │   CONTROL PLANE   │
        │                   │
        │ state             │
        │ reconciliation    │
        │ risk              │
        │ health            │
        │ recovery          │
        └─────────┬─────────┘
                  │
                  ▼
             EXECUTION
                  │
                  ▼
              POLYMARKET
Enter fullscreen mode Exit fullscreen mode

That's the direction I find more interesting.

The strategy can change.

The operational infrastructure can remain.


Why I'm building this

I've spent a lot of time looking at the strategy side of automated Polymarket trading.

But the more realistic the system becomes, the more obvious the engineering problems around the strategy become.

A profitable signal doesn't solve:

  • stale state
  • partial fills
  • reconnects
  • inconsistent positions
  • risk breaches
  • operational failures

Those need their own architecture.

And that's the part I want to explore here.


The bigger lesson

The most dangerous trading-system failure isn't always a crash.

A crashed process is obvious.

A process that keeps running while believing the wrong thing is much harder to detect.

That's why I think a serious Polymarket trading system needs more than:

signal
→ order
→ profit
Enter fullscreen mode Exit fullscreen mode

It needs:

market data
→ events
→ state
→ reconciliation
→ risk
→ execution
→ monitoring
→ recovery
Enter fullscreen mode Exit fullscreen mode

The strategy answers:

What should I do?

The control plane answers:

Can I safely do it right now?

And the reconciliation layer answers the question underneath both:

Do I actually know what state the system is in?

That's the problem this project is designed to solve.


What's next

The next step is to make the event stream replayable.

Once important order, fill, position, risk, and reconciliation events can be stored and replayed, the same infrastructure can support:

  • debugging
  • historical analysis
  • recovery
  • simulation
  • backtesting
  • strategy research

That's when the control plane becomes more than a monitoring tool.

It becomes part of the trading system itself.


This project focuses on trading-system infrastructure, reliability, and operational controls. Proprietary trading strategies and implementation details are intentionally omitted.

Top comments (0)