To match a Polymarket tennis market to a live match, normalize both player names — strip accents, lowercase, collapse "F. Lastname" and "Lastname F." to a canonical form — then require both players to align before you pair a market with a live score. Never pair on a single name.
This is the join that every Polymarket tennis trading bot needs and the step that quietly breaks most of them. Polymarket's Gamma API gives you market prices keyed by human-typed player names ("Jiri Lehecka vs Arthur Fils"). A live-score feed keys the same match by its own player records. Nothing shares an ID across the two systems, so you have to resolve identity from names — and names are messy.
Why name matching is the hard part
The same player shows up written many different ways:
- Accents and transliteration —
LeheckavsLehečka,DjokovicvsĐoković. - Initial formats —
Arthur Fils,A. Fils,Fils A. - Name order —
Coco GauffvsGauff Coco. - Doubles, qualifiers and futures — a "Will Sinner win the US Open?" futures market has one name and no live match to pair with at all.
A naive market_name == feed_name comparison misses almost everything; a loose substring match creates false pairs (matching the wrong Fils). The safe rule is stricter than either: canonicalize each name, then only accept when both sides of the market map to the two players in one live match. If it is ambiguous, return nothing.
Prerequisites
- Python 3.10+
- A free Live Tennis API key — grab one at livetennisapi.com/subscribe/free (no card). The live-score side is on the free tier:
GET /matches?status=liveandGET /players/{id}. - Polymarket's Gamma API needs no key.
export LIVETENNIS_API_KEY=ltapi_...
Step 1 — Pull the two sides
Fetch open tennis markets from Gamma (tennis carries tag id 864) and live matches from the Live Tennis API. See the exact response fields in the docs.
import os, httpx
GAMMA = "https://gamma-api.polymarket.com"
LTA = "https://api.livetennisapi.com/api/public/v1"
KEY = os.environ["LIVETENNIS_API_KEY"]
def gamma_tennis_markets():
r = httpx.get(f"{GAMMA}/markets",
params={"tag_id": 864, "closed": "false", "limit": 200})
r.raise_for_status()
return r.json()
def live_matches():
r = httpx.get(f"{LTA}/matches",
params={"status": "live"},
headers={"X-API-Key": KEY})
r.raise_for_status()
return r.json()
A per-match moneyline market's two outcomes are the two player names — that pair is what you match against a live match's two players.
Step 2 — Canonicalize a name
Reduce every name to a comparable key: drop accents, lowercase, and normalize initials to firstinitial + lastname.
import unicodedata, re
def strip_accents(s: str) -> str:
return "".join(c for c in unicodedata.normalize("NFKD", s)
if not unicodedata.combining(c))
def canon(name: str) -> str:
"""'Jiří Lehečka' -> 'jlehecka'; 'Fils A.' -> 'afils'."""
s = strip_accents(name).lower()
s = re.sub(r"[^a-z\s.]", " ", s)
parts = [p.strip(".") for p in s.split() if p.strip(".")]
if not parts:
return ""
# Move a lone initial to the front: 'fils a' -> ['a','fils']
if len(parts[-1]) == 1 and len(parts[0]) > 1:
parts = [parts[-1]] + parts[:-1]
first, last = parts[0], parts[-1]
return (first[0] + last) if last != first else last
canon("Jiří Lehečka"), canon("J. Lehecka") and canon("Lehecka J.") all collapse to jlehecka. This is deliberately conservative — first initial plus last name — because full first names are the field most likely to differ between sources.
Step 3 — Pair only when BOTH players agree
Build a canonical key for each live match (a frozenset of its two players), then match a market only when its two outcome names produce the identical set. The frozenset makes order irrelevant, and requiring an exact two-name set is what stops false pairs.
def match_key(name_a: str, name_b: str):
return frozenset({canon(name_a), canon(name_b)})
def index_live(matches):
idx = {}
for m in matches:
a, b = m["home"]["name"], m["away"]["name"] # see docs for fields
idx[match_key(a, b)] = m
return idx
def match_market(market, live_index):
outcomes = market.get("outcomes")
if isinstance(outcomes, str):
import json; outcomes = json.loads(outcomes)
if not outcomes or len(outcomes) != 2:
return None # futures / non-moneyline: skip
key = match_key(outcomes[0], outcomes[1])
if len(key) != 2:
return None # both names collapsed the same: ambiguous
return live_index.get(key) # None = no live counterpart, don't guess
live_index = index_live(live_matches())
for mkt in gamma_tennis_markets():
hit = match_market(mkt, live_index)
if hit:
print(mkt["question"], "->", hit["id"])
Returning None for anything ambiguous is the whole point: a trading bot that acts on a wrong pairing reads the wrong score for the wrong market. Silence beats a confident mismatch.
Step 4 — Use the maintained matcher
The edge cases multiply — doubles teams, mid-tournament name variants, futures markets with no live match, players whose surnames collide within one event. The open-source polymarket-tennis toolkit (MIT) packages this resolution so you don't re-derive it. It discovers tennis markets via Gamma, matches them to live matches, and refuses to guess when a pairing is ambiguous.
pip install "polymarket-tennis @ git+https://github.com/livetennisapi/polymarket-tennis"
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)
if decision is None:
continue # ambiguous or no live counterpart
print(build_view(market, decision.match).render())
Or inspect one market's matching decision from the CLI:
pmtennis match atp-lehecka-fils-2026-08-17
Wrap-up
Identity resolution is the join that makes market price and live score meaningful together. Canonicalize names, require both players to agree, and treat ambiguity as "no match" rather than a coin flip. The toolkit above is observe-only — it reads public market data and live scores and never places orders. Order execution, wallets and keys are yours to build on Polymarket's own official interfaces. Next, join the matched price and score into one view in the pillar build guide.