Included with every recipe

The Safety Pack

Five files that stop the most common ways AI-built bots lose people money: leaked keys, runaway orders, and "it was only supposed to be a test."

Why this exists

AI coding agents are fast, and they'll happily do something dangerous if nothing stops them. Two real 2026 examples:

None of this needs a genius attacker. Bots scan GitHub for keys around the clock. The fix is boring: keep keys out of the repo, cap what the bot can do, and make stopping it a one-liner.

The three non-negotiables

Key handling

Env vars or a secrets manager. Key files live outside the project folder. Nothing secret in git, prompts or logs.

Position limits

Max per order, max exposure, max daily loss, max order rate. Enforced in code before every order.

Kill switch

touch KILL stops new orders. A cancel-all command clears resting orders through the venue's official endpoint.

1. The rules file (CLAUDE.md / AGENTS.md / Cursor rule)

Your AI coding tool reads this on every task. It sets paper mode as the default, forces hard limits and a kill switch, and tells the agent never to touch your secrets.

# BOT SAFETY RULES (BotCookery Safety Pack v1, Oct 2026)
# Save as CLAUDE.md (Claude Code), AGENTS.md (Codex and most agents)
# or .cursor/rules/bot-safety.mdc (Cursor). Keep it in the repo root.

You are helping me build a trading or prediction-market bot that runs on MY
machine, on MY account. Follow these rules even if I ask you not to.

## Secrets
- Never read, print, log, echo, copy or summarize the contents of .env, *.pem,
  *.key, or any file that holds API keys, private keys or seed phrases.
- Load credentials only from environment variables (or a path in an env var
  for PEM files). Never hardcode them. Never put them in prompts, tests,
  fixtures, comments, commit messages or README examples.
- Never start a web server, file server or tunnel that could expose the
  project folder (no `python -m http.server` in the repo root).
- Never make the repo public, never push without my explicit OK, and never
  add or edit CI that echoes secrets.

## Money
- PAPER/DEMO MODE IS THE DEFAULT. Live trading requires BOTH a --live flag
  AND an interactive typed confirmation. Never remove or bypass that gate.
- Enforce hard limits in code, read from config: max contracts per order,
  max total open exposure, max daily loss, max orders per minute. Reject any
  order that breaks a limit, log it, and do not retry it.
- Before every order: check the KILL file / BOT_KILL env var. If present,
  place nothing.
- Provide a `cancel-all` command that cancels every resting order using the
  venue's official cancel-all endpoint.
- Use client order IDs where the venue supports them (Kalshi) so retries
  can't double-submit. Polymarket US orders have no client order ID: record
  each order locally before sending and never auto-retry an order after a
  timeout or 5xx; check open orders instead.
- Retry reads and cancels with bounded exponential backoff on 429 and 5xx.
  Never retry rejected orders in a loop.
- Track daily loss as a positive dollar amount already lost, not signed PnL.

## Venue rules
- Use only official, documented endpoints from docs.kalshi.com or
  docs.polymarket.us. If you are unsure an endpoint exists, say so; do not
  invent one.
- Do not build features that trade for other people, copy other traders,
  share or republish exchange data, or get around geoblocks or KYC.
- For international Polymarket endpoints: read-only market data only, and
  check GET https://polymarket.com/api/geoblock before anything else.

## Code hygiene
- Log every decision (inputs, order intent, limit checks) to a local file
  without secrets.
- Write tests for the limit checks and the kill switch before writing the
  strategy.
- Prefer small, readable files over clever abstractions. Explain anything
  that touches money in a comment.

2. .gitignore

Add this before your first commit. Keys that never get committed can never leak from GitHub.

# secrets
.env
.env.*
!.env.example
*.pem
*.key
secrets/
# bot state
KILL
logs/
paper_ledger*.json
*.sqlite
# python / node
__pycache__/
.venv/
node_modules/

3. Block your AI tool from reading secrets (.claude/settings.json)

Claude Code deny rules use gitignore-style patterns. Cursor and others have similar ignore settings. Deny rules are a seatbelt, not a vault: a shell command can still read files, so the best move is to keep key files outside the project folder entirely (for example ~/.config/kalshi/key.pem, with only the path in an env var).

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "deny": [
      "Read(**/.env)",
      "Read(**/.env.*)",
      "Read(**/*.pem)",
      "Read(**/*.key)",
      "Read(./secrets/**)",
      "Bash(git push *)"
    ]
  }
}

4. Pre-commit secret scanning (gitleaks)

Blocks any commit that looks like it contains a key. Install pre-commit, save this file, run pre-commit install.

# .pre-commit-config.yaml  (then run: pre-commit install)
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.1
    hooks:
      - id: gitleaks

5. Hard limits + kill switch (risk.py)

Call check_order() before every single order, paper or live. Pass daily loss as a positive number (dollars lost today), not signed PnL, or the cap can never trip. The 15-second contracts limit mirrors a Kalshi order group; create the real group too. touch KILL stops new orders. The cancel-all endpoints clear what is already resting.

# risk.py: hard limits + kill switch. Call check_order() before ANY order, paper or live.
import os, json, pathlib

LIMITS = json.loads(pathlib.Path("limits.json").read_text())
# limits.json example (start this small):
# {"max_contracts_per_order": 1, "max_open_exposure_usd": 5,
#  "max_daily_loss_usd": 2, "max_orders_per_minute": 3,
#  "order_group_contracts_limit": 3}
REQUIRED = ["max_contracts_per_order", "max_open_exposure_usd", "max_daily_loss_usd",
            "max_orders_per_minute", "order_group_contracts_limit"]
missing = [k for k in REQUIRED if k not in LIMITS]
if missing:  # fail closed: no limits, no trading
    raise SystemExit(f"limits.json is missing {missing}")

class RiskError(Exception):
    pass

def kill_switch_on() -> bool:
    return pathlib.Path("KILL").exists() or os.getenv("BOT_KILL") == "1"

def check_order(contracts: float, price_usd: float, open_exposure_usd: float,
                daily_loss_usd: float, orders_last_minute: int,
                contracts_last_15s: float = 0) -> None:
    """Raise RiskError if this order breaks any limit.

    daily_loss_usd is a POSITIVE number: dollars already lost today
    (e.g. 2.0 after losing $2), not signed PnL. Compute it as
    max(0, -realized_pnl_today_usd) if your ledger tracks PnL.
    contracts_last_15s is what this bot sent in the last 15 seconds.
    """
    if kill_switch_on():
        raise RiskError("Kill switch is on. No orders.")
    if daily_loss_usd < 0:
        raise RiskError("daily_loss_usd must be a positive loss amount, not signed PnL.")
    if contracts <= 0 or contracts > LIMITS["max_contracts_per_order"]:
        raise RiskError("Order size outside limits.")
    if not 0 < price_usd < 1:
        raise RiskError("Price must be between $0 and $1.")
    if open_exposure_usd + contracts * price_usd > LIMITS["max_open_exposure_usd"]:
        raise RiskError("Exposure cap hit.")
    if daily_loss_usd >= LIMITS["max_daily_loss_usd"]:
        raise RiskError("Daily loss cap hit. Done for today.")
    if orders_last_minute >= LIMITS["max_orders_per_minute"]:
        raise RiskError("Order rate cap hit.")
    # Local mirror of the Kalshi order group's limit. Kalshi counts FILLED
    # contracts in a rolling 15-second window and cancels the whole group
    # when the total goes over the limit; we count contracts SENT, which
    # blocks earlier. Also create the group on Kalshi (POST
    # /portfolio/order_groups/create with contracts_limit) and pass its
    # order_group_id on every order: that's the server-side backstop.
    if contracts_last_15s + contracts > LIMITS["order_group_contracts_limit"]:
        raise RiskError("15-second contracts limit hit.")

def require_live_confirmation(live_flag: bool) -> bool:
    """Returns True only for --live AND a typed confirmation."""
    if not live_flag:
        return False
    typed = input("LIVE MODE. Real money. Type 'I accept the risk' to continue: ")
    return typed.strip() == "I accept the risk"

# Venue cancel-all endpoints (official, checked Oct 19, 2026):
#   Kalshi:        DELETE {BASE}/portfolio/events/orders   (signed request)
#   Polymarket US: POST https://api.polymarket.us/v1/orders/open/cancel  (signed request)

6. Use the venue's own safety tools

7. Stay inside the rules

8. Before you run anyone else's bot repo

Pre-live checklist