1.5 RateLimitGovernor
RateLimitGovernor prevents the system from exceeding Polymarket's CLOB order send-rate limits at both the per-market and per-account levels. It reads live rate-limit headers from CLOB responses (X-RateLimit-Remaining, X-RateLimit-Reset), maintains sliding-window counters, and either delays (reshapes) or rejects OrderIntents when the remaining budget falls below configured thresholds. Cancel and risk-flatten requests always receive priority allocation over new open orders. The bot is fail-closed: if rate-limit state cannot be determined, new order intents are rejected until state is re-established.
v3 readiness
A bot is done when all four scores are. What does done mean?
1. Bot Identity
| Layer | Risk Risk |
|---|---|
| Bot class | Guardrail |
| Authority | RejectReshape |
| Status | LIVE |
| Readiness | General live |
| Runs before | ExecutionPlan emit |
| Runs after | Strategy OrderIntent |
| Applies to | Every OrderIntent — checks per-market and per-account order send-rate against Polymarket CLOB limits before allowing execution |
| Default mode | general_live |
| User-visible | no |
| Developer owner | Polytraders core — Risk pod |
Operational profile
| Modes supported | quarantine |
|---|
2. Purpose
RateLimitGovernor prevents the system from exceeding Polymarket's CLOB order send-rate limits at both the per-market and per-account levels. It reads live rate-limit headers from CLOB responses (X-RateLimit-Remaining, X-RateLimit-Reset), maintains sliding-window counters, and either delays (reshapes) or rejects OrderIntents when the remaining budget falls below configured thresholds. Cancel and risk-flatten requests always receive priority allocation over new open orders. The bot is fail-closed: if rate-limit state cannot be determined, new order intents are rejected until state is re-established.
3. Why This Bot Matters
Order send-rate exceeds CLOB limit
Polymarket's CLOB enforces rate limits via Cloudflare. Exceeding the limit produces HTTP 429 responses, which cause order rejections, increased latency, and potential temporary IP/key bans that block all trading — including urgent risk-flatten operations.
No priority for cancel/risk-flatten requests
If rate budget is exhausted by new open orders, urgent cancellation or position-flatten requests may be queued or dropped, leaving dangerous open exposure during fast market moves.
Rate budget not read from live CLOB headers
Hard-coded rate limits may diverge from Polymarket's actual published limits. Reading live headers ensures the system adapts to any policy changes without a code deploy.
Per-market throttle not separated from per-account throttle
A high-frequency strategy on one market can exhaust the account's global rate budget, silently starving other markets of order capacity.
No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.
4. Required Polymarket Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| CLOB rate-limit response headers: X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Limit | clob_auth | Yes | Track the live remaining request budget and reset time per sliding window. Update internal counters after every CLOB response. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Per-market sliding-window order counter | internal | Yes | Count the number of order intents sent to each market within the current rate-limit window. |
| Per-account global order counter | internal | Yes | Count total order intents across all markets to enforce the account-level rate cap. |
| Intent classification: open / cancel / risk-flatten | internal | Yes | Cancel and risk-flatten intents always receive priority allocation from the reserved cancel budget; they bypass the standard rate check. |
| KillSwitch active flag | KillSwitch | Yes | If KillSwitch is active, reject all new open orders immediately. Cancel/flatten requests still use the priority path. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| public_req_per_min | 200 | 160 | 200 | Maximum number of public (unauthenticated) CLOB requests per minute across all strategies. |
| trading_req_per_min | 100 | 80 | 100 | Maximum number of authenticated order-placement requests per minute across all strategies and markets. |
| priority_cancel_over_open | True | None | True | When true, cancel requests are always processed before new open orders and draw from a reserved cancel budget that cannot be consumed by open orders. |
| priority_risk_flatten | True | None | True | When true, risk-flatten (emergency close-all) requests bypass rate checks entirely and are sent immediately regardless of remaining budget. |
7. Detailed Parameter Instructions
public_req_per_min
What it means
Maximum number of public (unauthenticated) CLOB requests per minute across all strategies.
Default
{ "public_req_per_min": 200 }
Why this default matters
Polymarket's published public API limit is ~200 requests/min. Staying at or below this prevents 429 responses on market-data reads that many strategies depend on.
Threshold logic
| Condition | Action |
|---|---|
| requests ≤ 160 / min | APPROVE |
| 160–200 / min | WARN — log rate pressure; begin shedding lowest-priority requests |
| > 200 / min | REJECT — RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED |
Developer check
if (windowCount.public >= p.hard) return reject('RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED'); else if (windowCount.public >= p.warning) return warn('RATE_LIMIT_GOVERNOR_BUDGET_WARN');
User-facing English
— not yet authored —
trading_req_per_min
What it means
Maximum number of authenticated order-placement requests per minute across all strategies and markets.
Default
{ "trading_req_per_min": 100 }
Why this default matters
Polymarket's published trading API limit is ~100 order operations/min per account. Exceeding this risks 429 and potential key suspension.
Threshold logic
| Condition | Action |
|---|---|
| requests ≤ 80 / min | APPROVE |
| 80–100 / min | RESHAPE — defer non-priority intents to next window |
| > 100 / min | REJECT — RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED |
Developer check
if (windowCount.trading >= p.hard) return reject('RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED'); else if (windowCount.trading >= p.warning) return reshape({defer_ms: msUntilWindowReset()});
User-facing English
— not yet authored —
priority_cancel_over_open
What it means
When true, cancel requests are always processed before new open orders and draw from a reserved cancel budget that cannot be consumed by open orders.
Default
{ "priority_cancel_over_open": true }
Why this default matters
Cancel operations are risk-reducing. Prioritising them over open orders ensures the system can always cancel stale quotes during fast market moves, even if the rate budget is nearly full.
Threshold logic
| Condition | Action |
|---|---|
| priority_cancel_over_open = true AND intent is cancel | Always APPROVE from reserved cancel budget, bypassing standard rate check |
| priority_cancel_over_open = false | Cancel requests compete with open orders for the shared rate budget |
Developer check
if (params.priority_cancel_over_open && intent.type == 'CANCEL') return approve_from_reserved_budget();
User-facing English
— not yet authored —
priority_risk_flatten
What it means
When true, risk-flatten (emergency close-all) requests bypass rate checks entirely and are sent immediately regardless of remaining budget.
Default
{ "priority_risk_flatten": true }
Why this default matters
Risk-flatten operations must never be delayed by a rate-limit check. Allowing them to bypass ensures the system can always respond to a KillSwitch or drawdown circuit-breaker event.
Threshold logic
| Condition | Action |
|---|---|
| priority_risk_flatten = true AND intent is risk-flatten | Immediately APPROVE — bypasses all rate counters |
| priority_risk_flatten = false | Risk-flatten intents compete with standard budget (not recommended) |
Developer check
if (params.priority_risk_flatten && intent.type == 'RISK_FLATTEN') return approve_unconditionally();
User-facing English
— not yet authored —
8. Default Configuration
{
"bot_id": "risk.rate_limit_governor",
"version": "2.0.0",
"mode": "hard_guard",
"defaults": {
"public_req_per_min": 200,
"trading_req_per_min": 100,
"priority_cancel_over_open": true,
"priority_risk_flatten": true
},
"locked": {
"priority_risk_flatten": {
"min": true
},
"trading_req_per_min": {
"max": 100
}
}
}9. Implementation Flow
- Receive OrderIntent from Strategy layer with intent type (open / cancel / risk-flatten).
- Check KillSwitch active flag; if active, reject all open-order intents immediately. Cancel and risk-flatten intents still proceed via their priority paths.
- If intent type is RISK_FLATTEN and priority_risk_flatten=true, approve unconditionally and bypass all rate counters.
- If intent type is CANCEL and priority_cancel_over_open=true, approve from the reserved cancel budget and bypass the standard trading counter.
- Read the current sliding-window counter for the target market (per-market bucket) and for the account (global trading bucket).
- Check account-level trading counter against trading_req_per_min. If at or above the hard limit, return HARD_REJECT with RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED.
- Check per-market counter against the per-market sub-limit (trading_req_per_min / active_market_count). If at or above the per-market hard limit, return HARD_REJECT with RATE_LIMIT_GOVERNOR_MARKET_THROTTLED.
- If either counter is in the warning zone (80–100% of limit), return RESHAPE with constraints.defer_ms set to the milliseconds until the current window resets (from X-RateLimit-Reset header).
- Approve the intent; increment both the per-market and per-account counters; update counters from the next CLOB response headers.
- On each CLOB response, sync internal counters with X-RateLimit-Remaining to detect drift between internal estimates and Polymarket-reported values.
10. Reference Implementation
Maintains per-market and per-account sliding-window counters. On each intent, checks the KillSwitch and priority paths first, then evaluates both counters before approving, reshaping, or rejecting. Syncs counters from live X-RateLimit headers on every CLOB response.
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.
FUNCTION evaluateRateLimit(intent):
// --- 0. KillSwitch gate ---
ks = FETCH internal.killswitch.status
IF ks.active AND intent.type == 'OPEN':
EMIT RiskVote(decision=HARD_REJECT, reason=KILL_SWITCH_ACTIVE)
RETURN
// --- 1. Priority paths ---
IF intent.type == 'RISK_FLATTEN' AND params.priority_risk_flatten:
EMIT RiskVote(decision=APPROVE, reason=RATE_LIMIT_GOVERNOR_PRIORITY_FLATTEN)
RETURN
IF intent.type == 'CANCEL' AND params.priority_cancel_over_open:
reserved = FETCH internal.counter.reserved_cancel_budget
IF reserved.remaining > 0:
reserved.remaining -= 1
EMIT RiskVote(decision=APPROVE, reason=RATE_LIMIT_GOVERNOR_PRIORITY_CANCEL)
ELSE:
EMIT RiskVote(decision=HARD_REJECT, reason=RATE_LIMIT_GOVERNOR_CANCEL_BUDGET_EXHAUSTED)
RETURN
// --- 2. Read counters ---
tradingWindow = FETCH internal.counter.trading_window
marketWindow = FETCH internal.counter.market_window(intent.market_id)
IF tradingWindow IS NULL OR marketWindow IS NULL:
EMIT RiskVote(decision=HARD_REJECT, reason=RATE_LIMIT_GOVERNOR_STATE_UNKNOWN)
RETURN
// --- 3. Hard limit checks ---
IF tradingWindow.count >= params.trading_req_per_min.hard:
EMIT RiskVote(decision=HARD_REJECT, reason=RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED)
RETURN
perMarketHard = params.trading_req_per_min.hard / activeMarketCount()
IF marketWindow.count >= perMarketHard:
EMIT RiskVote(decision=HARD_REJECT, reason=RATE_LIMIT_GOVERNOR_MARKET_THROTTLED)
RETURN
// --- 4. Warning zone → reshape with defer ---
IF tradingWindow.count >= params.trading_req_per_min.warning OR
marketWindow.count >= perMarketHard * 0.8:
resetMs = tradingWindow.reset_at_ms - now_ms()
EMIT RiskVote(decision=RESHAPE_REQUIRED,
reason=RATE_LIMIT_GOVERNOR_BUDGET_WARN,
constraints={defer_ms: max(resetMs, 0)})
RETURN
// --- 5. Approve and increment counters ---
tradingWindow.count += 1
marketWindow.count += 1
EMIT RiskVote(decision=APPROVE,
reason=RATE_LIMIT_GOVERNOR_PASS,
checked_at=now_iso())
// Called on every CLOB API response to sync internal counters
FUNCTION syncFromClobHeaders(response):
remaining = response.headers.get('X-RateLimit-Remaining')
reset_at = response.headers.get('X-RateLimit-Reset')
IF remaining IS NOT NULL:
tradingWindow.count = params.trading_req_per_min.hard - int(remaining)
tradingWindow.reset_at_ms = int(reset_at) * 1000
SDK calls used
clob_auth.GET('/orders') — reads X-RateLimit-Remaining header from responseclob_auth.POST('/order') — reads X-RateLimit-Remaining header from responseinternal.counter.trading_window.get()internal.counter.market_window.get(market_id)internal.killswitch.status()
Complexity: O(1) — constant-time counter lookup and increment per intent
11. Wire Examples
Input — what arrives on the wire
OrderIntent — open order when rate is in warning zone — internal
{
"intent_id": "int_e5f6a7b8c9d0e1f2",
"market_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
"side": "BUY",
"outcome": "YES",
"size_usd": 200,
"price": 0.48,
"intent_type": "OPEN",
"generated_at_ms": 1746787260000
}
Internal rate counter state — internal
{
"trading_window_count": 87,
"trading_hard_limit": 100,
"market_window_count": 22,
"market_hard_limit": 25,
"window_reset_at_ms": 1746787320000,
"last_header_remaining": 13
}
Output — what the bot emits
RiskVote — RESHAPE (defer until window reset)
{
"guard_id": "risk.rate_limit_governor",
"decision": "RESHAPE_REQUIRED",
"severity": "WARN",
"reason_code": "RATE_LIMIT_GOVERNOR_BUDGET_WARN",
"message": "Trading rate 87/100 req/min (87%). Defer by 4200ms until window resets at 2026-05-09T12:02:00Z.",
"constraints": {
"defer_ms": 4200,
"passive_only": false,
"close_only": false
},
"inputs_used": [
"clob_auth.ratelimit_headers",
"internal.sliding_window.trading",
"internal.sliding_window.market"
],
"checked_at": "2026-05-09T12:01:15Z"
}
RiskVote — HARD_REJECT (budget exhausted)
{
"guard_id": "risk.rate_limit_governor",
"decision": "HARD_REJECT",
"severity": "HARD",
"reason_code": "RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED",
"message": "Trading rate 100/100 req/min. Rate budget exhausted for this window. Intent dropped.",
"constraints": {},
"inputs_used": [
"clob_auth.ratelimit_headers",
"internal.sliding_window.trading"
],
"checked_at": "2026-05-09T12:01:30Z"
}12. Decision Logic
APPROVE
Both per-market and per-account trading counters are below the warning threshold for the current sliding window, and the intent is not a cancel or risk-flatten (which have their own paths).
RESHAPE_REQUIRED
Either the per-market or per-account counter is in the warning zone (80–100% of limit) — emit constraints.defer_ms indicating how long to wait before re-submitting.
REJECT
Account-level or per-market counter has reached the hard limit for the current window; KillSwitch is active (for open orders); or rate-limit state is unavailable (fail-closed).
WARNING_ONLY
Not used — RateLimitGovernor has reject authority. Rate pressure is either deferred (RESHAPE) or hard-rejected when the budget is exhausted.
13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"guard_id": "risk.rate_limit_governor",
"decision": "RESHAPE_REQUIRED",
"severity": "WARN",
"reason_code": "RATE_LIMIT_GOVERNOR_BUDGET_WARN",
"message": "Trading rate at 87/100 req/min (87%). Deferring intent by 4200ms until window resets.",
"constraints": {
"defer_ms": 4200,
"passive_only": false,
"close_only": false
},
"inputs_used": [
"clob_auth.ratelimit_headers",
"internal.sliding_window.trading",
"internal.killswitch.status"
],
"checked_at": "2026-05-09T12:01:00Z"
}14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active; all open-order intents are rejected. Cancel and risk-flatten intents are unaffected. | Return HARD_REJECT for OPEN intents; CANCEL and RISK_FLATTEN intents proceed via priority path. | Trading is currently paused. Please try again later. |
RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED | HARD_REJECT | Account-level trading counter has reached the hard limit for the current sliding window. | Return HARD_REJECT; log window reset time. Intent may be requeued after window reset. | |
RATE_LIMIT_GOVERNOR_MARKET_THROTTLED | HARD_REJECT | Per-market order counter has reached the per-market sub-limit for the current window. | Return HARD_REJECT for this market; intents on other markets are unaffected. | |
RATE_LIMIT_GOVERNOR_BUDGET_WARN | RESHAPE | Trading counter is in the warning zone (80–100% of limit). Order deferred until window reset. | Return RESHAPE_REQUIRED with constraints.defer_ms = ms until window reset. | Your order has been briefly delayed to stay within trading rate limits. |
RATE_LIMIT_GOVERNOR_STATE_UNKNOWN | HARD_REJECT | Internal rate-limit state is uninitialised or cannot be read; fail-closed to prevent accidental over-sending. | Return HARD_REJECT for all non-priority intents until state is re-established from CLOB headers. | |
RATE_LIMIT_GOVERNOR_PRIORITY_CANCEL | INFO | Cancel intent approved from reserved cancel budget, bypassing the standard trading counter. | Decrement reserved cancel budget; emit APPROVE. | |
RATE_LIMIT_GOVERNOR_PRIORITY_FLATTEN | INFO | Risk-flatten intent approved unconditionally, bypassing all rate counters. | Emit APPROVE without touching any counter. | |
RATE_LIMIT_GOVERNOR_PASS | INFO | Both per-market and per-account counters are within limits. Intent approved. | Increment counters; emit APPROVE. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_risk_ratelimitgovernor_decisions_total | counter | count | decision, reason_code | Total RiskVote decisions emitted by RateLimitGovernor, broken down by decision type and reason code. |
polytraders_risk_ratelimitgovernor_trading_window_utilisation | gauge | ratio | Current trading window counter as a fraction of trading_req_per_min.hard. Alerts at 0.8. | |
polytraders_risk_ratelimitgovernor_market_window_utilisation | gauge | ratio | market_id | Per-market window counter as a fraction of the per-market sub-limit. |
polytraders_risk_ratelimitgovernor_clob_header_sync_age_seconds | gauge | seconds | Seconds since the last successful X-RateLimit-Remaining header was read from a CLOB response. Alerts if stale. | |
polytraders_risk_ratelimitgovernor_429_responses_total | counter | count | endpoint | Number of HTTP 429 responses received from the CLOB, indicating the governor failed to prevent an over-send. |
polytraders_risk_ratelimitgovernor_eval_latency_ms | histogram | milliseconds | Wall-clock time from OrderIntent receipt to RiskVote emit. P99 target < 5ms (in-memory counter lookup). |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
RateLimitGovernorBudgetCritical | polytraders_risk_ratelimitgovernor_trading_window_utilisation > 0.9 | page | #runbook-ratelimitgovernor-budget-critical |
RateLimitGovernor429Observed | rate(polytraders_risk_ratelimitgovernor_429_responses_total[5m]) > 0 | page | #runbook-ratelimitgovernor-429 |
RateLimitGovernorHeaderSyncStale | polytraders_risk_ratelimitgovernor_clob_header_sync_age_seconds > 60 | warn | #runbook-ratelimitgovernor-header-sync |
RateLimitGovernorHighRejectRate | rate(polytraders_risk_ratelimitgovernor_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_ratelimitgovernor_decisions_total[5m]) > 0.3 | warn | #runbook-ratelimitgovernor-reject-rate |
Dashboards
- Grafana — Risk overview / RateLimitGovernor
- Grafana — API health / CLOB rate-limit utilisation and 429 history
16. Developer Reporting
{
"bot_id": "risk.rate_limit_governor",
"decision": "RESHAPE_REQUIRED",
"reason_code": "RATE_LIMIT_GOVERNOR_BUDGET_WARN",
"inputs_used": [
"clob_auth.ratelimit_headers",
"internal.sliding_window.trading"
],
"metrics": {
"trading_counter": 87,
"trading_hard_limit": 100,
"market_counter": 22,
"market_hard_limit": 25,
"window_reset_in_ms": 4200,
"last_ratelimit_remaining_from_header": 13
},
"checked_at": "2026-05-09T12:01:00Z"
}17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| Order deferred — rate limit approaching | Your order has been briefly delayed to stay within trading rate limits. It will be submitted automatically in a few seconds. |
| Order blocked — rate limit exhausted | The maximum number of orders for this minute has been reached. Your order will be requeued for the next available window. |
18. Failure-Mode Block
| main_failure_mode | Approving too many intents because the internal sliding-window counter drifts from Polymarket's actual rate-limit state, leading to unexpected 429 responses that block all order operations including urgent cancellations. |
|---|---|
| false_positive_risk | Deferring or rejecting orders that could have been sent because the internal counter overestimates usage (e.g. after a burst that was partially absorbed by burst headroom on the CLOB side). |
| false_negative_risk | Approving too many intents during the brief window between a rate-limit reset and the next X-RateLimit-Remaining header being read, slightly exceeding the window budget. |
| safe_fallback | If the rate-limit header cannot be read (network error, malformed response) or the internal counter is in an unknown state, RateLimitGovernor hard-rejects all non-priority new open orders until the counter is re-synchronised from a successful CLOB response. Cancel and risk-flatten intents are never blocked. |
| required_dependencies | CLOB Auth API (X-RateLimit headers on responses), Internal sliding-window counter store, KillSwitch active flag |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
BUDGET_EXHAUSTED | Set internal trading_window.count = 100 (equal to hard limit) | Returns to APPROVE once the window resets (reset_at_ms passes) and counter is set back to 0. | |
429_FROM_CLOB | Return HTTP 429 from CLOB mock for POST /order | Returns to normal on next window reset after no further 429 responses. | |
HEADER_SYNC_STALE | Stop returning X-RateLimit-Remaining header in CLOB mock responses for 70s | Returns to full limit once headers are visible in responses again. | |
STATE_UNKNOWN_COLD_START | Restart bot with no pre-loaded counter state | Full rate budget available within one CLOB response cycle. | |
KILL_SWITCH_ON | Set internal.killswitch.status.active = true | Returns to normal on manual KillSwitch reset. |
20. State & Persistence
Cold-start recovery
On cold start, all counters initialise to 0. The first CLOB response syncs them to the live X-RateLimit-Remaining value. Until the first sync, intents are allowed up to 50% of the hard limit as a conservative bootstrap budget.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 1000 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 5 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | global mutex on counter increment to prevent double-counting under concurrent intent bursts |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| risk.kill_switch | Global brake — checked first; KillSwitch causes rejection of all open-order intents but does not block cancel/flatten. | RiskVote.HARD_REJECT(KILL_SWITCH_ACTIVE) for OPEN intents only. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| exec.smart_router | Approved RiskVotes pass to SmartRouter for ExecutionPlan construction. RESHAPE with defer_ms instructs SmartRouter to wait before submitting. | constraints.defer_ms is binding for SmartRouter scheduling. |
Sibling bots (same OrderIntent)
| Bot | Why | Contract |
|---|---|---|
| risk.portfolio_guard | Sibling guardrail; both must pass before SmartRouter runs. | |
| risk.liquidity_guard | Sibling guardrail. |
External services
| Service | Endpoint | SLA assumed | On failure |
|---|---|---|---|
| Polymarket CLOB V2 (rate-limit headers) | https://clob.polymarket.com | 99.95% / 200ms p99 (Polymarket-published) |
23. Security Surfaces
Abuse vectors considered
- Strategy flooding small-sized intents to consume the rate budget of other strategies
- Race condition: two concurrent strategies both checking the counter before either increments, causing double-approval near the hard limit
Mitigations
- Global mutex on counter increment prevents concurrent double-counting
- Per-market sub-limits prevent a single market from consuming the full account budget
- Cancel and risk-flatten budgets are reserved and cannot be consumed by open-order intents
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | no |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | RateLimitGovernor reads live X-RateLimit-* headers from the Polymarket CLOB V2 API to keep its internal sliding-window counters accurate. The bot does not inspect order fields for builder codes or negRisk flags; it operates purely on request-rate metadata. |
API surfaces declared
Networks supported
25. Versioning & Migration
| Field | Value |
|---|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|---|---|---|---|
| 2026-04-28 | v1 | v2 | CLOB V2 cutover | Switched to py-clob-client-v2; updated rate-limit header parsing to the V2 CLOB response schema. Removed feeRateBps from order-size estimation used in budget calculations. Rate counters now track by intent_id (V2 idempotency key) rather than nonce. EIP-712 domain version updated to '2' in outbound request accounting. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Approve when both counters are below warning threshold | trading_counter=50, trading_hard=100, market_counter=10, market_hard=25 | APPROVE with no constraints |
| Reshape with defer_ms when trading counter is in warning zone | trading_counter=85, trading_hard=100, window_reset_in_ms=5000 | RESHAPE with constraints.defer_ms=5000 |
| Hard-reject when trading counter reaches hard limit | trading_counter=100, trading_hard=100 | HARD_REJECT with reason_code=RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED |
| Hard-reject when per-market counter reaches per-market limit | market_counter=25, market_hard=25 | HARD_REJECT with reason_code=RATE_LIMIT_GOVERNOR_MARKET_THROTTLED |
| Cancel intent bypasses standard rate check via reserved budget | trading_counter=100, intent.type=CANCEL, priority_cancel_over_open=true | APPROVE from reserved cancel budget, regardless of trading_counter |
| Risk-flatten bypasses all rate checks | trading_counter=100, intent.type=RISK_FLATTEN, priority_risk_flatten=true | APPROVE unconditionally |
| Fail-closed when rate-limit state unknown | CLOB API returns no X-RateLimit headers and internal counter is uninitialised | HARD_REJECT(RATE_LIMIT_GOVERNOR_STATE_UNKNOWN) for all non-priority intents |
Integration Tests
| Test | Expected result |
|---|---|
| Internal counter syncs with live CLOB X-RateLimit-Remaining header | After a burst of 80 orders, internal counter reflects the value in X-RateLimit-Remaining within one response cycle |
| Cancel always passes when trading budget is exhausted | APPROVE for CANCEL intent when trading_counter == trading_req_per_min and priority_cancel_over_open=true |
| KillSwitch active rejects open orders but allows cancels | HARD_REJECT(KILL_SWITCH_ACTIVE) for OPEN intents; APPROVE for CANCEL intents when KillSwitch is active |
Property Tests
| Property | Required behaviour |
|---|---|
| Risk-flatten intents are never rejected or deferred by rate checks | Always true when priority_risk_flatten=true |
| Trading counter never exceeds hard limit | Always true — counter is checked before incrementing; only incremented on APPROVE |
| Rate-limit state unknown never results in APPROVE for open orders | Always true — RATE_LIMIT_GOVERNOR_STATE_UNKNOWN produces HARD_REJECT for non-priority intents |
27. Operational Runbook
RateLimitGovernor incidents are typically a 429 from Polymarket (counter miscalibrated or burst overrun) or a stale header sync. A 429 in production is a P1 incident because it may block all order operations including risk-flattening.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
RateLimitGovernor429Observed | ||||
RateLimitGovernorBudgetCritical | ||||
RateLimitGovernorHeaderSyncStale | ||||
RateLimitGovernorHighRejectRate |
Manual overrides
——
Healthcheck
GET /internal/health/ratelimitgovernor → 200 if trading window utilisation < 80%, last X-RateLimit-Remaining header read < 60s ago, and zero 429 responses in the last 5 minutes; red if utilisation >= 100%, any 429 observed, or header sync stale > 60s.28. Promotion Gates
A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.
Promote to Shadow
| Gate | How measured | Threshold |
|---|---|---|
| Unit tests pass including budget exhaustion, warning reshape, and priority cancel/flatten paths | CI test run | 100% pass |
| Integration test: counter syncs correctly from mock X-RateLimit-Remaining headers | Integration test suite | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| No 429 responses observed in 48h shadow run | polytraders_risk_ratelimitgovernor_429_responses_total counter | 0 events |
| p99 evaluation latency < 5ms | polytraders_risk_ratelimitgovernor_eval_latency_ms histogram | p99 < 5ms |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| Cancel and risk-flatten always pass even when trading budget is exhausted (7-day live evidence) | reason_code audit on PRIORITY_CANCEL and PRIORITY_FLATTEN decisions | 100% approval rate for cancel/flatten intents regardless of budget state |
| Zero 429 responses over 7 consecutive days | RateLimitGovernor429Observed alert history | 0 firings |
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|---|
| Purpose defined | ✓ done |
| Required inputs listed | ✓ done |
| Parameters defined | ✓ done |
| Defaults defined | ✓ done |
| Warning thresholds defined | ✓ done |
| Hard thresholds defined | ✓ done |
| Safe fallback defined | ✓ done |
| Structured output defined | ✓ done |
| Developer log defined | ✓ done |
| Plain-English explanation | ✓ done |
| Unit tests defined | ✓ done |
| Integration tests defined | ✓ done |
| Property tests defined | ✓ done |
| Failure-mode block complete | ✓ done |
| Reference implementation pseudocode | ✓ done |
| Wire examples (input + output) | ✓ done |
| Reason codes listed | ✓ done |
| Metrics & logs defined | ✓ done |
| State & persistence defined | ✓ done |
| Concurrency & idempotency defined | ✓ done |
| Dependencies declared | ✓ done |
| Security surfaces declared | ✓ done |
| Polymarket V2 compatibility declared | ✓ done |
| Version & migration history declared | ✓ done |
| Operational runbook defined | ✓ done |
| Promotion gates defined | ✓ done |
| Failure-injection recipes defined | ✓ done |