We built one, and the hard part was not the trading
We built one. It evaluates 4.76 million pairs a cycle and finds 5. Half its first matches were nonsense. Paper log down $28.30.
Our scanner evaluates 4,761,600 market pairs on each cycle, in about 48 seconds, and finds five candidates. On the first run that produced matches, five of the ten it returned were nonsense: it paired "Fed abolished before 2027?" on Polymarket with "Will Trump recognize Somaliland?" on Kalshi and reported a 4.9 cent edge.
The arithmetic was right. The API calls were right. The pairing was wrong, and a wrong pairing is not a hedge, it is two unrelated bets with a profit number printed underneath. If you buy both sides of that trade you can lose the lot, because both tickets can expire worthless.
That is the actual difficulty in this category. Placing orders is the easy part. Knowing that two markets are the same question is the whole problem.
The five things that break, in the order they bit us
1. Kalshi will not talk to a browser
Send an Origin header to Kalshi's API and it returns HTTP 403 Forbidden. Send the identical request without one and it returns 200 in 178ms. Browsers always attach Origin and cannot be told not to.
Your bot runs server side. There is no client-only version of this. We publish a JSON file from a machine and the website fetches that, because the website cannot fetch Kalshi.
2. Intra-venue arbitrage on Polymarket is arithmetically dead
A market we sampled on 20 August returned outcomePrices of ["0.1", "0.9"]. They sum to exactly 1.00. They always do. There is no gap between the Yes and No legs of the same Polymarket market to capture, because the venue prices them as complements by construction.
Anyone selling you a "Polymarket arbitrage bot" that trades both sides of one market is selling you a subtraction that always yields zero, minus fees. The opportunity, if there is one, is across venues, where two order books price the same real-world question independently.
3. The displayed price is not the price
The same market showed outcomePrices of 0.1 and a bestAsk of 0.2, with a 20 cent spread and $157 of liquidity. A bot computing edges from the displayed probability would have seen an opportunity that costs double to enter.
Read bestAsk for what you pay and bestBid for what you receive. Never the midpoint.
4. Minimum order size and tick size decide what is even expressible
On the CLOB, minimum_order_size was 15 on 962 of 1,000 markets and minimum_tick_size was 0.01 on 968.
A one cent tick means a half-cent edge cannot be bid. It is not a small edge, it is not an order. And a $15 floor means the $2 test position you wanted to run to validate the strategy is not available to you.
5. Fees round against you, and the rounding is not per contract
Kalshi's taker fee is roundup(0.07 x contracts x P x (1 - P)) and the contract count sits inside the ceiling. So the rounding applies to the whole trade, and it hurts small orders disproportionately:
| Contracts at 50c | Formula value | You pay | Per contract |
|---|---|---|---|
| 1 | 1.75c | 2c | 2.00c |
| 10 | 17.5c | 18c | 1.80c |
| 1,000 | $17.50 | $17.50 | 1.75c |
A backtest that applies the un-rounded formula will overstate every small trade. Polymarket, by contrast, charges makers 0.00, which we confirmed first-hand: maker_base_fee was 0 on all 1,000 markets in our sample.
The matcher is the product, and ours rejects 99.99% of pairs
600 Polymarket markets against 7,936 Kalshi markets is 4,761,600 pairs. Here is where they died on 20 August 2026:
| Rejected because | Pairs |
|---|---|
| Different subject | 4,586,615 |
| Numeric thresholds disagree | 111,035 |
| Different office | 11,907 |
| Different action | 7,586 |
| Polarity mismatch | 6,131 |
| Close dates too far apart | ~9,800 |
| Scored above zero | 439 |
| Survived to high confidence with an edge | 5 |
Four guards do almost all the work, and each one exists because it let something stupid through first:
- Subject. If both sides name entities and the sets are not identical,
reject. Not a subset. "Wes Moore wins the 2028 nomination" and "Wes Moore wins the 2028 South Carolina primary" share every entity but one, and they are different contests.
- Office. A governor race and a Senate race in the same state are both a
"race" and share every surviving entity. They were matching at 0.44.
- Action. This is the one that caught Fed-versus-Somaliland. Same subject,
different verb. "Musk wins the 2028 election" against "Musk supports the Democratic candidate" passes every other check because the only entity is Musk on both sides.
- Numeric threshold. "Above 76.00" and "above 78.50" are different markets.
Adding the action guard took the feed from 10 candidates to 5 and removed every false pair. Fewer results was the improvement.
What our bot has actually made
Nothing. Our paper log stands at 10 trades, 6 closed, net -$28.30 on $500 deployed, and we publish the losers alongside the winners because a log that shows only winners is an advertisement.
The reason is not that the edges were imaginary. It is that an edge of 3.3 cents per contract, on a book that runs out at $42, on a market where the minimum order is $15, is a real edge worth about a dollar before anything goes wrong. Then the fill is worse than the quote, or one leg fills and the other does not, and the hedge you modelled is a directional position you did not want.
Anyone advertising a prediction market bot with a return figure is either trading at a size these books cannot support or is not counting the leg that did not fill.
What we could not verify
- Rate limits on either venue. Never throttled at a 60 second cycle, never
probed for the ceiling. No numbers.
- Order placement. Our scanner is read-only. It has never sent an order to
either venue, so nothing here describes execution from experience.
- Kalshi's maker rate. Their newsroom says resting orders are fee-exempt,
their help centre says makers are charged. We model 0.0175 as our own working assumption so the arithmetic runs. It is not a fact about Kalshi.
- Whether the surviving five are tradeable in practice. We have not traded
them. Depth, fill quality and leg risk are all unmeasured by us.
Questions people actually ask
Can a prediction market bot be profitable? We cannot tell you it can, because ours has not been. Our paper log is 10 trades, 6 closed, net -$28.30 on $500. The edges we find are real but small, and they sit on books that run out at around $42, so the theoretical return and the tradeable return are very different numbers.
Do I need a server to run a Kalshi bot? Yes. Kalshi returns HTTP 403 to any request carrying an Origin header, which browsers always send. A client-side bot cannot read Kalshi at all, so you need a server, a cron job or a serverless function doing the reading.
Does Polymarket arbitrage work within one market? No. outcomePrices on a Polymarket market sum to exactly 1.00 by construction, so there is no gap between the Yes and No legs to capture. Any real opportunity is across venues, where two independent order books price the same question.
What programming language should I use? Whatever you already know. Both APIs are plain HTTP with JSON and need no key to read. Ours is Python because the matching logic is text processing, not because of any latency requirement. Nothing here is a speed problem.
How often should the bot poll? Per minute is ample. Kalshi caches for 15 seconds, Polymarket's Gamma for 300. Polling faster than the cache window returns identical bytes. Our full cycle, including matching 4.76 million pairs, takes about 48 seconds.
What is the hardest part of building one? Deciding that two markets are the same question. Our matcher rejects more than 4.7 million pairs per cycle and still let five false matches through until we added a guard on the verb. A false match is not a hedge, it is two unrelated bets with a profit number printed under them.
Sources
Every figure describing the scanner is from our own logs on the date given. The paper trade log is published in full including the losing positions, because a log that only shows winners is marketing.
- https://api.elections.kalshi.com/trade-api/v2/markets Kalshi markets endpoint. Returned HTTP 403 Forbidden when an Origin header was present, HTTP 200 without, both measured 20 Aug 2026 11:42 UTC.
- https://gamma-api.polymarket.com/markets Polymarket Gamma API. Cache-Control public max-age=300 measured 20 Aug 2026. outcomePrices observed summing to exactly 1.00.
- https://clob.polymarket.com/markets Polymarket CLOB. minimum_order_size 15 on 962 of 1,000 markets, minimum_tick_size 0.01 on 968, maker_base_fee 0 on all 1,000. Sampled 20 Aug 2026 11:45 UTC.
- https://docs.kalshi.com/ Kalshi developer documentation. Taker fee formula and whole-cent rounding.
- PredictionEdge scanner, scanner/cross_venue.py and scanner/fees.py : our own implementation. Cycle timings, pair counts and rejection breakdown logged 20 Aug 2026.
- PredictionEdge paper trade log : 10 trades, 6 closed, net -$28.30 on $500 deployed.