Day 5 of 7: Building a Polymarket Market Maker Bot From Zero to Complete
In Day 4, we explored how a Polymarket market maker bot calculates bid and ask prices, manages spreads, and adjusts quotes based on inventory.
But calculating the right price is only half the job.
What happens after your bot places an order?
Imagine your Python trading bot wants to buy YES tokens at $0.48 and sell them at $0.52.
Before either order fills:
- The market price changes.
- Another trader takes the available liquidity.
- Your order gets partially filled.
- Your API request times out.
- Your bot crashes while orders remain active.
If your bot cannot handle these situations, even a good market-making strategy can lose control of its positions.
In this tutorial, we'll explore how to build the order-management foundation of a Polymarket trading bot.
We'll cover:
- How limit orders work
- How to track an order's lifecycle
- How to handle partial fills
- When to cancel and replace orders
- How to avoid duplicate orders after API timeouts
- How to combine WebSockets with REST reconciliation
- How to protect open orders when a bot crashes
- How to test order-management failures before live trading
The examples use Python and Decimal for financial values. They demonstrate the decision-making logic, not a complete integration with the live Polymarket API.
1. What Is Order Management?
Order management is the part of a trading bot responsible for controlling the orders it sends to the exchange.
It answers questions such as:
- Which orders are currently active?
- How many tokens have been filled?
- How many tokens remain?
- Does an existing order still match the target price?
- Should the bot cancel an outdated order?
- What happens if an API request times out?
A market maker typically has three main components:
| Component | Responsibility |
|---|---|
| Pricing engine | Calculates target bid and ask prices |
| Order manager | Places, tracks, updates, and cancels orders |
| Risk manager | Checks inventory, available funds, and exposure limits |
For example, the pricing engine calculates a YES bid of $0.48.
Before submitting the order, the order manager checks whether a similar order already exists. The risk manager checks whether buying more YES tokens would exceed the configured position limit.
Only then should the bot consider submitting the order.
This separation makes the code easier to test, debug, and maintain.
2. Understand Limit Orders
A limit order specifies the maximum price you're willing to pay when buying or the minimum price you're willing to accept when selling.
Imagine the YES token order book looks like this:
| Order | Price | Quantity |
|---|---|---|
| Best ask | $0.52 | 100 |
| Second ask | $0.53 | 200 |
| Best bid | $0.47 | 150 |
| Second bid | $0.46 | 250 |
The spread is:
$0.52 - $0.47 = $0.05
Your market maker calculates a bid at $0.48 and an ask at $0.51.
If accepted and resting on the book, these orders offer to buy at $0.48 and sell at $0.51.
However, an accepted order is not necessarily a filled order.
A buy order at $0.48 might remain open because no seller is willing to trade at that price.
Similarly, a sell order at $0.51 might remain open because no buyer accepts the offer.
Your bot must track actual fills instead of assuming that an accepted order has executed.
What about post-only orders?
Where supported, a post-only order is intended to rest on the order book rather than immediately match against an existing order.
This can help a market maker provide liquidity instead of unintentionally taking it.
However, post-only orders don't guarantee profit. Your order might still be filled just before the market moves against you.
Always verify the supported order types and rules for the current Polymarket client and market.
3. Track the Order Lifecycle
A reliable bot needs to know the state of every order it manages.
A simplified order lifecycle looks like this:
Calculate target quote
|
v
Create order
|
v
Submit to exchange
|
+------> Rejected
|
v
Accepted
|
v
Open order
|
+------> Partially filled
| |
| v
| Update remaining quantity
| |
+------> Fully filled
|
+------> Canceled
Real order lifecycles are more complicated. An order can be partially filled before cancellation, and a fill can occur while a cancellation request is being processed.
Let's define a simple data model.
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class TrackedOrder:
order_id: str
token_id: str
side: str
price: Decimal
original_size: Decimal
matched_size: Decimal
status: str
@property
def remaining_size(self) -> Decimal:
return max(
Decimal("0"),
self.original_size - self.matched_size,
)
This model tracks the order ID, token, side, price, original quantity, matched quantity, and current status.
The remaining quantity is:
Remaining quantity = Original quantity - Matched quantity
Suppose your bot submits an order to buy 100 YES tokens.
If 35 tokens have been filled:
100 - 35 = 65 tokens remaining
The order manager must now manage the remaining 65 tokens, not the original 100.
This is a small example, but the principle is essential for accurate inventory management.
4. Why Partial Fills Matter
Partial fills are one of the most important challenges in market making.
Imagine your bot submits these orders:
- Buy 100 YES tokens at
$0.48 - Sell 100 YES tokens at
$0.52
The buy order fills for 70 tokens, but the sell order remains unfilled.
Your position has changed. You now own 70 additional YES tokens while the sell order may still have 100 tokens available to execute.
If market conditions change, the remaining sell order might no longer match your strategy.
The bot may need to reduce its size, reprice it, or cancel it.
Three rules help prevent common mistakes:
Rule 1: Update inventory using confirmed trade information.
An order acknowledgment doesn't mean the trade has completed.
Rule 2: Recalculate the remaining quantity.
Always account for the quantity already matched.
Rule 3: Recalculate risk after every fill.
A partial fill can make previously acceptable quotes too risky.
There is another important production consideration: duplicate trade events.
If the same trade update arrives twice, your bot must not add the filled quantity to inventory twice. Use stable trade identifiers and idempotent processing to prevent duplicate accounting.
5. When Should You Cancel and Replace an Order?
Market prices change constantly.
Suppose your bot has a resting buy order at $0.48.
A few seconds later, the best bid moves to $0.50, and the best ask moves to $0.51.
Your order is still valid, but it is now less competitive.
Your bot has several choices:
- Keep the existing order.
- Cancel it and submit a new order.
- Reduce the order size because inventory is too large.
- Stop quoting because the market has become too risky.
The correct decision depends on your strategy and risk limits.
Avoid excessive repricing
A beginner might replace an order every time the best bid changes by one tick.
That sounds responsive, but it can cause:
- Excessive API requests
- Frequent cancellations
- More opportunities for race conditions
- Unnecessary order-management complexity
- Increased exposure while orders are being updated
Instead, define explicit repricing rules.
For example, reconsider an order when:
- The target price changes by a configured amount.
- The order exceeds its maximum age.
- Inventory crosses a risk threshold.
- Market data becomes stale.
- Market conditions make quoting unsafe.
These are example rules, not universally optimal settings.
Here's a simple price-comparison function:
from decimal import Decimal
def should_reprice(
current_price: Decimal,
target_price: Decimal,
min_change: Decimal,
) -> bool:
return abs(current_price - target_price) >= min_change
Example:
current_price = Decimal("0.48")
target_price = Decimal("0.49")
min_change = Decimal("0.01")
print(should_reprice(
current_price,
target_price,
min_change,
))
# True
The function returns True because the price difference meets the configured threshold.
In production, use the market's actual tick size and valid price increments. Don't assume every market uses the same increment.
Also remember that deciding to reprice is different from successfully replacing an order.
6. The Dangerous Part: Canceling and Replacing Orders
Consider this sequence:
- Your bot has a buy order open at
$0.48. - The target price changes to
$0.49. - Your bot requests cancellation of the old order.
- It immediately submits a new order at
$0.49. - The old order fills before the cancellation takes effect.
Now both orders may have executed or remain relevant to your exposure.
Your bot could buy more tokens than intended.
This is called a race condition: two operations happen close together, and their timing affects the result.
A safer workflow is:
Target price changes
|
v
Mark old order for replacement
|
v
Request cancellation
|
v
Reconcile order status and fills
|
v
Recalculate inventory and exposure
|
v
Check risk limits
|
v
Submit replacement if still safe
The important part is reconciliation.
A cancellation response doesn't erase fills that happened before cancellation. Likewise, an API timeout doesn't prove that the cancellation failed.
Before creating additional exposure, your bot should determine the old order's actual state as accurately as possible.
7. What Happens When the API Times Out?
This is one of the most dangerous order-management problems.
Your bot submits an order, but the request times out.
Did the exchange receive the order?
There are at least two possibilities:
- The exchange never received it.
- The exchange accepted it, but your bot never received the response.
If your bot blindly retries the request, the second situation can create a duplicate order.
Use an explicit unknown state
Your internal order manager can track states such as:
from enum import Enum
class OrderStatus(str, Enum):
PENDING_SUBMISSION = "pending_submission"
OPEN = "open"
PARTIALLY_FILLED = "partially_filled"
PENDING_CANCEL = "pending_cancel"
FILLED = "filled"
CANCELED = "canceled"
REJECTED = "rejected"
UNKNOWN = "unknown"
These are example internal states, not a claim that Polymarket returns these exact status strings.
The UNKNOWN state is especially important.
It means your bot doesn't yet know what happened.
When a submission times out:
- Mark the submission as uncertain.
- Avoid blindly sending another order.
- Query the exchange's order or account information where possible.
- Process available order and trade updates.
- Reconcile the exchange state with local records.
- Retry only after the previous attempt is understood and the strategy still permits another order.
If the API supports a documented idempotency mechanism, use it correctly. Otherwise, build a reconciliation process that accounts for uncertain outcomes.
A timeout is a lack of information, not proof that an order was rejected.
8. WebSockets and REST: Why You Need Both
A market maker needs two kinds of information:
- What is happening in the market?
- What is happening to the bot's own orders?
A practical architecture combines WebSocket updates with REST reconciliation.
Market data stream
Provides order-book updates used by the pricing engine to calculate new quotes.
User and account updates
Where supported, provides updates related to your own orders and trades.
REST reconciliation
Allows the bot to query the current state of orders or trades and recover from missed events or reconnects.
Why not use WebSockets alone?
Connections can disconnect, applications can restart, and events can be missed while the bot is offline.
Why not use REST polling alone?
Frequent polling can create unnecessary API traffic and may be less responsive to market changes.
A useful architecture looks like this:
Market WebSocket
|
v
Market Data Cache
|
v
Pricing Engine
|
v
Risk Manager
|
v
Order Manager
|
v
Polymarket CLOB
|
+------> Order/Trade Updates
|
+------> REST Reconciliation
|
v
Local Order State
WebSockets provide timely updates. REST reconciliation helps the bot recover when its local view becomes incomplete.
Check the current official Polymarket documentation for the supported endpoints, authentication, subscription formats, and client methods before implementing this architecture.
9. Build a Basic Order Manager in Python
Let's bring the main concepts together.
The following code implements a small decision-making layer. It does not submit real orders or call the Polymarket API.
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
class OrderStatus(str, Enum):
PENDING_SUBMISSION = "pending_submission"
OPEN = "open"
PARTIALLY_FILLED = "partially_filled"
PENDING_CANCEL = "pending_cancel"
FILLED = "filled"
CANCELED = "canceled"
REJECTED = "rejected"
UNKNOWN = "unknown"
@dataclass
class Order:
order_id: str
side: str
price: Decimal
original_size: Decimal
matched_size: Decimal
status: OrderStatus
@property
def remaining_size(self) -> Decimal:
return max(
Decimal("0"),
self.original_size - self.matched_size,
)
def needs_replacement(
order: Order,
target_price: Decimal,
min_price_change: Decimal,
) -> bool:
active_states = {
OrderStatus.OPEN,
OrderStatus.PARTIALLY_FILLED,
}
if order.status not in active_states:
return False
if order.remaining_size <= 0:
return False
return (
abs(order.price - target_price)
>= min_price_change
)
Let's understand what this code does.
First, it defines the possible states the bot tracks.
Second, it calculates how much of an order remains unfilled.
Third, it checks whether an active order should be considered for replacement.
For example, an open buy order at $0.48 may need reconsideration when the target price changes to $0.50.
However, this function does not cancel or replace anything automatically.
A complete implementation must also check:
- Market data freshness
- Available balance
- Inventory and exposure limits
- Duplicate or conflicting orders
- Open-order limits
- Outstanding cancellation requests
- Exchange connectivity and market status
Keeping these responsibilities separate makes the system easier to reason about and test.
10. Add Risk Checks Before Sending Orders
Never place an order simply because the pricing engine produced a price.
Before submission, check the following:
| Check | Question |
|---|---|
| Market data | Is the order book fresh enough? |
| Price | Is the price valid for this market? |
| Quantity | Is the order size permitted? |
| Balance | Are sufficient funds or tokens available? |
| Inventory | Could this order exceed the position limit? |
| Open orders | Does a duplicate or conflicting order exist? |
| Order state | Is another submission or cancellation unresolved? |
| Market status | Is the market still accepting orders? |
| Connectivity | Can the bot monitor and reconcile its orders? |
One especially important rule is to consider potential exposure from open orders, not just current inventory.
Suppose your bot currently holds 80 YES tokens and has two open buy orders for 50 additional YES tokens each.
Its current inventory is 80 tokens.
If both orders fill, inventory could reach:
80 + 50 + 50 = 180 tokens
If the configured inventory limit is 150 tokens, the bot must account for those possible fills before submitting more orders.
The exact exposure calculation depends on your strategy, available collateral, related positions, and other outstanding orders.
Start with conservative limits and test the calculations carefully.
11. Protect Your Orders When the Bot Crashes
Imagine your bot runs on a VPS and has several active orders.
Then the process crashes because of a software bug or server restart.
The orders may remain active even though the process that manages them is no longer running.
The market can continue moving while your bot is unable to react.
A production system needs a recovery plan.
Before shutdown or restart
- Decide whether open orders should be canceled or preserved for controlled recovery.
- Persist enough information to identify previously submitted orders.
- Never assume that stopping the process automatically cancels exchange orders.
After restart
- Reconnect to the exchange.
- Retrieve relevant open orders and trade information.
- Rebuild local order state.
- Reconcile fills that occurred while offline.
- Recalculate inventory and available funds.
- Resume quoting only when the account state is sufficiently understood.
Where supported, investigate heartbeat or dead-man-switch functionality that can cancel resting orders if required heartbeats stop. Verify its exact behavior and timeout semantics before relying on it.
Additional protections can include maximum order lifetimes, stale-data shutdown rules, and emergency cancellation procedures.
These protections complement reconciliation; they don't replace it.
12. Test Failure Scenarios Before Live Trading
A bot that works during a normal demonstration isn't necessarily reliable.
Before using real funds, test how it responds to failures.
| Scenario | Expected behavior |
|---|---|
| Order accepted | Track it as active |
| Order rejected | Record the rejection and avoid treating it as filled |
| Partial fill | Update inventory and remaining quantity |
| Full fill | Mark the order complete |
| Cancellation races with a fill | Reconcile the final status and matched quantity |
| Submission times out | Mark the outcome uncertain and avoid blind retries |
| Duplicate trade event arrives | Do not count the same fill twice |
| WebSocket disconnects | Reconnect and reconcile missed updates |
| Bot restarts with open orders | Rebuild local state before resuming |
| Market becomes inactive | Stop submitting inappropriate orders |
| Balance changes | Recalculate available funds and exposure |
| Market data becomes stale | Stop new quoting until data is reliable |
For every scenario, define the expected behavior before writing the test.
For example:
Scenario: The order-submission request times out.
Expected behavior: The bot marks the outcome as uncertain, checks the exchange state, avoids blindly resubmitting, and prevents additional exposure from exceeding its risk limits while the outcome is unresolved.
This is how you move from a simple trading script toward a reliable trading system.
13. The Complete Order-Management Loop
A market maker's order-management loop can be summarized in ten steps:
- Read fresh market data.
- Calculate target bid and ask prices.
- Check market and account risk.
- Compare target prices with tracked orders.
- Decide whether to keep, cancel, or replace orders.
- Submit only permitted orders.
- Track acknowledgments and trade updates.
- Reconcile uncertain order states.
- Update inventory and available funds.
- Repeat.
The bot should stop placing new orders when market data becomes stale, account state cannot be reconciled, or a configured risk limit is breached.
A reliable market maker doesn't keep trading just because its main process is still running.
It trades only when it has enough reliable information to make a controlled decision.
Conclusion
Building a Polymarket market maker bot requires much more than calculating bid and ask prices.
The bot must manage the complete order lifecycle, including partial fills, cancellations, API timeouts, reconnects, and recovery after a crash.
Here are the most important lessons from Day 5:
- An accepted order is not necessarily a filled order.
- Partial fills must update inventory and remaining quantity.
- Cancellation and replacement require careful state management.
- An API timeout does not prove that an order failed.
- WebSocket updates should be combined with reconciliation.
- Risk checks must account for open orders as well as current inventory.
- Restarting a bot must not mean blindly trusting its previous local state.
In Day 6, we'll move from order management to inventory risk, market conditions, and adverse selection. We'll explore why market makers sometimes get filled just before the market moves against them, and how a bot can respond.
Continue the Project
GitHub repository: [Polymarket Trading Bot in Python]
Benj4minCup
/
Polymarket-Trading-Bot-python
Polymarket Trading Bot Twap Bot polymarket Arbitrage Bot
Polymarket Trading bot system
An open-source and Strong Strategy collection of Polymarket trading bot and Polymarket arbitrage bot and Polymarket TWAP trading bot in Python for high-performance automated trading on polymarket crypto 5min and 15min markets.
This repository is primarily intended for educational and research purposes. It includes strategy concepts, implementation approaches, and selected performance screenshots to help developers understand how different automated trading strategies can be designed and tested.
The repository does not provide a complete production-ready trading bot source code. Instead, it provides strategy descriptions and research materials that you can use as a foundation for developing your own system.
If you are interested in building a Polymarket Trading Bot, you can follow my tutorials and use the concepts in this repository to develop your own implementation.
For users who prefer a ready-to-deploy solution or require custom strategy development, commercial bot development and customization are also available.
Features
…YouTube tutorial: [Watch the Polymarket trading bot tutorial]
Telegram: Join the discussion
Follow along as we build a Polymarket trading bot step by step, from market data and pricing to order execution and risk management.

Top comments (0)