DEV Community

Cover image for I Vibe-Coded a Polymarket Tennis Market Watcher With One Prompt in Claude Code
Muhammad Furqan Ul Haq
Muhammad Furqan Ul Haq

Posted on

I Vibe-Coded a Polymarket Tennis Market Watcher With One Prompt in Claude Code

I ran the open-source polymarket-tennis kit through Claude Code using one-prompt build, during the US Open. Below is the exact prompt, what got built, what broke, and what the output looked like against real Polymarket tennis markets.

Note: Everything here is observe-only, paper-trading only — no real orders are placed anywhere in this project, and no profit is claimed or implied.

The kit

livetennisapi/polymarket-tennis is an MIT-licensed Python package with two clients:

  • GammaClient — keyless access to Polymarket's public Gamma API, with a discover_tennis_markets() helper that filters to real-match moneyline markets only (no futures, no outrights).
  • LiveTennisClient — a client for Live Tennis API: live scores, fixtures, and match state across ATP, WTA, Challenger, and ITF. Free tier: 30 requests/minute, 100/day, no card required.

The README includes a "Vibe-code a tennis market watcher with Claude Code (or Cursor)" section with a copy-pasteable prompt. I used the one-prompt to see what Claude Code would produce on its own.

The prompt

This is the exact prompt I pasted into the Claude Code terminal:

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).
Requirements:
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; free key at https://livetennisapi.com/subscribe/free) 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.
Enter fullscreen mode Exit fullscreen mode

No follow-up prompts were used.

What Claude Code did

Before writing code, Claude Code fetched the GitHub repo and read the installed package's source instead of relying on the README alone. It found the repo's own reference build (examples/claude-code-watcher/) with documented gotchas, used it to check field names, then wrote its own implementation.

Screenshots:

Gotchas it identified and handled:

  • match_market() returns a MatchDecision; its .match field is a plain dict, not a typed object.
  • match.outcome is not always populated — falls back to event_status ("Retired", "Walk Over", etc.).
  • TennisMarket has no .description attribute. The raw Gamma payload is on .raw, so settlement text is read from market.raw["description"] with a rules_secondary fallback — never hard-coded.
  • "Favourite facing a break point" only makes sense relative to who's serving. The server is mapped to a market outcome by folded-name equality; if names don't line up, no entry is recorded.
  • live_matches() + fixtures() is two API requests per poll, not one. At a 60-second cadence that's 120 requests/hour against a 100/day free-tier cap. Claude Code caught this and added a --daily-budget flag (default 96) that stops the watcher before it exceeds the limit.

Two core files came out of it: watcher.py (discovery, matching, logging, request-budget guard) and paper_book.py (a local JSON ledger — no exchange client, no wallet, no order code).

Project folder showing watcher.py, paper_book.py, tests, README, requirements.txt, and .gitignore

A self-check that failed on itself

The test suite scans the source for banned strings (wallet, private_key, place_order, clob, web3), to make "no order code" a build-breaking check instead of a comment. That test then failed on Claude Code's own docstrings, which used the word "wallet" to explain that there isn't one:

The test itself is too literal — my docstring's "no wallet" line trips its
own banned-word scan. Let me reword the docstring rather than weaken the test.
Enter fullscreen mode Exit fullscreen mode

It reworded the docstrings and kept the test as-is. The banner that prints on every run, live or offline:

##################################################################
#  PAPER BOOK ONLY -- NO REAL ORDERS ARE EVER SENT.             #
#  This program only observes public prices and live scores     #
#  and appends to a local JSON file. It has no exchange client,  #
#  no private keys, and no order-placement code at all.          #
##################################################################
Enter fullscreen mode Exit fullscreen mode

Running it

Open the project folder in a terminal, then set up a virtual environment and install dependencies:

python -m venv .venv

.venv\Scripts\activate        # Windows; use `source .venv/bin/activate` on macOS/Linux

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Offline first, against the kit's own fixtures — no API key, no network:

python watcher.py --once --fixtures tests/fixtures
Enter fullscreen mode Exit fullscreen mode

Terminal output showing the PAPER BOOK ONLY banner and fixture-backed market rows

Then I tried the live path without setting the API key environment variable first, which — correctly — refused to run:

polymarket_tennis.livetennis.MissingAPIKeyError: No Live Tennis API key found.
Set the LIVETENNIS_API_KEY environment variable. Free keys: https://livetennisapi.com/subscribe/free
Enter fullscreen mode Exit fullscreen mode

Get a free API key

A free key from livetennisapi.com/subscribe/free takes about a minute to get:

  1. Enter your email
  2. Pass the Cloudflare check if prompted
  3. Click "Get my key"
  4. You will receive a confirmation email to verify the email address and reveal your key. Click the button "Confirm and get my key" provided in the email to proceed.
  5. You will be redirected to a web page where you will see the API key twjp_... to set as LIVETENNIS_API_KEY. Copy the key and store it securely.

Live Tennis API free-tier signup page

You can set the API key in your project using below command:

$env:LIVETENNIS_API_KEY = "YOUR_API_KEY"     # Windows
Enter fullscreen mode Exit fullscreen mode
export LIVETENNIS_API_KEY="YOUR_API_KEY"     # Linux/macOS
Enter fullscreen mode Exit fullscreen mode

With the key set, run the following command to poll once and then exit:

python watcher.py --once
Enter fullscreen mode Exit fullscreen mode
[2026-08-26T09:50:15+00:00] US Open, Qualification ATP: Frederico Ferreira Silva vs Jurij Rodionov
  prices:     Frederico Ferreira Silva 0.365 | Jurij Rodionov 0.635
  score:      (no score yet)
  serving:    -
  break pt:   False
  staleness:  market 206s, live 0s
[2026-08-26T09:50:15+00:00] ITF M15 Cap d'Agde Men: Timeo Trufelli vs Liam Branger
  prices:     Timeo Trufelli 0.700 | Liam Branger 0.300
  score:      7-6 2-2 (0-0)
  serving:    Liam Branger
  break pt:   False
  staleness:  market 86s, live 134s
-- 84 moneyline markets, 8 confidently matched --
Enter fullscreen mode Exit fullscreen mode

84 moneyline markets discovered, 8 confidently matched. The rest were skipped because match_market() returned None — per requirement #2, unmatched markets are dropped, not guessed at.

8/8 offline tests pass, lint is clean. All of this came from one prompt plus Claude Code's own investigation of the package source — no manual code edits.

Note: Alternatively, you can also run the following command to poll every 60s until the daily budget runs out

python watcher.py
Enter fullscreen mode Exit fullscreen mode

What this is not

  • No order-placement code exists anywhere in the project. This is enforced by a test that scans the source, not just stated in a comment.
  • The "paper book" is a local JSON file recording what a break-point entry would have looked like and how the price moved after. It is not a trading strategy, a backtest, or a profit claim.
  • The only credential used is a free Live Tennis API key, for reading public live scores. No Polymarket wallet or private key is involved at any point.

pmtennis arena replay, part of the same package, produces a paper-P&L leaderboard from historical data if you want to take this further. Still paper, still not financial advice.

Conclusion

One prompt produced a working Polymarket Tennis market watcher: market discovery, live-match matching with no guessing on ambiguous pairs, a paper book that only logs at break points, and a settlement-text path that reads the venue's own wording instead of a hard-coded rule.

The result is a paper-only tool. It doesn't place orders, doesn't touch a wallet, and doesn't produce a trading edge on its own. What it shows is that a single, well-specified prompt against a well-documented package can get you a tested, working project in one pass, provided the prompt is explicit about what the code must never do.

Reference

Top comments (0)