{
  "schema_version": "1.0.0",
  "bot_id": "5.3",
  "bot_name": "SignaturePreviewer",
  "slug": "signaturepreviewer",
  "layer": "Security",
  "layer_key": "sec",
  "bot_class": "Guardrail",
  "authority": [
    "Reject",
    "Pause"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Security",
    "bot_class": "Guardrail",
    "authority": "Reject, Pause",
    "runs_before": "Wallet signing modal is shown to user",
    "runs_after": "Strategy OrderIntent and contract/permission checks",
    "applies_to": "Every EIP-712 signing request before user confirmation",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Render a plain-English summary of every EIP-712 signature before the wallet shows the modal.",
  "why_it_matters": [
    {
      "failure": "User signs a phishing or misrouted order without understanding it",
      "consequence": "Funds transferred to an unintended address or contract under user signature."
    },
    {
      "failure": "Domain separator not validated before display",
      "consequence": "A forged domain could cause a user to unknowingly sign for a different chain or contract."
    },
    {
      "failure": "Strategy deviates from declared envelope",
      "consequence": "User signs an order outside the parameters they approved at strategy setup."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "EIP-712 typed data for pending order",
      "source": "clob_auth",
      "required": true,
      "use": "Decode and display the full typed data in plain English."
    },
    {
      "input": "Market metadata (name, outcomes, expiry)",
      "source": "gamma",
      "required": true,
      "use": "Resolve token IDs to human-readable market names in the preview."
    }
  ],
  "internal_inputs": [
    {
      "input": "Strategy declared envelope (side, size, price range)",
      "source": "StrategyConfig",
      "required": true,
      "use": "Diff the pending order against the declared envelope; flag deviations."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Block signing if kill switch is active."
    }
  ],
  "raw_params": [
    "require_preview_for \u00b7 list",
    "min_detail_level \u00b7 enum",
    "block_on_envelope_mismatch \u00b7 bool",
    "retain_log_days \u00b7 int"
  ],
  "parameters": [
    {
      "name": "require_preview_for",
      "default": [
        "all"
      ],
      "warning": "Order fields deviate from strategy envelope",
      "hard": "Domain separator mismatch or envelope breach > 20%",
      "controls": "List of order types that require a preview before signing.",
      "why_default_matters": "Default ['all'] ensures every signing request is previewed without exception.",
      "threshold_logic": [
        {
          "condition": "order_type in require_preview_for",
          "action": "Generate preview; await user acknowledgement"
        },
        {
          "condition": "domain separator mismatch",
          "action": "REJECT \u2014 CONTRACT_GUARD_DOMAIN_MISMATCH"
        }
      ],
      "dev_check": "if (p.require_preview_for.includes('all') || p.require_preview_for.includes(order.type)) generatePreview(order);",
      "user_facing": "Please review this order before signing."
    },
    {
      "name": "block_on_envelope_mismatch",
      "default": true,
      "warning": "Order deviates 10\u201320% from declared envelope",
      "hard": "Order deviates > 20% from declared envelope",
      "controls": "Block signing if the order exceeds the strategy's declared parameters.",
      "why_default_matters": "Blocking on mismatch prevents a compromised strategy from signing orders the user did not intend.",
      "threshold_logic": [
        {
          "condition": "deviation <= 10%",
          "action": "APPROVE \u2014 no warning"
        },
        {
          "condition": "deviation 10\u201320%",
          "action": "WARN in preview"
        },
        {
          "condition": "deviation > 20% AND block_on_envelope_mismatch=true",
          "action": "REJECT \u2014 SIGNATURE_ENVELOPE_BREACH"
        }
      ],
      "dev_check": "if (p.block_on_envelope_mismatch && deviation > 0.20) return reject('SIGNATURE_ENVELOPE_BREACH');",
      "user_facing": "This order is outside your strategy's declared parameters."
    }
  ],
  "default_config": {
    "bot_id": "sec.signature_previewer",
    "version": "0.1.0",
    "mode": "hard_guard",
    "defaults": {
      "require_preview_for": [
        "all"
      ],
      "min_detail_level": "standard",
      "block_on_envelope_mismatch": true,
      "retain_log_days": 30
    }
  },
  "implementation_flow": [
    "Receive EIP-712 typed data for pending signing request.",
    "Check KillSwitch; if active, REJECT(KILL_SWITCH_ACTIVE).",
    "Verify domain separator version == '2' and verifyingContract in V2 allow-list.",
    "Resolve token IDs to market names via Gamma API.",
    "Compute diff between order fields and strategy declared envelope.",
    "If deviation > 20% and block_on_envelope_mismatch: REJECT(SIGNATURE_ENVELOPE_BREACH).",
    "Render plain-English preview: market name, side, size in pUSD, price, expiry, contract.",
    "Emit preview to wallet UI; await user acknowledgement.",
    "Emit RiskVote(APPROVE) after acknowledgement; log to audit trail."
  ],
  "decision_logic": {
    "approve": "Domain valid, order within envelope, user has acknowledged preview.",
    "reshape_required": "Not applicable \u2014 previewer approves or rejects; it does not modify orders.",
    "reject": "Domain separator mismatch, envelope breach > 20%, or KillSwitch active.",
    "warning_only": "Warn in preview when envelope deviation is 10\u201320%."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "vote_id": "sec.signature_previewer.20260509T140000Z",
    "decision": "APPROVE",
    "reason_code": null,
    "evidence": {
      "market": "US Election \u2014 Winner",
      "side": "BUY",
      "size_pusd": 400,
      "price": 0.55,
      "domain_ok": true,
      "envelope_deviation_pct": 2.0,
      "user_acknowledged": true
    },
    "checked_at": "2026-05-09T14:00:00Z"
  },
  "developer_log": {
    "bot_id": "sec.signature_previewer",
    "decision": "APPROVE",
    "inputs_used": [
      "eip712.typed_data",
      "gamma.market_metadata",
      "strategy.envelope"
    ],
    "checked_at": "2026-05-09T14:00:00Z"
  },
  "user_explanations": [
    {
      "situation": "Preview shown before signing",
      "message": "You are about to sign an order. Please review the details and confirm."
    },
    {
      "situation": "Order blocked \u2014 envelope breach",
      "message": "This order is outside the parameters you set for this strategy and has been blocked."
    },
    {
      "situation": "Order blocked \u2014 domain mismatch",
      "message": "The security parameters in this order do not match the current exchange. It has been blocked."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Preview not shown because Gamma API is unreachable, leaving user to sign a raw typed blob.",
    "false_positive_risk": "Legitimate strategy update triggers envelope mismatch alert before envelope cache refreshes.",
    "false_negative_risk": "Market name resolves stale, showing an outdated description while order targets a different market.",
    "safe_fallback": "If Gamma API is unreachable, display raw typed data with a warning that market names could not be resolved. Block if domain check also fails.",
    "required_dependencies": [
      "Gamma API for market metadata",
      "StrategyConfig for envelope",
      "KillSwitch"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Preview renders market name from Gamma API",
        "setup": "typed_data with token_id=0xABC, Gamma returns 'US Election'",
        "expected": "Preview shows 'US Election'"
      },
      {
        "test": "Reject when domain version is '1'",
        "setup": "eip712_domain_version='1'",
        "expected": "DENY(CONTRACT_GUARD_DOMAIN_MISMATCH)"
      },
      {
        "test": "Reject when envelope deviation > 20%",
        "setup": "order.size_usd=2000, envelope.max_size_usd=1000",
        "expected": "DENY(SIGNATURE_ENVELOPE_BREACH)"
      }
    ],
    "integration": [
      {
        "test": "Gamma API unreachable \u2014 raw typed data shown with warning",
        "expected": "Preview displayed with raw fields and warning banner; domain check still runs"
      },
      {
        "test": "User acknowledgement required before APPROVE",
        "expected": "RiskVote APPROVE not emitted until user confirms"
      }
    ],
    "property": [
      {
        "property": "Domain separator mismatch always produces DENY",
        "required": "Always true"
      },
      {
        "property": "Every signing request generates at least a minimal preview",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Render a plain-English summary of every EIP-712 signature before the wallet shows the modal.",
  "legacy_pm_signals": [
    "Order intent: side, size, price, market, expiry",
    "Domain-separator and chain ID checks",
    "Diff against the strategy\u2019s declared envelope"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "risk_compliance",
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_auth",
    "gamma",
    "internal"
  ],
  "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)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Validates EIP-712 domain version '2' in every signing request; displays pUSD amounts and builder code in preview."
  },
  "reference_implementation": {
    "pseudocode": "// SignaturePreviewer\nFUNCTION previewSignature(typedData, strategyEnvelope):\n  IF FETCH(internal.killswitch).active:\n    EMIT RiskVote(DENY, KILL_SWITCH_ACTIVE); RETURN\n  // Domain check\n  IF typedData.domain.version != '2':\n    EMIT RiskVote(DENY, CONTRACT_GUARD_DOMAIN_MISMATCH); RETURN\n  // Resolve market name\n  meta = FETCH(gamma.markets[typedData.message.tokenId])\n  marketName = meta ? meta.question : '(unresolved)'\n  // Envelope diff\n  deviation = ABS(typedData.message.size_usd - strategyEnvelope.size_usd)\n               / strategyEnvelope.size_usd\n  IF deviation > 0.20 AND params.block_on_envelope_mismatch:\n    EMIT RiskVote(DENY, SIGNATURE_ENVELOPE_BREACH); RETURN\n  // Render preview\n  preview = {\n    market: marketName,\n    side: typedData.message.side,\n    size_pusd: typedData.message.size_usd,\n    price: typedData.message.price,\n    contract: typedData.domain.verifyingContract\n  }\n  EMIT preview TO wallet_ui\n  AWAIT user.acknowledge()\n  EMIT RiskVote(APPROVE)\n  LOG(governance.audit, {preview, decision: 'APPROVE'})",
    "sdk_calls": [
      "gamma.get_market(token_id)",
      "clob_auth.get_typed_data(intent_id)",
      "internal.killswitch.status()"
    ],
    "complexity": "O(1) per signing request \u2014 single Gamma API call"
  },
  "wire_examples": {
    "input": [
      {
        "label": "EIP-712 typed data for BUY order",
        "source": "clob_auth",
        "payload": {
          "intent_id": "int_3c4d5e6f7a8b9c0d",
          "domain": {
            "name": "CTFExchange",
            "version": "2",
            "chainId": 137,
            "verifyingContract": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
          },
          "message": {
            "side": "BUY",
            "tokenId": "0x5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e",
            "size_usd": 400,
            "price": 0.55,
            "timestamp_ms": 1746768672000
          }
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 APPROVE with preview",
        "payload": {
          "vote_id": "sec.signature_previewer.20260509T140000Z",
          "decision": "APPROVE",
          "reason_code": null,
          "preview": {
            "market": "US Election \u2014 Winner",
            "side": "BUY",
            "size_pusd": 400,
            "price": 0.55
          },
          "checked_at": "2026-05-09T14:00:00Z"
        }
      }
    ]
  },
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Immediately return DENY.",
      "user_message": "Trading is currently paused."
    },
    {
      "code": "CONTRACT_GUARD_DOMAIN_MISMATCH",
      "severity": "HARD_REJECT",
      "meaning": "EIP-712 domain version is not '2' or verifyingContract is not in V2 allow-list.",
      "action": "Return DENY; emit security alert.",
      "user_message": "The security parameters in this order do not match the current exchange."
    },
    {
      "code": "SIGNATURE_ENVELOPE_BREACH",
      "severity": "HARD_REJECT",
      "meaning": "Order parameters deviate more than 20% from strategy declared envelope.",
      "action": "Return DENY; display explanation to user.",
      "user_message": "This order is outside your strategy's declared parameters."
    },
    {
      "code": "SIGNATURE_ENVELOPE_WARN",
      "severity": "WARN",
      "meaning": "Order deviates 10\u201320% from envelope.",
      "action": "Display warning in preview; allow user to proceed.",
      "user_message": "This order is slightly outside your typical parameters. Review before signing."
    },
    {
      "code": "MARKET_UNRESOLVED",
      "severity": "INFO",
      "meaning": "Gamma API unavailable; market name could not be resolved.",
      "action": "Show raw token ID with warning banner.",
      "user_message": "Market name could not be loaded. Showing raw order details."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_sec_signaturepreviewer_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision"
        ],
        "meaning": "Total preview decisions."
      },
      {
        "name": "polytraders_sec_signaturepreviewer_envelope_breaches_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Orders rejected for envelope breach."
      },
      {
        "name": "polytraders_sec_signaturepreviewer_domain_mismatches_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Domain separator mismatches detected."
      },
      {
        "name": "polytraders_sec_signaturepreviewer_preview_latency_ms",
        "type": "histogram",
        "unit": "ms",
        "labels": [],
        "meaning": "Time to render and display preview."
      }
    ],
    "alerts": [
      {
        "name": "SignaturePreviewerDomainMismatch",
        "condition": "rate(polytraders_sec_signaturepreviewer_domain_mismatches_total[5m]) > 0",
        "severity": "P0",
        "runbook": "#runbook-signaturepreviewer-domain"
      },
      {
        "name": "SignaturePreviewerEnvelopeBreach",
        "condition": "rate(polytraders_sec_signaturepreviewer_envelope_breaches_total[5m]) > 0",
        "severity": "P1",
        "runbook": "#runbook-signaturepreviewer-envelope"
      }
    ]
  },
  "state": {
    "store": "in-process; preview log stored for retain_log_days",
    "shape": "{intent_id -> {preview, user_acknowledged, timestamp}}",
    "ttl": "retain_log_days (default 30 days)",
    "recovery": "Preview log rebuilt from audit trail on restart.",
    "size_estimate": "< 1 KB per preview; 30-day log < 10 MB"
  },
  "concurrency": {
    "execution_model": "async per signing request",
    "max_in_flight": 50,
    "idempotency_key": "intent_id",
    "timeout_ms": 2000,
    "backpressure": "queue; reject if user does not acknowledge within timeout",
    "locking": "none \u2014 each preview is independent"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate.",
        "contract": "DENY(KILL_SWITCH_ACTIVE) short-circuits."
      },
      {
        "bot_id": "sec.contract_address_guard",
        "why": "Domain separator pre-validated.",
        "contract": "Contract guard runs before previewer in pipeline."
      }
    ],
    "emits_to": [
      {
        "bot_id": "gov.builder_attribution",
        "why": "Log every signed preview and decision.",
        "contract": "GovernanceLog entry on each APPROVE or DENY."
      }
    ],
    "sibling": [
      "sec.wallet_permission_guard"
    ],
    "external": [
      {
        "service": "Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9%",
        "failure_mode": "Show raw typed data with warning; domain check still enforced."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "signing-only",
    "abuse_vectors": [
      "Phishing payload with misleading market name in typed data",
      "Forged domain separator to redirect signing to different chain"
    ],
    "mitigations": [
      "Market names resolved from trusted Gamma API, not from order payload",
      "Domain separator validated against V2 allow-list before display"
    ]
  },
  "failure_injection": [
    {
      "scenario": "DOMAIN_VERSION_V1",
      "how_to_inject": "Set typedData.domain.version='1'",
      "expected_behaviour": "DENY(CONTRACT_GUARD_DOMAIN_MISMATCH) before preview render",
      "recovery": "Automatic on next order with correct domain."
    },
    {
      "scenario": "ENVELOPE_BREACH",
      "how_to_inject": "Set order.size_usd to 5x strategy envelope max",
      "expected_behaviour": "DENY(SIGNATURE_ENVELOPE_BREACH); user sees rejection message",
      "recovery": "Strategy must re-declare envelope or reduce order size."
    },
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block Gamma API endpoint",
      "expected_behaviour": "Preview shown with raw token IDs and MARKET_UNRESOLVED warning; domain check still runs",
      "recovery": "Automatic when Gamma API recovers."
    }
  ],
  "runbook": {
    "summary": "Domain mismatch and envelope breach alerts are P0/P1 security events requiring immediate investigation.",
    "oncall_actions": [
      {
        "alert": "SignaturePreviewerDomainMismatch",
        "first_action": "Check submitted domain version and verifyingContract against V2 allow-list.",
        "escalate_to": "Security pod lead immediately."
      },
      {
        "alert": "SignaturePreviewerEnvelopeBreach",
        "first_action": "Identify strategy and compare submitted order to declared envelope.",
        "escalate_to": "Strategy team and security pod."
      }
    ],
    "manual_overrides": [
      {
        "name": "Refresh strategy envelope cache",
        "how": "polytraders admin refresh-envelope sec.signature_previewer --strategy <id>",
        "when": "Strategy envelope updated but cache not invalidated."
      }
    ],
    "healthcheck": "GET /internal/health/signaturepreviewer \u2192 green if Gamma API reachable; last preview rendered within 30s.; red if Gamma API unreachable for > 60s or domain mismatch alert firing."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Preview renders correctly for BUY and SELL orders with known market IDs",
        "how_measured": "CI integration test",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Domain mismatch injection test fires DENY correctly",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero false-positive envelope breaches in 48h shadow",
        "how_measured": "Grafana EnvelopeBreach alert history",
        "threshold": "0 false positives"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "RiskVote"
    ],
    "topics": [
      "polytraders.reports.risk"
    ],
    "retention_class": "2y",
    "cadence": "every-event",
    "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": 5,
    "phase_name": "Execution rails",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}