In my last post, I wrote about stream delay and why realtime on Twitch is secretly a timeline problem.
This time: odds.
I am building elo.market, a live prediction system for CS2 streams. Viewers bet virtual elo on things like round winners, bomb plants, and clutches.
At first, dynamic odds sounded easy.
Votes come in -> odds move.
Done, right?
Not really.
Because if odds move too slowly, the market feels fake.
And if they move too fast, one smart whale can make the whole thing feel rigged.
The Problem In One Line
This was the failure mode I kept running into:
small early vote -> juicy odd appears -> whale grabs it -> market feels broken
Or visually:
+------------------+ +--------------------+ +------------------+ +------------------+
| Small early vote | --> | Big price appears | --> | Whale takes it | --> | Everyone else |
| from normal user | | on the other side | | before it settles| | thinks: "lol ok" |
+------------------+ +--------------------+ +------------------+ +------------------+
That is technically a dynamic market.
It is also terrible product design.
The First Version Was Too Naive
My first version was basically:
- start from some base probability
- add a seed so the first bet does not nuke the market
- recalculate odds from the current pool
Conceptually:
adjustedProb = (amountOnOutcome + baseProb * seed) / (totalPool + seed);
That was fine as a first step.
But it had two problems.
First, some markets should not even start 50/50.
A round_winner market is not actually even if one side is broke and the other has rifles.
So I started feeding real game context into the base probabilities:
- loss streaks
- score momentum
- streamer economy as a team signal
That helped the opening price feel smarter.
But it still did not solve the whale problem.
The Whale Problem
The obvious model is amount-based.
More money on one side means the odds shift harder.
Sounds fair, until one large bet starts overpowering the market.
So I tried blending two signals:
- how much money was bet
- how many people bet that side
That was better, but my first attempt still had a bug in the idea.
I used average bet size in the formula.
Which meant a whale did not just affect the amount part. They also inflated the "crowd consensus" part by making average bet size bigger.
That was dumb.
I accidentally gave large bets influence twice.
The Fix That Actually Mattered
The best change was surprisingly small.
I stopped using average bet size for voter influence and switched to a fixed vote unit instead.
So the logic became:
effectiveAmount =
(1 - voterWeight) * rawAmount +
voterWeight * voterCount * FIXED_VOTE_UNIT;
That changed the feel of the market a lot.
Now a whale can still move odds because size should matter.
But they cannot also pretend to be "the crowd" just because their bet was huge.
That one change made the system feel much less abusable.
Then I Hit The Next Problem
Even with a better formula, fixed seeds still broke at different economy sizes.
If one streamer has average bets around 50 elo and another has average bets around 1000, the same seed value behaves completely differently.
So I made the seed scale with season behavior instead of hardcoding it.
Conceptually:
dynamicMinSeed = seasonAvgBet * 50;
effectiveSeed = max(dynamicMinSeed, totalPool * seedMultiplier);
That made early odds much harder to snipe across both small and large channels.
Not Every Market Should Behave The Same
Another lesson: a bomb prediction and a map-wide prediction should not react with the same personality.
So I ended up with templates.
-
dynamicfor markets that can move more -
balancedfor standard round predictions -
stablefor short live windows -
anchoredfor longer markets that should respect the opening line more
That was much better than pretending one formula could fit everything.
Delay Came Back Again
And because Twitch loves making every problem slightly worse, delay showed up here too.
If I delayed both PREDICTION_CREATED and ODDS_UPDATE the same way, viewers saw stale odds.
So the final model became:
- delay the prediction event
- send odds updates immediately
- buffer them on the client until the prediction is actually visible
That way, when the card appears, viewers see current odds instead of ancient history.
Fairness Needed One More Layer
Even after all that, there was still one ugly edge case:
the user clicks at one price, but by the time the vote reaches the backend, the odds are worse.
So I added slippage protection.
If the odds move too far against the user, the vote is rejected and the UI can ask them to accept the new number.
That turned out to matter almost as much as the formula itself.
Because fair pricing is not just math.
It is also about whether users feel tricked.
Where I Landed
The current system is basically this:
game state priors
+
adaptive seed
+
voter consensus
+
slippage protection
+
delay-aware delivery
=
dynamic odds that still feel fair
- smarter base probabilities from real game state
- adaptive seed so early votes do not create nonsense
- voter weighting so one whale does not dominate instantly
- fixed vote units so big bets do not fake consensus
- different templates for different market types
- slippage protection for bad fills
- delay-aware delivery so live odds still look live
The funny part is that dynamic odds sounded like a tiny feature when I started.
In reality, it turned into a product trust problem.
If the odds feel fake, people stop caring.
If the odds feel exploitable, people stop trusting.
The goal was not "perfect market making."
The goal was simpler:
make odds move enough to feel alive, but not enough to become a farming strategy for whales.
If you have built low-liquidity markets, game economies, or live pricing systems, I would love to hear what tradeoffs you made.

Top comments (0)