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.9 StrategySuitabilityGate

1.9 StrategySuitabilityGate

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

StrategySuitabilityGate screens every OrderIntent against the user's declared experience tier and capital envelope, blocking strategy types that exceed the user's configured risk profile. It prevents novice-tier users from executing advanced multi-leg or high-leverage strategies without explicit elevation.

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
AuthorityVetoReshape
StatusPLANNED
ReadinessPlanned
Runs beforeExecutionPlan emit
Runs afterStrategy OrderIntent
Applies toEvery OrderIntent — validates that the strategy type matches the user's declared experience tier and capital envelope
Default modeplanned
User-visiblesummary-only
Developer ownerPolytraders core — Risk pod

Operational profile

Modes supportedquarantine

2. Purpose

StrategySuitabilityGate screens every OrderIntent against the user's declared experience tier and capital envelope, blocking strategy types that exceed the user's configured risk profile. It prevents novice-tier users from executing advanced multi-leg or high-leverage strategies without explicit elevation.

3. Why This Bot Matters

  • Unsupported strategy executed

    A user configured for basic strategies executes a complex multi-outcome strategy they have not validated, leading to unexpected losses and support escalation.

  • Capital envelope exceeded

    A strategy-level order exceeds the user's configured per-strategy capital cap, concentrating more exposure than intended.

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
Gamma market category and complexity flagsgammaYesDetermine whether the target market requires advanced knowledge (e.g. negRisk multi-outcome).
Market outcome countgammaYesFlag multi-outcome markets that require elevated experience tier.

5. Required Internal Inputs

InputSourceRequired?Use
User experience tier and capital envelope configinternalYesCompare against the strategy type of the incoming OrderIntent.
KillSwitch active flagKillSwitchYesIf active, reject immediately.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
allowed_strategy_classes['basic']NoneTrueList of strategy class identifiers the user is authorised to trade. Any OrderIntent whose strategy_class is not in this list is hard-rejected.
max_capital_per_strategy_usd10008001000Maximum pUSD notional allowed for a single strategy's active exposure.
require_elevation_for_negriskTrueNoneTrueWhen true, negRisk multi-outcome markets require an elevated experience tier.

7. Detailed Parameter Instructions

allowed_strategy_classes

What it means

List of strategy class identifiers the user is authorised to trade. Any OrderIntent whose strategy_class is not in this list is hard-rejected.

Default

{ "allowed_strategy_classes": ["basic"] }

Why this default matters

Defaults to basic only; operator must explicitly grant access to advanced strategies.

Threshold logic

ConditionAction
intent.strategy_class in allowed_strategy_classesAPPROVE (this check)
intent.strategy_class not in allowed_strategy_classesREJECT — SUITABILITY_STRATEGY_CLASS_BLOCKED

Developer check

if (!params.allowed_strategy_classes.includes(intent.strategy_class)) return reject('SUITABILITY_STRATEGY_CLASS_BLOCKED');

User-facing English

This strategy type is not enabled for your account.

max_capital_per_strategy_usd

What it means

Maximum pUSD notional allowed for a single strategy's active exposure.

Default

{ "max_capital_per_strategy_usd": 1000 }

Why this default matters

Caps exposure at 1000 pUSD per strategy for default tier users; prevents inadvertent concentration.

Threshold logic

ConditionAction
intent.size_usd <= 1000APPROVE
800 < intent.size_usd <= 1000WARN
intent.size_usd > 1000REJECT — SUITABILITY_CAPITAL_CAP_EXCEEDED

Developer check

if (intent.size_usd > params.max_capital_per_strategy_usd) return reject('SUITABILITY_CAPITAL_CAP_EXCEEDED');

User-facing English

Your order exceeds the capital limit for this strategy.

require_elevation_for_negrisk

What it means

When true, negRisk multi-outcome markets require an elevated experience tier.

Default

{ "require_elevation_for_negrisk": true }

Why this default matters

NegRisk markets have more complex resolution dynamics; blocking them for basic-tier users prevents misuse.

Threshold logic

ConditionAction
negrisk=true AND tier<advanced AND require_elevation_for_negrisk=trueREJECT — SUITABILITY_NEGRISK_BLOCKED
tier>=advanced OR require_elevation_for_negrisk=falseAPPROVE

Developer check

if (params.require_elevation_for_negrisk && intent.neg_risk && user.tier < 'advanced') return reject('SUITABILITY_NEGRISK_BLOCKED');

User-facing English

This market type requires an elevated account tier.

8. Default Configuration

{
  "bot_id": "risk.strategy_suitability_gate",
  "version": "0.1.0",
  "mode": "hard_guard",
  "defaults": {
    "allowed_strategy_classes": [
      "basic"
    ],
    "max_capital_per_strategy_usd": 1000,
    "require_elevation_for_negrisk": true
  },
  "locked": {
    "max_capital_per_strategy_usd": {
      "min": 50
    }
  }
}

9. Implementation Flow

  1. Receive OrderIntent including strategy_class, size_usd, neg_risk flag, and user context.
  2. Check KillSwitch; if active, HARD_REJECT(KILL_SWITCH_ACTIVE).
  3. Load user profile (experience tier, capital envelope) from internal config store.
  4. If intent.strategy_class not in allowed_strategy_classes, HARD_REJECT(SUITABILITY_STRATEGY_CLASS_BLOCKED).
  5. If intent.size_usd > max_capital_per_strategy_usd, HARD_REJECT(SUITABILITY_CAPITAL_CAP_EXCEEDED).
  6. If require_elevation_for_negrisk and intent.neg_risk and user.tier < advanced, HARD_REJECT(SUITABILITY_NEGRISK_BLOCKED).
  7. All checks passed — APPROVE with checked_at timestamp.

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 evaluateSuitability(intent):
  ks = FETCH internal.killswitch.status
  IF ks.active:
    EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN

  profile = FETCH internal.user_profile(intent.user_id)
  IF profile IS NULL:
    EMIT RiskVote(HARD_REJECT, SUITABILITY_DATA_UNAVAILABLE); RETURN

  IF intent.strategy_class NOT IN profile.allowed_strategy_classes:
    EMIT RiskVote(HARD_REJECT, SUITABILITY_STRATEGY_CLASS_BLOCKED); RETURN

  IF intent.size_usd > params.max_capital_per_strategy_usd:
    EMIT RiskVote(HARD_REJECT, SUITABILITY_CAPITAL_CAP_EXCEEDED); RETURN

  IF params.require_elevation_for_negrisk AND intent.neg_risk:
    IF profile.tier < 'advanced':
      EMIT RiskVote(HARD_REJECT, SUITABILITY_NEGRISK_BLOCKED); RETURN

  EMIT RiskVote(APPROVE, checked_at=now_ms())

SDK calls used

  • internal.user_profile(user_id)
  • internal.killswitch.status()
  • gamma.getMarketByConditionId(market_id)

Complexity: O(1)

11. Wire Examples

Input — what arrives on the wire

OrderIntent — strategy class blockedinternal

{
  "intent_id": "int_a1b2c3d4e5f60001",
  "market_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
  "strategy_class": "multi_leg",
  "size_usd": 500,
  "neg_risk": false,
  "generated_at_ms": 1746800000000
}

Output — what the bot emits

RiskVote — HARD_REJECT

{
  "guard_id": "risk.strategy_suitability_gate",
  "decision": "HARD_REJECT",
  "severity": "HARD",
  "reason_code": "SUITABILITY_STRATEGY_CLASS_BLOCKED",
  "message": "strategy_class 'multi_leg' not in allowed list ['basic'].",
  "constraints": {},
  "checked_at": "2026-05-10T08:00:00Z"
}

12. Decision Logic

APPROVE

Strategy class is permitted, order size within capital cap, and negRisk elevation requirement is met.

RESHAPE_REQUIRED

Not used currently; future versions may downsize to the capital cap instead of rejecting.

REJECT

Strategy class blocked, capital cap exceeded, negRisk elevation required but not present, or KillSwitch active.

WARNING_ONLY

— not yet authored —

13. Standard Decision Output

This bot returns a RiskVote object. See RiskVote schema.

{
  "guard_id": "risk.strategy_suitability_gate",
  "decision": "HARD_REJECT",
  "severity": "HARD",
  "reason_code": "SUITABILITY_STRATEGY_CLASS_BLOCKED",
  "message": "Strategy class 'multi_leg' is not in the user's allowed_strategy_classes list.",
  "constraints": {},
  "inputs_used": [
    "internal.user_profile",
    "internal.killswitch.status"
  ],
  "checked_at": "2026-05-10T08: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.
SUITABILITY_STRATEGY_CLASS_BLOCKEDHARD_REJECTStrategy class not in user's allowed list.Return HARD_REJECT; log strategy_class and user_tier.This strategy type is not enabled for your account.
SUITABILITY_CAPITAL_CAP_EXCEEDEDHARD_REJECTOrder size exceeds per-strategy capital cap.Return HARD_REJECT; log size_usd and cap.Your order exceeds the capital limit for this strategy.
SUITABILITY_NEGRISK_BLOCKEDHARD_REJECTNegRisk market requires elevated tier not held by user.Return HARD_REJECT.This market type requires an elevated account tier.
SUITABILITY_DATA_UNAVAILABLEHARD_REJECTUser profile store unavailable; cannot evaluate suitability.Return HARD_REJECT (fail-closed).We could not verify your account settings. Please try again.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_risk_strategysuitabilitygate_decisions_totalcountercountdecision, reason_codeTotal RiskVote decisions by decision type and reason.
polytraders_risk_strategysuitabilitygate_class_blocks_totalcountercountstrategy_classCount of orders blocked per strategy class.
polytraders_risk_strategysuitabilitygate_eval_latency_mshistogrammillisecondsLatency from intent receipt to RiskVote emit.

Alerts

AlertConditionSeverityRunbook
SuitabilityGateHighRejectRaterate(polytraders_risk_strategysuitabilitygate_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_strategysuitabilitygate_decisions_total[5m]) > 0.3P2#runbook-suitabilitygate-reject
SuitabilityGateDataUnavailablerate(polytraders_risk_strategysuitabilitygate_decisions_total{reason_code='SUITABILITY_DATA_UNAVAILABLE'}[5m]) > 0P1#runbook-suitabilitygate-data

16. Developer Reporting

{
  "bot_id": "risk.strategy_suitability_gate",
  "decision": "HARD_REJECT",
  "reason_code": "SUITABILITY_STRATEGY_CLASS_BLOCKED",
  "inputs_used": [
    "internal.user_profile"
  ],
  "metrics": {
    "strategy_class": "multi_leg",
    "allowed": [
      "basic"
    ],
    "user_tier": "basic",
    "size_usd": 500
  },
  "checked_at": "2026-05-10T08:00:00Z"
}

17. Plain-English Reporting

SituationUser-facing explanation
Order blocked — strategy type not enabledThis strategy type is not available for your account tier. Contact support to request access.
Order blocked — capital cap exceededYour order size exceeds the per-strategy capital limit for your account. Please reduce the order size.
Order blocked — advanced market tier requiredThis market type requires an elevated account tier. Please review your profile settings.

18. Failure-Mode Block

main_failure_modeApproving a strategy type that exceeds the user's experience tier, exposing them to complex risk they did not consent to.
false_positive_riskRejecting a legitimate order because the user's profile cache is stale and does not reflect a recent tier upgrade.
false_negative_riskApproving an order against an outdated allowed_strategy_classes list before a restriction has propagated to the cache.
safe_fallbackIf user profile is unavailable, HARD_REJECT with SUITABILITY_DATA_UNAVAILABLE. Never approve on missing profile data.
required_dependenciesUser profile store, KillSwitch active flag, Gamma market category metadata

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
PROFILE_UNAVAILABLEBlock Redis and wait for cache TTL to expireReturns to normal within one cache-refresh cycle after Redis is restored.
BLOCKED_STRATEGY_CLASSSubmit intent with strategy_class='advanced' when allowed=['basic']Immediate on next intent with allowed class.
CAPITAL_CAP_EXCEEDEDSubmit intent with size_usd > max_capital_per_strategy_usdImmediate on next intent within cap.

20. State & Persistence

Cold-start recovery

On cold start, profile is fetched from Redis synchronously. If Redis unavailable, HARD_REJECT until restored.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight300
Idempotency keyintent_id
Per-call timeout (ms)50
Backpressure strategydrop newest
Locking / mutual exclusionprofile cache reads are lock-free; writes use Redis SET with TTL

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_routerApproved RiskVote passes to SmartRouter.APPROVE or HARD_REJECT; no RESHAPE in current version.

External services

ServiceEndpointSLA assumedOn failure
Gamma API (market category)https://gamma-api.polymarket.com99.9% / 300ms p99HARD_REJECT(SUITABILITY_DATA_UNAVAILABLE) if market metadata unavailable.

23. Security Surfaces

Abuse vectors considered

  • Submitting an OrderIntent with a spoofed strategy_class field
  • Profile cache poisoning to elevate a user's apparent tier

Mitigations

  • strategy_class is validated against an operator-managed enum; unknown values are rejected
  • Profile cache is write-protected; only the user-config service may update entries

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
NotesEvaluates negRisk flag from V2 Gamma metadata to enforce tier-gating on multi-outcome markets. Does not sign orders.

25. Versioning & Migration

FieldValue
spec2.0.0
implementation0.1.0
schema2
releasedNone
planned_releaseQ3-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 strategy class is allowedstrategy_class=basic, allowed=[basic]APPROVE
Reject when strategy class not in allowed liststrategy_class=multi_leg, allowed=[basic]HARD_REJECT(SUITABILITY_STRATEGY_CLASS_BLOCKED)
Reject when size exceeds capital capsize_usd=1200, max=1000HARD_REJECT(SUITABILITY_CAPITAL_CAP_EXCEEDED)
Reject negRisk for basic tierneg_risk=true, tier=basic, require_elevation=trueHARD_REJECT(SUITABILITY_NEGRISK_BLOCKED)

Integration Tests

TestExpected result
User tier upgrade propagates within TTLAPPROVE after tier upgraded to advanced within cache refresh window
KillSwitch bypasses all profile checksHARD_REJECT(KILL_SWITCH_ACTIVE) without reading profile

Property Tests

PropertyRequired behaviour
Blocked strategy classes never result in APPROVEAlways true
Missing profile always results in HARD_REJECTAlways true — fail-closed on data unavailability

27. Operational Runbook

Incidents typically involve a stale user profile cache or a misconfigured allowed_strategy_classes list. Confirm Redis connectivity and profile cache age before adjusting parameters.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
SuitabilityGateDataUnavailableCheck Redis connectivity; confirm profile cache TTL not expired.Risk pod lead if sustained > 2 minutes.
SuitabilityGateHighRejectRateCheck reason_code distribution; if SUITABILITY_STRATEGY_CLASS_BLOCKED dominates, review allowed_strategy_classes config.Risk pod lead if false positives confirmed.

Manual overrides

  • polytraders risk refresh-profile --user-id <id> — After a user tier upgrade that has not propagated within the cache TTL.

Healthcheck

GET /internal/health/strategysuitabilitygate → green: Redis reachable, profile cache age < 60s, p99 eval latency < 50ms; red: Redis unreachable, cache age > 120s, or SUITABILITY_DATA_UNAVAILABLE rate > 0

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 fail-closed scenariosCI test run100% pass

Promote to Limited live

GateHow measuredThreshold
Shadow reject rate within 5% of expected baseline over 48hGrafana shadow vs live dashboard< 5% divergence

Promote to General live

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