{
  "schema_version": "1.0.0",
  "bot_id": "1.4",
  "bot_name": "LiquidityGuard",
  "slug": "liquidityguard",
  "layer": "Risk",
  "layer_key": "risk",
  "bot_class": "Guardrail",
  "authority": [
    "Reject",
    "Reshape"
  ],
  "status": "live",
  "readiness": "General live",
  "flagship": false,
  "is_reference": true,
  "public_export": false,
  "identity": {
    "layer": "Risk",
    "bot_class": "Guardrail",
    "authority": "Reject, Reshape",
    "runs_before": "ExecutionPlan emit",
    "runs_after": "Strategy OrderIntent",
    "applies_to": "Every OrderIntent before it reaches the execution layer",
    "default_mode": "general_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Risk pod"
  },
  "purpose": "LiquidityGuard prevents strategies from placing orders that would consume too much of the visible order-book depth on a given market. It checks book depth, spread, and top-of-book freshness on every OrderIntent and either approves, downsizes, or rejects the order. It cannot change the market, the direction, or the strategy intent \u2014 only the size and the timing of execution.",
  "why_it_matters": [
    {
      "failure": "Thin-book consumption",
      "consequence": "An oversized order eats through the visible top-of-book and walks the price several ticks against the user before fully filling, resulting in worse-than-expected execution."
    },
    {
      "failure": "Stale book approved",
      "consequence": "If depth data is not refreshed, the system may believe there is enough liquidity when the book has thinned out since the last snapshot, leading to surprise price impact."
    },
    {
      "failure": "Excessive spread ignored",
      "consequence": "Trading into a wide spread means crossing more slippage than the strategy priced in, which can turn a positive-expected-value order into a losing one."
    },
    {
      "failure": "No size floor on top-of-book",
      "consequence": "An order placed on a market with near-zero resting size can move the price dramatically even for small notional amounts."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "CLOB order book \u2014 top 50 levels (bid and ask)",
      "source": "CLOB",
      "required": true,
      "use": "Compute total visible USD depth and the inside spread; compare against thresholds."
    },
    {
      "input": "Best-bid / best-ask resting size",
      "source": "WebSocket",
      "required": true,
      "use": "Determine how large the top-of-book is to enforce min_top_of_book_usd."
    },
    {
      "input": "30-day median spread for the target market",
      "source": "Data API",
      "required": true,
      "use": "Calculate the spread multiple: current spread divided by 30d median, compared to max_spread_multiple."
    },
    {
      "input": "Top-of-book last-update timestamp",
      "source": "WebSocket",
      "required": true,
      "use": "Detect stale book data; reject if older than stale_top_seconds."
    }
  ],
  "internal_inputs": [
    {
      "input": "Strategy budget remaining for this market",
      "source": "PortfolioGuard",
      "required": true,
      "use": "Cap the reshape size to the budget remaining so downsized orders don't exceed the portfolio limit."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "If KillSwitch is active, reject all orders immediately without consulting book data."
    }
  ],
  "raw_params": [
    "max_pct_of_visible_depth \u00b7 0\u2013100",
    "min_top_of_book_usd \u00b7 int",
    "max_spread_multiple \u00b7 float",
    "stale_top_seconds \u00b7 int"
  ],
  "parameters": [
    {
      "name": "max_pct_of_visible_depth",
      "default": 25,
      "warning": 35,
      "hard": 60,
      "controls": "Maximum share of top-50-level USD depth that a single order may consume.",
      "why_default_matters": "At 25% the order can fill without moving the inside quote more than one tick on a typical Polymarket book.",
      "threshold_logic": [
        {
          "condition": "\u2264 25% of visible depth",
          "action": "APPROVE"
        },
        {
          "condition": "25\u201360% of visible depth",
          "action": "RESHAPE_REQUIRED \u2014 downsize to 25%"
        },
        {
          "condition": "> 60% of visible depth",
          "action": "REJECT \u2014 INSUFFICIENT_VISIBLE_DEPTH"
        }
      ],
      "dev_check": "if (orderSizeUsd / visibleDepthUsd > p.hard) return reject('INSUFFICIENT_VISIBLE_DEPTH'); else if (orderSizeUsd / visibleDepthUsd > p.default) return reshape({ max_size_usd: visibleDepthUsd * p.default });",
      "user_facing": "We reduced your order because the market did not have enough visible liquidity to fill it without moving the price."
    },
    {
      "name": "min_top_of_book_usd",
      "default": 250,
      "warning": 100,
      "hard": 50,
      "controls": "Minimum USD size required at the best bid or best ask before any order is allowed.",
      "why_default_matters": "A top-of-book below $250 means the market is thin enough that even small orders may gap the price.",
      "threshold_logic": [
        {
          "condition": "top-of-book \u2265 250 USD",
          "action": "APPROVE"
        },
        {
          "condition": "50\u2013250 USD",
          "action": "RESHAPE_REQUIRED \u2014 downsize order to at most top-of-book USD"
        },
        {
          "condition": "< 50 USD",
          "action": "REJECT \u2014 INSUFFICIENT_VISIBLE_DEPTH"
        }
      ],
      "dev_check": "if (topOfBookUsd < p.hard) return reject('INSUFFICIENT_VISIBLE_DEPTH'); else if (topOfBookUsd < p.default) return reshape({ max_size_usd: topOfBookUsd });",
      "user_facing": "The market has very little resting liquidity right now, so we blocked the order to protect you from large price impact."
    },
    {
      "name": "max_spread_multiple",
      "default": 2.5,
      "warning": 2.0,
      "hard": 4.0,
      "controls": "Maximum allowed spread expressed as a multiple of the 30-day median spread for that market.",
      "why_default_matters": "A spread more than 2.5\u00d7 the 30d median indicates the market is abnormally wide, which increases execution cost and may signal a data or liquidity anomaly.",
      "threshold_logic": [
        {
          "condition": "spread \u2264 2.5\u00d7 median",
          "action": "APPROVE"
        },
        {
          "condition": "2.5\u20134\u00d7 median",
          "action": "WARN (logged, not blocking by default)"
        },
        {
          "condition": "> 4\u00d7 median",
          "action": "REJECT \u2014 SPREAD_TOO_WIDE"
        }
      ],
      "dev_check": "const mult = currentSpread / medianSpread30d; if (mult > p.hard) return reject('SPREAD_TOO_WIDE'); else if (mult > p.default) return warn('SPREAD_TOO_WIDE');",
      "user_facing": "The market spread was much wider than usual, which would make this trade unexpectedly expensive. We blocked it to protect your position."
    },
    {
      "name": "stale_top_seconds",
      "default": 60,
      "warning": 45,
      "hard": 120,
      "controls": "Maximum age in seconds of the top-of-book snapshot before it is considered stale.",
      "why_default_matters": "A book that has not moved in 60 seconds may reflect a disconnected data feed or an inactive market. Approving on stale data risks acting on a snapshot that no longer reflects real liquidity.",
      "threshold_logic": [
        {
          "condition": "book updated within 60 s",
          "action": "APPROVE"
        },
        {
          "condition": "60\u2013120 s since last update",
          "action": "WARN \u2014 flag latency to monitor"
        },
        {
          "condition": "> 120 s since last update",
          "action": "REJECT \u2014 STALE_MARKET_DATA"
        }
      ],
      "dev_check": "const ageSeconds = (Date.now() - bookLastUpdatedMs) / 1000; if (ageSeconds > p.hard) return reject('STALE_MARKET_DATA'); else if (ageSeconds > p.default) return warn('STALE_MARKET_DATA');",
      "user_facing": "The market data had not updated recently enough to safely process this order. We blocked it until a fresh snapshot is available."
    }
  ],
  "default_config": {
    "bot_id": "risk.liquidity_guard",
    "version": "1.0.0",
    "mode": "hard_guard",
    "defaults": {
      "max_pct_of_visible_depth": 25,
      "min_top_of_book_usd": 250,
      "max_spread_multiple": 2.5,
      "stale_top_seconds": 60
    },
    "locked": {
      "min_top_of_book_usd": {
        "min": 50
      },
      "stale_top_seconds": {
        "max": 120
      }
    }
  },
  "implementation_flow": [
    "Receive OrderIntent from Strategy layer including market_id, side, size_usd, and price.",
    "Check KillSwitch active flag from KillSwitch service; if active, return REJECT with reason code KILL_SWITCH_ACTIVE immediately.",
    "Pull top 50 levels from the CLOB WebSocket book channel for the target market_id.",
    "Check top-of-book last-update timestamp; if age > stale_top_seconds hard limit, return REJECT with STALE_MARKET_DATA.",
    "Compute total visible_depth_usd from top 50 levels on the relevant side. If top-of-book USD < min_top_of_book_usd hard floor, return REJECT with INSUFFICIENT_VISIBLE_DEPTH.",
    "Compute current spread in percentage points and compare to median30d spread from Data API. If spread_multiple > max_spread_multiple hard ceiling, return REJECT with SPREAD_TOO_WIDE.",
    "Compute pct_of_depth = order.size_usd / visible_depth_usd. If > hard ceiling (60%), return REJECT with INSUFFICIENT_VISIBLE_DEPTH.",
    "If pct_of_depth > default threshold (25%), compute safe_size_usd = visible_depth_usd \u00d7 0.25 and return RESHAPE_REQUIRED with constraints.max_size_usd = safe_size_usd.",
    "If spread_multiple > warning threshold, attach a warning annotation to the approval without blocking.",
    "Return APPROVE with inputs_used list and checked_at timestamp."
  ],
  "decision_logic": {
    "approve": "Top-of-book \u2265 min_top_of_book_usd, book updated within stale_top_seconds, spread \u2264 max_spread_multiple \u00d7 30d median, and order size \u2264 max_pct_of_visible_depth of total visible depth.",
    "reshape_required": "Order size exceeds max_pct_of_visible_depth default (25%) but is below the hard ceiling (60%), or top-of-book is between warning and hard floor \u2014 emit constraints.max_size_usd capped at the safe level.",
    "reject": "Top-of-book is stale (> stale_top_seconds), depth is below the hard floor (min_top_of_book_usd), spread is above the hard multiple (max_spread_multiple \u00d7 4.0), order size exceeds 60% of visible depth, or KillSwitch is active.",
    "warning_only": "Not used \u2014 LiquidityGuard has reject authority. Spread between warning and hard multiple emits a log annotation but does not block the order."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "guard_id": "risk.liquidity_guard",
    "decision": "RESHAPE_REQUIRED",
    "severity": "WARN",
    "reason_code": "INSUFFICIENT_VISIBLE_DEPTH",
    "message": "Order size 1850 USD exceeded 25% of 5200 USD visible top-of-book depth. Resized to 1300 USD.",
    "constraints": {
      "max_size_usd": 1300,
      "passive_only": false,
      "close_only": false
    },
    "inputs_used": [
      "clob.book.top50",
      "data_api.spread.median30d",
      "internal.killswitch.status"
    ],
    "checked_at": "2026-05-09T05:51:12Z"
  },
  "developer_log": {
    "bot_id": "risk.liquidity_guard",
    "decision": "RESHAPE_REQUIRED",
    "reason_code": "INSUFFICIENT_VISIBLE_DEPTH",
    "inputs_used": [
      "clob.book.top50",
      "data_api.spread.median30d"
    ],
    "metrics": {
      "visible_depth_usd": 5200,
      "requested_size_usd": 1850,
      "pct_of_depth": 0.356,
      "top_of_book_usd": 820,
      "spread_multiple": 1.4,
      "book_age_seconds": 12
    },
    "safe_size_usd": 1300,
    "checked_at": "2026-05-09T05:51:12Z"
  },
  "user_explanations": [
    {
      "situation": "Order downsized due to thin book",
      "message": "We reduced your order because filling the full size would have consumed too much of the visible liquidity in this market and moved the price against you."
    },
    {
      "situation": "Order blocked \u2014 market data too old",
      "message": "We blocked this order because the market data had not updated recently. We wait for a fresh snapshot before allowing new orders to protect you from acting on stale information."
    },
    {
      "situation": "Order blocked \u2014 spread too wide",
      "message": "The gap between the buy and sell prices was much larger than usual. We blocked this order because the high spread would make it significantly more expensive than expected."
    },
    {
      "situation": "Order blocked \u2014 book too thin",
      "message": "There was not enough resting liquidity in this market to safely place your order. We blocked it to prevent your trade from moving the price by an unusually large amount."
    },
    {
      "situation": "Order blocked \u2014 size exceeds hard limit",
      "message": "Your order was larger than what this market can safely absorb. Even after considering a downsize, the remaining liquidity was insufficient. Please try a smaller amount."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Allowing an oversized order through a thin book, causing significant adverse price impact for the user.",
    "false_positive_risk": "Downsizing or rejecting a legitimate order on a temporarily quiet but sufficiently deep market, such as early in the trading day before resting liquidity has built up.",
    "false_negative_risk": "Approving an order against a book that was valid at snapshot time but has since been pulled, if the staleness window is set too wide.",
    "safe_fallback": "If CLOB book data is absent or older than stale_top_seconds hard limit, always reject with STALE_MARKET_DATA. LiquidityGuard never approves on missing or unverifiable data.",
    "required_dependencies": [
      "CLOB WebSocket book channel",
      "Data API 30-day median spread",
      "PortfolioGuard ledger (budget remaining)",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when all thresholds pass",
        "setup": "depth_usd=2000, size_usd=400, top_of_book_usd=600, spread_multiple=1.2, book_age_s=10",
        "expected": "APPROVE with no constraints"
      },
      {
        "test": "Reshape when size is 30% of depth",
        "setup": "depth_usd=1000, size_usd=300, hard=60, default=25",
        "expected": "RESHAPE_REQUIRED with constraints.max_size_usd=250"
      },
      {
        "test": "Reject when size exceeds 60% hard ceiling",
        "setup": "depth_usd=1000, size_usd=650",
        "expected": "REJECT with reason_code=INSUFFICIENT_VISIBLE_DEPTH"
      },
      {
        "test": "Reject when book age exceeds hard stale limit",
        "setup": "book_age_s=130, stale_top_seconds=60",
        "expected": "REJECT with reason_code=STALE_MARKET_DATA"
      },
      {
        "test": "Reject when spread_multiple > 4.0",
        "setup": "current_spread=0.08, median_spread=0.01 (multiple=8.0)",
        "expected": "REJECT with reason_code=SPREAD_TOO_WIDE"
      },
      {
        "test": "Reshape when top-of-book is between warning and hard floor",
        "setup": "top_of_book_usd=150, min_top_of_book_usd=250, hard=50",
        "expected": "RESHAPE_REQUIRED with constraints.max_size_usd=150"
      },
      {
        "test": "Reject when top-of-book is below hard floor",
        "setup": "top_of_book_usd=30, min_top_of_book_usd hard=50",
        "expected": "REJECT with reason_code=INSUFFICIENT_VISIBLE_DEPTH"
      }
    ],
    "integration": [
      {
        "test": "Rejects on stale book snapshot from live WebSocket",
        "expected": "REJECT(STALE_MARKET_DATA) when WebSocket book channel has not emitted an update within stale_top_seconds"
      },
      {
        "test": "Reshape flows through to ExecutionPlan with reduced size",
        "expected": "ExecutionPlan downstream receives constraints.max_size_usd and does not exceed it"
      },
      {
        "test": "KillSwitch active causes immediate rejection before book check",
        "expected": "REJECT emitted without querying CLOB when KillSwitch active flag is true"
      }
    ],
    "property": [
      {
        "property": "Missing or absent book data never results in APPROVE",
        "required": "Always true \u2014 null or empty book must produce REJECT(STALE_MARKET_DATA)"
      },
      {
        "property": "Reshape size is always strictly \u2264 requested order size",
        "required": "Always true \u2014 constraints.max_size_usd \u2264 original order size_usd"
      },
      {
        "property": "Approved order size never exceeds max_pct_of_visible_depth of visible depth",
        "required": "Always true \u2014 for any APPROVE, size_usd / visible_depth_usd \u2264 default threshold"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Stop strategies from chewing through thin books.",
  "legacy_pm_signals": [
    "CLOB book depth on the target market",
    "Best-bid / best-ask resting size",
    "Spread vs. 30-day median spread for that market",
    "Top-of-book staleness (no movement in 60s)"
  ],
  "legacy_external_feeds": [],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "ws_market",
    "clob_public",
    "data_api"
  ],
  "reference_implementation": {
    "summary": "Fetches the top-50 CLOB book for the target market, checks KillSwitch, then evaluates depth, spread, and book freshness against configured thresholds. Returns a RiskVote of APPROVE, RESHAPE_REQUIRED, or HARD_REJECT.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "FUNCTION evaluateLiquidity(intent):\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n  IF ks.active:\n    EMIT RiskVote(decision=HARD_REJECT, reason=KILL_SWITCH_ACTIVE)\n    RETURN\n\n  // --- 1. Fetch book ---\n  book = fetchClobPublic('/book?market=' + intent.market_id)\n  IF book IS NULL OR book.updated_at IS NULL:\n    EMIT RiskVote(decision=HARD_REJECT, reason=STALE_MARKET_DATA)\n    RETURN\n\n  // --- 2. Staleness check ---\n  ageSeconds = (now_ms() - book.updated_at_ms) / 1000\n  IF ageSeconds > params.stale_top_seconds.hard:\n    EMIT RiskVote(decision=HARD_REJECT, reason=STALE_MARKET_DATA)\n    RETURN\n\n  // --- 3. Compute depth ---\n  side = IF intent.side == BUY THEN book.asks ELSE book.bids\n  visibleDepthUsd = SUM(level.price * level.size * collateralDecimals\n                        FOR level IN side[:50])\n  topOfBookUsd = side[0].price * side[0].size\n\n  // --- 4. Top-of-book floor ---\n  IF topOfBookUsd < params.min_top_of_book_usd.hard:\n    EMIT RiskVote(decision=HARD_REJECT, reason=INSUFFICIENT_VISIBLE_DEPTH)\n    RETURN\n\n  // --- 5. Spread check ---\n  spread = book.asks[0].price - book.bids[0].price\n  median30d = FETCH fetchClobPublic('/spread-stats?market=' + intent.market_id).median30d\n  spreadMultiple = spread / median30d\n  IF spreadMultiple > params.max_spread_multiple.hard:\n    EMIT RiskVote(decision=HARD_REJECT, reason=SPREAD_TOO_WIDE)\n    RETURN\n\n  // --- 6. NegRisk check (optional) ---\n  IF intent.neg_risk AND isStale(book, params.stale_top_seconds.default):\n    EMIT RiskVote(decision=WARN, reason=LIQUIDITY_GUARD_NEGRISK_THIN_BOOK)\n\n  // --- 7. Depth percentage ---\n  pctOfDepth = intent.size_usd / visibleDepthUsd\n  IF pctOfDepth > params.max_pct_of_visible_depth.hard:\n    EMIT RiskVote(decision=HARD_REJECT, reason=INSUFFICIENT_VISIBLE_DEPTH)\n    RETURN\n\n  IF pctOfDepth > params.max_pct_of_visible_depth.default:\n    safeSizeUsd = visibleDepthUsd * params.max_pct_of_visible_depth.default\n    safeSizeUsd = toUsdcUnits(safeSizeUsd)  // round to pUSD precision\n    EMIT RiskVote(decision=RESHAPE_REQUIRED,\n                  reason=INSUFFICIENT_VISIBLE_DEPTH,\n                  constraints={ max_size_usd: safeSizeUsd })\n    RETURN\n\n  // --- 8. Happy path ---\n  EMIT RiskVote(decision=APPROVE, checked_at=now_iso())\n",
    "helpers": [
      {
        "name": "toUsdcUnits",
        "signature": "toUsdcUnits(rawUsd: float) -> int",
        "purpose": "Round a raw USD float to the integer pUSD unit used by CTFExchangeV2 (6 decimals)."
      },
      {
        "name": "isStale",
        "signature": "isStale(book: BookSnapshot, maxAgeS: int) -> bool",
        "purpose": "Returns true if book.updated_at_ms is older than maxAgeS seconds relative to now."
      },
      {
        "name": "fetchClobPublic",
        "signature": "fetchClobPublic(path: str) -> JSON",
        "purpose": "Authenticated-free GET against https://clob.polymarket.com; returns parsed JSON or null on error."
      },
      {
        "name": "platformFee",
        "signature": "platformFee(notional: float, prob: float, feeRate: float) -> float",
        "purpose": "Computes C * feeRate * p * (1-p); peaks at p=0.5. Used to estimate transaction cost for depth comparisons."
      }
    ],
    "sdk_calls": [
      "fetchClobPublic('/book?market=0xabc123...&side=asks&depth=50')",
      "fetchClobPublic('/spread-stats?market=0xabc123...')",
      "fetchClobPublic('/markets/0xabc123...')",
      "internal.killswitch.status()"
    ],
    "complexity": "O(N) where N = book depth levels (max 50)"
  },
  "wire_examples": {
    "input": [
      {
        "label": "OrderIntent from strategy",
        "source": "internal",
        "payload": {
          "intent_id": "int_7f3a1b2c9d4e5f60",
          "market_id": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",
          "side": "BUY",
          "outcome": "YES",
          "size_usd": 1850,
          "price": 0.62,
          "neg_risk": false,
          "generated_at": "2026-05-09T05:51:00Z"
        }
      },
      {
        "label": "CLOB book snapshot (ws_market)",
        "source": "ws_market",
        "payload": {
          "market": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",
          "asks": [
            {
              "price": "0.62",
              "size": "820"
            },
            {
              "price": "0.63",
              "size": "1200"
            },
            {
              "price": "0.64",
              "size": "3180"
            }
          ],
          "bids": [
            {
              "price": "0.61",
              "size": "950"
            },
            {
              "price": "0.60",
              "size": "2100"
            }
          ],
          "updated_at_ms": 1746768672000
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 RESHAPE_REQUIRED (order too large for visible depth)",
        "payload": {
          "guard_id": "risk.liquidity_guard",
          "decision": "RESHAPE_REQUIRED",
          "severity": "WARN",
          "reason_code": "INSUFFICIENT_VISIBLE_DEPTH",
          "message": "Order size 1850 pUSD exceeded 25% of 5200 pUSD visible top-50 depth. Resized to 1300 pUSD.",
          "constraints": {
            "max_size_usd": 1300,
            "passive_only": false,
            "close_only": false
          },
          "inputs_used": [
            "clob_public.book.top50",
            "data_api.spread.median30d",
            "internal.killswitch.status"
          ],
          "checked_at": "2026-05-09T05:51:12Z"
        }
      },
      {
        "label": "RiskVote \u2014 HARD_REJECT (stale book)",
        "payload": {
          "guard_id": "risk.liquidity_guard",
          "decision": "HARD_REJECT",
          "severity": "HARD",
          "reason_code": "STALE_MARKET_DATA",
          "message": "Book last updated 135s ago; stale_top_seconds hard limit is 120s.",
          "constraints": {},
          "inputs_used": [
            "clob_public.book.top50"
          ],
          "checked_at": "2026-05-09T06:05:00Z"
        }
      }
    ],
    "curl": "curl 'https://clob.polymarket.com/book?market=0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b'"
  },
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active; no orders may proceed.",
      "action": "Immediately return HARD_REJECT without consulting book data.",
      "user_message": "Trading is currently paused. Please try again later."
    },
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "Book snapshot is older than stale_top_seconds hard limit.",
      "action": "Return HARD_REJECT; wait for fresh book before retrying.",
      "user_message": "Market data had not updated recently. The order was blocked until a fresh snapshot is available."
    },
    {
      "code": "INSUFFICIENT_VISIBLE_DEPTH",
      "severity": "HARD_REJECT",
      "meaning": "Order size exceeds the hard depth ceiling or top-of-book is below the hard floor.",
      "action": "Return HARD_REJECT or RESHAPE_REQUIRED depending on which threshold was breached.",
      "user_message": "There was not enough resting liquidity to safely place your order at the requested size."
    },
    {
      "code": "SPREAD_TOO_WIDE",
      "severity": "HARD_REJECT",
      "meaning": "Current spread exceeds max_spread_multiple times the 30-day median.",
      "action": "Return HARD_REJECT; log spread_multiple value.",
      "user_message": "The gap between the buy and sell prices was much wider than usual. The order was blocked to protect against unexpectedly high transaction cost."
    },
    {
      "code": "LIQUIDITY_GUARD_RESHAPE_DEPTH",
      "severity": "RESHAPE",
      "meaning": "Order size is above the default depth percentage but below the hard ceiling.",
      "action": "Return RESHAPE_REQUIRED with constraints.max_size_usd = visibleDepthUsd * default_pct.",
      "user_message": "Your order was reduced because filling the full size would have consumed too much of the visible liquidity."
    },
    {
      "code": "LIQUIDITY_GUARD_SPREAD_WARN",
      "severity": "WARN",
      "meaning": "Spread is between the warning and hard multiple thresholds.",
      "action": "Attach warning annotation to APPROVE; do not block.",
      "user_message": ""
    },
    {
      "code": "LIQUIDITY_GUARD_NEGRISK_THIN_BOOK",
      "severity": "WARN",
      "meaning": "NegRisk market book is thin relative to the requested size, increasing definition-shift exposure.",
      "action": "Attach warning annotation; Strategy may reduce size further.",
      "user_message": ""
    },
    {
      "code": "LIQUIDITY_GUARD_TOP_BOOK_RESHAPE",
      "severity": "RESHAPE",
      "meaning": "Top-of-book USD is between the warning and hard floor thresholds.",
      "action": "Return RESHAPE_REQUIRED with constraints.max_size_usd = topOfBookUsd.",
      "user_message": "The market has very little resting liquidity at the best price. Your order was reduced to the available top-of-book size."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_risk_liquidityguard_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision",
          "reason_code",
          "market_id"
        ],
        "meaning": "Total RiskVote decisions emitted, broken down by decision type and reason."
      },
      {
        "name": "polytraders_risk_liquidityguard_book_age_seconds",
        "type": "histogram",
        "unit": "seconds",
        "labels": [
          "market_id"
        ],
        "meaning": "Age of the book snapshot at evaluation time; alerts when p99 approaches stale_top_seconds."
      },
      {
        "name": "polytraders_risk_liquidityguard_visible_depth_usd",
        "type": "gauge",
        "unit": "usd",
        "labels": [
          "market_id"
        ],
        "meaning": "Total visible USD depth across top-50 levels at the time of last check."
      },
      {
        "name": "polytraders_risk_liquidityguard_spread_multiple",
        "type": "gauge",
        "unit": "ratio",
        "labels": [
          "market_id"
        ],
        "meaning": "Current spread divided by 30-day median spread; triggers WARN above 2.5x."
      },
      {
        "name": "polytraders_risk_liquidityguard_reshape_size_usd",
        "type": "histogram",
        "unit": "usd",
        "labels": [
          "market_id"
        ],
        "meaning": "Size delta (original minus reshaped) for RESHAPE_REQUIRED decisions."
      },
      {
        "name": "polytraders_risk_liquidityguard_eval_latency_ms",
        "type": "histogram",
        "unit": "seconds",
        "labels": [],
        "meaning": "Wall-clock latency from intent receipt to RiskVote emit."
      }
    ],
    "alerts": [
      {
        "name": "LiquidityGuardStaleBook",
        "condition": "histogram_quantile(0.99, rate(polytraders_risk_liquidityguard_book_age_seconds_bucket[5m])) > 90",
        "severity": "P1",
        "runbook": "#runbook-liquidityguard-stale-book"
      },
      {
        "name": "LiquidityGuardHighRejectRate",
        "condition": "rate(polytraders_risk_liquidityguard_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_liquidityguard_decisions_total[5m]) > 0.5",
        "severity": "P2",
        "runbook": "#runbook-liquidityguard-reject-rate"
      },
      {
        "name": "LiquidityGuardSpreadSpike",
        "condition": "polytraders_risk_liquidityguard_spread_multiple > 3.5",
        "severity": "P2",
        "runbook": "#runbook-liquidityguard-spread"
      },
      {
        "name": "LiquidityGuardHighLatency",
        "condition": "histogram_quantile(0.99, rate(polytraders_risk_liquidityguard_eval_latency_ms_bucket[5m])) > 200",
        "severity": "P2",
        "runbook": "#runbook-liquidityguard-latency"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Risk overview / LiquidityGuard",
      "Grafana \u2014 Market quality / spread and depth heatmap"
    ],
    "log_levels": {
      "DEBUG": "Per-level depth computation and spread_multiple value on every evaluation.",
      "INFO": "RiskVote decision emitted (decision, reason_code, market_id, size_usd).",
      "WARN": "Book age approaching stale threshold; spread_multiple between warning and hard limit.",
      "ERROR": "CLOB book endpoint returned null or non-200; KillSwitch flag unreadable."
    }
  },
  "state": {
    "summary": "LiquidityGuard is stateless per evaluation; it holds no persistent state beyond an in-memory cache of the last book snapshot per market.",
    "stores": [
      {
        "name": "book_cache",
        "kind": "in-memory",
        "key": "market_id",
        "value": "{ asks: Level[], bids: Level[], updated_at_ms: int }",
        "ttl": "120s",
        "durability": "best-effort"
      }
    ],
    "recovery": "On cold start, the cache is empty. The first evaluation for each market_id triggers a fresh CLOB fetch.",
    "on_restart": "Book snapshots are re-fetched on first evaluation; no durable state is loaded. If the WebSocket reconnects before the first evaluation, snapshots are populated from the reconnection event."
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 200,
    "idempotency_key": "intent_id",
    "replay_safe": true,
    "deduplication": "by intent_id within a 24h window",
    "ordering_guarantees": "FIFO per market_id",
    "timeout_ms": 150,
    "backpressure": "drop newest",
    "locking": "per-market_id mutex"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Global brake \u2014 checked first before any book data is read.",
        "contract": "RiskVote.HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all further evaluation."
      },
      {
        "bot_id": "risk.portfolio_guard",
        "why": "Budget remaining for this market caps the reshape ceiling.",
        "contract": "Reshape size is min(safe_depth_size, portfolio_budget_remaining)."
      }
    ],
    "emits_to": [
      {
        "bot_id": "exec.smart_router",
        "why": "Approved or reshaped RiskVote passes to SmartRouter for ExecutionPlan construction.",
        "contract": "APPROVE or RESHAPE_REQUIRED RiskVote is consumed; HARD_REJECT causes SmartRouter to discard the intent."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "CLOB API (read)",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.95% / 200ms p99",
        "failure_mode": "HARD_REJECT(STALE_MARKET_DATA) until book is fresh."
      },
      {
        "service": "WS market feed",
        "endpoint": "wss://ws-subscriptions-clob.polymarket.com/ws/market",
        "sla": "best-effort / sub-100ms",
        "failure_mode": "Falls back to REST poll; if REST also fails, HARD_REJECT."
      },
      {
        "service": "Data API (spread stats)",
        "endpoint": "https://data-api.polymarket.com",
        "sla": "99.9% / 500ms p99",
        "failure_mode": "WARN emitted; evaluation continues with best available spread estimate."
      }
    ]
  },
  "security_surfaces": {
    "summary": "LiquidityGuard is read-only and stateless. It never signs orders or holds secrets.",
    "signing": "This bot does NOT sign anything.",
    "secrets": [],
    "contract_calls": [],
    "abuse_vectors": [
      "Replaying a stale book snapshot to bypass depth checks",
      "Injecting artificially large depth values to allow oversized orders"
    ],
    "mitigations": [
      "per-intent_id idempotency prevents replay of the same evaluation",
      "book.updated_at_ms is checked against wall clock; any snapshot older than stale_top_seconds is rejected regardless of depth values"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "@polymarket/clob-client-v2 ^2.x",
    "settlement_contract": "CTFExchangeV2 on Polygon",
    "notes": "All depth values are denominated in pUSD (USDC-backed ERC-20). Order fields evaluated here use the V2 schema (timestamp/metadata/builder); nonce and feeRateBps fields are not present."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.3",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1 (USDC.e + HMAC builder)",
      "to": "v2 (pUSD + builderCode field)",
      "reason": "Polymarket V2 cutover",
      "action_taken": "Migrated SDK, replaced HMAC builder logic with on-order builderCode, removed feeRateBps from order construction. Depth values now denominated in pUSD."
    }
  ],
  "failure_injection": [
    {
      "scenario": "STALE_BOOK",
      "how_to_inject": "Freeze WS market channel for 130s",
      "expected_behavior": "All evaluations return HARD_REJECT(STALE_MARKET_DATA) once book_age_seconds > 120",
      "recovery": "Returns to APPROVE within 5s of fresh book delivery."
    },
    {
      "scenario": "EMPTY_BOOK",
      "how_to_inject": "Return empty asks/bids arrays from CLOB mock",
      "expected_behavior": "topOfBookUsd=0 triggers HARD_REJECT(INSUFFICIENT_VISIBLE_DEPTH)",
      "recovery": "Immediate on next evaluation with a non-empty book."
    },
    {
      "scenario": "WIDE_SPREAD",
      "how_to_inject": "Set asks[0].price=0.90, bids[0].price=0.10 so spread_multiple > 4.0",
      "expected_behavior": "HARD_REJECT(SPREAD_TOO_WIDE)",
      "recovery": "Next evaluation where spread_multiple falls below hard limit resumes normally."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set internal.killswitch.status.active = true",
      "expected_behavior": "HARD_REJECT(KILL_SWITCH_ACTIVE) on every intent without book fetch",
      "recovery": "Returns to normal pipeline on manual KillSwitch reset."
    },
    {
      "scenario": "CLOB_ENDPOINT_DOWN",
      "how_to_inject": "Block TCP to clob.polymarket.com",
      "expected_behavior": "fetchClobPublic returns null; HARD_REJECT(STALE_MARKET_DATA)",
      "recovery": "Returns to APPROVE within one evaluation cycle after endpoint is reachable."
    }
  ],
  "runbook": {
    "summary": "LiquidityGuard incidents are typically caused by a stale CLOB book feed or an abnormal spread spike. On-call should first confirm whether the WS market feed is connected before adjusting parameters.",
    "oncall_actions": [
      {
        "alert": "LiquidityGuardStaleBook",
        "first_step": "Check WS market feed connection status in the Grafana panel.",
        "diagnosis": "If WS is disconnected, check clob.polymarket.com status page. If WS is connected, check for clock skew between the bot host and exchange.",
        "mitigation": "Reconnect WS feed; if clock skew, resync NTP. Do not increase stale_top_seconds without approval.",
        "escalation": "Risk pod lead if feed is down > 5 minutes."
      },
      {
        "alert": "LiquidityGuardHighRejectRate",
        "first_step": "Check reason_code distribution on the HARD_REJECT counter.",
        "diagnosis": "If dominated by STALE_MARKET_DATA, follow stale-book runbook. If INSUFFICIENT_VISIBLE_DEPTH, check whether a specific market has thinned unusually.",
        "mitigation": "For thin markets, pause the affected strategy until liquidity recovers. Do not lower depth thresholds.",
        "escalation": "Risk pod lead if reject rate persists > 10 minutes."
      },
      {
        "alert": "LiquidityGuardSpreadSpike",
        "first_step": "Identify which market_id is driving the spread_multiple gauge above 3.5.",
        "diagnosis": "Check Gamma API for market metadata; confirm no oracle dispute or resolution event.",
        "mitigation": "If spread is genuine (thin book, not a data error), the guard is working correctly. If a data error, restart the WS subscription for that market.",
        "escalation": "OracleRiskMonitor on-call if oracle dispute is active."
      },
      {
        "alert": "LiquidityGuardHighLatency",
        "first_step": "Check CLOB REST response times in the latency histogram.",
        "diagnosis": "If CLOB latency is high, the REST fallback path is being used more than expected.",
        "mitigation": "Confirm WS is connected. Reduce bot concurrency if host is CPU-bound.",
        "escalation": "Infra on-call if CLOB p99 latency > 500ms sustained."
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders bot pause risk.liquidity_guard",
        "effect": "Stops emitting RiskVotes; all intents fall through to the next guardrail without a liquidity check. Use only during a known feed outage."
      },
      {
        "command": "polytraders bot flush-cache risk.liquidity_guard --market <market_id>",
        "effect": "Evicts the in-memory book cache for a specific market, forcing a fresh CLOB fetch on the next evaluation."
      }
    ],
    "healthcheck": "GET /health \u2192 200 if WS market feed is connected and last book update for any tracked market is within stale_top_seconds."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass including all acceptance_tests.unit cases",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "Integration test: stale book \u2192 HARD_REJECT verified",
        "how_measured": "Integration test suite",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Shadow mode reject rate matches expected baseline within 10%",
        "how_measured": "Grafana shadow vs live comparison dashboard",
        "threshold": "< 10% divergence over 48h"
      },
      {
        "gate": "p99 evaluation latency < 150ms",
        "how_measured": "polytraders_risk_liquidityguard_eval_latency_ms histogram",
        "threshold": "p99 < 150ms"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero HARD_REJECT(STALE_MARKET_DATA) during normal operating hours over 7 days",
        "how_measured": "LiquidityGuardStaleBook alert history",
        "threshold": "0 firings"
      },
      {
        "gate": "Reshape decisions correctly cap order size in E2E flow",
        "how_measured": "E2E integration tests + manual audit of fill logs",
        "threshold": "100% compliance"
      }
    ]
  },
  "reporting_groups": [
    "risk_compliance"
  ],
  "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"
  }
}