Skip to main content

Heartbeats — Dead-Man’s-Switch

If your trading bot crashes between placing orders and the next intended cancel, those orders sit on the book exposed to adverse selection until you notice and intervene. The heartbeat dead-man’s-switch protects unattended bots by auto-cancelling all of your resting orders if the server stops receiving heartbeats for longer than a deadline you choose. This mirrors Polymarket’s POST /heartbeats dead-man’s-switch behaviour — the same path, the same interval_ms-style cadence, and the same “miss a heartbeat → all your resting orders are auto-cancelled” guarantee. A PM SDK’s heartbeat loop keeps your orders protected when pointed at PolySimulator.
The 200 response body differs from Polymarket — it is not wire-identical. PolySimulator returns {"ok": true, "expires_at_ms": <int>}, whereas Polymarket’s /heartbeats returns a {"status": <string>} body (and its newer SDKs thread a heartbeat_id through each call). If your bot reads PM’s status / heartbeat_id field off the response, it will see None / a KeyError against PolySimulator — read ok / expires_at_ms instead. The dead-man’s-switch fires on the absence of a heartbeat, so a bot that ignores the response body entirely (just keeps pinging on a timer) is fully protected on both platforms.

Endpoints

PolySimulator exposes two paths that route through the same handler: Both accept the same body and emit the same response.

Request

Response — 200 OK

The expires_at_ms value is last_heartbeat_at_ms + interval_ms + grace, where:
  • grace = max(1000ms, 0.25 × interval_ms) — absorbs network jitter and the ~1-second evaluation cadence so bots pinging at exact intervals don’t trigger spurious cancels.

How to use it (the heartbeat loop)

The expected pattern is to ping at half your interval_ms so you stay comfortably ahead of expiry even with one missed beat.

What happens when a heartbeat is missed?

Once your deadline lapses without a fresh heartbeat — typically within about a second of the expires_at_ms you were given — the server:
  1. Retires the lapsed registration.
  2. Cancels all pending limit orders for the API key’s account (same logic as POST /v1/cancel-all — refunds BUY notional, returns SELL shares to position).
To resume, the bot just calls POST /v1/heartbeats again — a new registration is created from scratch.
The dead-man’s-switch cancels every resting order for the API key’s account, including orders placed by other processes sharing the same key. If you run multiple bot strategies on one key, use client_label to register independent heartbeats — but be aware that the cancel-all still cancels EVERY pending order, not just the labelled subset. Use distinct API keys per strategy if you need strategy-level isolation.

Bounds and error responses

The [1000, 60000] bound is deliberate:
  • Below 1000ms would burn rate-limit quota (2 RPS per registration just for heartbeats) with no safety benefit beyond what 1-second sweeps already provide.
  • Above 60000ms defeats the point — a crashed bot would stay exposed for a full minute before its orders cancel.

Durability guarantee

Heartbeat registrations are persisted, not held only in process memory:
  • Server restarts don’t drop your protection. A deploy or graceful reload preserves every active heartbeat. Your bot’s next refresh after the restart simply bumps the expiry forward — no need to re-register from scratch.
  • Refreshes and the expiry sweep never race. A heartbeat that lands at the same instant the deadline is being evaluated keeps your orders alive — the switch never fires spuriously from a timing race. It fires only on a genuine, sustained absence of heartbeats past your deadline.