{"slug": "polymarket-us-api-in-python-cloudflare-rate-limited-us-long-before-25-req-s", "title": "Polymarket US API in Python: Cloudflare rate-limited us long before 25 req/s", "summary": "A developer-built Python starter kit for the CFTC-regulated Polymarket US gateway found that Cloudflare rate limiting kicks in far below the documented 25 requests per second, with only 16 of 40 calls succeeding at a polite one request per second on a single order book. The runs, conducted Oct 25 and re-run Oct 30, 2026, showed a repeating pattern of six successful calls, eleven HTTP 429 responses with Retry-After counting down from 10 seconds, then five more successes before another block. The rebuilt toolkit uses plain GETs for markets, search, bbo and book endpoints, with Ed25519-signed reads for account balance and no order-placement code.", "body_md": "*Originally published on [BotCookery](https://botcookery.com/recipes/polymarket-api-starter-kit/), 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.*\n\nThe 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`.\n\nThis morning we measured the limit directly, at a polite one request per second on a single order book:\n\n```\n13:59:28 call  1 status 200 ct=application/json retry-after=None body='{\"marketData\":{\"marketSlug\":\"paccc-usho-'\n...\n13:59:32 call  6 status 200 ct=application/json retry-after=None body='{\"marketData\":{\"marketSlug\":\"paccc-usho-'\n13:59:33 call  7 status 429 ct=text/html; charset=UTF-8 retry-after=10 body='<!doctype html>\\n<!--[if lt IE 7]> <html '\n13:59:34 call  8 status 429 ct=text/html; charset=UTF-8 retry-after=9 body='<!doctype html>\\n<!--[if lt IE 7]> <html '\n...\n13:59:43 call 17 status 429 ct=text/html; charset=UTF-8 retry-after=0 body='<!doctype html>\\n<!--[if lt IE 7]> <html '\n13:59:44 call 18 status 200 ct=application/json retry-after=None body='{\"marketData\":{\"marketSlug\":\"paccc-usho-'\n...\n13:59:48 call 22 status 200 ct=application/json retry-after=None body='{\"marketData\":{\"marketSlug\":\"paccc-usho-'\n13:59:50 call 23 status 429 ct=text/html; charset=UTF-8 retry-after=10 body='<!doctype html>\\n<!--[if lt IE 7]> <html '\n```\n\nSix 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.\n\nThis post is about that, and the three other things a live run turned up that the docs and unit tests didn't.\n\nThe 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.\n\nThe 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.\n\nOn 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.\n\nPublic reads are plain GETs on `https://gateway.polymarket.us`:\n\n`/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.\nEvery 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.\n\nThe 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()`.\n\nOur 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:\n\n```\nslugs 69\n[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}\n    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\n    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\n...\nexit 124\n```\n\nThat'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.\n\nThe 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.\n\nOur 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.\n\nWhat the prompt says now:\n\n`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.\nEven at one call every 2 seconds, Cloudflare still pushed back once in each watch. On Oct 25:\n\n```\n[17:43:19 UTC] loop 1: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)\n[17:44:58 UTC] loop 2: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)\n[17:46:47 UTC] loop 3: 20 two-sided, 0 one-sided, 0 errors, 1 rate-limit waits (11 s)\n...\n[17:51:41 UTC] loop 6: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)\n```\n\nAnd today, with the same code, about 30 seconds after the probe finished:\n\n```\n[14:01:29 UTC] loop 1: 20 two-sided, 0 one-sided, 0 errors, 1 rate-limit waits (11 s)\n[14:03:07 UTC] loop 2: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)\n[14:04:45 UTC] loop 3: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)\n[14:06:23 UTC] loop 4: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)\n[14:08:01 UTC] loop 5: 20 two-sided, 0 one-sided, 0 errors, 0 rate-limit waits (0 s)\n```\n\n100 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.\n\nThe 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`:\n\n```\n  two-sided bid 0.929 (92.9%)  ask 0.930 (93.0%)  Democratic Party         paccc-usho-midterms-2026-11-03-dem\n  two-sided bid 0.300 (30.0%)  ask 0.310 (31.0%)  Ken Paxton (R)           ewc-usse-tx-2026-11-03-rep\n  two-sided bid 0.696 (69.6%)  ask 0.697 (69.7%)  Democratic Party         paccc-usse-midterms-2026-11-03-dem\n  two-sided bid 0.314 (31.4%)  ask 0.315 (31.5%)  Republican Party         paccc-usse-midterms-2026-11-03-rep\n  two-sided bid 0.690 (69.0%)  ask 0.700 (70.0%)  James Talarico (D)       ewc-usse-tx-2026-11-03-dem\n```\n\nEven 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.\n\nFour 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.\n\nPart 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.\n\nThe rebuilt search hides markets that aren't open and says how many it hid. On Oct 25, `search nfl` ended with:\n\n```\n(29 resolved markets hidden; --all to show)\n```\n\nToday 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:\n\n```\nNorth Florida (W) vs. Little Rock (W)\n   - aec-wcbb-nfl-arlr-2026-11-12                 Ospreys / Trojans\nNorth Florida (W) vs. Florida State (W)\n   - aec-wcbb-nfl-flst-2026-11-05                 Ospreys / Seminoles\n...\nWAS Commanders vs. NY Giants\n   - aec-nfl-was-nyg-2026-11-12                   Giants / Commanders\nWeek 8 Top D/ST Fantasy Football\n   - ftsc-wk8-2026-11-02-topdst-kc                KC Chiefs\n```\n\nThe football game came sixth. Read the event title before you trust a search hit.\n\n`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:\n\n```\npaccc-usho-midterms-2026-11-03-dem  state=MARKET_STATE_OPEN\n  bid:  0.929 (92.9%)\n  ask:  0.930 (93.0%)\n  NO:   0.071 (7.1%)  (= 1 - best bid; to buy NO you sell YES)\n  last trade: 0.929 (92.9%)\n  taker fee, 100 @ ask: $0.45 (coef 0.0695)\n```\n\nCheck 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.\n\n`book` sorts both sides itself before taking the best. Today, at depth 5:\n\n```\n  BIDS\n    0.929 (92.9%)  x 758529\n    0.928 (92.8%)  x 4621\n    ...\n  OFFERS\n    0.930 (93.0%)  x 12105\n    0.931 (93.1%)  x 200\n```\n\nAnd a bad slug is one line and exit code 1, not a traceback:\n\n```\nERROR: HTTP 404 from /v1/markets/no-such-market-xyz/book: '{\"code\":5,\"message\":\"The server was unable to process your request.\",\"details\":[]}'\n```\n\nWith 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`.\n\n```\nUpdate my Polymarket US starter:\n1. _get: one shared throttle, one call every 2 s (env PM_MIN_INTERVAL).\n   On 429 (an HTML page from Cloudflare, not JSON) sleep Retry-After + 1\n   (10 if missing), retry up to 4 times, then raise PMError. Retry 5xx and\n   timeouts too. Never call .json() on a non-200.\n2. list_markets: add orderBy=volume&orderDirection=desc and drop markets\n   whose status isn't MARKET_STATUS_OPEN.\n3. Print each market's title (the side) next to the slug; for moneylines\n   with no title, print the outcomes. markets shows bid/ask from\n   bestBidQuote/bestAskQuote.\n4. search: limit=20, hide markets that aren't MARKET_STATUS_OPEN unless\n   --all, and print how many were hidden.\n5. Sort bids and offers yourself before taking the best; either side\n   can be empty.\n6. bbo: show NO = 1 - best bid and the taker fee for 100 contracts at the\n   ask: feeCoefficient x 100 x p x (1 - p).\n7. Add `watch [--n 10] [--loops 1] [--interval 60]` with one line per\n   market and a \"[HH:MM:SS UTC] loop N: ... errors, W rate-limit waits\"\n   summary, flush=True; one market's error never stops the loop.\n8. The CLI catches PMError, prints one line and exits 1.\nAdd mocked tests for each change and run them. Still no order placement.\n```\n\nStarting fresh? The full prompt is on the recipe page. After it builds:\n\n```\npython3 -m venv .venv && source .venv/bin/activate\npip install requests python-dotenv cryptography pytest\npython3 -m pytest -q\npython3 cli.py markets --n 20\npython3 cli.py bbo <slug-from-the-list>\npython3 cli.py book <slug> --depth 5\npython3 cli.py watch --n 20 --loops 5 --interval 60 | tee watch.log\n```\n\nThe 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.*\n\nThe 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.\n\nThe 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](https://botcookery.com/recipes/polymarket-api-starter-kit/)**. To try strategies without real money, see our [Polymarket paper-mode bot built with Claude Code](https://botcookery.com/recipes/polymarket-bot-claude-code-paper-mode/). For the international order book, see the [Polymarket CLOB API starter](https://botcookery.com/recipes/polymarket-clob-api-starter/).", "url": "https://wpnews.pro/news/polymarket-us-api-in-python-cloudflare-rate-limited-us-long-before-25-req-s", "canonical_source": "https://dev.to/botcookery/polymarket-us-api-in-python-cloudflare-rate-limited-us-long-before-25-reqs-2866", "published_at": "2026-10-30 14:09:30+00:00", "updated_at": "2026-10-30 14:20:09.521404+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "developer-tools"], "entities": ["Polymarket", "Cloudflare", "Polymarket US", "CFTC", "BotCookery"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/polymarket-us-api-in-python-cloudflare-rate-limited-us-long-before-25-req-s", "markdown": "https://wpnews.pro/news/polymarket-us-api-in-python-cloudflare-rate-limited-us-long-before-25-req-s.md", "text": "https://wpnews.pro/news/polymarket-us-api-in-python-cloudflare-rate-limited-us-long-before-25-req-s.txt", "jsonld": "https://wpnews.pro/news/polymarket-us-api-in-python-cloudflare-rate-limited-us-long-before-25-req-s.jsonld"}}