Engineering

Polymarket Bot Security: Keys, Wallets, Risk Limits

Most Polymarket bots that lose money to operator error lose it to a key or risk-limit mistake, not a strategy mistake. A practical security guide covering keys, wallet split, API scope, and kill switches.

Last reviewed · Jamal Okafor, Poly Syncer

Polymarket bot security is not a checklist; it is a budget for how much money you are willing to lose to one mistake. Most retail operators who blow up a Polymarket bot do not blow it up on a bad strategy. They blow it up on a leaked private key, an over-scoped L2 API key committed to a public repo, or a missing risk limit that let a runaway loop drain the funding wallet in 90 seconds. This guide is the security model worth starting with: a defense-in-depth split between cold and hot wallets, a sane policy for L2 API keys, a small set of hard-coded risk limits that cannot be disabled by accident, a kill switch that actually works, and a monitoring layout that catches drift before it becomes loss.

The mistakes that drain bot operators the fastest

Across two years of incident reports from the Polysyncer operator community, four mistake classes account for roughly 90 percent of unrecoverable losses.

Leaked private keys. A funding wallet key gets pasted into a Discord thread, committed to a public repo in a .env file, or read off-screen during a livestream. Once exposed, a sweep bot drains the wallet within minutes. The only mitigation is to never expose the key and to keep the hot wallet float bounded.

Runaway loops. A bug in the coordinator submits the same order in a tight loop because a confirmation never arrives. The bot burns through gas, blows past position limits, and ends with a one-sided position several times larger than intended. The mitigation is a per-process rate limiter and a per-wallet position cap enforced before signing.

Over-scoped API keys. The Polymarket CLOB uses an L2 API key for order submission. If that key is generated with maximum permissions and stored on a long-running VM, a host compromise hands the attacker the ability to submit arbitrary orders, even if the EOA key never left the cold device. Scope the key, rotate it, store it outside the application file system.

Missing kill switch. The bot detects an obvious anomaly and keeps trading because there is no centralised stop control. Mitigation is a kill switch that any monitoring rule can flip, that every order path checks before signing, and that defaults to ON during cold start. The rest of this guide engineers those four mitigations.

Private key handling: the EOA risk surface

Polymarket on Polygon uses an externally owned account (EOA) as the funding wallet and signer of root authorisations. Whoever controls that key controls the funds. The first rule of Polymarket bot security is that this key never touches a long-running process with inbound network exposure. If the EOA private key shares memory with a websocket listener, the security model is already broken.

The practical pattern is a three-tier separation. The EOA root key lives on a hardware wallet or in an HSM-backed signing service; it authorises the L2 API key and moves funds on a manual schedule. The bot wallet is a separate EOA whose private key is loaded into the running process; this signs trades. The cold wallet is a third EOA holding the bulk of capital and feeding the bot wallet on a top-up schedule. The bot wallet holds only the float required for one to three days of trading; if its key leaks, loss is bounded by the float. Wallet choice itself matters; the wallet comparison for Polymarket walks the tradeoffs.

Loading the bot wallet key should never involve a plaintext file in version control. The minimum acceptable pattern is an environment variable populated from a secrets manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, 1Password Service Accounts) at process start.

# keys.py — loading the bot wallet key for polymarket bot security
import os
import boto3
from functools import lru_cache

@lru_cache(maxsize=1)
def get_bot_wallet_key() -> str:
    # 1. Prefer secrets manager in production
    if os.environ.get("ENV") == "prod":
        client = boto3.client("secretsmanager", region_name="us-east-1")
        resp = client.get_secret_value(SecretId="polymarket/bot-wallet")
        return resp["SecretString"].strip()
    # 2. Local dev: read from a .env file outside the repo, never from the repo
    path = os.path.expanduser("~/.config/polymarket/bot.key")
    with open(path, "r") as fh:
        return fh.read().strip()

# Never log the key. Never print(it). Never include it in error reports.
# The key is not even passed to functions; the signer takes the cached handle.

Three properties matter. The key is cached, so it does not appear in repeated function arguments where a stack trace could expose it. The dev path lives outside the repo, so a careless git add . cannot pick it up. The environment branch makes it impossible for production to read a developer file. Beyond loading: never paste the key into an LLM, chat thread, or support ticket; never store it in a file whose path contains the wallet address; rotate the bot wallet on any suspicion of compromise. Rotation is cheap; total loss is not.

Hot vs cold wallet split

The defense-in-depth diagram below captures the full layering. Each ring constrains the blast radius of a compromise of the inner ring; the outermost rings exist precisely so that a compromise of the innermost is not catastrophic.

Polymarket bot security — defense-in-depth layers

Defense-in-depth layers for a Polymarket trading bot Concentric rings showing six layers of bot security. From the outside in: monitoring catches drift, kill switch halts trading, rate limiter caps trade frequency, L2 API key scopes order permissions, hot bot wallet bounds float exposure, and the cold wallet holds the bulk of capital out of reach. Each ring is labelled with the failure mode it defends against. Monitoring & alerts defends against: silent drift Kill switch defends against: runaway loops Rate limiter & position caps defends against: bug-driven losses L2 API key (scoped, rotated) defends against: host compromise Hot bot wallet 1–3 days of float Cold wallet Each ring bounds the blast radius of a compromise of the ring inside it. Defense is layered, not a single fence.
The defense-in-depth layout for a Polymarket bot. The cold wallet at the centre holds the bulk of capital and is touched only on a manual schedule. The hot bot wallet holds the float the bot needs to trade. The L2 API key, scoped and rotated, mediates order submission. A rate limiter and position cap bound the damage a buggy code path can do. The kill switch lets any monitoring rule halt all trading instantly. Outermost, monitoring is what notices when any of the inner rings is in trouble.

The cold wallet is a hardware-backed EOA whose key never enters a hot process. Top-ups happen on a schedule (weekly for most operators; daily for higher volume) and require a manual hardware signature. The hot wallet has a target, minimum, and maximum float; the operator (or a scheduled job on a separate machine) tops up when below minimum and sweeps back to cold when above maximum. The bot itself never initiates a top-up; that asymmetry is what keeps a bot compromise from draining the cold wallet. The hot float should be at most 5 percent of the operator total stack, ideally under 2 percent.

API keys (L2): scope, rotation, storage

Polymarket order submission uses an L2 API key derived from a signed EOA message. The key has its own secret and can be revoked without rotating the EOA. Three rules. Scope to order-submission only; do not generate keys with broader permissions than the bot needs. Rotate every 30 to 60 days, immediately on any host compromise suspicion, and after any incident. Store the same way as the EOA key: secrets manager in production, out-of-repo file in development, never a checked-in .env. The L2 key has a property the EOA does not: it is cheap to revoke and re-issue. Monthly rotation catches the case where the key has been quietly exfiltrated but not yet used. For multi-bot operators, generate a separate L2 key per bot and per region; a compromise of one host should not hand the attacker keys to every bot.

Risk limits in code

Risk limits turn a bug into a contained incident instead of an unbounded loss. The minimum set is three: maximum position per market, maximum daily loss per wallet, and maximum trade frequency per minute. All three are enforced in code, on every order path, with no flag to disable them.

Maximum position per market caps exposure on any single outcome. For a copy-trade bot, the cap is typically a fixed dollar amount per outcome (say $500) regardless of leader sizing; this protects against an oversized leader trade that, if mirrored proportionally, would blow through the float. The broader sizing rationale is in the risk-management guide for copy trading.

Maximum daily loss is a wallet-level circuit breaker. The bot tracks realised plus marked-to-market PnL since UTC midnight; if it crosses the negative threshold, the kill switch flips. The threshold is typically 5 to 15 percent of float; tighter for arbitrage, looser for directional. The point is that the limit exists and is mechanical.

Maximum trade frequency caps the per-minute submission rate and is the single most important limit against runaway loops because it bounds damage even if every other layer fails. A copy-trade bot rarely submits more than 30 orders per minute; cap at 60 and the bot literally cannot fire 1000 orders before someone notices. Implement as a token bucket in front of the signer. All three limits are enforced pre-signing; a limiter that kicks in after signing is too late, because the bot has already committed to the trade.

The kill switch: what it must do and how fast

The kill switch is a single boolean that every order path checks before signing. If set, no order is signed and no order is submitted. Complexity here is a smell.

Three properties matter. Speed: sub-millisecond, which means the boolean lives in process memory. A check requiring a database round trip is not a kill switch; it is a suggestion. Breadth: every order path, including recovery and unwinds, checks the same flag. A switch that does not stop the unwind path leaves a recovery loop running while the operator thinks trading has stopped. Default behaviour: ON at cold start, flipped to OFF by an explicit operator action or successful health check. This prevents a crashed bot from auto-resuming into a market state that may have changed.

Who can flip it? The operator manually (via CLI or webhook), every monitoring rule, and the bot itself on any internal invariant violation (a position outside expected range, a fill far from the signal, an RPC returning unexpected data). Reset must be manual. Auto-reset produces oscillation incidents where the bot trips, resets after 60 seconds, trips again, and repeats until the operator notices. The cost of manual reset is a few minutes of downtime on a false positive; the cost of auto-reset is potentially the float.

Monitoring and alerts

Monitoring is the outer ring of defense-in-depth. The bot emits structured events; a monitoring layer evaluates rules and either alerts a human or flips the kill switch. The table maps failure modes to signals, blast radius, and mitigations.

Failure modeLikelihoodBlast radiusPrimary mitigationSecondary mitigationMonitoring signal
Leaked bot wallet keyMediumFloat ($500–$5,000)Secrets manager, no plaintextBounded float, fast rotationUnexpected withdrawal from bot wallet
Leaked EOA root keyLowTotal stackHardware wallet onlyCold wallet splitAny signed message not initiated by operator
Runaway order loopMediumFloat plus gasPer-minute rate limiterPosition cap per marketOrders-per-minute above baseline
Over-scoped L2 keyMediumFloatLeast-privilege scope30-day rotationOrder submitted from unknown IP
Stale order book cacheHighPer-trade slippageCache TTL 1.5 secondsPre-trade re-readWebSocket disconnect events
RPC returning stale stateMediumSeveral bad tradesDual-provider failoverBlock-number sanity checkRPC block lag > 3 blocks
Gas spike during congestionHighTrade-level lossPre-trade gas ceilingAbort if ceiling exceededMedian gas above threshold
Host compromiseLowFloat plus key reuseMinimal attack surfacePer-bot L2 keysUnexpected process or outbound connection

A reasonable alerting layout has three tiers. Tier one (informational) goes to a low-traffic Slack channel: rate limit warnings, mild gas spikes, websocket reconnects. Tier two (actionable) pages the operator: daily loss approaching threshold, repeated single-leg fills, monitoring rule violations. Tier three (critical) pages and flips the kill switch automatically: unexpected withdrawal, rate limiter saturated, RPC lag beyond tolerance. Tier one is allowed to be noisy; tier three must be rare and always real. A tier-three false positive triggers a postmortem on the rule, not a higher threshold. The guidance from the OWASP top-ten on web-application risk maps surprisingly well onto bot operator risk.

Incident response: when something does go wrong

Eventually something will go wrong. The operator who has thought about incident response loses an hour; the one who has not loses the float or worse. The playbook is short.

Contain. Flip the kill switch, manually if it has not flipped itself. No diagnosis happens with the bot still trading. If the switch itself is suspected of compromise, revoke the L2 API key from the Polymarket dashboard; that severs the bot from order submission regardless of local state.

Snapshot. Before changing anything, capture state: open positions, recent orders, recent fills, the bot log for the last hour, RPC client state if interesting. The temptation to start fixing immediately destroys evidence. A snapshot takes 60 seconds and pays for itself in every postmortem.

Assess scope. Is the loss bounded by the float, or is the cold wallet involved? Has the EOA root key been used to sign anything the operator did not initiate? Is the host suspected of compromise (treat every secret on it as burned)? Scope decides whether the response is a short reset (rotate the bot wallet, redeploy, resume) or a long one (rotate every key, redeploy from scratch, audit the codebase).

Recover. Move open positions to a closing state under manual oversight; do not let the bot do this automatically until the cause is understood. Top up the bot wallet from cold only after the root cause is fixed and the kill switch is confirmed working. Resume at reduced size for the first 24 hours to catch any residual bug.

Postmortem. Write up what happened, what the loss was, which layer failed and which caught it. The output is at least one new monitoring rule, test, or architectural change. The Polymarket bot architecture overview walks the component layout this guide hardens; for broader context on the cryptographic primitives the EOA and L2 key models rely on, the Ethereum foundation security overview is a solid grounding.

Polymarket bot security is the discipline of bounding the cost of each mistake. No operator runs incident-free for long; the ones who run profitably do so because their incidents are small. A leaked bot wallet costs the float, not the stack. A runaway loop hits the rate limiter. A host compromise rotates one L2 key, not every secret. The model is layered specifically so that ordinary failure does not become catastrophic failure.

About the author

Jamal Okafor is an execution engineer on the Poly Syncer team. He works on low-latency order routing, key-handling, and the unglamorous plumbing that keeps small operators from losing their float to mistakes that have nothing to do with strategy. The defense-in-depth model in this guide is the one he runs on his own bot.

Frequently asked questions

Where should the bot wallet private key actually live?

In a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or 1Password Service Accounts) in production, fetched at process start and cached in memory. In development, in a file outside the repository tree with restrictive permissions. Never in a committed .env, never in a chat thread, never pasted into an LLM. If the bot wallet leaks, the loss is bounded by the float on that wallet, which is why the float is sized to what the operator can afford to lose.

How often should the L2 API key be rotated?

Every 30 to 60 days for active operators, immediately on any suspicion of host compromise, and after any security incident. The L2 key is cheap to revoke and re-issue. Monthly rotation also catches the case where the key has been quietly exfiltrated and not yet used; rotation forces the attacker to act within the window or lose access entirely.

What is the right size for the hot bot wallet?

At most 5 percent of the operator total stack, ideally under 2 percent. The hot wallet is the maximum loss the operator is signing up to accept on a bad day. If the strategy needs more float to be profitable, it is too capital-intensive for a retail security posture.

Does the kill switch need to be more sophisticated than a single boolean?

No. Complexity in the switch itself is a smell because every additional state is a way to fail in some condition the operator did not anticipate. Sophistication belongs in the rules that flip the switch, not the switch. Reset is manual; auto-reset produces oscillation incidents.

What is the first thing to do when an incident is detected?

Flip the kill switch, manually if it has not flipped itself. No diagnosis happens while the bot is still trading. If the switch itself is suspected of compromise, revoke the L2 API key from the Polymarket dashboard. Only after trading is halted should the operator snapshot state, assess scope, and begin recovery.