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:
- Discover — pull open tennis markets and their live prices from Polymarket's Gamma API (keyless, tag id
864). - Match — link each market ("Alcaraz vs Sinner") to a live match in your data feed. Player-name matching is the genuinely hard part.
- Read state — for each matched live match, get the current score, server, and set state from the Live Tennis API.
- 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
- Dry-run first. Log every signal for a full session and check the matching is correct before wiring
execute()to anything real. - Handle stale data. If a score hasn't updated, don't act on it — gate on freshness.
- Respect rate limits. A backoff on HTTP 429 keeps you inside your tier.
Next steps
- How to trade tennis on Polymarket (2026): markets to data — the manual map of the site.
- Can you trade tennis on Polymarket? (2026 developer guide) — the background before you build.
- Find tennis markets with the Gamma API — deep dive on stage 1.
- Match a Polymarket market to a live match — deep dive on stage 2.
- Free live tennis scores for a trading bot — the free read side in Python & JavaScript.
- Polymarket & Kalshi tennis retirement/walkover rules (2026) — the edge cases that settle a market without a final score, quoted verbatim per venue.
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.