Originally published on BotCookery, free build recipes for AI-built trading and prediction-market bots. Written with AI assistance. Every output below is from real runs against the public Polymarket US gateway, on Oct 25, 2026 (the rebuild) and Oct 30, 2026 (a re-run of the rebuilt code). No API key was used, the code can't place an order, and nothing here is financial advice.
The old version of our Polymarket US prompt told the AI to "stay well under 25 requests/second" and to back off 1 s, 2 s, then 4 s on HTTP 429. That sounds careful. On the live gateway it fell apart in the first loop: 28 of 138 CLI calls crashed with HTTPError: 429 Client Error: Too Many Requests.
This morning we measured the limit directly, at a polite one request per second on a single order book:
13:59:28 call 1 status 200 ct=application/json retry-after=None body='{"marketData":{"marketSlug":"paccc-usho-'
...
13:59:32 call 6 status 200 ct=application/json retry-after=None body='{"marketData":{"marketSlug":"paccc-usho-'
13:59:33 call 7 status 429 ct=text/html; charset=UTF-8 retry-after=10 body='<!doctype html>\n<!--[if lt IE 7]> <html '
13:59:34 call 8 status 429 ct=text/html; charset=UTF-8 retry-after=9 body='<!doctype html>\n<!--[if lt IE 7]> <html '
...
13:59:43 call 17 status 429 ct=text/html; charset=UTF-8 retry-after=0 body='<!doctype html>\n<!--[if lt IE 7]> <html '
13:59:44 call 18 status 200 ct=application/json retry-after=None body='{"marketData":{"marketSlug":"paccc-usho-'
...
13:59:48 call 22 status 200 ct=application/json retry-after=None body='{"marketData":{"marketSlug":"paccc-usho-'
13:59:50 call 23 status 429 ct=text/html; charset=UTF-8 retry-after=10 body='<!doctype html>\n<!--[if lt IE 7]> <html '
Six calls went through, then eleven 429s while Retry-After counted down from 10 to 0, then five more 200s, then another 10-second block. The pattern repeated a third time. Of 40 calls in about 40 seconds, 16 succeeded.
This post is about that, and the three other things a live run turned up that the docs and unit tests didn't.
The Polymarket API starter kit is a prompt, not a repo. You paste it into an AI coding tool in an empty folder, and it builds a small read-only Python toolkit for Polymarket US, the CFTC-regulated exchange at gateway.polymarket.us. It's a different API from the international CLOB.
The CLI has markets, search, bbo, book, watch, and a signed balance read for when you have a key. Public market data needs no key. Account reads are signed with Ed25519. There is no order code at all.
On Oct 25 we pasted the old prompt into a clean folder, let it build, and pointed a harness at it. Then we fixed the prompt, rebuilt from the new version, and ran that too. Today we re-ran the rebuilt code, unchanged, to check it still holds.
Public reads are plain GETs on https://gateway.polymarket.us:
/v1/markets?active=true&closed=false&limit=N lists markets./v1/markets/{slug}/bbo gives the top of book under marketData: bestBid, bestAsk, currentPx, lastTradePx, state, plus depth and open-interest fields./v1/markets/{slug}/book gives the full book. Bids are marketData.bids, asks are marketData.offers (there is no asks key), and each level is {px: {value}, qty}./v1/search?query=... returns {"events": [...]}, each event holding its markets.
Every market is a YES contract on one side, priced 0.00 to 1.00. To be short you sell YES, so the NO price you can get is 1 minus the best bid.
The old build did what the old prompt asked. It throttled itself to 10 requests a second (_MIN_INTERVAL = 1 / 10 # stay well under 25 req/s) and retried a 429 after 1, 2 and 4 seconds before calling raise_for_status().
Our harness took the top markets plus the first two events from four searches a beginner might try ("temperature", "bitcoin", "election", "nfl"): 69 markets. For each one it ran the CLI's bbo and book, then checked the market's state. Loop 1 finished at 17:36:50 UTC:
slugs 69
[17:36:50 UTC] loop 1: {'bbo_ok': 60, 'book_ok': 50, 'book_crash': 19, 'check_err': 23, 'bbo_ask_ne_book': 2, 'bbo_crash': 9, 'no_bids': 27, 'no_offers': 20, 'spread>=0.10': 4} states={'MARKET_STATE_OPEN': 26, 'MARKET_STATE_EXPIRED': 20}
book tec-mlb-champ-2026-09-27-tb: HTTPError: 429 Client Error: Too Many Requests for url: https://gateway.polymarket.us/v1/markets/tec-mlb-champ-2026-09-27-tb/book
bbo tec-mls-winner-2026-11-07-sea: HTTPError: 429 Client Error: Too Many Requests for url: https://gateway.polymarket.us/v1/markets/tec-mls-winner-2026-11-07-sea/bbo
...
exit 124
That's 9 bbo and 19 book crashes, and 23 state checks that errored too. The harness hit its time limit (exit 124) before loop 2 finished.
The probe at the top explains why. The gateway sits behind Cloudflare, and the 429 isn't a JSON API error. It's an HTML page with a Retry-After header that starts at 10 and counts down. The old backoff waited 1 + 2 + 4 = 7 seconds in total, then gave up while the block still had seconds to run. And any code that calls .json() on that response gets a JSONDecodeError from an HTML page, which is a confusing way to learn you were rate limited.
Our box is a cloud server, and Cloudflare may treat home connections more gently. Still, a script polling the public gateway should plan for roughly five calls, then a 10-second wait.
What the prompt says now:
PM_MIN_INTERVAL overrides it).Retry-After + 1 (10 if the header is missing) and retry up to 4 times, then raise one PMError saying it was rate limited. Retry 5xx and timeouts the same way..json() on a non-200. Put the status and the first 120 characters of the body in the error.
Even at one call every 2 seconds, Cloudflare still pushed back once in each watch. On Oct 25:
[17:43:19 UTC] loop 1: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)
[17:44:58 UTC] loop 2: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)
[17:46:47 UTC] loop 3: 20 two-sided, 0 one-sided, 0 errors, 1 rate-limit waits (11 s)
...
[17:51:41 UTC] loop 6: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)
And today, with the same code, about 30 seconds after the probe finished:
[14:01:29 UTC] loop 1: 20 two-sided, 0 one-sided, 0 errors, 1 rate-limit waits (11 s)
[14:03:07 UTC] loop 2: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)
[14:04:45 UTC] loop 3: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)
[14:06:23 UTC] loop 4: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)
[14:08:01 UTC] loop 5: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)
100 quotes, one wait of 11 seconds, then it carried on. No crashes, no tracebacks. The summary line counts the waits on purpose, so you can see how close you're running to the limit.
The old markets command printed the slug and the market's question. On Polymarket US the question is often the event, shared by every side of it, so rows looked alike. The rebuilt version asks the gateway to sort (orderBy=volume&orderDirection=desc; the docs don't list sort values, but this pair works), drops anything whose status isn't open, and prints each market's title, which is the side you're actually pricing. Today's top of the list, from watch:
two-sided bid 0.929 (92.9%) ask 0.930 (93.0%) Democratic Party paccc-usho-midterms-2026-11-03-dem
two-sided bid 0.300 (30.0%) ask 0.310 (31.0%) Ken Paxton (R) ewc-usse-tx-2026-11-03-rep
two-sided bid 0.696 (69.6%) ask 0.697 (69.7%) Democratic Party paccc-usse-midterms-2026-11-03-dem
two-sided bid 0.314 (31.4%) ask 0.315 (31.5%) Republican Party paccc-usse-midterms-2026-11-03-rep
two-sided bid 0.690 (69.0%) ask 0.700 (70.0%) James Talarico (D) ewc-usse-tx-2026-11-03-dem
Even the title isn't always enough. "Democratic Party" appears twice in the top three: one is the House (usho), the other the Senate ( usse). Always print the slug next to the title. Game moneylines have no title at all, so the toolkit prints their outcomes instead (today: "Giants / Commanders"). One more small thing from today: one title came back with a tab inside it (Peggy Flanagan<TAB>(D)), which knocked that row's column alignment out. Titles are data, not display strings.
Four days before the US midterms, all 20 of today's top markets by volume were Nov 3 election markets. On Oct 25 the top 20 also included World Series markets. Over today's five loops, the only quote that moved was the Senate "Republican Party" market, from 0.314/0.315 to 0.311/0.312. A quiet watch is normal.
Part of the old harness's market list came from search. Of the 46 markets whose state it could check, 20 were MARKET_STATE_EXPIRED, and 27 had no bids at all. If your script searches and then reads a book, expect to land on dead markets.
The rebuilt search hides markets that aren't open and says how many it hid. On Oct 25, search nfl ended with:
(29 resolved markets hidden; --all to show)
Today showed a different trap. Search matches slugs, and in college basketball nfl is North Florida. The first five events for search nfl were North Florida games:
North Florida (W) vs. Little Rock (W)
- aec-wcbb-nfl-arlr-2026-11-12 Ospreys / Trojans
North Florida (W) vs. Florida State (W)
- aec-wcbb-nfl-flst-2026-11-05 Ospreys / Seminoles
...
WAS Commanders vs. NY Giants
- aec-nfl-was-nyg-2026-11-12 Giants / Commanders
Week 8 Top D/ST Fantasy Football
- ftsc-wk8-2026-11-02-topdst-kc KC Chiefs
The football game came sixth. Read the event title before you trust a search hit.
bbo shows the NO price and the taker fee, using the market's own feeCoefficient and the fee page's formula, coefficient x contracts x p x (1 - p). Today on the top market:
paccc-usho-midterms-2026-11-03-dem state=MARKET_STATE_OPEN
bid: 0.929 (92.9%)
ask: 0.930 (93.0%)
NO: 0.071 (7.1%) (= 1 - best bid; to buy NO you sell YES)
last trade: 0.929 (92.9%)
taker fee, 100 @ ask: $0.45 (coef 0.0695)
Check it: 0.0695 x 100 x 0.93 x 0.07 = 0.452. On Oct 25 the same market was 0.900/0.901 and the fee for 100 at the ask was $0.62. The fee peaks at 50% and shrinks toward the edges, so the same 100 contracts cost less in fees as the price nears 1.
book sorts both sides itself before taking the best. Today, at depth 5:
BIDS
0.929 (92.9%) x 758529
0.928 (92.8%) x 4621
...
OFFERS
0.930 (93.0%) x 12105
0.931 (93.1%) x 200
And a bad slug is one line and exit code 1, not a traceback:
ERROR: HTTP 404 from /v1/markets/no-such-market-xyz/book: '{"code":5,"message":"The server was unable to process your request.","details":[]}'
With no key set, balance prints no key set (POLYMARKET_KEY_ID / POLYMARKET_SECRET_KEY) and exits cleanly. The rebuilt version has 9 mocked tests, including an HTML 429 with Retry-After, giving up after retries, one-sided books, and search hiding resolved markets. Today: 9 passed in 0.10s.
Update my Polymarket US starter:
1. _get: one shared throttle, one call every 2 s (env PM_MIN_INTERVAL).
On 429 (an HTML page from Cloudflare, not JSON) sleep Retry-After + 1
(10 if missing), retry up to 4 times, then raise PMError. Retry 5xx and
timeouts too. Never call .json() on a non-200.
2. list_markets: add orderBy=volume&orderDirection=desc and drop markets
whose status isn't MARKET_STATUS_OPEN.
3. Print each market's title (the side) next to the slug; for moneylines
with no title, print the outcomes. markets shows bid/ask from
bestBidQuote/bestAskQuote.
4. search: limit=20, hide markets that aren't MARKET_STATUS_OPEN unless
--all, and print how many were hidden.
5. Sort bids and offers yourself before taking the best; either side
can be empty.
6. bbo: show NO = 1 - best bid and the taker fee for 100 contracts at the
ask: feeCoefficient x 100 x p x (1 - p).
7. Add `watch [--n 10] [--loops 1] [--interval 60]` with one line per
market and a "[HH:MM:SS UTC] loop N: ... errors, W rate-limit waits"
summary, flush=True; one market's error never stops the loop.
8. The CLI catches PMError, prints one line and exits 1.
Add mocked tests for each change and run them. Still no order placement.
Starting fresh? The full prompt is on the recipe page. After it builds:
python3 -m venv .venv && source .venv/bin/activate
pip install requests python-dotenv cryptography pytest
python3 -m pytest -q
python3 cli.py markets --n 20
python3 cli.py bbo <slug-from-the-list>
python3 cli.py book <slug> --depth 5
python3 cli.py watch --n 20 --loops 5 --interval 60 | tee watch.log
The page's verified line, word for word: Verified working on live data in read-only mode, Oct 25, 2026. Public market data only: a 10-minute, 6-loop watch of the top 20 markets plus markets, search, bbo and book. No API key was created, so balance and positions were never called; the request signing was checked offline against the official SDK.
The toolkit reads public prices and, if you give it a key, your own balance. It has no order code. It prints prices, depth and fees. It doesn't rank markets or suggest trades. If you take it further, keep keys in .env, out of git, and check what the gateway's rate limit is doing from the machine you'll actually run on.
The full recipe, with the complete prompt, setup steps, FAQ and links to every Polymarket US doc we checked, is at botcookery.com/recipes/polymarket-api-starter-kit. To try strategies without real money, see our Polymarket paper-mode bot built with Claude Code. For the international order book, see the Polymarket CLOB API starter.