Finding a 3¢ arbitrage opportunity on Polymarket looks simple.
Your bot sees:
YES + NO < $1.00
and assumes the difference is profit.
But the displayed gap is not your real edge.
Two costs can destroy the trade:
- The spread
- Taker fees
And when you're trading both sides of an arbitrage, those costs can add up quickly.
This article shows how to calculate the actual cost before your bot sends an order.
1. The spread is a hidden trading cost
Every order book has:
- Best bid — highest price someone will pay
- Best ask — lowest price someone will sell for
The difference is the spread.
For example:
Best bid: 0.52
Best ask: 0.55
Spread: 0.03
You don't see "0.03" as a fee on your trading statement.
But if you buy at 0.55 instead of 0.52, you're paying that spread.
A simple calculation:
def spread(book):
best_bid = book.bids[0].price
best_ask = book.asks[0].price
return round(best_ask - best_bid, 4)
The important point for an arbitrage bot is:
The spread is already inside the price you pay.
2. Arbitrage crosses the spread twice
This becomes more important when you're trading both sides of a market.
Suppose your bot finds:
YES
Ask: 0.54
NO
Ask: 0.43
The pair costs:
0.54 + 0.43 = 0.97
So the gross arbitrage edge appears to be:
1.00 - 0.97 = 0.03
A 3¢ edge.
But don't calculate this using mid-prices.
Your bot actually has to buy at the ask.
def true_entry_cost(yes_book, no_book):
yes_ask = yes_book.asks[0].price
no_ask = no_book.asks[0].price
return round(yes_ask + no_ask, 4)
The rule is simple:
Measure your arbitrage using executable prices, not theoretical mid-prices.
The mid is a quote.
The ask is the bill.
3. Taker fees are not a flat percentage
The second major cost is the taker fee.
The fee depends on the price and market category rather than simply being one fixed percentage.
The basic calculation used in this model is:
def taker_fee(shares, price, fee_rate):
return round(
shares * fee_rate * price * (1 - price),
6
)
The important part is:
price × (1 - price)
This reaches its maximum around:
price = 0.50
and becomes smaller as the price approaches 0 or 1.
That means a trade around 50¢ can have a significantly different fee cost from a trade near the edges of the probability range.
4. Market category matters
The fee rate is category-dependent.
For the rates used in this example:
FEE_RATES = {
"crypto": 0.07,
"economics": 0.05,
"culture": 0.05,
"weather": 0.05,
"politics": 0.04,
"finance": 0.04,
"tech": 0.04,
"sports": 0.03,
"geopolitics": 0.00,
}
At a price of 0.50, the maximum fee per 100 shares in this model is:
def max_fee_per_100(category):
return round(
100 * FEE_RATES[category] * 0.5 * 0.5,
4
)
For example:
Crypto: $1.75 / 100 shares
Sports: $0.75 / 100 shares
Geopolitics: $0.00 / 100 shares
This matters because crypto Up/Down markets can have a much larger fee impact than the raw arbitrage gap suggests.
5. Maker vs. taker changes the calculation
This is one of the most important parts of the strategy.
If your order rests on the book and another trader fills it, you're a maker.
If your order crosses the spread and immediately takes liquidity, you're a taker.
The distinction matters because the fee treatment is different.
Maker
Order rests
↓
Someone fills you
↓
Maker
Taker
Order crosses the book
↓
You immediately consume liquidity
↓
Taker
And there is an important trap:
A limit order is not automatically a maker order.
For example:
def is_taker(order_price, side, book):
if side == "BUY":
return order_price >= book.asks[0].price
return order_price <= book.bids[0].price
If your buy limit price reaches the current ask, you're crossing the book.
That order can execute as a taker.
6. The real arbitrage calculation
Now we can combine everything.
The gross edge is:
1 - YES ask - NO ask
Then we subtract the taker fees when the orders are taking liquidity.
def real_edge(yes_book, no_book, shares, category, taking=True):
yes_ask = yes_book.asks[0].price
no_ask = no_book.asks[0].price
# Spread is already reflected in executable ask prices
pair_cost = yes_ask + no_ask
gross_edge = 1.0 - pair_cost
fees = 0.0
if taking:
rate = FEE_RATES[category]
fees += (
taker_fee(shares, yes_ask, rate)
/ shares
)
fees += (
taker_fee(shares, no_ask, rate)
/ shares
)
net = gross_edge - fees
return {
"gross_edge": round(gross_edge, 4),
"fees_per_share": round(fees, 4),
"net_edge": round(net, 4),
}
The important flow is:
Order book
↓
Executable YES ask + NO ask
↓
Gross edge
↓
Taker fee on YES
↓
Taker fee on NO
↓
NET EDGE
Your bot should make its trading decision based on the net edge, not the gross gap.
7. Worked example: the 3¢ arbitrage that isn't profitable
Let's use:
YES ask = 0.54
NO ask = 0.43
The pair costs:
0.54 + 0.43 = 0.97
Gross edge:
1.00 - 0.97 = 0.03
So it looks like a 3¢ arbitrage.
Now assume:
100 shares
Crypto market
Both orders execute as takers
Fee rate = 0.07
YES fee
100 × 0.07 × 0.54 × 0.46
≈ $1.74
Per share:
≈ $0.017
NO fee
100 × 0.07 × 0.43 × 0.57
≈ $1.72
Per share:
≈ $0.017
Total fee per share:
≈ $0.034
But the gross arbitrage edge was only:
$0.030
So:
Gross edge: +$0.030
Fees: -$0.034
--------------------
Net edge: -$0.004
The supposed 3¢ arbitrage is actually negative after fees.
That's the kind of trade an automated system should reject before placing the orders.
8. Build a minimum-edge filter
You can turn this calculation into a simple safety filter.
def survives(
yes_book,
no_book,
shares,
category,
min_net=0.005,
taking=True
):
result = real_edge(
yes_book,
no_book,
shares,
category,
taking
)
return result["net_edge"] >= min_net, result
Now your bot can ask:
Is the net edge large enough?
instead of:
Does YES + NO look smaller than $1?
That is a much more useful condition for an arbitrage engine.
9. The real decision: speed vs. cost
There is a trade-off here.
Aggressive execution
You cross the spread and execute immediately.
Advantages:
- Faster execution
- Higher chance of capturing the current gap
- Both legs can potentially execute quickly
Disadvantages:
- You pay taker fees
- You consume liquidity
- Slippage can reduce the edge
Passive execution
You place resting orders.
Advantages:
- Different fee treatment
- You avoid immediately crossing the spread
- You may receive a rebate
Disadvantages:
- Your order may never fill
- The arbitrage gap can disappear
- One leg may fill while the other doesn't
So the strategy isn't simply:
Find arbitrage → buy both sides
It's closer to:
Find candidate
↓
Check executable prices
↓
Calculate gross edge
↓
Calculate fees
↓
Check liquidity
↓
Check execution type
↓
Calculate net edge
↓
Trade only if the expected edge survives
10. What an arbitrage bot should actually monitor
A production bot shouldn't rely on one number.
At minimum, the execution engine should track:
- Best bid
- Best ask
- Spread
- YES executable price
- NO executable price
- Available liquidity
- Market category
- Expected fee
- Maker/taker status
- Gross edge
- Net edge
- Minimum required edge
For larger orders, top-of-book prices aren't enough.
If your order consumes several levels of the order book, your actual average execution price can be worse than the first ask.
That means a more advanced system should calculate depth-aware execution cost, not just the first level.
Conclusion
The biggest mistake in automated arbitrage is confusing a visible price gap with actual profit.
A market can show:
YES + NO = $0.97
and appear to offer:
$0.03
of arbitrage.
But your real result depends on:
Executable prices
+ spread
+ liquidity
+ execution type
+ fees
= actual trading cost
The practical rules are:
- Use executable ask prices, not mid-prices.
- Calculate both legs of the arbitrage.
- Include the applicable taker fee.
- Determine whether your orders actually execute as makers or takers.
- Check liquidity and potential slippage.
- Trade based on net edge, not gross edge.
The gross gap is what your detector sees.
The net edge is what your account actually gets.
If you're building automated Polymarket infrastructure, this calculation belongs inside the execution engine—not after the trade.
Project
I'm building Polymarket trading infrastructure and bots in Python.
GitHub:
Polymarket Trading Bot | Polymarket Arbitrage Bot | Polymarket TWAP Trading Bot
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…
Telegram: https://telegram.me/BenjaminCup
If you're working on Polymarket bots, prediction-market infrastructure, market-data systems, or algorithmic trading tools, feel free to connect.


Top comments (1)
The worked example (a 3¢ gap that's -0.4¢ after fees in a crypto market) is the most useful thing in the post, and the price × (1 − price) shape of the fee is easy to miss.
Two more costs the filter should see before it says yes:
Depth.
real_edgeprices all 100 shares atasks[0].price, but the best ask on each side might only have 20 shares behind it. The remaining 80 fill at the next levels, and the gap is usually gone after the first level. Walking the book on both sides for the actual size (volume-weighted ask forshares) and recomputing the edge on that gives the number the bot will really pay.Legging. The two orders don't fill at the same instant. If YES fills and NO moves before your second order lands, you're holding a directional position instead of an arbitrage. With passive orders it's worse, because the leg that fills first is usually the one the market was happy to sell you. Logging how often only one leg filled, and what that leg was worth at resolution, tells you whether the "risk-free" trades are carrying hidden directional risk. In practice that number decides whether the strategy works more than the fee model does.