← Developer Blog

Build a Polymarket Tennis Trading Bot (Python)

Build a Polymarket tennis trading bot in Python: discover markets with the Gamma API, match live matches, read free live scores, and plug in execution.

· · By the Live Tennis API team

To build a Polymarket tennis trading bot, you wire four parts together: discover tennis markets from Polymarket's free Gamma API, match each market to a live match, read that match's live state from a tennis data API, and feed your own logic. Order execution is yours to add.

This is the pillar tutorial. It shows the full read-and-signal pipeline end to end with runnable Python, and marks exactly where trade execution plugs in. Everything on the read side is free or free-to-start. Nothing here places orders, sizes positions, or offers financial advice — that logic is the builder's responsibility.

The architecture

A working bot is a loop over four stages:

  1. Discover — pull open tennis markets and their live prices from Polymarket's Gamma API (keyless, tag id 864).
  2. Match — link each market ("Alcaraz vs Sinner") to a live match in your data feed. Player-name matching is the genuinely hard part.
  3. Read state — for each matched live match, get the current score, server, and set state from the Live Tennis API.
  4. Decide + execute — compare the market price to your own read of the match and act. Execution is a separate, self-contained step you own.

The open-source toolkit livetennisapi/polymarket-tennis (MIT) handles stages 1 and 2 for you — it discovers tennis markets via Gamma, matches them to live matches, and joins market price with live score. It is observe-only: no wallets, no keys, no orders. Treat it as your reference starting point.

Step 1 — Discover markets from Gamma

Polymarket's public Gamma API needs no key. Tennis markets carry tag id 864. On 2026-08-18 there were 400+ live tennis moneyline markets open (Cincinnati Open singles, Challengers, ITF, US Open futures), so there is real, continuous market activity to work with.

import requests

GAMMA = "https://gamma-api.polymarket.com"

def open_tennis_markets(limit=100):
    r = requests.get(
        f"{GAMMA}/markets",
        params={"tag_id": 864, "closed": "false", "limit": limit},
        timeout=15,
    )
    r.raise_for_status()
    return r.json()

for m in open_tennis_markets():
    # outcomePrices are the live market prices for each outcome
    print(m["question"], m.get("outcomePrices"))

Each market gives you the question, the outcomes, and outcomePrices — the market's current price per outcome (a probability between 0 and 1). Gamma is the price source; you do not need a paid API for prices. The dedicated walkthrough is Find tennis markets with the Gamma API.

Step 2 — Match a market to a live match

A market says "Carlos Alcaraz vs Jannik Sinner". Your data feed says "Alcaraz C." vs "Sinner J.". Diacritics, initials, doubles teams, and qualifiers all break naive string equality. This matching step is where most home-grown bots quietly go wrong.

polymarket-tennis implements normalized name matching against live matches so you don't reinvent it:

pip install git+https://github.com/livetennisapi/polymarket-tennis.git
from polymarket_tennis import (
    GammaClient, LiveTennisClient,
    discover_tennis_markets, match_market, build_view,
)

with GammaClient() as gamma, LiveTennisClient() as lta:
    markets = discover_tennis_markets(gamma, market_types={"moneyline"},
                                      matches_only=True)
    candidates = lta.live_matches() + lta.fixtures()
    for market in markets:
        decision = match_market(market, candidates)   # None if ambiguous — never guessed
        if decision is None:
            continue
        view = build_view(market, decision.match)      # price + live score, no orders
        print(view.render())

If you'd rather build the matching yourself, the deep dive is Match a Polymarket market to a live match.

Step 3 — Read live state (free)

Now read what's actually happening on court. The Live Tennis API free tier covers the live read side: live matches and current scores, players with current ranking and Elo, and fixtures. Get a key at livetennisapi.com/subscribe/free — self-serve, no card. Auth is a header: X-API-Key: <key>.

Free tier limits are 30 requests/minute and 100/day, and it returns the current state of a live match — the live score right now, which is exactly what a signal loop needs.

import os, requests

API = "https://api.livetennisapi.com/api/public/v1"
HEADERS = {"X-API-Key": os.environ["LIVE_TENNIS_API_KEY"]}

def live_matches():
    r = requests.get(f"{API}/matches", params={"status": "live"},
                     headers=HEADERS, timeout=15)
    r.raise_for_status()
    return r.json()["data"]   # list endpoints return {"data": [...], "meta": {...}}

def score(match_id):
    r = requests.get(f"{API}/matches/{match_id}/score",
                     headers=HEADERS, timeout=15)
    r.raise_for_status()
    return r.json()

for m in live_matches():
    print(m["id"], score(m["id"]))

The free read side across curl, Python, and JavaScript is covered in Free live tennis scores for a trading bot.

Step 4 — The decision loop

Now assemble the pieces: for each matched pair, compare the market price to your own read of the live match, and decide. What "decide" means is entirely your logic — this tutorial deliberately does not tell you what to trade.

import time
from polymarket_tennis import (
    GammaClient, LiveTennisClient,
    discover_tennis_markets, match_market, build_view,
)

def poll_once(gamma, lta):
    markets = discover_tennis_markets(gamma, market_types={"moneyline"},
                                      matches_only=True)
    candidates = lta.live_matches() + lta.fixtures()   # one live-board read per poll
    for market in markets:
        decision = match_market(market, candidates)
        if decision is None:
            continue                        # ambiguous or no live match — skip
        view = build_view(market, decision.match)   # market price + live score
        # YOUR logic here: turn `view` into a signal however you decide.
        signal = evaluate(view)             # you define evaluate()
        if signal:
            print("signal:", view.render(), signal)
            # execute(signal)  <-- your responsibility; see below

with GammaClient() as gamma, LiveTennisClient() as lta:
    while True:
        poll_once(gamma, lta)
        time.sleep(60)   # stay within 30 req/min and 100 req/day on the free tier

Watch your request budget. On the free tier, a once-a-minute poll with a handful of matches stays under 30/min, but 100/day is tight for all-day running — if you need more headroom, Basic ($9.99/mo) raises limits to 60/min and 1k/day and adds completed-match history and point-by-point tape. Pro ($29.99/mo) adds market prices/events and the rank-ordered rankings listing; Ultra ($99.99/mo) adds win-probability analysis, live per-point events, and a WebSocket stream.

Vibe-code it: the one-prompt version (Claude Code / Cursor)

If you would rather not write Steps 1–4 by hand, this is the single prompt we paste into Claude Code in an empty folder, with a free key in LIVETENNIS_API_KEY:

Build me a Python tennis market watcher on top of the `polymarket-tennis` package
(pip install polymarket-tennis; docs: https://github.com/livetennisapi/polymarket-tennis).
1. Use GammaClient + discover_tennis_markets(market_types={"moneyline"}, matches_only=True)
   to list open Polymarket tennis markets, and LiveTennisClient (key from the env var
   LIVETENNIS_API_KEY) to fetch lta.live_matches() + lta.fixtures().
2. For each market call match_market(market, candidates); skip None (never guess).
3. Build view = build_view(market, decision.match) and, once per minute (free tier:
   30 req/min, 100 req/day — stay under it), log: market question, both outcome prices,
   the live score line, who is serving, the break-point flag, and both staleness ages.
4. Keep a local JSON "paper book": when the favourite is facing a break point, record a
   PAPER entry {time, market, side, price}; when the game resolves, record the price
   move. Paper only — print a loud banner that no real orders are ever sent.
5. If the match's `outcome` becomes "retired" or "walkover" print the venue's own
   settlement text from the market `description` (do NOT hard-code a payout rule).
6. Add a README, a requirements.txt, and tests that run offline with fixtures.
Observe-only. No wallets, no keys other than the tennis API key, no order code.

What came out of that prompt is checked in, unedited except for lint, at examples/claude-code-watcher: a 222-line watcher.py, a paper_book.py, 7 offline tests (all green), ruff clean, and a --once --fixtures dry run that prints the joined view without a key:

############################################################
#  PAPER BOOK ONLY — NO REAL ORDERS ARE EVER SENT.         #
############################################################
cadence 60s, 2 Live Tennis API requests per poll; stopping after 96 requests (free tier: 30/min, 100/day).
[2026-08-23T14:27:57+00:00] Cincinnati Open: Jiri Lehecka vs Arthur Fils
  prices:     Jiri Lehecka 0.095 | Arthur Fils 0.905
  score:      4-6 3-4 (15-40)
  serving:    Jiri Lehecka
  break pt:   True

Two honest corrections the model had to make to the prompt, both worth knowing before you run yours: the prompt's "once a minute" with live_matches() + fixtures() is 2 requests per poll = 120/hour, which blows the free tier's 100/day, so the watcher caps itself at 96 requests and documents --interval 300; and the market's settlement text lives in the raw Gamma description (the toolkit's TennisMarket exposes it via raw), which is exactly why the rule is read, never hard-coded — see the verbatim rules per venue.

Where execution plugs in

Everything above is read-only and observe-only. Placing a trade — wallets, private keys, signing, and the CLOB order client — is a separate concern that you own. Polymarket publishes the official py-clob-client for order placement; that library, your keys, and your risk limits are your responsibility. Neither this article nor polymarket-tennis places orders, holds funds, or gives financial advice, and no data feed guarantees a profitable trade.

Keep execution isolated behind your own execute() function so the read pipeline (which is the reusable, testable part) never touches keys.

Before you go live

Next steps

Start with a free key and the polymarket-tennis repo, get the read loop printing signals, and add execution only once the pipeline is proven.

Frequently asked questions

What do I need to build a Polymarket tennis trading bot?

Four things: Polymarket's free Gamma API for market discovery and prices (tennis tag id 864), a way to match markets to live matches, a live tennis data feed for current scores (the Live Tennis API free tier covers this), and your own decision and execution logic. Only execution requires wallets and keys.

Is there an API for Polymarket tennis prices?

Yes. Polymarket's public Gamma API at https://gamma-api.polymarket.com is keyless and returns open tennis markets and their live prices when you filter by tag id 864. You do not need a paid API for market prices; a separate tennis data API supplies live on-court scores.

Can a Polymarket trading bot be fully automated?

The read-and-signal side can be fully automated: discovering markets, matching them to live matches, and polling live scores all run in a loop with public and free-tier APIs. Order execution is a separate step you build yourself using Polymarket's official py-clob-client, your wallet, and your own risk limits.

Is it free to start building a Polymarket tennis bot?

Yes. Polymarket's Gamma API is free and keyless, and the Live Tennis API free tier (30 req/min, 100 req/day, no card) gives live matches and current scores. The open-source polymarket-tennis toolkit that discovers and matches markets is MIT-licensed and observe-only.

Does the polymarket-tennis toolkit place trades?

No. livetennisapi/polymarket-tennis is observe-only: it discovers tennis markets via Gamma, matches them to live matches, and joins market price with live score. It never touches wallets, keys, or orders. Execution is the builder's own responsibility via Polymarket's py-clob-client.

Built with the Live Tennis API — real-time scores, players, odds and model win-probability for ATP, WTA, Challenger and ITF.

API reference SDKs on GitHub Plans from $9.99/mo