Build observe-only Polymarket and Kalshi tennis market tooling on the polymarket-tennis Python package (MIT) plus the Live Tennis API free tier. Use when asked for a Polymarket tennis bot or market watcher, a Kalshi tennis trading bot, tennis prediction-market data, Gamma API tennis markets, matching a market to a live match, break-point or serving state next to market prices, or how a tennis retirement, walkover, or cancelled match settles (venue rule text, never hard-coded). Covers the real...
280 stars
0 votes
0 copies
1 view
Added September 19, 2026
developmentpythongobashrailsgitapi
Works with
cli
api
mcp
Security analysis
A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Polymarket Tennis?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/livetennisapi-polymarket-tennis)
---
name: polymarket-tennis
description: Build observe-only Polymarket and Kalshi tennis market tooling on the polymarket-tennis Python package (MIT) plus the Live Tennis API free tier. Use when asked for a Polymarket tennis bot or market watcher, a Kalshi tennis trading bot, tennis prediction-market data, Gamma API tennis markets, matching a market to a live match, break-point or serving state next to market prices, or how a tennis retirement, walkover, or cancelled match settles (venue rule text, never hard-coded). Covers the real package API (GammaClient, LiveTennisClient, discover_tennis_markets, match_market, build_view, pmtennis CLI), the free-tier request budget (30/min, 100/day), the outcome and event_status fields, and the verbatim 2026 settlement matrix. No order execution, wallets, or strategy advice.
license: MIT
compatibility: Python 3.10+, pip install polymarket-tennis (single runtime dependency httpx). Live match data needs a free Live Tennis API key in LIVETENNIS_API_KEY; Polymarket Gamma and Kalshi market reads are keyless.
metadata:
author: Live Tennis API (hello@livetennisapi.com)
version: "0.1.0"
homepage: https://github.com/livetennisapi/polymarket-tennis
attribution: Vendor-authored. The live-score side of every join comes from the Live Tennis API; judge accordingly.
---
# Polymarket / Kalshi tennis trading data
> **Vendor-authored, observe-only.** This skill is maintained by the team behind the
> [Live Tennis API](https://livetennisapi.com). It teaches the `polymarket-tennis`
> package, which reads public market data and live scores. It contains no order
> execution, no wallet or private-key handling, no CLOB client, and no strategy
> advice. Execution is permanently out of scope. Nothing here is financial advice.
## Hard guardrails (apply to every file you write with this skill)
1. **Observe-only.** Never add order placement, wallet, private-key, or CLOB code to
anything built on this package. If the user wants execution, it belongs in their
own code behind a clearly separated seam, using the venue's own official
interfaces, and this skill does not write it.
2. **Never hard-code a settlement rule.** Retirement and walkover payouts differ by
venue (polymarket.com vs Polymarket US vs Kalshi) and by tour (ATP/WTA vs ITF).
Read the market's own text — Gamma `description` (`market.raw["description"]`),
Kalshi `rules_secondary` — and print it. The reference matrix in
[references/settlement-rules.md](references/settlement-rules.md) is for the
human, not for code branches.
3. **Respect the free tier: 30 requests/minute, 100 requests/day.** Every
`LiveTennisClient` call costs one request; Gamma and Kalshi reads cost nothing.
`pmtennis watch` is 1 request per poll at a 60 s default (minimum 30 s). A
watcher that calls `live_matches()` + `fixtures()` per poll costs 2 per poll
and must self-cap (the reference build caps at 96/day, `--interval 300`).
4. **Detect match endings with `outcome` and `event_status`, not `status`.**
`status` is only the lifecycle (`upcoming|live|completed|cancelled`).
`outcome` is `completed|retired|walkover|default|abandoned` and `null` until
settled; `event_status` is the feed designator (`Retired`, `Walk Over`,
`Cancelled`, `Postponed`, `Interrupted`); `withdrew` names who stopped.
5. **Never guess a market-to-match pairing.** `match_market` returns `None` on
ambiguity; skip it. Use `override_match_id` / `--match-id` only when the user
supplies the id explicitly.
6. **Tests stay offline.** Use trimmed fixtures and `httpx.MockTransport`; never
put live calls in tests.
## Quick start
```bash
pip install polymarket-tennis # Python 3.10+, depends only on httpx
pmtennis discover --matches-only --moneyline-only # keyless, Gamma only
export LIVETENNIS_API_KEY=ltapi_... # free, no card: https://livetennisapi.com/subscribe/free
pmtennis match atp-lehecka-fils-2026-08-17 # show the pairing decision + confidence
pmtennis watch atp-lehecka-fils-2026-08-17 # 1 request per poll, 60 s default
```
Env-var names differ between the two Live Tennis API tools: the Python package reads
`LIVETENNIS_API_KEY`; the `livetennisapi-mcp` server/plugin reads `LIVETENNISAPI_KEY`.
Same key value works in both.
Library form — every symbol below is exported from `polymarket_tennis.__init__`:
```python
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() # 2 free-tier requests
for market in markets:
decision = match_market(market, candidates)
if decision is None:
continue # ambiguous or no live counterpart — never guessed
view = build_view(market, decision.match)
print(view.render())
```
Sample `view.render()` output:
```text
Cincinnati Open: Jiri Lehecka vs Arthur Fils [atp-lehecka-fils-2026-08-17]
market: Jiri Lehecka 0.095 | Arthur Fils 0.905 (as of 12s ago)
live: Jiri Lehecka vs Arthur Fils 4-6 3-4 (15-40) serving: Jiri Lehecka [BREAK POINT] (as of 8s ago)
```
## The package, in one screen
| Symbol | What it does | Network cost |
|---|---|---|
| `GammaClient()` | Polymarket Gamma API (keyless): `events()`, `event_by_slug()`, `market_by_id()`, `market_by_slug()`, `market()` | none against your key |
| `LiveTennisClient(api_key=None)` | Live Tennis API; key from `LIVETENNIS_API_KEY`: `matches()`, `live_matches()`, `match(id)`, `fixtures()`, `players(search)` | 1 request per call |
| `discover_tennis_markets(client, market_types=None, include_closed=False, matches_only=False, limit=100)` | normalized `TennisMarket` list from the `tennis` tag | Gamma only |
| `find_market(client, id_or_slug)` | one `TennisMarket` by Gamma id, market slug, or event slug | Gamma only |
| `match_market(market, candidates, override_match_id=None, threshold=0.70, ambiguity_margin=0.10)` | `MatchDecision` or `None` | pure |
| `build_view(market, match)` | `LiveMarketView` with both feeds' staleness | pure |
| `derive_break_point(score)`, `score_line(score)` | helpers used by the view | pure |
| `TennisMarket.price_by_outcome`, `.slug_date`, `.raw` | prices dict, slug date, the raw Gamma object (settlement text lives in `raw["description"]`) | — |
Full signatures, dataclass fields, and the live-match JSON shape are in
[references/api.md](references/api.md). Read it before writing code that touches
fields not shown above — in particular, `TennisMarket` has **no** `description`
attribute and `LiveMarketView` does **not** expose `outcome`; go through `.raw`
and `.match`.
## Workflow: build a watcher or bot data layer
1. **Discover** with `discover_tennis_markets(gamma, market_types={"moneyline"}, matches_only=True)`.
Futures/outrights are dropped by `matches_only`; doubles markets are rejected by
the matcher in v0.1.
2. **Fetch candidates once per poll**: `lta.live_matches()` (+ `lta.fixtures()` if
you need pre-start matches). Count the requests; budget them against 100/day.
3. **Pair** each market with `match_market(market, candidates)`; skip `None`.
Log `decision.confidence` and `decision.method` (`"explicit" | "names+date" | "names"`).
4. **Join** with `build_view(market, decision.match)`; read `view.prices`,
`view.score_line`, `view.server` (1/2), `view.break_point`, `view.is_tiebreak`,
`view.event_status`, and both `view.market_staleness()` / `view.live_staleness()`.
5. **Settlement branch**: read `view.match.get("outcome")` and
`view.match.get("event_status")`; when `outcome in ("retired", "walkover",
"default", "abandoned")` or `event_status == "Cancelled"`, print
`view.market.raw.get("description")` verbatim (fall back to `rules_secondary`,
then say the payload carries no rule text). Do not compute a payout.
6. **Paper only**: any "signal" the user asks for is logged to a local JSON book
with a loud banner that no orders are sent. See the verified reference build.
```python
def settlement_text(view) -> str:
raw = view.market.raw
text = raw.get("description") or raw.get("rules_secondary")
return text or "(market payload carries no settlement text; read it on the venue)"
```
## Kalshi
The package discovers **Polymarket** markets (Gamma). Kalshi publishes each market's
rule text in its public read endpoint, keyless; the same `outcome`/`event_status`
detection applies. Use the package's `LiveTennisClient` for the live side and read
Kalshi directly for the market side (see
[references/settlement-rules.md](references/settlement-rules.md) for the Kalshi
snippet and the ATP/WTA vs ITF series difference). Do not hard-code the ITF `$0.50`
rule; print `rules_secondary`.
## One-prompt build (verified output checked in)
When the user wants a watcher "vibe-coded" end to end, use the prompt in
[references/one-prompt-build.md](references/one-prompt-build.md). It is the exact
prompt from the package README; what it produced, unedited except for lint, is in
`examples/claude-code-watcher/` of the repository (7 offline tests, `ruff` clean,
`--once --fixtures` dry run without a key). That README also lists the honest
deviations — read them before re-running the prompt, because two of them are
traps: the prompt's "once a minute" costs 2 requests per poll (so the watcher must
self-cap), and the offline fixtures carry `event_status` but no `outcome` key.
## Retirement / walkover rule matrix (retrieved 2026-08-23)
The short version, for the human reading this — the code must still print the
market's own text:
| Scenario | Polymarket (polymarket.com) | Polymarket US | Kalshi (ATP/WTA) | Kalshi (ITF) |
|---|---|---|---|---|
| Walkover / withdrawal before the match starts | 50-50 | Last fair market price at announcement | Fair price per rules | $0.50 per contract |
| Match cancelled, not played | 50-50 | Last fair market price | Fair price per rules | $0.50 |
| Retirement after play starts (injury, default, DQ) | Advancing player wins | Awarded winner settles $1.00 | Winner resolves Yes ("after a ball has been played") | Winner Yes; withdrawing/forfeiting player No |
| Delayed / postponed | 50-50 if beyond 7 days with no winner | — (see venue FAQ) | Stays open, closes after rescheduled match (within two weeks) | Same as ATP/WTA |
| What counts as "started" | "the match begins" | First serve is struck | A ball has been played | A ball has been played |
Verbatim venue quotes, source URLs, retrieval dates, and the live-data mapping
(`outcome`/`event_status`/`withdrew` to each venue column) are in
[references/settlement-rules.md](references/settlement-rules.md). Rules are per
market and can change; the market's own text always wins over that file.
## Verification checklist before you hand code back
- `ruff check` clean; tests run with no network (`httpx.MockTransport` or fixture files).
- Every import resolves to a symbol listed in `references/api.md`.
- Request count per poll is computed and documented against 30/min, 100/day.
- No payout number appears in code; the venue text is printed instead.
- Match-end detection reads `outcome` / `event_status`, never `status == "completed"` alone.
- A banner states that no orders are ever sent.
## Further reading
- Package: https://github.com/livetennisapi/polymarket-tennis (PyPI `polymarket-tennis`)
- Pillar guide: https://blog.livetennisapi.com/blog/build-polymarket-tennis-trading-bot
- Verbatim rules: https://blog.livetennisapi.com/blog/polymarket-kalshi-tennis-retirement-walkover-rules
- Kalshi walkthrough: https://blog.livetennisapi.com/blog/kalshi-tennis-trading-bot
- Live Tennis API docs: https://docs.livetennisapi.com — free key: https://livetennisapi.com/subscribe/free
- Sibling skill (entry gate, simmer-sdk style): https://github.com/livetennisapi/simmer-tennis-live-gate
- MCP server for the same data: https://github.com/livetennisapi/livetennisapi-mcp