Originally published on BotCookery, free build recipes for AI-built trading and prediction-market bots. Written with AI assistance. Every number and log line below comes from read-only runs against the live Kalshi and Polymarket US public APIs on the afternoon of Oct 25, 2026. The monitor has no order code at all. Nothing here is financial advice.
"Risk-free arbitrage between Polymarket and Kalshi" is one of the most common bot pitches around. The idea is simple: if YES on one venue plus NO on the other costs less than $1.00, one of them has to pay $1.00, so you lock in the difference.
It only works if two things are true at once: the combined cost is under $1.00 after fees, and both contracts settle on exactly the same outcome. Our recipe builds an alerts-only monitor that measures both, for market pairs you map by hand. It never places, previews or cancels an order.
On Oct 25 we pasted the Oct 22 version of the prompt into a clean folder, let an AI coding tool build it, and ran it read-only on three real pairs: Magic at Knicks (NBA), Chiefs at Seahawks (NFL), and the 83-84°F bucket for Miami's Oct 26 high temperature. The skip rules, the stale-date rule and the "price is always YES" math were right. Six other things weren't.
Problem 1: the recommended Kalshi host answered 403
external-api.kalshi.com is the host Kalshi's docs recommend. From our network it answered 403, so the first run skipped all three pairs and reported nothing. The other documented host, api.elections.kalshi.com, worked fine.
The fix is boring but necessary: try the recommended host, switch once on a 403, and keep whichever works.
Kalshi https://external-api.kalshi.com/trade-api/v2 answered 403; switching to https://api.elections.kalshi.com/trade-api/v2 Problem 2: placeholder fees made a losing trade look break-even
The first build used a flat 1¢ per contract per venue as a placeholder. That doesn't match either venue's formula. Both fees scale with p × (1 - p), so they're largest near 50¢:
- Kalshi taker:
fee_multiplier × 0.07 × C × P × (1 - P), rounded up to the cent. The multiplier comes fromGET /series/{series_ticker}. - Polymarket US taker:
0.0695 × C × p × (1 - p), rounded to the cent (banker's rounding).
On these books the real fees were 1.4-1.8¢ per contract on each leg. Here's the old build's first cycle at 3:41 PM ET:
!!! WARNING: fees.yaml has placeholder values. THESE EDGES ARE NOT REAL. !!! Magic @ Knicks, Oct 25 (Orlando wins) [K YES + PM short] cost 0.2900+0.7200=1.0100 edge_after_fees -0.0300 depth 261080 Magic @ Knicks, Oct 25 (Orlando wins) [K NO + PM long] cost 0.7200+0.2900=1.0100 edge_after_fees -0.0300 depth 12550 Chiefs @ Seahawks, Oct 25 (Seattle ticker vs Chiefs-long) [K YES + PM long] cost 0.5900+0.4150=1.0050 edge_after_fees -0.0250 depth 1504150 Chiefs @ Seahawks, Oct 25 (Seattle ticker vs Chiefs-long) [K NO + PM short] cost 0.4200+0.5875=1.0075 edge_after_fees -0.0275 depth 651638 Miami high Oct 26, 83-84F [K YES + PM short] cost 0.6300+0.4100=1.0400 edge_after_fees -0.0600 depth 37 Miami high Oct 26, 83-84F [K NO + PM long] cost 0.3800+0.6000=0.9800 edge_after_fees +0.0000 depth 12 SKIP Stale test pair: rules_checked_on is 24 days old (>14) That last Miami line is the only gross gap we saw all afternoon: Kalshi NO at $0.38 plus Polymarket US YES at $0.60 = $0.98. With placeholders it looks like a wash. Now the real math for 12 contracts:
- Kalshi: 0.07 × 12 × 0.38 × 0.62 = $0.198, rounded up to $0.20
- Polymarket US: 0.0695 × 12 × 0.6008 × 0.3992 = $0.20 (0.6008 is the 12-contract average, see problem 3)
- Total per contract: 0.38 + 0.6008 + 0.40 / 12 = 1.0142
So the edge is -1.4¢ a contract. The one "arbitrage" of the afternoon loses money before you even think about timing. The new prompt computes both published formulas per leg, at the size you'd actually trade, keeps the coefficients in fees.yaml with a checked_on date, and warns when that date is over 30 days old.
Problem 3: "depth" meant the top level only
Polymarket US showed 11.66 contracts at $0.60. Twelve contracts don't fit in 11.66, so the real 12-contract average was $0.6008. A few minutes later someone had taken most of that offer, and the same 12 contracts averaged $0.6109.
The monitor now walks each book across levels, volume-weighted, for the size you'd trade, and logs "too thin" if either side can't fill it. One more trap: in Polymarket US's /bbo response, askDepth and bidDepth are not contract counts. Take sizes from /book.
Problem 4: the Miami gap wasn't an arbitrage anyway
Even with a positive edge, that pair would have been a bet, not an arbitrage. On Oct 25 Kalshi's Miami high-temperature market settled "according to The Weather Company". Polymarket US's settled on the National Weather Service's daily climate report. Same city, same day, same bucket, two different sources. They can disagree by a degree, and one degree moves a bucket.
The NFL pair differed too. Both venues paid $0.50 on a tie, but on a postponement Kalshi went to a fair price after 48 hours while Polymarket US waited up to two weeks.
The old prompt had a free-text notes field, which let us type "same bucket, same day" and move on. The new one makes you prove it:
- Each pair gets
kalshi_source,polymarket_source,tie_and_delay,rules_match(defaultfalse) andrules_hash. -
--show-rulesprints Kalshi'srules_primary,rules_secondaryand the seriessettlement_sourcesnext to the Polymarket US description, with one sha256 of the combined text. - A pair is skipped unless
rules_matchis true and the live hash still equals the saved one. If either venue edits its rules, the pair stops until you re-read them.
We tested that last part by changing one saved fingerprint:
SKIP Chiefs @ Seahawks, Oct 25 (Seattle): live rules hash 4acddea46b19a75e != saved 0000000000000000; re-read with --show-rules Problem 5: it couldn't tell you how long a gap lasted
The first build would have re-alerted every 30 seconds while a gap stayed open and never logged when it closed. Gap duration is the number that matters most if you ever wanted to act by hand, so now it alerts once and logs a CLOSED line with how many seconds it lasted and the best edge seen.
Problem 6: piped output stayed empty
Our first 100-second run, piped through tee, printed nothing. Python buffers stdout when it isn't a terminal. Every line now prints with flush=True.
One near-miss: which end of Kalshi's book is the best bid
Kalshi's GET /markets/{ticker}/orderbook returns bids only, as [price, size] pairs sorted ascending, so the best bid is the last element. The YES ask is $1 minus the best NO bid. Our build happened to get this right, but the old prompt never said it, and taking the first element gives you a $0.99 ask. The new prompt spells it out and has a test for it.
After the fixes
Fresh rebuild from the new prompt, 9 tests passing, 3:46 PM ET, 10 contracts, the real pairs file:
Kalshi https://external-api.kalshi.com/trade-api/v2 answered 403; switching to https://api.elections.kalshi.com/trade-api/v2 Magic @ Knicks, Oct 25 (Orlando wins) [K YES + PM short] x10: K 0.2900 fee $0.15 | PM 0.7200 fee $0.14 | edge -0.0390/contract Magic @ Knicks, Oct 25 (Orlando wins) [K NO + PM long] x10: K 0.7200 fee $0.15 | PM 0.2900 fee $0.14 | edge -0.0390/contract SKIP Chiefs @ Seahawks, Oct 25 (Seattle): rules_match is not true SKIP Miami high Oct 26, 83-84F: rules_match is not true To check the alert path and the math, we ran a copy with every pair forced to rules_match: true, 12 contracts and a deliberately negative margin so alerts would fire (Miami lines, 3:48-3:51 PM ET):
ALERT Miami high Oct 26, 83-84F [K NO + PM long] x12: K 0.3800 fee $0.20 | PM 0.6008 fee $0.20 | edge -0.0142/contract CLOSED Miami high Oct 26, 83-84F [K NO + PM long] after 73s, best edge -0.0142 (edge -0.0242) Miami high Oct 26, 83-84F [K NO + PM long] x12: K 0.3800 fee $0.20 | PM 0.6109 fee $0.20 | edge -0.0242/contract That's the whole story of the afternoon in three lines: the only gap was negative after fees, it got worse when the offer was taken, and the monitor logged exactly how long it lasted.
Built this before Oct 25? The follow-up prompt
If you built an earlier version, paste this into the same project:
Update my alerts-only arbitrage monitor with these fixes. Still no order code of any kind; public reads only. 1. Kalshi host: try https://external-api.kalshi.com/trade-api/v2 first; on a 403, switch once to https://api.elections.kalshi.com/trade-api/v2 and keep it. KALSHI_BASE still overrides both. 2. Check the Kalshi book math: arrays are bids sorted ASCENDING, so the best bid is the LAST element. YES ask = 1 - last NO bid, NO ask = 1 - last YES bid. Taking the first element gives a $0.99 ask. 3. Replace the placeholder fees with the real formulas, computed per leg for the size you'd trade: Kalshi taker = fee_multiplier x 0.07 x C x P x (1-P), rounded UP to the cent (fee_multiplier from GET /series/{series_ticker}). Polymarket US taker = 0.0695 x C x p x (1-p), banker's rounding to the cent. Keep the coefficients in fees.yaml with a checked_on date and warn if it's older than 30 days. 4. Price min_size contracts by walking each book across levels (volume-weighted). If either side can't fill it, log "too thin". Take sizes from /book; /bbo askDepth and bidDepth are not sizes. 5. Add kalshi_source, polymarket_source, tie_and_delay, rules_match (default false) and rules_hash to each pair. Add --show-rules to print Kalshi rules_primary + rules_secondary + series settlement_sources next to the Polymarket US description, with a sha256 of both. Skip a pair unless rules_match is true and the live hash equals rules_hash. 6. Alert once when a gap opens; log a "closed" line with how many seconds it lasted and its best edge. Print with flush=True. 7. Tests for each of the above. The full build prompt, the step-by-step for mapping pairs by hand, and the safety checklist are on the recipe page.
Takeaways if you're building one
- Use the real fee formulas at your real size. Both fees peak near 50¢. A flat placeholder will show you gaps that don't exist.
- Walk the book. Top-of-book size is often smaller than the trade, and it moves within minutes.
- Read both rulebooks for every pair, and fingerprint them. "Same event" isn't the same contract if the settlement source, tie rule or postponement rule differs.
- Log how long gaps last. That, plus size and edge after fees, is the honest answer to "do arbitrage bots work?"
- Keep it private. Kalshi's Developer Agreement limits API data to your own trading and bars sharing it with third parties without written permission, so no public arb dashboards or paid alert channels.
What we verified, and what we didn't
From the recipe page: Verified working on live data in read-only mode, Oct 25, 2026. Ran read-only against the live Kalshi and Polymarket US public APIs on the afternoon of Oct 25, for several cycles on three hand-mapped pairs (one NBA game, one NFL game, one Miami temperature bucket), plus a forced-alert drill and a changed-rules drill. No gap that survived fees appeared, so no real alert fired. The Telegram and desktop alerts were not tested and no message was sent.
Fee coefficients and contract rules are as we read them on Oct 25. They change, so check both venues' fee pages and rules before you trust any number here.
