Case studySolo projectApr to Sep 2026

Tick to Trade

A Dota 2 model that bets real money on Polymarket.

Illustration. Teams and numbers are made up.

I built a bot that watches pro Dota 2 live and bets on who wins.

Role
Solo, end to end
Timeline
April to September 2026
Team
Just me, plus AI helpers
Status
Traded live, then I stopped it on purpose. Findings kept.
  • Python 3.13
  • asyncio
  • FastAPI
  • XGBoost
  • pandas
  • Socket.IO
  • OpenCV + ffmpeg
  • Polymarket trading API
  • Next.js 16
  • React 19
  • TypeScript
  • pytest
  • file-locked JSON stores

Overview

It watches a match and trades the odds

Up to eight live data streams go in. Every minute one win chance comes out, and real money follows it. Just me, six months.

  1. Watch

    every match

  2. Guess

    every minute

  3. Trade

    real money

The problem

Eight data sources, and none were both fast and right

The best data is slow. It refreshes every 25 to 30 seconds, and at big tournaments it ran 15 minutes behind the game.

14.8min behind, typically

One blank data point once flipped my win chance from 36% to 61%.

The build

102 code updates, all mine

I built every layer. AI helpers wrote code, then others tried to break it.

Code updates per month. April was notes only.

  1. APR
    0 code updates
    • Notes beginNotes start on 21 April. The first model aims for fair odds above all.
  2. MAY
    3 code updates
    • Code starts 18 MayThe code project starts on 18 May. Training and live data get matched up, and the hero list shrinks from 42 to 29.
  3. JUN
    23 code updates
    • Stuck-sell bug fixedA sale went through but my records missed it, so every later sale failed. Now the exchange's own balance is the truth.
  4. JUL
    0 code updates
    • 1 of 10 ideas passed reviewA review found 1 of 10 experiments worth keeping. A 100-clue model was pulled after two clues used the wrong units live.
  5. AUG
    75 code updates
    • Training stops peeking aheadI stopped the model peeking one minute ahead, and now test it only on newer matches.
    • Wrong-side sell fixedA sale that timed out once bet on the wrong team. Fixed. The Twitch video reader also joins, as an optional extra.
  6. SEP
    1 code updates
    • 15-min delay foundValve's 900-second delay is found to be behind the bad September data. A price-posting idea is dropped: none of 480 variants made money.

The system

One program does everything

Data in, order out. Every step is logged so I can replay it.

  1. Live feeds8 sourcesUp to eight live sources, some fast and some slow.
  2. Mergeone clean rowAll sources are combined into one clean snapshot, with checks against bad data.
  3. Modelwin chanceThe model turns that row into a win chance.
  4. Bet sizehow muchHow much to bet. It only ever shrinks when the data looks shaky.
  5. Orderwait or takeWait in line for a better price, or take the price on offer now.
  6. Polymarketthe exchangeThe prediction market where the orders go.
  7. Trade confirmedexchange says soA trade only counts once the exchange confirms it.
  8. Saved recordprotected filesConfirmed trades go into protected files so two parts cannot record the same one.

sizes the next bet

Every step is loggedDashboardEvery prediction, decision and trade is appended to a log. The dashboard reads those same logs live.

The feeds

Fast feeds fix the slow one

Faster sources patch the slow data, each with safety checks. If the main data looks wrong, a backup source and a smaller backup model take over.

Steam game listsays when it is overSteam, the game store, says a match is over once it vanishes from its list 18 checks in a row.
Quick feeds patch it
  • CyberScore · game clock
  • hawk.live · towers
  • esport.vision · kills
  • Twitch video · kills
  • Steam · towers
Faster sources overwrite parts of the main feed, each one behind guards.
DLTV main feedslow but detailedThe detailed per-hero data, but it only refreshes every 25 to 30 seconds.
Sanity checkempty or stuck?Catches empty, stuck or impossible numbers. It cannot catch data that is merely old.
Backup feed1xBet plus 7 mirror sitesI ask 7 mirror sites at once and use the freshest answer.
Backup model84 clues, not 96A smaller model that works without player-by-player data. The main one returns after 6 good updates.
Win chancemain or backupThe model turns the snapshot into a win chance. The main model looks at 96 clues.

The model

Training and live must see the same data

I taught the model with 8,450 pro matches, one snapshot per game minute. Live snapshots are built the exact same way.

Training

  1. 8,450 matchespro gamesTop pro games from OpenDota, with full match history from Stratz.
  2. One row a minute334,090 rowsA snapshot for every game minute, each one copied with the teams swapped.
  3. Train model300 small trees300 tiny yes-or-no flowcharts, each only 3 questions deep.
  4. Clue listexact input orderThe exact order of the model's clues. Live snapshots are lined up to it.

The same code builds the row on both sides

same order

Live

  1. Live feeds8 sourcesLive data from DLTV, 1xBet and the quick feeds.
  2. Build rowsame recipeBuilds the row the same way training did.
  3. Startup checkmain model onlyRefuses to start if any clue is missing. It covers the main model only.
  4. Win chanceevery game minuteOne new win chance for each game minute.

The backup model has no such check.

The old model peeked one minute ahead. Fixing that made my scores look worse. I kept the fix anyway.

Mistakes, lower is better

Old0.4851
Shipped0.5119

Over-confidence, lower is better

Old0.0031
Shipped0.0020

Backup model share: 94 to 97% in September.

31 to 36%
August
94 to 97%
September

on the backup model

The orders

From win chance to filled order

I compare my win chance with the real price. Bets shrink on shaky data.

  1. Model's chancewho winsWhat my model thinks each team's chance is.
    Market pricewhat I can getThe price I could really buy or sell at: the seller's price to buy, the buyer's price to sell.
  2. The gapchance vs priceThe difference between the two. A trade needs a big enough gap.
  3. Bet sizeshrinks if shakyScaled down for early game, the backup model and shaky data.
  4. Wait in linecheap, no feeWaits in the queue at a better price and pays no fee.
    Take nowfast, pays a feeTrades right away for a fee. Used for big late-game gaps.
    Sell out15 s, then sells anywayTries a patient sale first, then sells whatever is left after 15 seconds.
  5. PolymarketPolymarket, where the orders go.
  6. Trade confirmedexchange says soNothing counts until the exchange confirms the fill.
  7. Saved recordprotected filesThe only place holdings change, and only once a trade is confirmed.
Saved record sizes next bet

September limit on oversized extra buys is built, not switched on

The control room

My dashboard

Ten pages let me watch it, pause it or stop it. New rules run quietly first and go live only when the records show they help.

Features page: every clue the model uses, with its value, source, age, warning flags such as frozen clock and stuck, and a mini trend line.

Features

Every clue, with its source

Every clue the model uses shows where it came from and how old it is.

Tower map: three lanes with towers, barracks and the main base for both teams. Click one to mark it destroyed; it cannot be undone.

Structures

A hand on the wheel

When the data goes dark, I can mark a tower as destroyed by hand.

Numbers

The numbers, with caveats

  • 8

    live feeds

    Seven companies, one with two streams.

  • 334,090

    training snapshots

    From 8,450 pro matches.

  • 0.66 → 0.89

    winner-picking accuracy score (0 to 1)

    Early to late game. From older tests, so possibly too rosy.

  • 14.8min

    hidden delay found

    Typical data delay across all 59 September matches.

  • 3,118

    self-checks that catch bugs

    789 of them check the data sources.

  • 86

    days it ran

    Late April to late September, some without real money.

  • Twitch clock reader, correct readings

    Before and after the fix.

Model vs market

One match, me vs the market

Over 70 minutes the lines mostly agree. When they split, the market caught up fast.

Match clock00:00
My model73%
Market60%
Gap+14 pts
FeedMain
My model, Radiant team's win chanceMarket pricebackup data

LGD Gaming (Radiant) vs Team Resilience · 13 Aug 2026

  1. 00:00 to 12:00

    The lines move together

    Both start above half. From minute 5 they slide down together.

  2. 16:56

    The lines split

    My model says 22%. The market says 45%. A gap of 23 points.

  3. 25:56

    A 33-point gap

    My model says 24.3%, the market 57.5%. By minute 30 the lines meet again.

  4. Minute 70

    Radiant won

    The model ends at 98%, the market at 100%. 10 of the 198 ticks came from the backup data.

Table: every tick of the match
MinuteModelMarketGap (pts)Source
00:0073%60%+14main
00:5382%63%+19main
01:3772%63%+9main
02:0173%63%+10main
02:2569%69%0main
02:5184%67%+17main
03:1380%71%+9main
04:2580%74%+6main
04:4875%75%+1main
05:1174%80%−6main
06:2670%70%0main
06:5063%66%−3main
07:3758%65%−6main
08:2652%61%−8main
08:5050%56%−5main
09:1353%66%−12main
10:0049%60%−11main
10:2443%56%−12main
11:1346%55%−8main
11:4043%60%−16main
12:0553%63%−10main
13:0064%70%−6main
13:2570%70%+1main
14:1461%66%−4main
15:0040%48%−8main
15:0540%50%−10main
15:2435%51%−16main
16:1023%33%−9main
16:5622%45%−23main
17:0927%44%−16main
17:1823%42%−19main
17:2127%37%−9main
17:2227%36%−9main
17:2628%41%−13main
17:4428%41%−13main
18:0721%43%−21main
18:3130%50%−19main
18:4230%49%−18main
18:4430%49%−19main
18:5230%50%−19main
18:5530%50%−20main
18:5530%50%−20main
18:5930%50%−19main
19:0631%45%−14main
19:1928%41%−13main
20:0526%46%−19main
20:0830%45%−15main
20:2932%41%−9main
21:1421%42%−20main
22:0121%32%−10main
22:2517%24%−6main
23:1318%33%−15main
23:5922%36%−13main
24:0821%37%−16main
24:2024%39%−15main
24:2524%39%−15main
24:4927%39%−12main
25:1327%41%−14main
25:3523%39%−16main
25:5624%58%−33main
25:5924%57%−32main
26:0124%58%−33main
26:0531%51%−19main
26:1031%50%−19main
26:1431%45%−13main
26:2131%46%−14main
26:2231%46%−14main
26:2428%48%−20main
26:2931%48%−17main
26:4434%48%−14main
27:0741%57%−16main
27:5554%61%−7main
28:2052%58%−6main
28:5548%69%−21main
29:0648%70%−22main
29:0755%70%−15main
29:1455%66%−11main
29:1655%67%−12main
29:2155%71%−16main
29:2555%71%−16main
29:3155%71%−16main
29:3255%70%−15main
29:3761%70%−8main
29:5373%70%+4main
30:1773%74%−1main
30:5574%80%−5main
31:0473%79%−6main
31:1373%80%−6main
31:4978%81%−3main
32:1477%82%−5main
32:5580%86%−6main
33:0883%86%−3main
33:4679%85%−6backup
34:1083%84%−1main
34:5381%89%−7main
34:5981%89%−8main
35:2090%92%−2main
35:3291%93%−1main
35:3593%93%+1main
35:4096%93%+3main
36:0696%93%+3main
36:5397%94%+3main
36:5391%94%−3backup
37:4193%95%−2backup
38:0792%97%−4backup
38:5293%95%−1backup
39:1598%95%+3main
39:5397%94%+3main
40:1897%87%+10main
40:2396%86%+10main
40:2696%82%+15main
40:2896%86%+10main
40:3396%84%+12main
40:3896%84%+13main
40:4391%82%+9main
40:4796%82%+15main
40:4996%82%+15main
40:5291%80%+11main
41:1281%63%+19main
41:5970%69%+2main
42:4870%66%+5main
43:1167%66%+1main
43:3467%66%+1main
43:5866%62%+5main
44:2166%64%+3main
44:4466%63%+4main
45:0763%64%−1main
45:5461%66%−4main
46:3967%66%+2main
47:0471%68%+3main
47:5377%68%+9main
48:1775%71%+4main
49:0570%68%+2main
49:5364%70%−6main
50:4163%67%−4main
51:0462%65%−3main
51:2363%67%−4main
51:2961%68%−7main
51:3261%68%−7main
51:3761%64%−2main
51:4662%67%−5main
51:4962%67%−5main
52:1379%73%+6backup
52:1779%68%+11backup
52:2179%74%+5backup
52:3579%78%+1main
52:4479%80%−1main
52:5179%80%−1main
52:5473%80%−7backup
52:5979%79%0backup
53:2187%79%+8main
53:4486%81%+5main
54:3090%83%+8main
55:1792%80%+13main
55:4192%80%+13main
55:5693%82%+12main
56:0093%82%+11main
56:0493%80%+13main
56:0593%80%+13main
56:1193%80%+13main
56:1593%83%+10main
56:2193%83%+10main
56:2593%83%+10main
56:2793%83%+10main
56:3093%83%+10main
56:3594%84%+10main
56:5093%84%+10main
57:1494%85%+10main
58:0294%85%+10main
58:2695%87%+9main
58:5095%86%+9main
59:1394%85%+10main
59:3795%79%+16main
60:0194%75%+20main
60:2495%78%+17main
60:4795%78%+17main
61:1096%78%+18main
61:3396%78%+18main
61:5695%78%+17main
62:1995%78%+17main
62:4395%73%+22main
63:2993%92%+1main
63:3296%92%+4main
63:5395%93%+3main
64:0196%95%+2main
64:3895%95%0main
65:0195%95%0main
65:4896%93%+3main
66:5496%95%+1main
67:0697%97%0main
67:2097%100%−2main
67:3497%99%−1main
68:0597%99%−1main
68:2897%99%−2main
68:4397%100%−2main
69:0998%99%−2main
69:2798%100%−1main
69:3798%100%−2main

LGD Gaming (Radiant) vs Team Resilience, 13 August 2026. Radiant won. Hover or drag across the chart, or use the arrow keys, to scrub.

Lessons

What I learned

  1. My test scores lied

    A live test found the market's odds beat mine every time, mostly with older models.

  2. Check the cause before fixing

    September looked like unfamiliar teams. A raw recording showed Valve delaying the data by 15 minutes.

  3. Some feeds froze but kept answering

    Some sources kept answering on time while repeating frozen numbers.

  4. Test new rules quietly

    Most ideas looked great on old matches. A review passed 1 of 10.

  5. One slow site froze everything

    One dead mirror site froze the whole program, even its exchange connection.

The ending

Why I stopped

  1. I sized up too big

    Overall I made money. But a few times I bet far too large, and those days gave a lot of it back.

  2. Some games looked fixed

    I came to suspect certain matches were rigged. A few months later, some of the teams I had flagged were banned for good.

  3. It never stopped needing me

    Keeping the trader alive ate my time. Feeds broke, sources changed, and automated trading is hard to predict.

So I paused it and kept what I learned. This page is that.

← All work