cam's blog

What I learned from running a liquidity incentive bot on Kalshi

Kalshi has a Liquidity Incentive Program. Each market gets a small daily reward pool, split between whoever has buy orders resting near the top of the book. It's counted in contracts, not dollars, so a 1¢ bid looked like a cheat code: five bucks buys 500 contracts of depth.

Here's why. My first real order put $10 behind 5,000 contracts at 0.2¢. The model said that was 38% of a market paying $54 a day, and Kalshi splits each pool between the YES and NO sides, so that's about $10 a day. On ten dollars. Leave it up for a week and that's $70 on ten bucks, without anyone ever trading with you.

But there's always a catch. In this case, two of them:

I spent a few days building a bot to try and farm this. Here's how it went.

Paper trading

Before risking anything, I ran paper accounts for three days: fake orders scored against Kalshi's real order book and trade feed. The good: Every account earned rewards. The bad: Every filled position that settled lost money. 367 fills, $392, zero winners. Pennies in front of a steamroller, and the steamroller kept showing up.

Each account tried a different idea:

Strategy The idea
join Bid right at the reference price, the level where Kalshi starts giving full credit. Most reward per contract.
below Bid one tick under it. Half the credit per contract, but the crowd at the reference price sits in front of you and soaks up small sells.
low_flow join, but skip markets where past selling into the bids would have cost more than the rewards.
focus Put $1,000 into ten $100 quotes. Big shares of a few pools. Ran with and without the flow filter.
spread The opposite: $1,000 as up to 66 quotes of $15 each. Diversify.
complete Find a side that's short of Kalshi's depth target and fill the gap. Nobody earns until both sides qualify, so whoever completes it gets a big share.

Paper can't know where you'd sit in the queue at your price, so each account ran twice: once at the back of the line and once always first. Reality is somewhere in between.

Strategy Capital out Rewards/day Lost to fills/day Net/day, back of line Net/day, first in line
low_flow $4,960 $217 $63 +$154 −$2
below $4,131 $159 $15 +$144 +$87
focus, with filter $996 $53 $0.41 +$52 +$36
focus, no filter $985 $55 $32 +$23 −$10
spread $994 $88 $96 −$8 −$96
join $4,957 $207 $235 −$28 −$136
complete $106 $19 $54 −$35 −$39

A few things jump out. Joining the crowd earned the most rewards and lost the most money. The flow filter was worth about $180 a day on the same strategy (join vs low_flow). Completing short sides was the worst idea per dollar by a mile: if you're the only depth on a side, you're the one who gets hit. And only below and filtered focus stayed positive when first in line.

Then I estimated what happens when competitors copy every quote, using the quotes each account held on Nov 2:

Strategy Alone 1 rival 3 rivals
below +$124 +$74 +$38
focus, with filter +$48 +$32 +$20
join −$86 −$131 −$168

This was interesting, but paper can't show how other traders react to your orders, which I was about to find out.

Going live

On Nov 2 I put up two real orders. Before that, I set some ground rules:

The first order was on whether Biden gets charged before 2027. Charged with what? I don't know, I'm just making markets over here. I bid 1.6¢ for 625 YES, and the model said I'd get 61% of the reward.

Within 90 seconds the other bids at my price went from about 256 contracts to about 2,550. Kalshi splits each side's reward roughly by size, so my 625 went from most of the pile to about a sixth of it: 17%. By that afternoon I was under 1%. My best guess is that other reward bots watch the same pools and pile into any market where one quote is taking most of the pot. Nothing that fast is a human.

It never filled. Nobody wanted to sell Biden-gets-charged at 1.6¢, so I sat there earning a shrinking slice until my own guardian pulled the order: there weren't enough bigger, higher-priced bids left in front of me to soak up a sweep.

The second was "Will McConnell resign before Nov 3?" This is the one that got me:

Time (UTC) What happened
03:07 I bid 0.2¢ for 5,000 YES.
05:45 Other traders have 11,800 contracts at 0.3¢, enough to cover the whole reward zone. I'm earning nothing.
13:24 I'd rewritten the bot to chase whichever quote was actually earning, which meant joining the top of the book. It moves me up to 0.3¢: 3,333 contracts, $10.
13:44:53 11 trades in the same instant buy 15,451 NO contracts and take every YES bid at 0.3¢, mine included.

The 0.2¢ bids I'd just left were never touched.

That fill limit was supposed to prevent this: a sweep takes 250 contracts, Kalshi pulls the rest, and I lose 75¢ instead of ten bucks. But the limit only reacts to fills that have already happened, and all 3,333 of my contracts went in one trade. There was no rest to pull. Same story for the watcher's half-cost rule.

I got out of the position by hand, through sheer luck: someone wanted to hedge a last-minute exit and took the other side. My cash ended right where it started. The plan for those two orders had said about $21 in rewards, if they'd stayed up their full planned lives: about 30 hours for McConnell and 92 for Biden. For all my troubles, I came away with a grand total of $1.85.

Making it boring

So I rebuilt it to be as boring as possible:

Then I scanned all 9,875 active reward programs. Nothing passed. Not one quote cleared even the $1 minimum, even after raising my price cap from 2 to 15 cents. The best safe quote earns $1 to $2 a day, and that's if nobody competes for it.

Finding profitable rewards programs on Kalshi
Finding profitable rewards programs on Kalshi

Three bugs found along the way

1. An empty book crashed the feed. Kalshi leaves a side out of the order book snapshot when nobody's bidding on it. My code assumed both sides would always be there, so every empty side threw a KeyError and reset the whole WebSocket. That was 181 of the roughly 233 resets in my recording.

# before
book[side] = dict(msg[f"{side}_dollars_fp"])
# after
book[side] = dict(msg.get(f"{side}_dollars_fp") or [])

2. Unsubscribing is a race. When the bot dropped a market, it deleted the book right away, but Kalshi's last few updates for it were still in flight. They landed 1 to 127 ms later, my code took them for corrupt data, and it threw away every book on the connection: 41 more resets. The fix: mark dropped markets as removed, ignore their stragglers, and wait for a fresh snapshot if one comes back.

3. My backtest and my live bot had different bedtimes. Paper rechecked min_days_left = 2 (plus a 12-hour blackout) every two hours, so it never quoted within about 60 hours of a deadline. The live tool only checked min_hours_left = 24 when placing, then left the order up until 6 hours before. The McConnell fill came 25 hours before close, in a window paper never tested. Replaying paper with the live timing made 25 of 30 accounts worse.


So, what did I learn?

  1. The rewards are real but small, and the $1-per-market floor kills the "lots of tiny orders" plan.
  2. The reward is basically payment for the risk of getting filled, and in my data the fills cost more than the reward.
  3. Your share of the reward can look great when you enter, but the book keeps shifting up under you. You're tempted to chase it, and every tick you chase puts you closer to the front of the next sweep. That's adverse selection, and it's exactly how McConnell got me.
  4. Scared money doesn't make money. Everything that kept my losses small (bidding below the crowd, avoiding busy markets, tiny size) also kept me out of the rewards. With losses capped where I had them, there's nothing worth quoting.
  5. I didn't really have an "edge," I was just punting but for a different reward than a correct outcome.

What would it actually take?

Somebody is making money here, because somebody keeps taking my share. Here's what they might have that I don't, and the clues I saw:

Lever Why it would help What I saw
Capital The $1 floor and the occasional bad fill are fixed costs per market. Spread across hundreds of programs, they average out. Other traders stacked 11,800 contracts at 0.3¢ within three hours of my McConnell order.
The other side On a long shot, a fill means you bought something that almost never pays. On the favorite, a fill usually pays $1. Same reward per contract, very different fill risk, but about 300x the capital per contract. After the sweep, 37,568 NO bids sat at 99.7¢. That's roughly $37,000 parked on the favorite's side.
Knowing the price A real market maker quotes around what the contract is actually worth, so fills aren't losers on average and the rewards are a bonus. My bot only knew what was cheap, not what was mispriced.
Speed and signals You can't outrun a single sweep, but you can pull quotes when something upstream moves: news, a related market, the other side thinning out. Rivals reacted within 90 seconds. The sweep took a whole price level in one instant.
Hedging Quote related markets (strikes of the same event, say) so one fill is offset by another position. Untested. I was taking it one market at a time.

I ran the numbers on the other side

I went back to my recording (about a week of order books and two weeks of trades on every LIP market) and asked two questions about each side of the book: what does it pay, and what happens when you get filled?

Long-shot side (≤15¢) Favorite side (≥85¢)
Reward per $100 a day, joining at the reference price (median) $2.48 $0.20
Average price when a resting bid got filled 2.6¢ 96¢
How often those filled contracts paid out 0.17% 99.86%
Return on money spent in fills −94% +4%
Events that lost money on fills 230 of 235 3 of 183

The long-shot side pays about 10 times more per dollar than the favorite side of the same market. But every dollar that gets filled there is basically gone, so you can only afford to have about 2.6% of your capital filled per day before fills eat the rewards. My paper join account lost 4.75% a day to fills. below lost 0.36%, which is why it worked.

The favorite side pays a tenth as much, but its fills have been winners. Your bid gets hit by someone buying the long shot, and the long shot almost never comes in. Take out the three biggest events and it's still +3.4%. So on the favorite side you get the reward and a small edge on fills, which is the exact opposite of what I was doing.

The catch is the tail. A favorite bought at 96¢ that loses costs you 96¢, and two weeks of data can't tell you how often that happens. It also eats capital: $100 at 95¢ is 105 contracts, a small slice of a side holding a median of about $4,900. Which is exactly the picture from McConnell: $37,000 of NO bids sitting at 99.7¢.

(Caveats: these are every maker's fills, not just reward quoters'. Maker fees are ignored as most maker fees for LIP markets are waived. It only covers markets that settled by Nov 6, so most fills came in the last 60 hours before close. And the reward assumes you keep your share and nobody reacts.)

In summary

This was a fun intro to prediction markets, but it's nowhere near a business. It's also nothing like LP or points farming in crypto, where you can mostly deposit, wait, and let VC-funded incentives pay you for showing up. Here every dollar of reward sits behind a live order that someone better informed can hit at any second, so you have to stay on edge the whole time. My hunch is that the winners aren't running rewards bots at all. They're running market-making operations that already work, and the rewards are just gravy on top. If that's right, the real question was never how to farm the rewards. It's whether I can price these contracts better than the people already quoting them.

And the people already quoting them have real balance sheets. Susquehanna became Kalshi's first official market maker in 2024 and now runs dedicated prediction desks, plus Rothera, an exchange it launched with Robinhood this June. Jump Trading reportedly makes markets on Kalshi and Polymarket in exchange for equity in both. Citadel Securities isn't in yet, but in April its president called it "certainly possible", for non-sports contracts. Every giant that piles in makes the pricing edge left for a hobbyist a little thinner.

Questions I don't have answers to yet:

For now I'm still running the bot, just in shadow mode, without risking dollars to make a few cents.

In the meantime, I'm still trying out a few ways to price contracts in places where I think there's an edge that the "big boys" haven't bothered with yet. Hint: it's definitely not the 15 minute BTC up/down markets, those are a slaughterhouse. More on this later. Thanks for reading!