polymarket-tennis: market discovery and score matching for Polymarket tennis bots
Polymarket tennis trading toolkit (Python, MIT, observe-only): discover tennis markets on the Gamma API, match them to live tennis matches, watch market price vs live score, server and break point. The data layer for a Polymarket or Kalshi tennis trading bot.
At a glance
- What is it?
- A vendor-authored, observe-only Python toolkit that finds tennis markets on Polymarket's Gamma API, refuses to guess when matching them to live matches, and joins prices with score state. It is the data layer, not the bot.
- Who is it for?
- polymarket-tennis is for Python developers building a Polymarket or Kalshi tennis bot who want the fiddly identity-matching problem solved and are comfortable with a vendor-authored dependency on the Live Tennis API. It is not for anyone expecting execution, strategy, or a self-contained data source: the package places no orders and never will, and the live-score side needs an API key with a free tier of 30 requests/minute and 100 requests/day.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 13 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 10, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The identity problem this package actually solves
Anyone who has tried to wire a tennis trading bot together hits the same wall before writing a single signal. Polymarket lists a market as an event slug such as atp-lehecka-fils-2026-08-17, while a live-score feed calls the same match something else, with its own player ordering, its own date handling, and its own idea of whether the match is a fixture or in progress. Joining the two by hand is where most of the plumbing time goes, and a silent mismatch is worse than no match at all because it feeds wrong prices into whatever logic sits downstream.
polymarket-tennis targets exactly that seam. It is for developers building a Polymarket or Kalshi tennis bot who want market discovery, market-to-match identity resolution, and a joined snapshot of price against live score, and who are willing to bring their own decisions and execution. The README is explicit that the package contains no order execution, no wallet or key handling, and no strategy advice, and that execution is out of scope permanently. That boundary is the most useful thing about it: it removes any ambiguity about what you are adopting.
The second thing worth knowing before you read further is the disclosure in the README. The toolkit is built and maintained by the team behind the Live Tennis API, and it joins Polymarket's public market data with that vendor's live-score feed. This is vendor-authored tooling, and the README says so plainly. Judge it on that basis.
Discovery, matching and the joined view: how the three modules fit
The package splits into three modules, and the split maps onto the data flow. The discovery module queries Polymarket's public Gamma API, which is keyless, filters by the tennis tag, and normalizes the results into plain Python objects carrying question, outcomes, prices, volume and end date. The README states that on 2026-08-18 a count found more than 400 open singles moneyline markets across ATP, WTA, Challenger and ITF events, alongside set-winner, games over/under and futures markets.
The matching module takes one of those market objects and a list of candidate matches, and returns a decision or None. The semantics are documented precisely, and they are stricter than most people would write by default. Confidence comes from folded full-name agreement, with diacritics stripped and punctuation collapsed, tried in both orientations so that reversed name order still resolves. That comparison is gated by the market slug's date against the match's scheduled date, with a tolerance of plus or minus one day, on the reasoning that matches often start after midnight UTC. Two near-equal candidates produce None. Only one player agreeing produces None. A date disagreeing by more than a day produces None. The design position is that ambiguity is returned, not resolved, and an explicit match-id override is always available when you know better.
The join module produces a LiveMarketView: a single snapshot holding the market question and outcome prices next to the live score line, server, break-point flag, and set and game state, with staleness timestamps for both feeds. Those two timestamps matter more than they look. The market price and the score arrive from different sources at different cadences, and the view tells you how old each side is rather than presenting them as simultaneous. The break-point flag has its own defined rules: receiver at AD, or receiver at 40 while the server is at 0, 15 or 30; never in tiebreaks; and False whenever server or points are null, which is the case for completed matches.
Installing polymarket-tennis and running a first watch
The package requires Python 3.10 or later and has a single runtime dependency, httpx. Install it from PyPI, or from source via the git URL the README gives.
pip install polymarket-tennis
# or from source:
pip install "polymarket-tennis @ git+https://github.com/livetennisapi/polymarket-tennis"The first command you can run needs no key at all, because discovery goes through the keyless Gamma API. It lists current tennis markets, filtered to per-match moneyline events.
pmtennis discover --matches-only --moneyline-onlyTo go further you need a Live Tennis API key, which the README points at a free subscription page. Export it as an environment variable. Note the prefix: the README shows keys beginning with ltapi_.
export LIVETENNIS_API_KEY=ltapi_...Before watching anything, inspect the matching decision for a single market. This is the step worth doing first, because it tells you whether the matcher resolved your market, refused it as ambiguous, or found no live counterpart.
pmtennis match atp-lehecka-fils-2026-08-17Then watch market price against live match state, one poll per minute by default.
pmtennis watch atp-lehecka-fils-2026-08-17The README shows the joined output as a two-line block: a header naming the event and slug, a market line with both outcome prices and an age, then a live line with the score, the current server, a break-point marker and its own age. If you would rather not use the CLI, the same pipeline is available as a library: GammaClient and LiveTennisClient as context managers, then discover_tennis_markets, match_market and build_view. The README's example skips any market where match_market returns None, with a comment stating it is never guessed.
The free-tier budget and why watch cadence is a design choice
The README gives budget arithmetic that is unusually candid for a vendor-authored package. The Live Tennis API free tier allows 30 requests per minute and 100 requests per day, and includes live scores with score, server and state, players with each player's current ranking, fixtures, and usage. The pmtennis watch command spends one Live Tennis API request per poll, because the market price comes from Gamma, which is keyless and does not touch the quota.
That produces a concrete ceiling. At the default 60-second cadence, watching consumes 60 requests per hour, which works out to roughly 100 minutes of watching per day on a free key. At a 300-second cadence, it is 12 requests per hour and roughly 8 hours. The README's own summary is that the free tier comfortably covers developing, testing and following a handful of tracked matches at a gentle cadence.
Read that as a real constraint rather than a footnote. If your intended use is continuous monitoring of many matches through a full tournament day, the free tier does not cover it, and the arithmetic above is the reason. The package does not attempt to hide this by batching or caching live-score calls; the one-request-per-poll model is stated directly. If you are prototyping, 60 seconds is fine for short sessions. If you are running something longer, the cadence is the dial you turn, and the table in the README is the place to check the consequence before you set it.
Where the matcher refuses, and why that is the point
The most consequential design decision here is that the matcher returns None instead of a best guess. Two near-equal candidates, one player agreeing, or a date off by more than a day all produce None. For a trading context that is the right default, because a wrong match does not fail loudly; it produces a plausible-looking price against the wrong score, and nothing downstream can detect that from the shape of the data.
The costs of that choice are real and should be counted. Markets that a human would resolve instantly, perhaps from context or a tournament schedule, will come back unresolved, and your code has to handle the None path explicitly. The README's library example does exactly that, skipping unresolved markets rather than forcing a match. If your pipeline assumes every discovered market yields a view, it will break on the first ambiguous one.
Doubles are the clearest gap. The README states that doubles markets are rejected in v0.1 because team-name matching is a separate problem, and that you must use --match-id or override_match_id explicitly if you need one. So the package's coverage is singles-first by construction, not by oversight, and doubles users are on the override path from day one. Retirement and walkover wording is handled by the matcher according to the README, and the break-point flag has its own edge cases: it is never set in tiebreaks, and it is False whenever server or points are null, which is what completed matches carry. Anyone building around break-point state needs to treat False as ambiguous between not-a-break-point and not-enough-information.
Swapping the score source, and what that buys you
The vendor relationship is the obvious objection to this package, and the README addresses it with an interface rather than an assurance. Matching and view code operate on plain dicts, never on a client object. The only place the pipeline touches a live-score source is a four-method surface captured by the LiveScoreProvider protocol: live_matches(), matches(), fixtures(), and match(match_id). LiveTennisClient is the default implementation and satisfies it out of the box.
To feed the matcher and view from somewhere else, you implement those four methods, returning dicts in the same shape (players.p1.name and players.p2.name, score, status, id), and hand your object to the same _load_candidates and _decide helpers. A network-free implementation ships as StaticLiveScoreProvider, which the offline tests use. The README credits issues #1 and #2 for raising the coupling, which is a reasonable signal that the abstraction was added in response to a real complaint rather than designed in from the start.
This is a genuine alternative to accepting the vendor feed, but be precise about what it does and does not cover. Replacing the score provider removes the Live Tennis API from the live-score side and with it the key and the request budget. It does not remove the Gamma API, which is the market side and is keyless anyway. And the matching heuristics, including the date gate and the ambiguity rules, are the package's own logic; they are not part of the provider protocol, so a different score source still goes through the same matcher with the same refusals. The protocol is a seam, not a plug-in ecosystem.
Maintenance, licence and the upgrade question
The repository is not archived, and the last push was on 2026-09-12. The only release listed is v0.1.0, dated 2026-08-18, and pyproject.toml carries version 0.1.0 with the classifier Development Status :: 4 - Beta. That combination describes a young package: one release, a beta classifier, and a short history. The README itself refers to v0.1 when explaining that doubles markets are rejected, which tells you the current behaviour is scoped to that first version.
The practical upgrade cost follows from the dependency surface. There is one runtime dependency, httpx>=0.24, which keeps the transitive risk small. The public surface a bot would build on is the three functions and two clients in the library example, plus the LiveScoreProvider protocol. The protocol is the part most likely to matter across versions, because it is the boundary the README presents as the supported extension point; anything that reaches past it into the _load_candidates and _decide helpers, which are named with a leading underscore in the README's own example, is working against internals rather than the documented interface.
On licensing, the package is MIT, stated in pyproject.toml with license = "MIT" and a LICENSE file at the repository root. MIT is permissive and places few conditions on reuse. Two things sit outside that licence and are worth separating: the Live Tennis API is a service with its own terms and a free tier of 30 requests per minute and 100 requests per day, and Polymarket's Gamma API is a third party's public interface. The MIT grant covers this package's code, not access to either service. That is a factual boundary, not legal advice; terms for the services are theirs to state.
What to check before you build on it
The package is honest about its scope, and the fastest way to decide is to test the matcher against your own markets rather than reading about it. Run pmtennis match on a set of slugs from the events you care about and count how many resolve. If your targets are singles moneyline markets at ATP and WTA level, the folded-name comparison and the plus or minus one day date gate should cover most of them. If you are looking at Challenger and ITF events, or at anything around midnight UTC, that date tolerance is the first thing to probe, because the README's own justification for it is exactly that boundary.
The second check is the provider seam. If you intend to use the Live Tennis API, the free-tier arithmetic in the README is your operating budget and you should pick a cadence before writing the polling loop. If you intend to bring your own scores, confirm that your source can populate the dict shape the README lists (players.p1.name, players.p2.name, score, status, id) for all four protocol methods, including fixtures, since the matcher draws candidates from live_matches() plus fixtures() in the README's example.
The third check is the one people skip: decide what your code does with None. The matcher will return it, deliberately, and the README's example treats that as a skip. Any design that assumes a resolved view for every discovered market is fighting the package's central design choice rather than using it.
Editorial conclusion
polymarket-tennis is for Python developers building a Polymarket or Kalshi tennis bot who want the fiddly identity-matching problem solved and are comfortable with a vendor-authored dependency on the Live Tennis API. It is not for anyone expecting execution, strategy, or a self-contained data source: the package places no orders and never will, and the live-score side needs an API key with a free tier of 30 requests/minute and 100 requests/day. Before adopting, verify two things against your own use case: whether the matcher's date gate of plus or minus one day holds for your timezone and event types, and whether your score source can satisfy the four-method LiveScoreProvider protocol. If you need doubles, v0.1 rejects those markets outright and you must pass an explicit match-id.
Frequently asked questions
Can you do sports on Polymarket?
The README describes tennis as a large, active category on Polymarket, and states that a count on 2026-08-18 found more than 400 open singles moneyline markets on the Gamma API across ATP, WTA, Challenger and ITF events, plus set-winner, games over/under and futures markets alongside them. This package is built to discover those markets and match them to live scores.
Can you bet on tennis matches?
Polymarket lists tennis event markets that this toolkit can discover through the keyless Gamma API, including per-match events, futures and doubles, normalized to objects with question, outcomes, prices, volume and end date. The package itself does not place bets: the README states it contains no order execution, no wallet or key handling, and no strategy advice, and that execution is out of scope permanently.
What happens if a tennis player retires on Polymarket?
The README does not state Polymarket's settlement rules for retirement. What it does document is that the matcher handles retirement and walkover wording when pairing a market with a match, and that break-point flags are False whenever server or points are null, which is the case for completed matches.
What is the best app for tennis predictions?
The README does not compare prediction apps or make recommendations. It describes this package as an observe-only data layer that joins Polymarket market prices with live score state, and leaves signals, execution and risk to your own code.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/livetennisapi-polymarket-tennis)