Skip to content

VALUEODDSAPI / DEVELOPER RESOURCES

Build a value betting bot in Python

A value betting bot does the scanning so you do not have to. This guide builds a complete one in about 80 lines of Python: it reads an odds feed, finds prices above the fair price and sends you a Telegram alert. You stay in charge of every bet.

Published by ValueOddsAPI · 3 November 2026

Value betting bot architecture in six steps: fetch odds, match the same bet, price it, filter, log once in SQLite and send a Telegram alert, next to two example alerts from the bot
The bot in this guide: six steps from the odds feed to a Telegram alert. It never places bets itself.

In 30 seconds

What a value betting bot is, and is not

A value betting bot is a program that does the boring part of value betting for you. It collects prices from many bookmakers, works out the fair price from a sharp reference, and flags every price that is above it. The method behind it is explained in How to find value bets.

The bot in this guide stops at the alert. It does not log in to bookmakers or place bets. Most bookmakers forbid automated betting in their terms, and accounts that use it are usually closed. An alert bot gives you the speed of automation and leaves the decision, and the responsibility, with you.

How the bot works

The diagram at the top of this page shows the six steps. Every 15 seconds the bot:

1. Fetches odds for a sharp reference and the soft bookmakers you bet with. 2. Matches the same bet: event, market, period, line and settlement rules must all be identical. 3. Prices it with EV = odds × fair probability − 1. 4. Filters on minimum EV, maximum price age and an odds range. 5. Logs it once in SQLite, which also stops duplicate alerts. 6. Alerts you on Telegram with the price, the fair price, a suggested stake and the age of the price.

What you need

What you need to run the bot
ItemWhere it comes from
Python 3.9 or newerpython.org, or the version that ships with macOS and most Linux servers
The requests librarypip install requests
An odds API keyYour ValueOddsAPI account, with the bookmakers you want to scan
A Telegram bot tokenMessage @BotFather in Telegram and create a bot
Your Telegram chat IDSend your bot a message, then read the chat ID from the getUpdates call
pip install requests

export VALUEODDS_BASE_URL="https://..."      # from your ValueOddsAPI account
export VALUEODDS_API_KEY="..."
export TELEGRAM_BOT_TOKEN="..."              # from @BotFather
export TELEGRAM_CHAT_ID="..."                # your own chat with the bot

python bot.py

The complete script

This is the whole bot, about 80 lines. The settings at the top are the ones to change: which bookmakers to scan, the minimum EV, the maximum price age, the odds range and the staking rule (half Kelly, capped at 3% of the bankroll).

import os
import sqlite3
import time
import requests

BASE = os.environ["VALUEODDS_BASE_URL"]
KEY = os.environ["VALUEODDS_API_KEY"]
TG_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TG_CHAT = os.environ["TELEGRAM_CHAT_ID"]

REFERENCE = "pinnacle"
SOFT = ["bet365", "unibet", "betano"]
MIN_EV, MAX_AGE = 0.02, 10          # +2% or better, prices at most 10 s old
MIN_ODDS, MAX_ODDS = 1.50, 5.00
BANKROLL, KELLY_FRACTION, MAX_STAKE = 1000, 0.5, 0.03

db = sqlite3.connect("value_bets.db")
db.execute("""CREATE TABLE IF NOT EXISTS alerts (
    bet_key TEXT PRIMARY KEY, bookmaker TEXT, odds REAL, fair_odds REAL,
    ev REAL, stake REAL, alerted_at INTEGER, closing_odds REAL)""")

def fetch(bookmakers):
    r = requests.get(f"{BASE}/v1/odds",
                     params={"phase": "prematch", "bookmakers": ",".join(bookmakers)},
                     headers={"Authorization": f"Bearer {KEY}"}, timeout=10)
    r.raise_for_status()
    return r.json()["data"]

def same_bet(row, outcome):
    m = row["market"]
    return (row["event_id"], m["id"], m["period"], m["line"], m["settlement"], outcome["id"])

def kelly_stake(odds, p):
    f = (odds * p - 1) / (odds - 1) * KELLY_FRACTION
    return round(BANKROLL * min(f, MAX_STAKE), 2)

def alert(text):
    requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
                  json={"chat_id": TG_CHAT, "text": text}, timeout=10)

def scan():
    rows = [r for r in fetch([REFERENCE, *SOFT])
            if r["market"]["status"] == "open" and r["data_age_seconds"] <= MAX_AGE]
    fair = {same_bet(r, o): o["fair_probability"]
            for r in rows if r["bookmaker"] == REFERENCE and r["market"]["fair_status"] == "available"
            for o in r["market"]["outcomes"]}
    found = new = 0
    for r in rows:
        if r["bookmaker"] == REFERENCE:
            continue
        for o in r["market"]["outcomes"]:
            p, odds = fair.get(same_bet(r, o)), o["bookmaker_odds"]
            if p is None or not MIN_ODDS <= odds <= MAX_ODDS:
                continue
            ev = odds * p - 1
            if ev < MIN_EV:
                continue
            found += 1
            key = "|".join(map(str, same_bet(r, o) + (r["bookmaker"],)))
            stake = kelly_stake(odds, p)
            cur = db.execute("INSERT OR IGNORE INTO alerts VALUES (?,?,?,?,?,?,?,NULL)",
                             (key, r["bookmaker"], odds, round(1 / p, 3), ev, stake, int(time.time())))
            if cur.rowcount:                # first time we see this bet: send it
                new += 1
                alert(f'VALUE {ev:+.1%} | {r["event_id"]} {r["market"]["id"]} {o["id"]} '
                      f'{r["market"]["line"] or ""}\n{r["bookmaker"]} {odds:.2f} vs fair {1 / p:.2f} '
                      f'| stake {stake:.2f} | price age {r["data_age_seconds"]:.0f}s')
    db.commit()
    print(f"{time.strftime('%H:%M:%S')} scanned {len(rows)} rows | {found} value bets | {new} new alerts")

while True:
    try:
        scan()
    except requests.RequestException as e:
        print("fetch failed, retrying:", e)
    time.sleep(15)

same_bet() builds the key that decides whether two prices are comparable. kelly_stake() sizes the suggested stake; the reasoning is in Kelly criterion for betting. INSERT OR IGNORE is the deduplication: a bet that is already in the database is not alerted again. The try / except around the scan keeps the bot running when a request fails.

Running it

The bot running on test data: three scans, two alerts in the first, none in the second and third, with the reasons other prices were skipped: below the EV threshold, stale, or a different line
Real output from the script above, run on test data. Every price that was skipped has a reason.

The first scan finds two value bets and sends two alerts. The other prices are skipped for exactly the reasons the filters exist: one is only +1% (below the threshold), one is 14 seconds old, and one is Over 3.25 while the reference only has 3.5, so it is not the same bet.

The second scan sees the same two bets again and sends nothing, because they are already in the database. In the third, the soft price has been cut and the value is gone. A working bot spends most of its time saying no.

Measuring whether the bot works

Profit and loss take thousands of bets to mean anything. The faster test is closing line value: if the prices the bot flags are consistently better than the closing price, the fair prices are good. Store the closing price after kick-off and calculate the average:

def record_closing(bet_key, closing_odds):
    """Store the last price before kick-off for a bet the bot alerted."""
    db.execute("UPDATE alerts SET closing_odds = ? WHERE bet_key = ?", (closing_odds, bet_key))
    db.commit()

# Average raw CLV of everything the bot has flagged so far
avg_clv = db.execute("SELECT AVG(odds / closing_odds - 1) FROM alerts "
                     "WHERE closing_odds IS NOT NULL").fetchone()[0]

This is raw CLV. For the stricter, margin-free version, see Closing line value.

Making it production-ready

From a script to a service
AreaIn this guideIn production
DataPolling every 15 secondsA WebSocket stream, so prices arrive as they change
Request budgetFixed intervalSized to your plan’s hourly quota; the API answers 429 when it is used up
RunningA terminal windowsystemd, Docker or a process manager that restarts on failure
StorageSQLite filePostgres if several processes write at once
MarketsEverything returnedA whitelist of markets and leagues you trust the reference for
AlertsOne message per betGrouped messages and a daily summary with CLV

Estimate how many requests your setup needs with the request calculator, and read the integration guide.

Common mistakes in betting bots

No freshness check. Without a maximum price age, the bot alerts prices that disappeared a minute ago.

Loose matching. Matching on team names and market name alone mixes lines, periods and rules, and produces edges that do not exist.

Alerting the same bet over and over. Without deduplication you get the same message every 15 seconds and stop reading them.

Trusting big edges. A +20% EV is far more likely to be a data error than a gift. Cap the stake and check those first.

Never measuring. If you do not store the closing price, you will never know whether the bot finds real value.

Frequently asked questions

Can I build a value betting bot myself?

Yes. With basic Python and an odds API, the script in this guide is a complete starting point: it fetches odds, finds value bets and sends alerts.

Is a value betting bot allowed?

An alert bot that only reads odds data and notifies you is simply a tool for choosing bets. Automatically placing bets is usually against bookmakers’ terms and can get accounts closed, which is why this bot only sends alerts.

How often should the bot check odds?

Often enough that prices are fresh, but within your request quota. Polling every 10–30 seconds is a common start; a WebSocket stream is better because prices arrive as they change.

Can the bot send alerts to Discord or Slack instead?

Yes. Replace the alert() function with a POST to a Discord or Slack webhook URL. The rest of the bot stays the same.

Which bookmaker should the bot use as the reference?

A sharp bookmaker such as Pinnacle for the markets it covers well. See Sharp vs soft bookmakers for the alternatives.

Can the bot run live, during matches?

Technically yes, by fetching phase=live, but in-play prices change every few seconds and bookmakers delay bets. Start pre-match and lower the maximum price age a lot before going live.

Related reading

ValueOddsAPI provides odds data, not betting advice. Betting involves risk of loss and is for adults (18+) only. If gambling stops being fun, contact your local support service, such as StopSpillet in Denmark.

Get the data into your product

Solo €899/month (personal use), Pro €1,800, Scale €4,000, Elite €8,000 and Enterprise from €13,000/month, excluding VAT. Coverage and usage rights are confirmed before activation. Missing a bookmaker? We add the sources you need, scoped and priced per project.