Sitemap
BenjaminCup

BenjaminCup

Blockchain and Full-Stack DEV | Polymarket and Robinhood Chain Trading Bot Builder https://www.youtube.com/@BenjaminCup Telegram : @BenjaminCup

How to Build a Polymarket Market Maker Bot: Order Management, Limit Orders, and Partial Fills

13 min read6 days ago

--

Day 5 of 7: Building a Polymarket Market Maker Bot From Zero to Complete

In Day 4, we learned how to calculate bid and ask prices for a Polymarket market maker bot. We also explored how inventory affects pricing and why a bot should adjust its quotes as its position changes.

But there is one important question:

What happens after your bot calculates a price?

Imagine your bot decides to buy YES tokens at $0.48 and sell them at $0.52. The strategy looks simple. However, the market changes constantly.

Another trader may buy the available tokens. Your order may be partially filled. The best bid may move from $0.47 to $0.49. Your connection may fail while you are submitting an order. Or your bot may crash while several orders are still active.

If your bot cannot handle these situations correctly, even a good pricing strategy can become unreliable.

In this tutorial, we’ll build the foundation of an order-management system for a Polymarket market maker bot using Python concepts.

We’ll cover:

  • How limit orders work.
  • How to track an order’s lifecycle.
  • When to cancel and replace an order.
  • How to handle partial fills.
  • How to avoid duplicate orders.
  • How to recover when the API response is uncertain.
  • How to protect your bot when something goes wrong.

The goal is to understand the engineering behind a market maker — not just how to calculate a price.

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 much of each order has been filled?
  • Does an order still match the strategy’s target price?
  • Should the bot cancel an outdated order?
  • Is the bot allowed to place another order?
  • What should happen if the connection fails?

Think of a market maker bot as having three main responsibilities.

1. Pricing engine: Calculates the prices at which the bot wants to buy and sell.

2. Order manager: Places, tracks, updates, and cancels orders.

3. Risk manager: Checks inventory, available funds, exposure limits, and whether trading should continue.

These parts work together.

For example, the pricing engine might calculate a YES bid of $0.48. The order manager checks whether an existing order already offers the same price. The risk manager checks whether the bot has enough available balance and whether buying more YES tokens would create excessive exposure.

Only after those checks should the bot submit an order.

2. Understand Limit Orders Before Writing Code

A limit order tells the market the maximum price you are willing to pay when buying or the minimum price you are willing to accept when selling.

Suppose the YES token has the following order book:

OrderPriceQuantityBest ask$0.52100 tokensSecond ask$0.53200 tokensBest bid$0.47150 tokensSecond bid$0.46250 tokens

The spread between the best ask and best bid is:

[
0.52–0.47=0.05
]

That is a five-cent spread.

Your market maker calculates a bid at $0.48 and an ask at $0.51.

If the orders are accepted and rest on the book, your bot is offering to buy at $0.48 and sell at $0.51.

However, placing an order does not mean that it has been filled.

A resting buy order at $0.48 may remain unfilled if no seller is willing to trade with it. A resting sell order at $0.51 may remain unfilled if no buyer takes the offer.

This distinction is essential. Your bot cannot assume that an order has traded simply because the API accepted it.

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. If it would execute immediately, the venue may reject it.

This can be useful for a market maker that wants to provide liquidity rather than accidentally take liquidity.

But post-only behavior does not guarantee profit or eliminate adverse selection. Your order can 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. Learn the Order Lifecycle

A reliable trading bot must know what state each order is in.

Here is a simplified lifecycle:

Strategy calculates quote
|
v
Create order
|
v
Submit to the CLOB
|
+-------+--------+
| |
v v
Rejected Accepted
|
v
Open order
|
+----------+----------+
| |
v v
Partial fill Full fill
| |
v v
Update remaining Mark completed
quantity
|
v
Continue tracking
|
+-----+------+
| |
v v
Canceled Filled

This diagram is simplified. In a real system, an order can be canceled after a partial fill, and a cancellation request can race with a fill that is already happening.

Your internal order record should store enough information to understand what happened.

For example:

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 is an illustrative local data model, not a complete Polymarket SDK integration.

It stores the order identifier, token, side, price, original size, matched size, and status.

The remaining quantity is calculated as:

Remaining quantity = Original quantity − Matched quantity

Suppose your bot places an order to buy 100 YES tokens.

If 35 tokens are filled, the remaining quantity is:

[
100–35=65
]

The bot must now manage 65 remaining tokens — not the original 100.

4. Why Partial Fills Matter

Partial fills are one of the most important problems in market making.

Imagine your bot places these two 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 has not filled at all.

Your inventory has changed.

You now own 70 additional YES tokens, and the bot still has a resting sell order for 100 tokens.

If the market moves quickly, the remaining sell order may no longer match your intended strategy. The bot may need to update that order, reduce its size, or cancel it.

A market maker must respond to actual fills, not just the orders it originally intended to place.

Three rules for handling partial fills

Rule 1: Update inventory from confirmed trade information.

Do not treat an accepted order as a completed trade.

Rule 2: Recalculate the remaining quantity.

Do not continue managing an order as if none of it had filled.

Rule 3: Recalculate risk after every fill.

A partial fill can change the bot’s inventory enough to make its previous quotes inappropriate.

For production systems, also account for delayed or duplicate trade events. Your inventory calculation should be idempotent: processing the same confirmed trade event twice must not increase inventory twice.

5. When Should the Bot Cancel and Replace an Order?

The market moves constantly. A quote that was attractive five seconds ago may no longer be appropriate.

Suppose your bot has a resting buy order at $0.48.

Then the best bid moves to $0.50, while the best ask moves to $0.51.

Your buy order is now below the best bid. It may still be valid, but it is less competitive.

The bot has several options:

  • Keep the order where it is.
  • Cancel it and submit a new order at a better price.
  • Reduce its size because inventory is already too large.
  • Stop quoting because the market has become too risky.

The right decision depends on your strategy.

Avoid canceling on every tiny price movement

A beginner might build a bot that immediately replaces its order whenever the best bid changes by one tick.

This sounds responsive, but it can create problems:

  • Excessive API requests.
  • More opportunities for order-management errors.
  • Frequent cancellation and replacement.
  • Increased exposure to price changes while orders are being updated.
  • Unnecessary activity when the price change is insignificant.

Instead, define explicit rules.

For example, a bot might reconsider an order when:

  1. Its target price changes by at least a configured number of ticks.
  2. The order becomes too old.
  3. Inventory crosses a risk threshold.
  4. The market data becomes stale.
  5. A market condition makes quoting unsafe.

These are example rules, not universal optimal settings.

A simple quote comparison

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

Suppose the current order price is $0.48, the new target is $0.49, and the configured minimum change is $0.01.

The function returns True.

If the target changes to $0.485 and the minimum change remains $0.01, it returns False.

In production, use the market’s actual tick size and valid price increments. Do not assume every market uses the same increment.

Also remember: deciding to reprice is different from successfully replacing an order. The order manager must handle the cancellation and submission process safely.

6. The Dangerous Part: Canceling and Replacing Orders

A common mistake is to treat order replacement as one atomic action.

Consider this sequence:

  1. The bot has a buy order open at $0.48.
  2. It decides the new target price is $0.49.
  3. It sends a cancellation request for the old order.
  4. It immediately submits a new order at $0.49.
  5. The old order fills just before the cancellation takes effect.

Now the bot may have bought tokens through the old order and still have a new buy order open.

Its exposure can be larger than expected.

This is a race condition: two events happen close together, and their order affects the final result.

A safer approach is to track the state of the old order and confirm its final status before deciding how much new exposure is safe to create.

For example:

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 appropriate

Even this sequence needs careful implementation. A cancellation response does not erase fills that happened before cancellation, and an API timeout does not prove that cancellation failed.

Get BenjaminCup’s stories in your inbox

Join Medium for free to get updates from this writer.

Your system should reconcile order and trade information before making assumptions.

7. What If the API Times Out?

This is another critical situation.

Your bot submits an order. The request times out. The bot does not know whether the exchange received it.

A dangerous implementation simply retries the same order immediately.

If the first request actually succeeded, the retry may create a second order.

This can lead to duplicate exposure.

Use an explicit uncertain state

Your internal order manager should distinguish between:

  • PENDING_SUBMISSION
  • OPEN
  • PARTIALLY_FILLED
  • FILLED
  • PENDING_CANCEL
  • CANCELED
  • REJECTED
  • UNKNOWN

These are suggested internal states, not a claim that Polymarket returns these exact status strings.

The important state is UNKNOWN.

It means your bot cannot yet determine what happened.

When an order submission times out:

  1. Mark the submission as uncertain.
  2. Do not blindly create another order.
  3. Query the appropriate order or account data where possible.
  4. Process order and trade updates.
  5. Reconcile the exchange’s state with your local records.
  6. Retry only when the previous attempt is understood and the strategy still permits it.

If the client or API supports a documented idempotency mechanism, use it correctly. Otherwise, create your own reconciliation process and do not assume that sending the same request twice is safe.

A timeout is a lack of information — not proof that an order was rejected.

8. WebSockets and REST: Use Both for Different Jobs

A market maker needs current market data and accurate information about its own orders.

Polymarket’s CLOB provides APIs for order-book data, authenticated order management, and WebSocket updates. The exact client methods and event formats should be checked against the current official documentation before implementation.

A useful architecture is:

Market data stream

Provides order-book changes that help the pricing engine calculate new quotes.

User/account stream

Provides updates related to your own orders and trades, where supported by the current subscription and client.

REST reconciliation

Lets the bot query the current state of orders or trades to recover from missed events and reconnects.

Why not use WebSockets alone?

Because connections can disconnect. Your process can restart. Events can be missed while your application is offline.

Why not use REST polling alone?

Because frequent polling may be less responsive and can create unnecessary API traffic.

A practical design uses WebSockets for timely updates and REST queries for reconciliation.

Example architecture

Market WebSocket
|
v
Market Data Cache
|
v
Pricing Engine
|
v
Risk Manager
|
v
Order Manager
|
v
Polymarket CLOB
|
+---------+---------+
| |
v v
User Order/Trade REST Reconciliation
Updates |
| |
+----------+-----------+
|
v
Local Order State

This design separates market information from the bot’s own execution state.

The market data cache answers, “What are other traders currently offering?”

The local order state answers, “What happened to the orders my bot submitted?”

Both are necessary.

9. Build a Basic Order Manager

Now let’s bring these ideas together.

The following example shows the decision-making layer. It does not submit real orders or call the Polymarket API. The actual exchange operations are deliberately left as separate functions so that the risk and state-management logic can be tested before live execution is added.

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 several possible order states.

Second, it calculates the unfilled quantity using the original order size and matched quantity.

Third, it checks whether an active order should be considered for replacement.

Notice the phrase considered for replacement.

The function does not cancel the order automatically. A complete order manager must also check market freshness, inventory, balance, open-order limits, and the result of previous cancellation or submission requests.

That separation is intentional. Pricing, risk checks, and exchange execution should not be mixed into one large function.

10. Add Risk Limits Before Sending Orders

A market maker should never place an order just because the pricing engine produced a number.

Before submitting, the bot should check at least the following:

CheckQuestionMarket dataIs the order book recent enough to trust?PriceIs the price valid for the market’s tick size?QuantityIs the order size valid?BalanceAre sufficient funds or tokens available?InventoryWould this order exceed the position limit if filled?Open ordersDoes a conflicting or duplicate order already exist?Order stateIs another submission or cancellation still unresolved?Market statusIs the market still active and accepting orders?ConnectivityIs the bot able to monitor and reconcile its orders?

One especially important rule is to calculate exposure based on what could happen if the bot’s open orders fill — not only on the tokens it already holds.

For example, suppose your bot currently holds 80 YES tokens and has two open buy orders for another 50 YES tokens each.

Your current inventory is 80 tokens, but your potential inventory after both orders fill is 180 tokens.

If your inventory limit is 150 tokens, the bot should not ignore those open orders.

The exact exposure calculation depends on the strategy, token relationships, available collateral, and other outstanding orders. Start with conservative limits and test the calculation thoroughly.

11. Protect Orders When the Bot Crashes

Imagine your bot is running on a VPS.

It has placed several buy and sell orders. Suddenly, the process crashes because of a software error or a server restart.

The orders may remain active even though the program that manages them is no longer running.

This is dangerous because the market can continue moving while the bot is unable to react.

A production system should have a recovery plan.

Before shutdown or restart:

  • Decide whether to cancel open orders or preserve them under a controlled recovery process.
  • Persist enough information to reconcile previously submitted orders.
  • Never assume that restarting the program automatically removes outstanding orders.

After restart:

  1. Connect to the exchange.
  2. Retrieve the relevant open orders and trade information.
  3. Rebuild the local order state.
  4. Reconcile fills that occurred while the bot was offline.
  5. Recalculate inventory and available funds.
  6. Resume quoting only after the state is sufficiently understood.

Where supported, investigate the exchange’s heartbeat or dead-man-switch functionality. Such a mechanism can help cancel resting orders if the process stops sending required heartbeats. Verify its exact semantics and timeout behavior before relying on it.

For additional protection, use a maximum order lifetime, a stale-data shutdown rule, and an emergency cancel procedure. None of these should be treated as a substitute for reconciliation.

12. Test These Failure Scenarios Before Live Trading

A bot that works in a normal demonstration is not necessarily reliable.

Before using real funds, simulate the situations below.

  • An order is accepted and remains open.
  • An order is rejected.
  • An order is partially filled.
  • An order is fully filled before the next market update.
  • A cancellation request races with a fill.
  • The API times out after an order submission.
  • The same trade event is delivered more than once.
  • The WebSocket disconnects and reconnects.
  • The process restarts with outstanding orders.
  • The market becomes inactive or stops accepting orders.
  • Available balance changes after another order fills.
  • A quote becomes invalid because the market’s price increment changes.
  • The bot loses access to fresh market data.
  • An order update arrives after the bot has already requested cancellation.

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 order as uncertain, checks the exchange state, avoids blindly resubmitting, and does not exceed its exposure limit while the outcome is unresolved.

This is how you move from a trading script toward a reliable trading system.

13. The Complete Order-Management Loop

Let’s summarize the logic.

1. Read fresh market data
2. Calculate target bid and ask
3. Check market and account risk
4. Compare target quotes with tracked orders
5. Decide whether to keep, cancel, or replace orders
6. Submit permitted orders
7. Track order acknowledgments and trade updates
8. Reconcile uncertain states
9. Update inventory and available funds
10. Repeat

The loop should also stop placing new orders if the market data becomes stale, account state cannot be reconciled, or a configured risk limit is breached.

A market maker should not keep trading merely because its main process is still running.

It should trade only when it has enough reliable information to make a controlled decision.

Conclusion

Building a Polymarket market maker bot involves much more than calculating a bid and an ask.

A reliable implementation needs to manage the complete order lifecycle, including open orders, partial fills, cancellation races, API timeouts, reconnects, and recovery after a crash.

The most important lessons from Day 5 are:

  1. An accepted order is not necessarily a filled order.
  2. Partial fills must update inventory and remaining quantity.
  3. Cancellation and replacement require careful state management.
  4. An API timeout does not prove that an order failed.
  5. WebSocket updates should be combined with reconciliation.
  6. Risk checks must account for outstanding orders as well as current inventory.

In Day 6, we’ll move from order management toward inventory risk, market conditions, and adverse selection — including why market makers sometimes get filled at exactly the wrong time, and how a bot can respond.

Continue the project

Follow along as we build a Polymarket trading bot step by step, from market data and pricing to order execution and risk management.

--

--

BenjaminCup
BenjaminCup

Blockchain and Full-Stack DEV | Polymarket and Robinhood Chain Trading Bot Builder https://www.youtube.com/@BenjaminCup Telegram : @BenjaminCup