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.15 SettlementExposureGuard

1.15 SettlementExposureGuard

Risk Guardrail VetoReshape PLANNED Planned capital · Direct P4 · Core risk pending stub

SettlementExposureGuard tracks how much pUSD is committed in markets that share the same UMA resolution window and blocks or downsizes new orders that would push the concurrent settlement exposure above the configured ceiling. It prevents the user from having more equity at risk in a single 2-hour UMA settlement window than they can afford to lose if all markets in that window resolve adversely.

v3 readiness

Docs27/27
donehow scored
Impl11/15
in progresshow scored
Backtest3/4
in progresshow scored
Runtime0/8
pendinghow scored

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

v3.5 · wired registry id risk.exposureguard

Maps to spec page settlementexposureguard. SEARCH_SPACE declared. Fixture pack pending.

Source: @polytraders/bots · src/risk/exposureguard.js · Impl 11/15 · Backtest 3/4

1. Bot Identity

LayerRisk  Risk
Bot classGuardrail
AuthorityVetoReshape
StatusPLANNED
ReadinessPlanned
Runs beforeExecutionPlan emit
Runs afterStrategy OrderIntent
Applies toEvery OrderIntent — caps simultaneous settlement resolution risk by limiting how much equity is locked in markets that could resolve in the same UMA oracle window
Default modeplanned
User-visiblesummary-only
Developer ownerPolytraders core — Risk pod

Operational profile

Modes supportedquarantine

2. Purpose

SettlementExposureGuard tracks how much pUSD is committed in markets that share the same UMA resolution window and blocks or downsizes new orders that would push the concurrent settlement exposure above the configured ceiling. It prevents the user from having more equity at risk in a single 2-hour UMA settlement window than they can afford to lose if all markets in that window resolve adversely.

3. Why This Bot Matters

  • Multiple markets resolving in same UMA window

    Markets resolving in the same 2-hour UMA window create a concentration of settlement risk; an adverse outcome across all of them results in simultaneous losses the user did not anticipate.

  • No cap on concurrent settlement exposure

    Without a settlement window cap, strategies can inadvertently concentrate the majority of the portfolio in a single 2-hour settlement batch.

4. Required Polymarket Inputs

InputSourceRequired?Use
Market resolution end_date and UMA challenge window metadatagammaYesGroup open positions by their UMA resolution window (2-hour buckets) to compute concurrent settlement exposure.
Open position notional by marketclob_publicYesSum the pUSD notional for all positions in each UMA window bucket.

5. Required Internal Inputs

InputSourceRequired?Use
Settlement window exposure config (max concurrent pUSD)internalYesLoad the per-window exposure ceiling.
KillSwitch active flagKillSwitchYesIf active, reject immediately.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
max_concurrent_settlement_usd300024003000Maximum pUSD allowed to resolve in a single UMA settlement window (default 2 hours).
uma_window_hours2.0NoneNoneDuration in hours of the UMA settlement window used for grouping positions. Matches the UMA optimistic oracle 2-hour challenge period.
warn_pct0.8NoneNoneFraction of max_concurrent_settlement_usd at which a WARN annotation is emitted without blocking.

7. Detailed Parameter Instructions

max_concurrent_settlement_usd

What it means

Maximum pUSD allowed to resolve in a single UMA settlement window (default 2 hours).

Default

{ "max_concurrent_settlement_usd": 3000 }

Why this default matters

3000 pUSD per window ensures that a worst-case adverse resolution of all markets in the window cannot exceed a manageable loss fraction of a typical 10 000 pUSD portfolio.

Threshold logic

ConditionAction
window_exposure + intent.size_usd <= 2400APPROVE
2400 < total <= 3000WARN — SETTLEMENT_EXPOSURE_APPROACHING
total > 3000RESHAPE or HARD_REJECT — SETTLEMENT_EXPOSURE_EXCEEDED

Developer check

if (windowExposure + intent.size_usd > params.max_concurrent_settlement_usd) return reshape_or_reject('SETTLEMENT_EXPOSURE_EXCEEDED');

User-facing English

Your exposure in this settlement window has reached the limit.

uma_window_hours

What it means

Duration in hours of the UMA settlement window used for grouping positions. Matches the UMA optimistic oracle 2-hour challenge period.

Default

{ "uma_window_hours": 2.0 }

Why this default matters

2 hours is the canonical UMA challenge window; grouping by this interval correctly identifies markets that will compete for the same resolution event slot.

Threshold logic

ConditionAction
alwaysGroup positions in 2-hour UMA buckets

Developer check

const bucketKey = Math.floor(market.end_date_ms / (params.uma_window_hours * 3600000));

User-facing English

— not yet authored —

warn_pct

What it means

Fraction of max_concurrent_settlement_usd at which a WARN annotation is emitted without blocking.

Default

{ "warn_pct": 0.8 }

Why this default matters

80% utilisation gives early warning before the hard ceiling is reached.

Threshold logic

ConditionAction
window_exposure / max >= warn_pctAttach WARN annotation
window_exposure / max < warn_pctNo annotation

Developer check

if (windowExposure / params.max_concurrent_settlement_usd > params.warn_pct) annotations.push(WARN);

User-facing English

— not yet authored —

8. Default Configuration

{
  "bot_id": "risk.settlement_exposure_guard",
  "version": "0.1.0",
  "mode": "hard_guard",
  "defaults": {
    "max_concurrent_settlement_usd": 3000,
    "uma_window_hours": 2.0,
    "warn_pct": 0.8
  },
  "locked": {
    "max_concurrent_settlement_usd": {
      "min": 100
    },
    "uma_window_hours": {
      "min": 2.0
    }
  }
}

9. Implementation Flow

  1. Receive OrderIntent with market_id, size_usd.
  2. Check KillSwitch; if active, HARD_REJECT(KILL_SWITCH_ACTIVE).
  3. Fetch target market end_date from Gamma to determine its UMA settlement window bucket.
  4. Fetch all open positions; group by UMA window bucket (floor(end_date_ms / window_ms)).
  5. Compute window_exposure = sum of notional for all positions in the same bucket as the target market.
  6. If window_exposure + intent.size_usd > max_concurrent_settlement_usd: compute safe_size.
  7. If safe_size > 0: RESHAPE(max_size_usd=safe_size). Else HARD_REJECT(SETTLEMENT_EXPOSURE_EXCEEDED).
  8. If window_exposure / max > warn_pct: attach WARN annotation.
  9. APPROVE with window_exposure and bucket_key attached.

10. Reference Implementation

Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. IF/THEN/ELSE = decision. Translate directly to TypeScript, Python, Go, or Rust.

FUNCTION evaluateSettlementExposure(intent):
  ks = FETCH internal.killswitch.status
  IF ks.active: EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN

  market = FETCH gamma.getMarket(intent.market_id)
  IF market IS NULL:
    EMIT RiskVote(HARD_REJECT, SETTLEMENT_EXPOSURE_DATA_UNAVAILABLE); RETURN

  windowMs = params.uma_window_hours * 3600000
  bucketKey = floor(market.end_date_ms / windowMs)

  positions = FETCH clob_public.positions(intent.user_id)
  IF positions IS NULL:
    EMIT RiskVote(HARD_REJECT, SETTLEMENT_EXPOSURE_DATA_UNAVAILABLE); RETURN

  windowExposure = 0
  FOR pos IN positions:
    posMarket = FETCH gamma.getMarket(pos.market_id)
    IF floor(posMarket.end_date_ms / windowMs) == bucketKey:
      windowExposure += pos.notional_usd

  IF windowExposure + intent.size_usd > params.max_concurrent_settlement_usd:
    safeSize = params.max_concurrent_settlement_usd - windowExposure
    IF safeSize > 0:
      EMIT RiskVote(RESHAPE_REQUIRED, SETTLEMENT_EXPOSURE_EXCEEDED,
                    constraints={max_size_usd: safeSize}); RETURN
    EMIT RiskVote(HARD_REJECT, SETTLEMENT_EXPOSURE_EXCEEDED); RETURN

  utilisation = windowExposure / params.max_concurrent_settlement_usd
  IF utilisation > params.warn_pct:
    annotations.append(WARN(SETTLEMENT_EXPOSURE_APPROACHING))

  EMIT RiskVote(APPROVE, window_exposure=windowExposure, bucket_key=bucketKey)

SDK calls used

  • gamma.getMarket(market_id)
  • clob_public.positions(user_id)
  • internal.killswitch.status()

Complexity: O(N) where N = open positions

11. Wire Examples

Input — what arrives on the wire

OrderIntent — window cap reachedinternal

{
  "intent_id": "int_a7b8c9d0e1f20007",
  "market_id": "0x5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f",
  "size_usd": 400,
  "generated_at_ms": 1746800000000
}

Output — what the bot emits

RiskVote — RESHAPE_REQUIRED

{
  "guard_id": "risk.settlement_exposure_guard",
  "decision": "RESHAPE_REQUIRED",
  "severity": "WARN",
  "reason_code": "SETTLEMENT_EXPOSURE_EXCEEDED",
  "message": "Window bucket exposure 2800 pUSD + 400 exceeds 3000. Resized to 200.",
  "constraints": {
    "max_size_usd": 200
  },
  "checked_at": "2026-05-10T14:00:00Z"
}

12. Decision Logic

APPROVE

Adding the proposed order to the UMA window bucket does not exceed max_concurrent_settlement_usd.

RESHAPE_REQUIRED

The bucket would exceed the ceiling with the full order but a reduced size fits.

REJECT

The bucket is fully utilised even at minimum size, or data is unavailable.

WARNING_ONLY

— not yet authored —

13. Standard Decision Output

This bot returns a RiskVote object. See RiskVote schema.

{
  "guard_id": "risk.settlement_exposure_guard",
  "decision": "RESHAPE_REQUIRED",
  "severity": "WARN",
  "reason_code": "SETTLEMENT_EXPOSURE_EXCEEDED",
  "message": "UMA window bucket has 2800 pUSD exposure; adding 400 pUSD exceeds 3000 ceiling. Resized to 200 pUSD.",
  "constraints": {
    "max_size_usd": 200
  },
  "inputs_used": [
    "gamma.market.end_date",
    "clob_public.positions",
    "internal.config"
  ],
  "checked_at": "2026-05-10T14:00:00Z"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch active.Immediate HARD_REJECT.Trading is paused. Please try again later.
SETTLEMENT_EXPOSURE_EXCEEDEDRESHAPEAdding the intent to the UMA window bucket would exceed the ceiling.RESHAPE if safe_size > 0; else HARD_REJECT.Your exposure in this settlement window has reached the limit.
SETTLEMENT_EXPOSURE_APPROACHINGWARNWindow exposure is above warn_pct of the ceiling.Attach WARN annotation; APPROVE.
SETTLEMENT_EXPOSURE_DATA_UNAVAILABLEHARD_REJECTGamma market metadata or position data unavailable.HARD_REJECT (fail-closed).We could not verify settlement window data. Please try again.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_risk_settlementexposureguard_decisions_totalcountercountdecision, reason_codeTotal decisions by type.
polytraders_risk_settlementexposureguard_window_exposure_usdgaugeusdbucket_keyCurrent pUSD exposure per UMA settlement window bucket.
polytraders_risk_settlementexposureguard_eval_latency_mshistogrammillisecondsLatency from intent to RiskVote emit.

Alerts

AlertConditionSeverityRunbook
SettlementExposureGuardWindowNearCappolytraders_risk_settlementexposureguard_window_exposure_usd / 3000 > 0.9P2#runbook-settlement-near-cap
SettlementExposureGuardDataUnavailablerate(polytraders_risk_settlementexposureguard_decisions_total{reason_code='SETTLEMENT_EXPOSURE_DATA_UNAVAILABLE'}[5m]) > 0P1#runbook-settlement-data

16. Developer Reporting

{
  "bot_id": "risk.settlement_exposure_guard",
  "decision": "RESHAPE_REQUIRED",
  "reason_code": "SETTLEMENT_EXPOSURE_EXCEEDED",
  "inputs_used": [
    "gamma.market.end_date",
    "clob_public.positions"
  ],
  "metrics": {
    "bucket_key": "1746799200",
    "window_exposure_usd": 2800,
    "intent_size_usd": 400,
    "ceiling_usd": 3000,
    "safe_size_usd": 200
  },
  "checked_at": "2026-05-10T14:00:00Z"
}

17. Plain-English Reporting

SituationUser-facing explanation
Order downsized — settlement window capYour order was reduced because your exposure in this settlement window has reached its limit. Adding more would concentrate too much risk in a single resolution event.
Order blocked — window fully utilisedYou have reached the maximum exposure in this settlement window. Please wait for some of those markets to resolve before placing new orders.

18. Failure-Mode Block

main_failure_modeApproving an order because the position cache is stale and does not include recently placed orders in the same UMA window, underestimating window exposure.
false_positive_riskBlocking an order because a position in the same window has already been closed but the cache has not been refreshed.
false_negative_riskTwo concurrent intents approved simultaneously before either updates the position cache, both contributing to the same window bucket.
safe_fallbackIf Gamma market metadata or position data is unavailable, HARD_REJECT with SETTLEMENT_EXPOSURE_DATA_UNAVAILABLE. Never approve on missing data.
required_dependenciesGamma API (market end_date), CLOB position data, Settlement exposure config, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
GAMMA_UNAVAILABLEReturn 503 from Gamma APIReturns to normal after Gamma API is reachable.
WINDOW_CAP_REACHEDLoad positions to fill bucket_key to 3000 pUSD, submit any intent for the same bucketReturns to APPROVE after positions in the bucket reduce below ceiling.
CONCURRENT_INTENTSSubmit two intents simultaneously for the same bucket_keyImmediate — second intent is evaluated after first updates the bucket exposure.

20. State & Persistence

Cold-start recovery

Position cache rebuilt from CLOB on cold start. Gamma cache populated on first market lookup.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight200
Idempotency keyintent_id
Per-call timeout (ms)100
Backpressure strategydrop newest
Locking / mutual exclusionper-bucket optimistic lock to prevent concurrent over-allocation in same window

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchGlobal brake checked first.HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all evaluation.

Emits to (downstream consumers)

BotWhyContract
exec.smart_routerAPPROVE or RESHAPE passes to SmartRouter.RESHAPE constraints respected by SmartRouter.

External services

ServiceEndpointSLA assumedOn failure
Gamma API (market end_date)https://gamma-api.polymarket.com99.9% / 300ms p99HARD_REJECT(SETTLEMENT_EXPOSURE_DATA_UNAVAILABLE) if Gamma unavailable.
CLOB API (positions)https://clob.polymarket.com99.95% / 200ms p99HARD_REJECT(SETTLEMENT_EXPOSURE_DATA_UNAVAILABLE) if position data unavailable.

23. Security Surfaces

Abuse vectors considered

  • Submitting concurrent intents for the same UMA window bucket to bypass per-intent idempotency
  • Selecting markets with staggered end_dates that fall just outside the window bucket to accumulate higher effective exposure

Mitigations

  • Per-bucket optimistic lock ensures at most one intent is processed at a time per bucket
  • Market end_date is fetched from Gamma at evaluation time, not trusted from the intent payload

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsyes
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesSettlement window grouping uses UMA optimistic oracle 2-hour challenge period per V2 protocol. NegRisk markets are included in window calculations using their Gamma end_date.

25. Versioning & Migration

FieldValue
spec2.0.0
implementation0.1.0
schema2
releasedNone
planned_releaseQ4-2026

Migration history

DateFromToReasonAction taken
2026-04-28n/av2-specSpec drafted post-CLOB-V2 cutover; bot not yet implementedDesigned against V2 schema (pUSD, builder codes, V2 EIP-712 domain)

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Approve when window exposure within warning thresholdwindow_exposure=2000, intent.size=300, ceiling=3000APPROVE
Reshape when window would exceed ceilingwindow_exposure=2800, intent.size=400, ceiling=3000RESHAPE_REQUIRED(max_size_usd=200)
Reject when window fully utilisedwindow_exposure=3000, intent.size=10, ceiling=3000HARD_REJECT(SETTLEMENT_EXPOSURE_EXCEEDED)
Warn when window exposure above warn_pctwindow_exposure=2500, max=3000, warn_pct=0.8APPROVE with WARN annotation

Integration Tests

TestExpected result
Multiple positions in same UMA bucket correctly summedWindow exposure reflects all open positions with matching bucket_key
KillSwitch bypasses settlement checkHARD_REJECT(KILL_SWITCH_ACTIVE) without reading Gamma or positions

Property Tests

PropertyRequired behaviour
Total window exposure never exceeds max_concurrent_settlement_usd after APPROVEAlways true
Reshape size is strictly <= original intent size_usdAlways true

27. Operational Runbook

SettlementExposureGuard incidents typically involve a strategy concentrating orders in a single UMA window. Verify the bucket distribution in Grafana before adjusting the ceiling.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
SettlementExposureGuardWindowNearCapCheck window_exposure_usd gauge; identify which bucket is near cap. Confirm whether this is a genuine concentration or a stale position cache.Risk pod lead if genuine concentration confirmed.
SettlementExposureGuardDataUnavailableCheck Gamma API and CLOB positions endpoint; confirm connectivity.Infra on-call if either service is down > 2 minutes.

Manual overrides

  • polytraders risk refresh-positions --user-id <id> — After a known CLOB sync delay causing stale position data.

Healthcheck

GET /internal/health/settlementexposureguard → green: Gamma cache populated, position cache age < 15s, no buckets within 20% of ceiling; red: Gamma or CLOB unavailable, position cache age > 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 for reshape and reject window scenariosCI test run100% pass

Promote to Limited live

GateHow measuredThreshold
Window bucket grouping verified against live Gamma end_date values over 48hManual spot-check of bucket_key assignment100% correct bucketing

Promote to General live

GateHow measuredThreshold
Zero DATA_UNAVAILABLE rejections during normal hours over 7 daysSettlementExposureGuardDataUnavailable 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