Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerRisk1.5 RateLimitGovernor

1.5 RateLimitGovernor

Risk Guardrail RejectReshape LIVE General live capital · Direct P4 · Core risk pending stub

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

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerRisk  Risk
Bot classGuardrail
AuthorityRejectReshape
StatusLIVE
ReadinessGeneral live
Runs beforeExecutionPlan emit
Runs afterStrategy OrderIntent
Applies toEvery OrderIntent — checks per-market and per-account order send-rate against Polymarket CLOB limits before allowing execution
Default modegeneral_live
User-visibleno
Developer ownerPolytraders core — Risk pod

Operational profile

Modes supportedquarantine

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

InputSourceRequired?Use
CLOB rate-limit response headers: X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Limitclob_authYesTrack the live remaining request budget and reset time per sliding window. Update internal counters after every CLOB response.

5. Required Internal Inputs

InputSourceRequired?Use
Per-market sliding-window order counterinternalYesCount the number of order intents sent to each market within the current rate-limit window.
Per-account global order counterinternalYesCount total order intents across all markets to enforce the account-level rate cap.
Intent classification: open / cancel / risk-flatteninternalYesCancel and risk-flatten intents always receive priority allocation from the reserved cancel budget; they bypass the standard rate check.
KillSwitch active flagKillSwitchYesIf KillSwitch is active, reject all new open orders immediately. Cancel/flatten requests still use the priority path.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
public_req_per_min200160200Maximum number of public (unauthenticated) CLOB requests per minute across all strategies.
trading_req_per_min10080100Maximum number of authenticated order-placement requests per minute across all strategies and markets.
priority_cancel_over_openTrueNoneTrueWhen 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_flattenTrueNoneTrueWhen 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

ConditionAction
requests ≤ 160 / minAPPROVE
160–200 / minWARN — log rate pressure; begin shedding lowest-priority requests
> 200 / minREJECT — 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

ConditionAction
requests ≤ 80 / minAPPROVE
80–100 / minRESHAPE — defer non-priority intents to next window
> 100 / minREJECT — 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

ConditionAction
priority_cancel_over_open = true AND intent is cancelAlways APPROVE from reserved cancel budget, bypassing standard rate check
priority_cancel_over_open = falseCancel 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

ConditionAction
priority_risk_flatten = true AND intent is risk-flattenImmediately APPROVE — bypasses all rate counters
priority_risk_flatten = falseRisk-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

  1. Receive OrderIntent from Strategy layer with intent type (open / cancel / risk-flatten).
  2. Check KillSwitch active flag; if active, reject all open-order intents immediately. Cancel and risk-flatten intents still proceed via their priority paths.
  3. If intent type is RISK_FLATTEN and priority_risk_flatten=true, approve unconditionally and bypass all rate counters.
  4. If intent type is CANCEL and priority_cancel_over_open=true, approve from the reserved cancel budget and bypass the standard trading counter.
  5. Read the current sliding-window counter for the target market (per-market bucket) and for the account (global trading bucket).
  6. 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.
  7. 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.
  8. 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).
  9. Approve the intent; increment both the per-market and per-account counters; update counters from the next CLOB response headers.
  10. 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 response
  • clob_auth.POST('/order') — reads X-RateLimit-Remaining header from response
  • internal.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 zoneinternal

{
  "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 stateinternal

{
  "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

CodeSeverityMeaningActionUser-facing message
KILL_SWITCH_ACTIVEHARD_REJECTGlobal 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_EXHAUSTEDHARD_REJECTAccount-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_THROTTLEDHARD_REJECTPer-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_WARNRESHAPETrading 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_UNKNOWNHARD_REJECTInternal 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_CANCELINFOCancel intent approved from reserved cancel budget, bypassing the standard trading counter.Decrement reserved cancel budget; emit APPROVE.
RATE_LIMIT_GOVERNOR_PRIORITY_FLATTENINFORisk-flatten intent approved unconditionally, bypassing all rate counters.Emit APPROVE without touching any counter.
RATE_LIMIT_GOVERNOR_PASSINFOBoth per-market and per-account counters are within limits. Intent approved.Increment counters; emit APPROVE.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_risk_ratelimitgovernor_decisions_totalcountercountdecision, reason_codeTotal RiskVote decisions emitted by RateLimitGovernor, broken down by decision type and reason code.
polytraders_risk_ratelimitgovernor_trading_window_utilisationgaugeratioCurrent trading window counter as a fraction of trading_req_per_min.hard. Alerts at 0.8.
polytraders_risk_ratelimitgovernor_market_window_utilisationgaugeratiomarket_idPer-market window counter as a fraction of the per-market sub-limit.
polytraders_risk_ratelimitgovernor_clob_header_sync_age_secondsgaugesecondsSeconds since the last successful X-RateLimit-Remaining header was read from a CLOB response. Alerts if stale.
polytraders_risk_ratelimitgovernor_429_responses_totalcountercountendpointNumber of HTTP 429 responses received from the CLOB, indicating the governor failed to prevent an over-send.
polytraders_risk_ratelimitgovernor_eval_latency_mshistogrammillisecondsWall-clock time from OrderIntent receipt to RiskVote emit. P99 target < 5ms (in-memory counter lookup).

Alerts

AlertConditionSeverityRunbook
RateLimitGovernorBudgetCriticalpolytraders_risk_ratelimitgovernor_trading_window_utilisation > 0.9page#runbook-ratelimitgovernor-budget-critical
RateLimitGovernor429Observedrate(polytraders_risk_ratelimitgovernor_429_responses_total[5m]) > 0page#runbook-ratelimitgovernor-429
RateLimitGovernorHeaderSyncStalepolytraders_risk_ratelimitgovernor_clob_header_sync_age_seconds > 60warn#runbook-ratelimitgovernor-header-sync
RateLimitGovernorHighRejectRaterate(polytraders_risk_ratelimitgovernor_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_ratelimitgovernor_decisions_total[5m]) > 0.3warn#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

SituationUser-facing explanation
Order deferred — rate limit approachingYour order has been briefly delayed to stay within trading rate limits. It will be submitted automatically in a few seconds.
Order blocked — rate limit exhaustedThe 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_modeApproving 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_riskDeferring 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_riskApproving 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_fallbackIf 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_dependenciesCLOB Auth API (X-RateLimit headers on responses), Internal sliding-window counter store, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
BUDGET_EXHAUSTEDSet 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_CLOBReturn HTTP 429 from CLOB mock for POST /orderReturns to normal on next window reset after no further 429 responses.
HEADER_SYNC_STALEStop returning X-RateLimit-Remaining header in CLOB mock responses for 70sReturns to full limit once headers are visible in responses again.
STATE_UNKNOWN_COLD_STARTRestart bot with no pre-loaded counter stateFull rate budget available within one CLOB response cycle.
KILL_SWITCH_ONSet internal.killswitch.status.active = trueReturns 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

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight1000
Idempotency keyintent_id
Per-call timeout (ms)5
Backpressure strategydrop newest
Locking / mutual exclusionglobal mutex on counter increment to prevent double-counting under concurrent intent bursts

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchGlobal 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)

BotWhyContract
exec.smart_routerApproved 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)

BotWhyContract
risk.portfolio_guardSibling guardrail; both must pass before SmartRouter runs.
risk.liquidity_guardSibling guardrail.

External services

ServiceEndpointSLA assumedOn failure
Polymarket CLOB V2 (rate-limit headers)https://clob.polymarket.com99.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

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsno
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesRateLimitGovernor 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

clob_authinternal

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation2.1.0
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1v2CLOB V2 cutoverSwitched 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

TestSetupExpected result
Approve when both counters are below warning thresholdtrading_counter=50, trading_hard=100, market_counter=10, market_hard=25APPROVE with no constraints
Reshape with defer_ms when trading counter is in warning zonetrading_counter=85, trading_hard=100, window_reset_in_ms=5000RESHAPE with constraints.defer_ms=5000
Hard-reject when trading counter reaches hard limittrading_counter=100, trading_hard=100HARD_REJECT with reason_code=RATE_LIMIT_GOVERNOR_BUDGET_EXHAUSTED
Hard-reject when per-market counter reaches per-market limitmarket_counter=25, market_hard=25HARD_REJECT with reason_code=RATE_LIMIT_GOVERNOR_MARKET_THROTTLED
Cancel intent bypasses standard rate check via reserved budgettrading_counter=100, intent.type=CANCEL, priority_cancel_over_open=trueAPPROVE from reserved cancel budget, regardless of trading_counter
Risk-flatten bypasses all rate checkstrading_counter=100, intent.type=RISK_FLATTEN, priority_risk_flatten=trueAPPROVE unconditionally
Fail-closed when rate-limit state unknownCLOB API returns no X-RateLimit headers and internal counter is uninitialisedHARD_REJECT(RATE_LIMIT_GOVERNOR_STATE_UNKNOWN) for all non-priority intents

Integration Tests

TestExpected result
Internal counter syncs with live CLOB X-RateLimit-Remaining headerAfter 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 exhaustedAPPROVE for CANCEL intent when trading_counter == trading_req_per_min and priority_cancel_over_open=true
KillSwitch active rejects open orders but allows cancelsHARD_REJECT(KILL_SWITCH_ACTIVE) for OPEN intents; APPROVE for CANCEL intents when KillSwitch is active

Property Tests

PropertyRequired behaviour
Risk-flatten intents are never rejected or deferred by rate checksAlways true when priority_risk_flatten=true
Trading counter never exceeds hard limitAlways true — counter is checked before incrementing; only incremented on APPROVE
Rate-limit state unknown never results in APPROVE for open ordersAlways 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

AlertFirst stepDiagnosisMitigationEscalate 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

GateHow measuredThreshold
Unit tests pass including budget exhaustion, warning reshape, and priority cancel/flatten pathsCI test run100% pass
Integration test: counter syncs correctly from mock X-RateLimit-Remaining headersIntegration test suitePass

Promote to Limited live

GateHow measuredThreshold
No 429 responses observed in 48h shadow runpolytraders_risk_ratelimitgovernor_429_responses_total counter0 events
p99 evaluation latency < 5mspolytraders_risk_ratelimitgovernor_eval_latency_ms histogramp99 < 5ms

Promote to General live

GateHow measuredThreshold
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 decisions100% approval rate for cancel/flatten intents regardless of budget state
Zero 429 responses over 7 consecutive daysRateLimitGovernor429Observed alert history0 firings

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
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