{"slug": "our-grok-kalshi-paper-bot-ignored-retry-after-and-treated-a-login-page-as-json", "title": "Our Grok Kalshi paper bot ignored Retry-After and treated a login page as JSON", "summary": "A developer rebuilt a Grok-generated Kalshi prediction-market paper-trading bot on Nov 5, 2026 and found its HTTP edge mishandled rate limiting: on a 429 with Retry-After: 3, the bot retried after roughly 0.5 seconds of homemade backoff, and it also treated a gateway login page as JSON, producing an 'Expecting value: line 1 column 1' error. The rebuild, run in paper mode against Kalshi's public demo data with no keys and no orders sent, fixed the retry logic to sleep the header value (capped at 8 seconds) and added a 5/10-second timeout with at most two retries, while the earlier Oct 25 bookkeeping fixes held.", "body_md": "*Originally published on [BotCookery](https://botcookery.com/recipes/grok-trading-bot/), free build recipes for AI-built trading and prediction-market bots. Written with AI assistance. Every log line and count below is from the Nov 5, 2026 rebuild against Kalshi's live public demo data in paper mode: no demo keys, and no order was sent. Nothing here is financial advice. Paper and demo only.*\n\nPeople keep asking whether Grok can build a trading bot. The useful answer is narrower: an AI coding agent can scaffold a Kalshi **demo** bot whose spending limits live in code, whose research step only sees public market text, and that you can stop with one command. The recipe on BotCookery is that prompt. On the morning of Nov 5, 2026 (ET) we pasted the published one-shot into a clean folder, built it by hand, and ran it in paper mode against Kalshi's live public demo. The Oct 25 bookkeeping held. The HTTP edge did not.\n\nThe first fault we drilled was a 429 with `Retry-After: 3`. The bot retried after about half a second of homemade backoff. That is the finding this post leads with, because it is the kind of bug that looks fine in a happy-path run and then burns you the first time the venue asks you to wait.\n\nThe page title on the site is a how-to for building a Kalshi paper bot with Grok and spending limits. Under the hood it is the same shape as our Claude and ChatGPT starters: the agent writes the code; **you** set the rules and the dollar caps. Paper mode is the default. Public market reads hit `https://external-api.demo.kalshi.co/trade-api/v2` with no key. Demo orders need keys, `PAPER_MODE=0`, `--live`, and a typed confirmation; we had none of that, so nothing was signed.\n\nLimits sit in `limits.json` and `risk.py`, not in the agent's instructions. An agent can misread a prompt; it cannot skip a check it does not control. Create Order V2 is `POST /portfolio/events/orders` with `side`, `count`, `price`, `time_in_force` (`fill_or_kill`, `good_till_canceled`, or `immediate_or_cancel`) and `self_trade_prevention_type`. Cancel-all is `DELETE /portfolio/events/orders`. Order groups cap matched contracts over a rolling 15 seconds, so the bot must create one group and reuse it. The optional research step posts only public title, rules text, and prices to xAI's Chat Completions endpoint; its output is logged and never changes the bid. We left `research_enabled` false and never called it.\n\nOn Nov 5, 2026 we pasted this page's one-shot prompt into a clean folder, built it by hand and ran it in paper mode against Kalshi's live public demo data that morning (ET). No demo keys, so nothing was signed or sent. The Oct 25 bookkeeping fixes (one order group, paper IOC fill, held-ticker skip, settle-first daily loss, cancel_all leaves filled exposure alone) held up. What broke was the HTTP edge:\n\n`Retry-After: 3` retried after ~0.5 s of homemade backoff. Reads now sleep the header value (cap 8 s) when it is present.`Expecting value: line 1 column 1`. Now: `fetch failed: response was not JSON: gateway login`.` timeout=(5, 10)`, at most 2 retries, then `fetch failed: timed out` (~16 s in our drill).`503 Service Unavailable`).` mve_filter=exclude`) and found 56 with a real YES ask, 18 of them ≤ $0.15. Around 9:04 AM the first 1,000 had 13 real asks and zero ≤ $0.15. Pick the ticker right before you run.\nOct 25 condensed (still true): a new order group every run meant the server-side 15 s cap never bit; cancel_all had been zeroing exposure for IOC fills; paper reserved the limit price instead of filling at the book; the bot re-bought the same ticker every run; `daily_loss_usd` was never written. Those are all in the one-shot now.\n\nFrom the fresh rebuild this morning (16 tests pass), 8:58–9:03 AM ET:\n\n```\n08:58:39 KXNFLRECYDS-26NOV08CINATL-ATLAHOOPER81-40 (Austin Hooper: 40+ receiving yards) ask 0.0600 x50.00, limit 0.1500: PAPER IOC bid 1 @ 0.1500 -> filled 1.00 @ 0.0600 fee 0.01; exposure $0.070000, group paper-6cfc6744\n08:58:39 KXNFLRECYDS-26NOV08CINATL-ATLAHOOPER81-40 … already hold 1.00, skip\nKILL is on. No order, no order group.\n{'paper_cancelled_resting': 0}\npositions kept: {'KXNFLRECYDS-26NOV08CINATL-ATLAHOOPER81-40': {…}} exposure $0.070000\n08:58:50 settled KXLIGAPROMATCH-26NOV050700LOBALO-LOB: no, pnl -0.13\n08:58:51 KXNFLTD-26NOV08DENCAR-CARJCOKER18-3 (Jalen Coker: 3+ touchdowns) ask 0.0100 x25.00, limit 0.1500: PAPER IOC bid 1 @ 0.1500 -> filled 1.00 @ 0.0100 fee 0.01; exposure $0.090000, group paper-6cfc6744\n```\n\nThe settlement line is a drill: we injected a test position on a demo market Kalshi had already finalized as \"no\", and the bot booked a $0.13 loss toward the daily limit. The second buy reused the first run's order group. After the HTTP fixes, fault drills against a local proxy: 429+Retry-After slept ~3 s; HTML 200 and 503 printed one clean line; hang ended with `timed out` in 16.5 s.\n\nWhat we still can't test without demo keys: real demo fills, order-group rejection, signed cancels. Without an xAI key: the Grok research reply. `kalshi.com/fee-schedule` returned 429 during this check; the taker fee formula in the prompt (`ceil` to the cent of `0.07 * count * price * (1 - price)`) still matches what we charge in paper. Docs pages we rely on were 200; demo and elections hosts both answered. Changelog Nov 5: `market_closed` → `market_inactive` on order errors; KXRFQ series excluded from unscoped lists.\n\nBefore the fix, a single 429 with `Retry-After: 3` still retried after ~0.5 s. A drill that always returned 429 took about 22.6 s across five long retries and then printed a noisy client error. After the fix: when the header is present, sleep that many seconds (cap 8); at most two retries; timeout `(5, 10)`. The same 429-once drill slept about 3 s and finished in 3.84 s. An always-429 drill now fails in 1.51 s with one clear line: `fetch failed: 429 {\"error\":\"too many requests\"}`.\n\nThat is the difference between \"we rate-limit politely\" and \"we look like a broken client.\" Homemade exponential backoff is fine when the server is silent. When the server tells you when to come back, ignore that number and you are guessing.\n\nA gateway login page served as HTTP 200 is not a JSON body. The pre-fix client raised `Expecting value: line 1 column 1` — the classic `JSONDecodeError` surface. After the fix the same drill prints `fetch failed: response was not JSON: gateway login`. A 503 that returned HTML used to leak tags into the error string; it now collapses to `503 Service Unavailable`.\n\nThe hung-host drill is the one that wastes a whole morning if you leave generous defaults in place. Against a proxy that never answered, the old client sat until an outer 55 s kill with empty stdout. The fixed client uses `timeout=(5, 10)`, at most two retries, then `fetch failed: timed out` in about 16.5 s in our drill. You still will not get an instant Ctrl-C mid-request — that remains a noted limit — but you get a useful line and a bounded wait.\n\n`yes_ask_dollars` of `\"0.0000\"` or `\"1.0000\"` means nobody is offering YES. Treat both as no ask. Never compute \"100 − X\" on dollar prices; the book returns bids only, and the implied YES ask is `$1.00` minus the highest NO bid when you have to derive it from the book.\n\nOn Nov 5 around 8:57 AM ET, a scan of 5,000 open markets with `mve_filter=exclude` found 56 with a real YES ask, 18 of them ≤ $0.15. Around 9:04 AM the first 1,000 open markets had 13 real asks and zero ≤ $0.15. The ticker we used at 8:58 (`KXNFLRECYDS-26NOV08CINATL-ATLAHOOPER81-40`, Austin Hooper 40+ receiving yards) had ask `0.0600` x50 against a `0.1500` limit. A few minutes later the cheap end of the book had moved. Pick the ticker right before you run.\n\nUnscoped `GET /markets` also skips on-demand KXRFQ series as of Kalshi's Nov 5, 2026 changelog. If your scan looks emptier than you expect, check the filter and the series, not only your pagination loop.\n\nAs of that same changelog, orders on a halted or closed market return `market_inactive` (it used to say `market_closed`). Market lifecycle `status` values like `active` / `finalized` are unchanged; this bot still only bids when status is `active`. If your error handler still keys on the old string, a halt looks like an unknown failure instead of a clean skip.\n\nThe 8:58 ET paper pass filled one contract on the Hooper ticker at `0.0600` with a `$0.01` fee, booked `$0.07` exposure under group `paper-6cfc6744`, skipped the held ticker on the next run, refused under KILL without creating a group, left exposure alone after `cancel_all` (`paper_cancelled_resting: 0`), settled the injected Ligamento position as `no` with `pnl -0.13`, then filled a second ticker (` KXNFLTD-26NOV08DENCAR-CARJCOKER18-3` at ask `0.0100`) reusing the **same** order group and ending at `$0.09` exposure. Sixteen offline tests passed after the HTTP hardening. A second paper pass on the fixed build around 9:02 ET repeated the Hooper fill under a new paper group id, held skip, kill, and cancel-keeps-exposure.\n\nNone of those fills left the box. Paper mode reads the live public demo book and simulates an immediate-or-cancel fill locally. Real demo fills, order-group rejection, and signed cancels remain untested without keys.\n\nPaste this follow-up into the same project:\n\n```\nUpdate the Kalshi demo bot with these HTTP and docs fixes. Stay in\npaper mode; send nothing to the network except public market reads.\n\n1. Public GETs: timeout=(5, 10), at most 2 retries. On 429/5xx use\n   bounded exponential backoff. If the response has Retry-After, sleep\n   that many seconds instead (cap 8).\n2. Never print a traceback for HTTP or JSON failures. One line only:\n   \"fetch failed: …\". HTML or non-JSON 200 -> \"response was not JSON: …\"\n   (strip tags, truncate). 503 HTML -> \"503 Service Unavailable\".\n3. Hang/timeout -> \"fetch failed: timed out\" after the retries, then exit.\n4. Catch BrokenPipeError around main (exit 0) so `| head` is quiet.\n   KeyboardInterrupt -> one line, exit 130.\n5. Docs: orders on a halted/closed market return market_inactive (Kalshi\n   changelog Nov 5, 2026; was market_closed). Unscoped GET /markets skips\n   on-demand KXRFQ series. Fee formula unchanged\n   (ceil-cent 0.07 * count * price * (1 - price)); kalshi.com/fee-schedule\n   may 429 — use the formula in the prompt.\n6. Beginner gotcha: demo books are thin. When picking a ticker, scan\n   status=open&mve_filter=exclude until yes_ask_dollars is strictly\n   between 0 and 1 (\"0.0000\" and \"1.0000\" mean no ask).\n7. Keep Oct 25 behaviour: one reused order group, paper IOC fill at the\n   ask with fee, held-ticker skip, settle-first daily loss, cancel_all\n   does not clear filled exposure.\n```\n\nStarting fresh? The full one-shot, the Oct 25 follow-up, the safety checklist, and every Kalshi doc we checked are on the recipe page. Run paper first with no keys. Touch `KILL`, confirm it refuses, then run `cancel_all` and confirm filled exposure stays put.\n\nThe page's verified line, word for word: *Verified working on live data in paper mode, Nov 5, 2026. Ran live in paper mode against Kalshi's public demo market data on the morning of Nov 5 (ET), for several runs plus kill-switch, cancel-all, settlement and HTTP fault drills. We had no demo keys, so no signed request was sent and no demo order was placed; real demo fills and order-group rejections are untested. The optional research step was not called (research_enabled false; no xAI API call). Settlement was drilled with a test position on a demo market that had already been finalized.*\n\nThese runs prove plumbing: retries, timeouts, paper IOC math, held-ticker skip, settle-first daily loss, kill, cancel-all. They do not prove a strategy. Demo books are thin and change by the minute; Kalshi notes that demo prices and behavior may differ from production. If you turn the research step on later, treat the model's probabilities as advisory. Kalshi's Developer Agreement allows API use for your own trading, not for running a bot for other people. Be wary of any account offering to \"run your bot\" or sell \"Grok signals.\"\n\nThe full recipe — prompt, steps, FAQs on thin books and `market_inactive`, and the dated rebuild write-up — is at **[botcookery.com/recipes/grok-trading-bot](https://botcookery.com/recipes/grok-trading-bot/)**.\n\nAPI changes and new field notes: [@botcookery](https://dev.to/botcookery) on X ([https://x.com/botcookery](https://x.com/botcookery))", "url": "https://wpnews.pro/news/our-grok-kalshi-paper-bot-ignored-retry-after-and-treated-a-login-page-as-json", "canonical_source": "https://dev.to/botcookery/our-grok-kalshi-paper-bot-ignored-retry-after-and-treated-a-login-page-as-json-26ad", "published_at": "2026-11-06 15:48:03+00:00", "updated_at": "2026-11-06 15:59:38.513368+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-products"], "entities": ["Grok", "Kalshi", "xAI", "BotCookery"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/our-grok-kalshi-paper-bot-ignored-retry-after-and-treated-a-login-page-as-json", "markdown": "https://wpnews.pro/news/our-grok-kalshi-paper-bot-ignored-retry-after-and-treated-a-login-page-as-json.md", "text": "https://wpnews.pro/news/our-grok-kalshi-paper-bot-ignored-retry-after-and-treated-a-login-page-as-json.txt", "jsonld": "https://wpnews.pro/news/our-grok-kalshi-paper-bot-ignored-retry-after-and-treated-a-login-page-as-json.jsonld"}}