{
  "schema_version": "1.0.0",
  "bot_id": "4.16",
  "bot_name": "LiquidityDecayMonitor",
  "slug": "liquidity_decay_monitor",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Observe"
  ],
  "status": "planned",
  "readiness": "Spec ready",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal",
    "authority": "Observe",
    "runs_before": "\u2014",
    "runs_after": "intel.orderflowanalyzer, exec.order_lifecycle_manager",
    "applies_to": "Continuous",
    "default_mode": "shadow",
    "user_visible": "No",
    "developer_owner": "Intelligence pod"
  },
  "purpose": "Tracks how quickly inside liquidity decays after our orders rest on the book. The output is a per-market liquidity-decay signal (in USD/second) that the smart router and order shaper consume to size and slice future orders. Pure observer \u2014 never blocks anything.",
  "why_it_matters": [
    {
      "failure": "Order shaping with no liquidity model",
      "consequence": "Without a decay estimate, the order shaper either over-slices (paying spread many times) or under-slices (eating its own resting depth)."
    },
    {
      "failure": "Static liquidity assumptions",
      "consequence": "Polymarket markets have wildly different microstructure across event types; a single config does not fit all of them."
    },
    {
      "failure": "Hidden execution cost",
      "consequence": "Decay is the dominant hidden cost on illiquid markets; making it observable is the first step to controlling it."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "OrderBookSnapshot stream",
      "source": "WebSocket / data-flow",
      "required": true,
      "use": "Top-of-book depth at high frequency."
    },
    {
      "input": "Our resting orders + fills",
      "source": "OrderLifecycleManager",
      "required": true,
      "use": "Identify the windows where we were the dominant resting size."
    }
  ],
  "internal_inputs": [
    {
      "input": "Per-market trade tape",
      "source": "intel.orderflowanalyzer",
      "required": true,
      "use": "Used to attribute decay to taker flow vs cancellations."
    }
  ],
  "raw_params": [
    "window_seconds \u00b7 5\u2013600",
    "min_observations \u00b7 5\u2013500"
  ],
  "parameters": [
    {
      "name": "window_seconds",
      "default": 60,
      "warning": "120",
      "hard": "300",
      "controls": "Trailing window over which decay is estimated.",
      "why_default_matters": "60 seconds balances responsiveness and stability for typical Polymarket binary markets.",
      "threshold_logic": [
        {
          "condition": "< 5s",
          "action": "Too noisy"
        },
        {
          "condition": "60s",
          "action": "Default"
        },
        {
          "condition": "> 600s",
          "action": "Too slow to react to regime changes"
        }
      ],
      "dev_check": "decay = (depth_t0 - depth_now) / window_seconds;",
      "user_facing": "(Internal \u2014 not shown to users.)"
    },
    {
      "name": "min_observations",
      "default": 30,
      "warning": "15",
      "hard": "5",
      "controls": "Minimum sample count before publishing a decay estimate (otherwise emit confidence=null).",
      "why_default_matters": "Avoids emitting wildly noisy estimates from a handful of ticks.",
      "threshold_logic": [
        {
          "condition": "< 30",
          "action": "Suppressed"
        },
        {
          "condition": "\u2265 30",
          "action": "Published"
        }
      ],
      "dev_check": "if (samples >= p.min_observations) emit(decayUsdPerSecond, confidence);",
      "user_facing": "(Internal \u2014 not shown to users.)"
    }
  ],
  "default_config": {
    "window_seconds": 60,
    "min_observations": 30
  },
  "flow": "Subscribe to per-market book snapshots \u2192 maintain rolling top-of-book depth + fill volume \u2192 on each tick, compute decay = (depth_baseline - depth_now)/window \u2192 emit SignalEnvelope(kind=liquidity_decay, value=usd_per_second, confidence=samples/min_observations).",
  "decision_logic": {
    "approve": "Maintain rolling baseline depth and decayed depth. Attribute decay to (taker_volume, cancel_volume) using trade tape. Emit signal when sample count clears the threshold.",
    "reshape_required": "This bot does not reshape orders.",
    "reject": "No reject path defined for this bot \u2014 it is observe-only.",
    "warning_only": "No warn-only path defined."
  },
  "decision_output_example": {
    "kind": "liquidity_decay",
    "market_id": "0xabc",
    "value": 12.4,
    "unit": "usd_per_second",
    "confidence": 0.62,
    "ts_ms": 1715260000000
  },
  "developer_log": "Per emission: market_id, window_seconds, samples, baseline_depth_usd, current_depth_usd, decay_usd_per_second, confidence.",
  "user_explanations": [
    {
      "situation": "When this bot acts",
      "message": "(Internal signal \u2014 not user-facing.)"
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Mis-attributing depth changes to decay when the underlying event is a market-wide spread blowout.",
    "false_positive_risk": "Sudden venue-wide spread widening looks like decay; mitigation: gate emission on MarketHaltDetector status.",
    "false_negative_risk": "Slow, smooth decay across a long event window may not register as significant; mitigation: also publish a long-window estimate alongside the short-window one.",
    "safe_fallback": "On any sensor failure, suppress emission and emit confidence=null. Consumers default to a conservative slice size."
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Synthetic 100\u219250 USD depth drop in 10s emits decay \u2248 5 USD/s.",
        "setup": "Synthetic fixture per template.",
        "expected": "Behaviour matches the rule described in the test name."
      },
      {
        "test": "No depth change emits decay \u2248 0 with confidence trending to 1.0.",
        "setup": "Synthetic fixture per template.",
        "expected": "Behaviour matches the rule described in the test name."
      }
    ],
    "integration": [
      {
        "test": "Replay a recorded busy market for 10 minutes; assert emitted decay tracks observed taker volume within 20%.",
        "expected": "End-to-end behaviour matches the spec without manual intervention."
      }
    ],
    "property": [
      {
        "property": "Decay estimate is in [0, baseline_depth/window_seconds] for any non-negative depth series.",
        "required": "Always true across all generated inputs."
      }
    ]
  },
  "reference_implementation": {
    "language": "pseudocode",
    "pseudocode": "for each book_tick:\n  baseline = rolling_baseline(market_id, p.window_seconds)\n  decay = max(0, baseline - book.top_depth_usd) / p.window_seconds\n  if samples(market_id) >= p.min_observations:\n    emit_signal('liquidity_decay', market_id, decay, confidence(samples))"
  },
  "wire_examples": {
    "input": {
      "market_id": "0xabc",
      "ts_ms": 1715260000000,
      "top_depth_usd": 87.5
    },
    "output": {
      "kind": "liquidity_decay",
      "market_id": "0xabc",
      "value": 12.4,
      "confidence": 0.62
    }
  },
  "reason_codes": [
    {
      "code": "INTEL_DECAY_PUBLISHED",
      "severity": "info",
      "meaning": "Intel Decay Published",
      "action": "See decision output and developer log for context.",
      "user_message": "(Internal signal \u2014 not user-facing.)"
    },
    {
      "code": "INTEL_DECAY_SUPPRESSED_LOW_SAMPLES",
      "severity": "info",
      "meaning": "Intel Decay Suppressed Low Samples",
      "action": "See decision output and developer log for context.",
      "user_message": "(Internal signal \u2014 not user-facing.)"
    },
    {
      "code": "INTEL_DECAY_SUPPRESSED_HALTED",
      "severity": "info",
      "meaning": "Intel Decay Suppressed Halted",
      "action": "See decision output and developer log for context.",
      "user_message": "(Internal signal \u2014 not user-facing.)"
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "decay_emissions_total",
        "type": "counter",
        "unit": "event",
        "labels": [
          "bot_id"
        ],
        "meaning": "Decay emissions total."
      },
      {
        "name": "decay_suppressed_total",
        "type": "counter",
        "unit": "event",
        "labels": [
          "bot_id"
        ],
        "meaning": "Decay suppressed total."
      },
      {
        "name": "decay_value_histogram",
        "type": "counter",
        "unit": "event",
        "labels": [
          "bot_id"
        ],
        "meaning": "Decay value histogram."
      }
    ],
    "alerts": [],
    "dashboards": [
      "4.16 overview dashboard"
    ]
  },
  "state": {
    "summary": "Per-market rolling buffers (baseline depth + sample count). In-memory; reseeds on restart.",
    "stores": [
      {
        "name": "liquidity_decay_monitor_state",
        "kind": "in-memory + fast KV mirror",
        "key": "market_id",
        "value": "Per-market rolling buffers (baseline depth + sample count). In-memory; reseeds on restart.",
        "ttl": "24h",
        "durability": "crash-safe via KV mirror"
      }
    ],
    "recovery": "Cold-start hydrates from fast KV; missing keys default to safe fallback.",
    "on_restart": "All in-flight decisions are re-evaluated; no bot decision is trusted across restart without re-emit."
  },
  "concurrency": {
    "execution_model": "One worker per market_id; lock-free ring buffer per series.",
    "max_in_flight": 32,
    "idempotency_key": "order_intent_id",
    "replay_safe": true,
    "deduplication": "By idempotency_key within a 60s window.",
    "ordering_guarantees": "Per-market_id FIFO; cross-market unordered.",
    "timeout_ms": 250,
    "backpressure": "Bounded queue; oldest-dropped with metric increment when full.",
    "locking": "Per-market_id mutex; no global locks."
  },
  "dependencies": {
    "depends_on": [
      "intel.orderflowanalyzer",
      "exec.order_lifecycle_manager"
    ],
    "emits_to": []
  },
  "graph": {
    "requires": [
      "intel.orderflowanalyzer",
      "exec.order_lifecycle_manager"
    ],
    "required_before": [],
    "consumes": [
      "OrderBookSnapshot",
      "TradeTick",
      "FillEvent"
    ],
    "emits": [
      "SignalEnvelope(kind=liquidity_decay)"
    ],
    "blocks": false
  },
  "mode_support": [
    "off",
    "shadow",
    "advisory"
  ],
  "latency_budget_ms": {
    "p50": 8,
    "p99": 30
  },
  "data_freshness": {
    "max_market_data_age_ms": 2000,
    "max_orderbook_age_ms": 2000,
    "on_stale_data": "Suppress emission; emit confidence=null."
  },
  "ownership": {
    "owner": "Intelligence pod",
    "on_call": "intel-oncall",
    "channel": "#polytraders-intel",
    "escalation": "Head of Intelligence",
    "severity_class": "P3"
  },
  "human_override": {
    "allowed": false,
    "who": "\u2014",
    "log_event": "\u2014",
    "time_bound": "\u2014",
    "scope": "\u2014",
    "second_approval": false
  },
  "security_surfaces": {
    "summary": "Read-only on internal data-flow streams. Emits internal signals only.",
    "signing": "Reads pending signed orders; never signs on user behalf.",
    "secrets": [],
    "contract_calls": [],
    "abuse_vectors": [],
    "mitigations": [
      "Rate-limit per source",
      "Audit-log every override",
      "Require role-based authz on admin paths"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "V2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": true,
    "multichain_ready": true,
    "sdk_used": "Polymarket CLOB V2 SDK",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Consumes V2 OrderBookSnapshot and Fill events."
  },
  "version": {
    "current": "0.1.0",
    "contract_version": "1.0.0",
    "last_breaking_change": "none",
    "deprecation_window_days": 30
  },
  "migration_history": [],
  "runbook": {
    "summary": "If consumers report wildly noisy slice sizes, raise window_seconds. If decay never publishes for a market, check sample count vs min_observations.",
    "oncall_actions": [
      {
        "alert": "4.16_anomaly",
        "first_step": "Open the bot's reporting page and confirm the alert is real (not a metric hiccup).",
        "diagnosis": "Inspect developer log entries for the affected market_id over the last 30 minutes.",
        "mitigation": "Force-clear via Admin UI if the rule is clearly stale; otherwise leave engaged and notify owner.",
        "escalation": "Intelligence pod"
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders bot pause 4.16",
        "effect": "Disables the bot's enforcement layer; downstream consumers fall back to safe defaults."
      }
    ],
    "healthcheck": "GET /healthz/liquidity_decay_monitor \u2192 200 if last successful evaluation < 60s ago."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Stub",
        "how_measured": "replay test against a recorded market.",
        "threshold": "Documented threshold met for the full window."
      }
    ],
    "to_limited_live": [
      {
        "gate": "Shadow",
        "how_measured": "14 days; consumers compare against baseline static decay.",
        "threshold": "Documented threshold met for the full window."
      },
      {
        "gate": "Advisory",
        "how_measured": "14 days; consumers can adopt the signal voluntarily.",
        "threshold": "Documented threshold met for the full window."
      }
    ],
    "to_general_live": [
      {
        "gate": "Owner sign-off",
        "how_measured": "Bot owner reviews 14 days of advisory data.",
        "threshold": "No P1 incidents attributable to this bot."
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "Drop one in 10 book ticks and assert no spurious decay spikes",
      "how_to_inject": "Drop one in 10 book ticks and assert no spurious decay spikes.",
      "expected_behavior": "Bot detects within its latency budget and emits the corresponding reason code.",
      "recovery": "Remove the injected fault; bot returns to healthy state within one debounce window."
    },
    {
      "scenario": "Inject a halt and assert emission suppression",
      "how_to_inject": "Inject a halt and assert emission suppression.",
      "expected_behavior": "Bot detects within its latency budget and emits the corresponding reason code.",
      "recovery": "Remove the injected fault; bot returns to healthy state within one debounce window."
    }
  ],
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 2,
    "phase_name": "Data normalisation",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}