{
  "schema_version": "1.0.0",
  "bot_id": "1.9",
  "bot_name": "StrategySuitabilityGate",
  "slug": "strategysuitabilitygate",
  "layer": "Risk",
  "layer_key": "risk",
  "bot_class": "Guardrail",
  "authority": [
    "Veto",
    "Reshape"
  ],
  "status": "planned",
  "readiness": "Planned",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Risk",
    "bot_class": "Guardrail",
    "authority": "Veto, Reshape",
    "runs_before": "ExecutionPlan emit",
    "runs_after": "Strategy OrderIntent",
    "applies_to": "Every OrderIntent \u2014 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 \u2014 Risk pod"
  },
  "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.",
  "why_it_matters": [
    {
      "failure": "Unsupported strategy executed",
      "consequence": "A user configured for basic strategies executes a complex multi-outcome strategy they have not validated, leading to unexpected losses and support escalation."
    },
    {
      "failure": "Capital envelope exceeded",
      "consequence": "A strategy-level order exceeds the user's configured per-strategy capital cap, concentrating more exposure than intended."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Gamma market category and complexity flags",
      "source": "gamma",
      "required": true,
      "use": "Determine whether the target market requires advanced knowledge (e.g. negRisk multi-outcome)."
    },
    {
      "input": "Market outcome count",
      "source": "gamma",
      "required": true,
      "use": "Flag multi-outcome markets that require elevated experience tier."
    }
  ],
  "internal_inputs": [
    {
      "input": "User experience tier and capital envelope config",
      "source": "internal",
      "required": true,
      "use": "Compare against the strategy type of the incoming OrderIntent."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "If active, reject immediately."
    }
  ],
  "raw_params": [
    "allowed_strategy_classes \u00b7 list",
    "max_capital_per_strategy_usd \u00b7 int",
    "require_elevation_for_negrisk \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "allowed_strategy_classes",
      "default": [
        "basic"
      ],
      "warning": null,
      "hard": true,
      "controls": "List of strategy class identifiers the user is authorised to trade. Any OrderIntent whose strategy_class is not in this list is hard-rejected.",
      "why_default_matters": "Defaults to basic only; operator must explicitly grant access to advanced strategies.",
      "threshold_logic": [
        {
          "condition": "intent.strategy_class in allowed_strategy_classes",
          "action": "APPROVE (this check)"
        },
        {
          "condition": "intent.strategy_class not in allowed_strategy_classes",
          "action": "REJECT \u2014 SUITABILITY_STRATEGY_CLASS_BLOCKED"
        }
      ],
      "dev_check": "if (!params.allowed_strategy_classes.includes(intent.strategy_class)) return reject('SUITABILITY_STRATEGY_CLASS_BLOCKED');",
      "user_facing": "This strategy type is not enabled for your account."
    },
    {
      "name": "max_capital_per_strategy_usd",
      "default": 1000,
      "warning": 800,
      "hard": 1000,
      "controls": "Maximum pUSD notional allowed for a single strategy's active exposure.",
      "why_default_matters": "Caps exposure at 1000 pUSD per strategy for default tier users; prevents inadvertent concentration.",
      "threshold_logic": [
        {
          "condition": "intent.size_usd <= 1000",
          "action": "APPROVE"
        },
        {
          "condition": "800 < intent.size_usd <= 1000",
          "action": "WARN"
        },
        {
          "condition": "intent.size_usd > 1000",
          "action": "REJECT \u2014 SUITABILITY_CAPITAL_CAP_EXCEEDED"
        }
      ],
      "dev_check": "if (intent.size_usd > params.max_capital_per_strategy_usd) return reject('SUITABILITY_CAPITAL_CAP_EXCEEDED');",
      "user_facing": "Your order exceeds the capital limit for this strategy."
    },
    {
      "name": "require_elevation_for_negrisk",
      "default": true,
      "warning": null,
      "hard": true,
      "controls": "When true, negRisk multi-outcome markets require an elevated experience tier.",
      "why_default_matters": "NegRisk markets have more complex resolution dynamics; blocking them for basic-tier users prevents misuse.",
      "threshold_logic": [
        {
          "condition": "negrisk=true AND tier<advanced AND require_elevation_for_negrisk=true",
          "action": "REJECT \u2014 SUITABILITY_NEGRISK_BLOCKED"
        },
        {
          "condition": "tier>=advanced OR require_elevation_for_negrisk=false",
          "action": "APPROVE"
        }
      ],
      "dev_check": "if (params.require_elevation_for_negrisk && intent.neg_risk && user.tier < 'advanced') return reject('SUITABILITY_NEGRISK_BLOCKED');",
      "user_facing": "This market type requires an elevated account tier."
    }
  ],
  "default_config": {
    "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
      }
    }
  },
  "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 \u2014 APPROVE with checked_at timestamp."
  ],
  "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."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "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"
  },
  "developer_log": {
    "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"
  },
  "user_explanations": [
    {
      "situation": "Order blocked \u2014 strategy type not enabled",
      "message": "This strategy type is not available for your account tier. Contact support to request access."
    },
    {
      "situation": "Order blocked \u2014 capital cap exceeded",
      "message": "Your order size exceeds the per-strategy capital limit for your account. Please reduce the order size."
    },
    {
      "situation": "Order blocked \u2014 advanced market tier required",
      "message": "This market type requires an elevated account tier. Please review your profile settings."
    }
  ],
  "failure_modes": {
    "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"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when strategy class is allowed",
        "setup": "strategy_class=basic, allowed=[basic]",
        "expected": "APPROVE"
      },
      {
        "test": "Reject when strategy class not in allowed list",
        "setup": "strategy_class=multi_leg, allowed=[basic]",
        "expected": "HARD_REJECT(SUITABILITY_STRATEGY_CLASS_BLOCKED)"
      },
      {
        "test": "Reject when size exceeds capital cap",
        "setup": "size_usd=1200, max=1000",
        "expected": "HARD_REJECT(SUITABILITY_CAPITAL_CAP_EXCEEDED)"
      },
      {
        "test": "Reject negRisk for basic tier",
        "setup": "neg_risk=true, tier=basic, require_elevation=true",
        "expected": "HARD_REJECT(SUITABILITY_NEGRISK_BLOCKED)"
      }
    ],
    "integration": [
      {
        "test": "User tier upgrade propagates within TTL",
        "expected": "APPROVE after tier upgraded to advanced within cache refresh window"
      },
      {
        "test": "KillSwitch bypasses all profile checks",
        "expected": "HARD_REJECT(KILL_SWITCH_ACTIVE) without reading profile"
      }
    ],
    "property": [
      {
        "property": "Blocked strategy classes never result in APPROVE",
        "required": "Always true"
      },
      {
        "property": "Missing profile always results in HARD_REJECT",
        "required": "Always true \u2014 fail-closed on data unavailability"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Block strategies that don't match the user's declared experience and capital level.",
  "legacy_pm_signals": [
    "User profile: declared experience tier, account age, completed onboarding flags",
    "Strategy class tag (basic / intermediate / advanced)",
    "Account equity vs. strategy's required minimum"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "risk_compliance"
  ],
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch active.",
      "action": "Immediate HARD_REJECT.",
      "user_message": "Trading is paused. Please try again later."
    },
    {
      "code": "SUITABILITY_STRATEGY_CLASS_BLOCKED",
      "severity": "HARD_REJECT",
      "meaning": "Strategy class not in user's allowed list.",
      "action": "Return HARD_REJECT; log strategy_class and user_tier.",
      "user_message": "This strategy type is not enabled for your account."
    },
    {
      "code": "SUITABILITY_CAPITAL_CAP_EXCEEDED",
      "severity": "HARD_REJECT",
      "meaning": "Order size exceeds per-strategy capital cap.",
      "action": "Return HARD_REJECT; log size_usd and cap.",
      "user_message": "Your order exceeds the capital limit for this strategy."
    },
    {
      "code": "SUITABILITY_NEGRISK_BLOCKED",
      "severity": "HARD_REJECT",
      "meaning": "NegRisk market requires elevated tier not held by user.",
      "action": "Return HARD_REJECT.",
      "user_message": "This market type requires an elevated account tier."
    },
    {
      "code": "SUITABILITY_DATA_UNAVAILABLE",
      "severity": "HARD_REJECT",
      "meaning": "User profile store unavailable; cannot evaluate suitability.",
      "action": "Return HARD_REJECT (fail-closed).",
      "user_message": "We could not verify your account settings. Please try again."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_risk_strategysuitabilitygate_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision",
          "reason_code"
        ],
        "meaning": "Total RiskVote decisions by decision type and reason."
      },
      {
        "name": "polytraders_risk_strategysuitabilitygate_class_blocks_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "strategy_class"
        ],
        "meaning": "Count of orders blocked per strategy class."
      },
      {
        "name": "polytraders_risk_strategysuitabilitygate_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Latency from intent receipt to RiskVote emit."
      }
    ],
    "alerts": [
      {
        "name": "SuitabilityGateHighRejectRate",
        "condition": "rate(polytraders_risk_strategysuitabilitygate_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_strategysuitabilitygate_decisions_total[5m]) > 0.3",
        "severity": "P2",
        "runbook": "#runbook-suitabilitygate-reject"
      },
      {
        "name": "SuitabilityGateDataUnavailable",
        "condition": "rate(polytraders_risk_strategysuitabilitygate_decisions_total{reason_code='SUITABILITY_DATA_UNAVAILABLE'}[5m]) > 0",
        "severity": "P1",
        "runbook": "#runbook-suitabilitygate-data"
      }
    ]
  },
  "state": {
    "store": "redis",
    "shape": "User profile hash keyed by user_id; TTL 60s with background refresh.",
    "ttl": "60s",
    "recovery": "On cold start, profile is fetched from Redis synchronously. If Redis unavailable, HARD_REJECT until restored.",
    "size_estimate": "~1 KB per user profile entry"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 300,
    "idempotency_key": "intent_id",
    "timeout_ms": 50,
    "backpressure": "drop newest",
    "locking": "profile cache reads are lock-free; writes use Redis SET with TTL"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Global brake checked first.",
        "contract": "HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all evaluation."
      }
    ],
    "emits_to": [
      {
        "bot_id": "exec.smart_router",
        "why": "Approved RiskVote passes to SmartRouter.",
        "contract": "APPROVE or HARD_REJECT; no RESHAPE in current version."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Gamma API (market category)",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 300ms p99",
        "failure_mode": "HARD_REJECT(SUITABILITY_DATA_UNAVAILABLE) if market metadata unavailable."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "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"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "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."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q3-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "reference_implementation": {
    "pseudocode": "FUNCTION evaluateSuitability(intent):\n  ks = FETCH internal.killswitch.status\n  IF ks.active:\n    EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN\n\n  profile = FETCH internal.user_profile(intent.user_id)\n  IF profile IS NULL:\n    EMIT RiskVote(HARD_REJECT, SUITABILITY_DATA_UNAVAILABLE); RETURN\n\n  IF intent.strategy_class NOT IN profile.allowed_strategy_classes:\n    EMIT RiskVote(HARD_REJECT, SUITABILITY_STRATEGY_CLASS_BLOCKED); RETURN\n\n  IF intent.size_usd > params.max_capital_per_strategy_usd:\n    EMIT RiskVote(HARD_REJECT, SUITABILITY_CAPITAL_CAP_EXCEEDED); RETURN\n\n  IF params.require_elevation_for_negrisk AND intent.neg_risk:\n    IF profile.tier < 'advanced':\n      EMIT RiskVote(HARD_REJECT, SUITABILITY_NEGRISK_BLOCKED); RETURN\n\n  EMIT RiskVote(APPROVE, checked_at=now_ms())",
    "sdk_calls": [
      "internal.user_profile(user_id)",
      "internal.killswitch.status()",
      "gamma.getMarketByConditionId(market_id)"
    ],
    "complexity": "O(1)"
  },
  "wire_examples": {
    "input": [
      {
        "label": "OrderIntent \u2014 strategy class blocked",
        "source": "internal",
        "payload": {
          "intent_id": "int_a1b2c3d4e5f60001",
          "market_id": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
          "strategy_class": "multi_leg",
          "size_usd": 500,
          "neg_risk": false,
          "generated_at_ms": 1746800000000
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 HARD_REJECT",
        "payload": {
          "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"
        }
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "PROFILE_UNAVAILABLE",
      "how_to_inject": "Block Redis and wait for cache TTL to expire",
      "expected_behaviour": "HARD_REJECT(SUITABILITY_DATA_UNAVAILABLE) on every intent",
      "recovery": "Returns to normal within one cache-refresh cycle after Redis is restored."
    },
    {
      "scenario": "BLOCKED_STRATEGY_CLASS",
      "how_to_inject": "Submit intent with strategy_class='advanced' when allowed=['basic']",
      "expected_behaviour": "HARD_REJECT(SUITABILITY_STRATEGY_CLASS_BLOCKED)",
      "recovery": "Immediate on next intent with allowed class."
    },
    {
      "scenario": "CAPITAL_CAP_EXCEEDED",
      "how_to_inject": "Submit intent with size_usd > max_capital_per_strategy_usd",
      "expected_behaviour": "HARD_REJECT(SUITABILITY_CAPITAL_CAP_EXCEEDED)",
      "recovery": "Immediate on next intent within cap."
    }
  ],
  "runbook": {
    "summary": "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.",
    "oncall_actions": [
      {
        "alert": "SuitabilityGateDataUnavailable",
        "first_step": "Check Redis connectivity; confirm profile cache TTL not expired.",
        "escalation": "Risk pod lead if sustained > 2 minutes.",
        "diagnosis": "",
        "mitigation": ""
      },
      {
        "alert": "SuitabilityGateHighRejectRate",
        "first_step": "Check reason_code distribution; if SUITABILITY_STRATEGY_CLASS_BLOCKED dominates, review allowed_strategy_classes config.",
        "escalation": "Risk pod lead if false positives confirmed.",
        "diagnosis": "",
        "mitigation": ""
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders risk refresh-profile --user-id <id>",
        "effect": "After a user tier upgrade that has not propagated within the cache TTL."
      }
    ],
    "healthcheck": "GET /internal/health/strategysuitabilitygate \u2192 green: Redis reachable, profile cache age < 60s, p99 eval latency < 50ms; red: Redis unreachable, cache age > 120s, or SUITABILITY_DATA_UNAVAILABLE rate > 0"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass including fail-closed scenarios",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Shadow reject rate within 5% of expected baseline over 48h",
        "how_measured": "Grafana shadow vs live dashboard",
        "threshold": "< 5% divergence"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero SUITABILITY_DATA_UNAVAILABLE rejections during normal hours over 7 days",
        "how_measured": "SuitabilityGateDataUnavailable alert history",
        "threshold": "0 firings"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "RiskVote"
    ],
    "topics": [
      "polytraders.reports.risk"
    ],
    "partition_by": "trace_id",
    "cadence": "every-event",
    "retention_class": "2y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "fail-closed",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ObservationReport"
    ]
  },
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 4,
    "phase_name": "Core risk",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}