1. Bot Identity
| Layer | Risk Risk |
|---|
| Bot class | Guardrail |
|---|
| Authority | VetoReshape |
|---|
| Status | PLANNED |
|---|
| Readiness | Planned |
|---|
| Runs before | ExecutionPlan emit |
|---|
| Runs after | Strategy OrderIntent |
|---|
| Applies to | Every OrderIntent — validates that the strategy type matches the user's declared experience tier and capital envelope |
|---|
| Default mode | planned |
|---|
| User-visible | summary-only |
|---|
| Developer owner | Polytraders core — Risk pod |
|---|
Operational profile
| Modes supported | quarantine |
|---|
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.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| allowed_strategy_classes | ['basic'] | None | True | List 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_usd | 1000 | 800 | 1000 | Maximum pUSD notional allowed for a single strategy's active exposure. |
| require_elevation_for_negrisk | True | None | True | When 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
| Condition | Action |
|---|
| intent.strategy_class in allowed_strategy_classes | APPROVE (this check) |
| intent.strategy_class not in allowed_strategy_classes | REJECT — 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
| Condition | Action |
|---|
| intent.size_usd <= 1000 | APPROVE |
| 800 < intent.size_usd <= 1000 | WARN |
| intent.size_usd > 1000 | REJECT — 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
| Condition | Action |
|---|
| negrisk=true AND tier<advanced AND require_elevation_for_negrisk=true | REJECT — SUITABILITY_NEGRISK_BLOCKED |
| tier>=advanced OR require_elevation_for_negrisk=false | APPROVE |
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
- Receive OrderIntent including strategy_class, size_usd, neg_risk flag, and user context.
- Check KillSwitch; if active, HARD_REJECT(KILL_SWITCH_ACTIVE).
- Load user profile (experience tier, capital envelope) from internal config store.
- If intent.strategy_class not in allowed_strategy_classes, HARD_REJECT(SUITABILITY_STRATEGY_CLASS_BLOCKED).
- If intent.size_usd > max_capital_per_strategy_usd, HARD_REJECT(SUITABILITY_CAPITAL_CAP_EXCEEDED).
- If require_elevation_for_negrisk and intent.neg_risk and user.tier < advanced, HARD_REJECT(SUITABILITY_NEGRISK_BLOCKED).
- 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 blocked — internal
{
"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
| Code | Severity | Meaning | Action | User-facing message |
|---|
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch active. | Immediate HARD_REJECT. | Trading is paused. Please try again later. |
SUITABILITY_STRATEGY_CLASS_BLOCKED | HARD_REJECT | Strategy 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_EXCEEDED | HARD_REJECT | Order 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_BLOCKED | HARD_REJECT | NegRisk market requires elevated tier not held by user. | Return HARD_REJECT. | This market type requires an elevated account tier. |
SUITABILITY_DATA_UNAVAILABLE | HARD_REJECT | User 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
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_risk_strategysuitabilitygate_decisions_total | counter | count | decision, reason_code | Total RiskVote decisions by decision type and reason. |
polytraders_risk_strategysuitabilitygate_class_blocks_total | counter | count | strategy_class | Count of orders blocked per strategy class. |
polytraders_risk_strategysuitabilitygate_eval_latency_ms | histogram | milliseconds | | Latency from intent receipt to RiskVote emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
SuitabilityGateHighRejectRate | rate(polytraders_risk_strategysuitabilitygate_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_strategysuitabilitygate_decisions_total[5m]) > 0.3 | P2 | #runbook-suitabilitygate-reject |
SuitabilityGateDataUnavailable | rate(polytraders_risk_strategysuitabilitygate_decisions_total{reason_code='SUITABILITY_DATA_UNAVAILABLE'}[5m]) > 0 | P1 | #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
| Situation | User-facing explanation |
|---|
| Order blocked — strategy type not enabled | This strategy type is not available for your account tier. Contact support to request access. |
| Order blocked — capital cap exceeded | Your order size exceeds the per-strategy capital limit for your account. Please reduce the order size. |
| Order blocked — advanced market tier required | This market type requires an elevated account tier. Please review your profile settings. |
18. Failure-Mode Block
| main_failure_mode | Approving a strategy type that exceeds the user's experience tier, exposing them to complex risk they did not consent to. |
|---|
| false_positive_risk | Rejecting a legitimate order because the user's profile cache is stale and does not reflect a recent tier upgrade. |
|---|
| false_negative_risk | Approving an order against an outdated allowed_strategy_classes list before a restriction has propagated to the cache. |
|---|
| safe_fallback | If user profile is unavailable, HARD_REJECT with SUITABILITY_DATA_UNAVAILABLE. Never approve on missing profile data. |
|---|
| required_dependencies | User profile store, KillSwitch active flag, Gamma market category metadata |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
PROFILE_UNAVAILABLE | Block Redis and wait for cache TTL to expire | | Returns to normal within one cache-refresh cycle after Redis is restored. |
BLOCKED_STRATEGY_CLASS | Submit intent with strategy_class='advanced' when allowed=['basic'] | | Immediate on next intent with allowed class. |
CAPITAL_CAP_EXCEEDED | Submit intent with size_usd > max_capital_per_strategy_usd | | Immediate 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
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop |
| Max in-flight | 300 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 50 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | profile cache reads are lock-free; writes use Redis SET with TTL |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
| risk.kill_switch | Global brake checked first. | HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all evaluation. |
Emits to (downstream consumers)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| Gamma API (market category) | https://gamma-api.polymarket.com | 99.9% / 300ms p99 | HARD_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
| Aspect | Value |
|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | yes |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Evaluates negRisk flag from V2 Gamma metadata to enforce tier-gating on multi-outcome markets. Does not sign orders. |
25. Versioning & Migration
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q3-2026 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | n/a | v2-spec | Spec drafted post-CLOB-V2 cutover; bot not yet implemented | Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain) |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Approve when strategy class is allowed | strategy_class=basic, allowed=[basic] | APPROVE |
| Reject when strategy class not in allowed list | strategy_class=multi_leg, allowed=[basic] | HARD_REJECT(SUITABILITY_STRATEGY_CLASS_BLOCKED) |
| Reject when size exceeds capital cap | size_usd=1200, max=1000 | HARD_REJECT(SUITABILITY_CAPITAL_CAP_EXCEEDED) |
| Reject negRisk for basic tier | neg_risk=true, tier=basic, require_elevation=true | HARD_REJECT(SUITABILITY_NEGRISK_BLOCKED) |
Integration Tests
| Test | Expected result |
|---|
| User tier upgrade propagates within TTL | APPROVE after tier upgraded to advanced within cache refresh window |
| KillSwitch bypasses all profile checks | HARD_REJECT(KILL_SWITCH_ACTIVE) without reading profile |
Property Tests
| Property | Required behaviour |
|---|
| Blocked strategy classes never result in APPROVE | Always true |
| Missing profile always results in HARD_REJECT | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
SuitabilityGateDataUnavailable | Check Redis connectivity; confirm profile cache TTL not expired. | | | Risk pod lead if sustained > 2 minutes. |
SuitabilityGateHighRejectRate | Check 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
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 |