{
  "schema_version": "1.0.0",
  "bot_id": "1.7",
  "bot_name": "BlacklistKeeper",
  "slug": "blacklistkeeper",
  "layer": "Risk",
  "layer_key": "risk",
  "bot_class": "Guardrail",
  "authority": [
    "Reject",
    "Reshape"
  ],
  "status": "beta",
  "readiness": "Limited live",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Risk",
    "bot_class": "Guardrail",
    "authority": "Reject, Reshape",
    "runs_before": "ExecutionPlan emit",
    "runs_after": "Strategy OrderIntent",
    "applies_to": "Every OrderIntent \u2014 screens the target market and counterparty wallet against operator-maintained banned-market and banned-counterparty lists",
    "default_mode": "limited_live",
    "user_visible": "summary-only",
    "developer_owner": "Polytraders core \u2014 Risk pod"
  },
  "purpose": "BlacklistKeeper maintains a dual-registry of banned market condition IDs and banned counterparty wallet addresses. On every OrderIntent it checks both registries and hard-rejects any intent whose target market or counterparty appears on either list. The bot also monitors for ambiguity signals (undefined resolution sources, prior dispute history, time-to-resolution below threshold) and rejects structurally hostile markets before execution. It is fail-closed: if the registry cannot be read, the intent is rejected.",
  "why_it_matters": [
    {
      "failure": "Intent routed to a banned market",
      "consequence": "Trading a market that has been operator-banned (due to ambiguous resolution rules, prior disputes, or platform policy) exposes the system to unresolvable settlement risk and potential fund loss."
    },
    {
      "failure": "Order matched against a banned counterparty wallet",
      "consequence": "Engaging with a blacklisted counterparty (e.g. a wallet flagged for manipulation, wash-trading, or prior sanctions escalation) introduces regulatory exposure and may contaminate position records."
    },
    {
      "failure": "Market with ambiguous resolution rules passes through",
      "consequence": "Markets using vague trigger words (\u201csubstantial\u201d, \u201cprimary\u201d) or undefined resolution sources frequently go to UMA dispute. Each dispute costs a $750 pUSD bond and 24\u201348 h of DVM voting delay, eroding capital efficiency."
    },
    {
      "failure": "Single-source resolution market accepted",
      "consequence": "A market that resolves on a single oracle feed is vulnerable to feed manipulation or temporary outage; the bot blocks these when block_single_source is enabled."
    },
    {
      "failure": "Fail-open on registry outage",
      "consequence": "If BlacklistKeeper approves orders when the registry is unavailable, banned markets and counterparties can trade undetected. The bot must never approve when its data source is unreachable."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Gamma API market metadata \u2014 resolution rules text, single-source flag, time-to-resolution, negRisk flag, prior dispute history",
      "source": "gamma",
      "required": true,
      "use": "Evaluate market for ambiguity keywords, single-source resolution, time-to-resolution threshold, and structural hostility signals."
    }
  ],
  "internal_inputs": [
    {
      "input": "Banned-market registry \u2014 operator-curated list of banned condition_ids",
      "source": "internal",
      "required": true,
      "use": "Hard-reject any intent whose market_id appears in the banned-market set."
    },
    {
      "input": "Banned-counterparty registry \u2014 operator-curated list of banned wallet addresses",
      "source": "internal",
      "required": true,
      "use": "Hard-reject any intent whose counterparty wallet appears in the banned-counterparty set."
    },
    {
      "input": "Community / shared dispute history feed",
      "source": "internal",
      "required": false,
      "use": "Supplement the operator-curated list with cross-platform dispute signals to identify markets with a track record of resolution failure."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "If KillSwitch is active, reject all orders immediately without consulting the registry."
    },
    {
      "input": "ObservationReport stream",
      "source": "internal",
      "required": false,
      "use": "Consume ObservationReports from intel/disc bots to trigger registry additions when new market anomalies are detected."
    }
  ],
  "raw_params": [
    "blacklisted_condition_ids \u00b7 list",
    "blacklisted_counterparties \u00b7 list",
    "min_hours_to_resolution \u00b7 int",
    "block_single_source \u00b7 bool",
    "ambiguity_keywords \u00b7 list"
  ],
  "parameters": [
    {
      "name": "blacklisted_condition_ids",
      "default": [],
      "warning": null,
      "hard": true,
      "controls": "Explicit list of Polymarket condition IDs (32-byte hex) that are banned from trading. Any OrderIntent targeting one of these markets is hard-rejected immediately.",
      "why_default_matters": "Defaults to empty; operator must populate. The registry is the primary enforcement mechanism and must be kept current by the risk team.",
      "threshold_logic": [
        {
          "condition": "intent.market_id in blacklisted_condition_ids",
          "action": "REJECT \u2014 BLACKLIST_KEEPER_MARKET_BANNED"
        },
        {
          "condition": "intent.market_id not in blacklisted_condition_ids",
          "action": "APPROVE (this check)"
        }
      ],
      "dev_check": "if (blacklisted_condition_ids.includes(intent.market_id)) return reject('BLACKLIST_KEEPER_MARKET_BANNED');",
      "user_facing": "This market is not available for trading on this platform."
    },
    {
      "name": "blacklisted_counterparties",
      "default": [],
      "warning": null,
      "hard": true,
      "controls": "Explicit list of wallet addresses banned as counterparties. Any OrderIntent whose counterparty field matches an entry is hard-rejected.",
      "why_default_matters": "Defaults to empty; populated by the risk team when manipulation, wash-trading, or compliance flags are identified on specific wallets.",
      "threshold_logic": [
        {
          "condition": "intent.counterparty in blacklisted_counterparties",
          "action": "REJECT \u2014 BLACKLIST_KEEPER_COUNTERPARTY_BANNED"
        },
        {
          "condition": "intent.counterparty not in blacklisted_counterparties",
          "action": "APPROVE (this check)"
        }
      ],
      "dev_check": "if (blacklisted_counterparties.includes(intent.counterparty)) return reject('BLACKLIST_KEEPER_COUNTERPARTY_BANNED');",
      "user_facing": "This transaction cannot be completed due to a platform restriction."
    },
    {
      "name": "min_hours_to_resolution",
      "default": 2,
      "warning": 4,
      "hard": 2,
      "controls": "Minimum hours remaining before market resolution. Markets resolving within this window are treated as structurally hostile and rejected. Warning threshold fires at < 4h to give time to manage positions.",
      "why_default_matters": "Markets resolving in < 2 h give insufficient time for the UMA 2-hour challenge window, meaning a disputed resolution cannot be challenged before settlement. Hard floor at 2 h mirrors the UMA optimistic oracle challenge period.",
      "threshold_logic": [
        {
          "condition": "hours_to_resolution >= 4",
          "action": "APPROVE (this check)"
        },
        {
          "condition": "hours_to_resolution >= 2 and < 4",
          "action": "WARN \u2014 BLACKLIST_KEEPER_NEAR_RESOLUTION"
        },
        {
          "condition": "hours_to_resolution < 2",
          "action": "REJECT \u2014 BLACKLIST_KEEPER_NEAR_RESOLUTION (hard)"
        }
      ],
      "dev_check": "const htr = (market.end_date_iso - now()) / 3600000; if (htr < params.min_hours_to_resolution) return reject('BLACKLIST_KEEPER_NEAR_RESOLUTION');",
      "user_facing": "This market is too close to resolution to accept new orders."
    },
    {
      "name": "block_single_source",
      "default": true,
      "warning": null,
      "hard": true,
      "controls": "When true, markets that resolve on a single data source (no multi-oracle fallback) are rejected to protect against feed manipulation and outage risk.",
      "why_default_matters": "Single-source markets are inherently manipulable. Defaulting to block is the conservative posture; operators may disable for specific market categories with documented rationale.",
      "threshold_logic": [
        {
          "condition": "block_single_source = true AND market.single_source = true",
          "action": "REJECT \u2014 BLACKLIST_KEEPER_SINGLE_SOURCE"
        },
        {
          "condition": "block_single_source = false OR market.single_source = false",
          "action": "APPROVE (this check)"
        }
      ],
      "dev_check": "if (params.block_single_source && market.single_source) return reject('BLACKLIST_KEEPER_SINGLE_SOURCE');",
      "user_facing": "This market cannot be traded due to its resolution source configuration."
    },
    {
      "name": "ambiguity_keywords",
      "default": [
        "substantial",
        "primary",
        "significant",
        "material",
        "reasonable"
      ],
      "warning": [
        "substantial",
        "primary"
      ],
      "hard": [
        "substantial",
        "primary",
        "significant",
        "material",
        "reasonable"
      ],
      "controls": "List of keywords whose presence in market resolution rules text triggers an ambiguity check. If any keyword is found, the market is flagged as structurally ambiguous. Warning fires for the minimal 2-keyword set; hard block fires for any match.",
      "why_default_matters": "Markets with vague resolution language have historically high dispute rates on Polymarket. Blocking them at intent time avoids the $750 pUSD UMA bond cost and 24\u201348 h DVM delay.",
      "threshold_logic": [
        {
          "condition": "no keyword found in resolution_rules",
          "action": "APPROVE (this check)"
        },
        {
          "condition": "keyword found AND configured list < 5 keywords",
          "action": "WARN \u2014 BLACKLIST_KEEPER_AMBIGUOUS_RULES"
        },
        {
          "condition": "keyword found AND configured list >= 5 keywords",
          "action": "REJECT \u2014 BLACKLIST_KEEPER_AMBIGUOUS_RULES (hard)"
        }
      ],
      "dev_check": "const hit = params.ambiguity_keywords.find(kw => market.resolution_rules.includes(kw)); if (hit) return reject('BLACKLIST_KEEPER_AMBIGUOUS_RULES');",
      "user_facing": "This market has ambiguous resolution rules and is not available for trading."
    }
  ],
  "default_config": {
    "bot_id": "risk.blacklist_keeper",
    "version": "2.0.0",
    "mode": "hard_guard",
    "defaults": {
      "blacklisted_condition_ids": [],
      "blacklisted_counterparties": [],
      "min_hours_to_resolution": 2,
      "block_single_source": true,
      "ambiguity_keywords": [
        "substantial",
        "primary",
        "significant",
        "material",
        "reasonable"
      ]
    },
    "locked": {
      "min_hours_to_resolution": {
        "min": 2
      },
      "ambiguity_keywords": {
        "min_length": 2
      }
    }
  },
  "implementation_flow": [
    "Receive OrderIntent from Strategy layer including market_id, counterparty wallet, and intent metadata.",
    "Check KillSwitch active flag; if active, return REJECT with KILL_SWITCH_ACTIVE immediately.",
    "Look up intent.market_id in the banned-market registry (Redis SET). On any match, return HARD_REJECT with BLACKLIST_KEEPER_MARKET_BANNED. If registry is unavailable, return HARD_REJECT with BLACKLIST_KEEPER_DATA_UNAVAILABLE (fail-closed).",
    "Look up intent.counterparty in the banned-counterparty registry (Redis SET). On any match, return HARD_REJECT with BLACKLIST_KEEPER_COUNTERPARTY_BANNED.",
    "Fetch target market metadata from Gamma API: resolution_rules text, single_source flag, end_date_iso, and prior dispute history.",
    "If Gamma fetch fails or returns stale data (age > 300s), return HARD_REJECT with BLACKLIST_KEEPER_DATA_UNAVAILABLE.",
    "Compute hours_to_resolution = (market.end_date_iso - now_ms()) / 3600000. If hours_to_resolution < params.min_hours_to_resolution, return HARD_REJECT with BLACKLIST_KEEPER_NEAR_RESOLUTION.",
    "If params.block_single_source = true and market.single_source = true, return HARD_REJECT with BLACKLIST_KEEPER_SINGLE_SOURCE.",
    "Scan market.resolution_rules text for any term in params.ambiguity_keywords. On any match, return HARD_REJECT with BLACKLIST_KEEPER_AMBIGUOUS_RULES.",
    "Check market dispute history: if prior_disputes > 0, return HARD_REJECT with BLACKLIST_KEEPER_PRIOR_DISPUTE.",
    "All checks passed \u2014 return APPROVE with inputs_used list and checked_at timestamp (ms)."
  ],
  "decision_logic": {
    "approve": "Market condition ID is not banned, counterparty is not banned, market resolves >= min_hours_to_resolution from now, single-source flag is clear (or block disabled), resolution rules contain no ambiguity keywords, and no prior dispute history.",
    "reshape_required": "Not used in current implementation \u2014 all blacklist violations are hard structural issues that cannot be partially accommodated. Reshape authority is reserved for future partial-position sizing logic if operator policy allows.",
    "reject": "Market or counterparty is on the banned list, market resolves within the hard time-to-resolution threshold, single-source resolution is detected with block_single_source=true, ambiguity keywords are found in resolution rules, prior disputes are recorded, KillSwitch is active, or any registry/data source is unavailable (fail-closed).",
    "warning_only": "BLACKLIST_KEEPER_NEAR_RESOLUTION emits a WARN annotation when hours_to_resolution < 4h but >= 2h, allowing the upstream strategy to decide whether to reduce position size before the hard cutoff."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "guard_id": "risk.blacklist_keeper",
    "decision": "HARD_REJECT",
    "severity": "HARD",
    "reason_code": "BLACKLIST_KEEPER_MARKET_BANNED",
    "message": "Market 0x3f7a...d9c2 is on the banned-market registry. Order rejected.",
    "constraints": {},
    "inputs_used": [
      "internal.registry.banned_markets",
      "internal.registry.banned_counterparties",
      "gamma.market.resolution_rules",
      "gamma.market.single_source",
      "gamma.market.end_date_iso"
    ],
    "checked_at": "2026-05-09T11:05:00Z"
  },
  "developer_log": {
    "bot_id": "risk.blacklist_keeper",
    "decision": "HARD_REJECT",
    "reason_code": "BLACKLIST_KEEPER_MARKET_BANNED",
    "inputs_used": [
      "internal.registry.banned_markets",
      "gamma.market.resolution_rules"
    ],
    "metrics": {
      "market_id": "0x3f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
      "counterparty": "0xAbCd...redacted",
      "registry_hit": true,
      "hours_to_resolution": 18.3,
      "single_source": false,
      "ambiguity_keyword_found": null,
      "prior_disputes": 0
    },
    "checked_at": "2026-05-09T11:05:00Z"
  },
  "user_explanations": [
    {
      "situation": "Order blocked \u2014 market is banned",
      "message": "This market is not available for trading on this platform."
    },
    {
      "situation": "Order blocked \u2014 counterparty is banned",
      "message": "This transaction cannot be completed due to a platform restriction on the counterparty."
    },
    {
      "situation": "Order blocked \u2014 market resolves too soon",
      "message": "This market closes too soon to accept new orders. Please look for a market with more time remaining."
    },
    {
      "situation": "Order blocked \u2014 ambiguous resolution rules",
      "message": "This market has resolution rules that may lead to a disputed outcome and is not available for trading."
    },
    {
      "situation": "Order blocked \u2014 single-source resolution",
      "message": "This market resolves on a single data source and is not available for trading due to reliability requirements."
    },
    {
      "situation": "Warning \u2014 market near resolution",
      "message": "This market resolves soon. Consider whether your position size is appropriate given the limited time remaining."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Failing open during a registry outage, allowing orders to reach banned markets or banned counterparties. BlacklistKeeper must never approve when the registry is unavailable.",
    "false_positive_risk": "Incorrectly blocking a legitimate market due to a stale registry entry that has since been removed, or an ambiguity keyword match in boilerplate resolution-rules text that is not actually structurally ambiguous.",
    "false_negative_risk": "Approving an order for a market whose condition_id was recently added to the banned list but the in-memory cache has not yet refreshed, creating a window during which banned markets can be traded.",
    "safe_fallback": "If the banned-market registry, banned-counterparty registry, or Gamma market metadata is unavailable or returns stale data beyond TTL, BlacklistKeeper hard-rejects the intent with BLACKLIST_KEEPER_DATA_UNAVAILABLE. It never approves on missing data.",
    "required_dependencies": [
      "Banned-market registry (Redis SET, operator-populated)",
      "Banned-counterparty registry (Redis SET, operator-populated)",
      "Gamma API \u2014 market resolution rules, single_source flag, end_date_iso, dispute history",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when all checks pass",
        "setup": "market not banned, counterparty not banned, hours_to_resolution=48, single_source=false, no ambiguity keywords, no prior disputes",
        "expected": "APPROVE with no constraints"
      },
      {
        "test": "Reject when market is on banned list",
        "setup": "intent.market_id in blacklisted_condition_ids",
        "expected": "HARD_REJECT with reason_code=BLACKLIST_KEEPER_MARKET_BANNED"
      },
      {
        "test": "Reject when counterparty is on banned list",
        "setup": "intent.counterparty in blacklisted_counterparties",
        "expected": "HARD_REJECT with reason_code=BLACKLIST_KEEPER_COUNTERPARTY_BANNED"
      },
      {
        "test": "Reject when hours_to_resolution < min_hours_to_resolution",
        "setup": "market.end_date_iso = now + 1h, min_hours_to_resolution=2",
        "expected": "HARD_REJECT with reason_code=BLACKLIST_KEEPER_NEAR_RESOLUTION"
      },
      {
        "test": "Warn when hours_to_resolution between warning and hard threshold",
        "setup": "market.end_date_iso = now + 3h, min_hours_to_resolution=2, warning threshold=4h",
        "expected": "WARN annotation with reason_code=BLACKLIST_KEEPER_NEAR_RESOLUTION, verdict=APPROVE"
      },
      {
        "test": "Reject when single_source=true and block_single_source=true",
        "setup": "market.single_source=true, block_single_source=true",
        "expected": "HARD_REJECT with reason_code=BLACKLIST_KEEPER_SINGLE_SOURCE"
      },
      {
        "test": "Reject when ambiguity keyword found in resolution rules",
        "setup": "market.resolution_rules contains 'substantial'",
        "expected": "HARD_REJECT with reason_code=BLACKLIST_KEEPER_AMBIGUOUS_RULES"
      },
      {
        "test": "Reject when prior disputes > 0",
        "setup": "market.prior_disputes=1",
        "expected": "HARD_REJECT with reason_code=BLACKLIST_KEEPER_PRIOR_DISPUTE"
      },
      {
        "test": "Reject when registry unavailable (fail-closed)",
        "setup": "Redis returns connection error for banned-market registry",
        "expected": "HARD_REJECT with reason_code=BLACKLIST_KEEPER_DATA_UNAVAILABLE"
      }
    ],
    "integration": [
      {
        "test": "Registry update propagates within TTL window",
        "expected": "HARD_REJECT(BLACKLIST_KEEPER_MARKET_BANNED) within 30s of a condition_id being added to the banned-market registry and cache refresh completing"
      },
      {
        "test": "Gamma API market metadata fetch triggers ambiguity check end-to-end",
        "expected": "HARD_REJECT(BLACKLIST_KEEPER_AMBIGUOUS_RULES) when Gamma returns resolution_rules text containing a banned keyword for a live market"
      },
      {
        "test": "KillSwitch active bypasses all registry checks",
        "expected": "HARD_REJECT(KILL_SWITCH_ACTIVE) without consulting banned-market registry or Gamma API"
      }
    ],
    "property": [
      {
        "property": "Banned markets are never approved regardless of other check outcomes",
        "required": "Always true \u2014 registry hit is unconditional HARD_REJECT before any structural checks run"
      },
      {
        "property": "Registry unavailability never results in APPROVE",
        "required": "Always true \u2014 any registry read error produces HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE)"
      },
      {
        "property": "KillSwitch active always produces HARD_REJECT before registry is consulted",
        "required": "Always true \u2014 KillSwitch check is gate zero in the evaluation chain"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Refuse to trade markets that are structurally hostile.",
  "legacy_pm_signals": [
    "Market resolution rules text (ambiguous trigger words: \"substantial\", \"primary\", undefined sources)",
    "Single-source resolution flag",
    "Market history of prior disputes",
    "Time-to-resolution < configured threshold"
  ],
  "legacy_external_feeds": [
    "Operator-curated blacklist",
    "Community / shared dispute history"
  ],
  "reporting_groups": [
    "risk_compliance"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.0",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1",
      "to": "v2",
      "reason": "CLOB V2 cutover",
      "action_taken": "Switched to py-clob-client-v2; all notional amounts now denominated in pUSD (collateral field updated from USDC.e). Removed feeRateBps and nonce references from intent inspection payloads. Updated Gamma market-metadata fetch to use V2 negRisk and enableNegRisk flags. EIP-712 domain version updated to '2' in downstream intent inspection. Registry wire format updated to include V2 condition_id format (32-byte hex)."
    }
  ],
  "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": "BlacklistKeeper inspects Gamma V2 market metadata including negRisk and enableNegRisk flags to classify multi-outcome markets; NegRisk markets with ambiguous resolution rules or prior disputes are subject to the same registry and structural checks as standard binary markets. The bot does not sign orders or call CTFExchangeV2 directly."
  },
  "reference_implementation": {
    "summary": "Runs five sequential guardrail checks \u2014 KillSwitch, banned-market registry lookup, banned-counterparty registry lookup, structural market checks (time-to-resolution, single-source, ambiguity keywords, dispute history) \u2014 before emitting a RiskVote. All checks are fail-closed: any registry or data-source error produces HARD_REJECT.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input or call service. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "FUNCTION evaluateBlacklist(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. Banned-market registry check ---\n  bannedMarkets = FETCH internal.registry.banned_markets\n  IF bannedMarkets IS NULL:\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_DATA_UNAVAILABLE,\n                  detail=\"banned_markets registry unavailable\")\n    RETURN\n  IF bannedMarkets.contains(intent.market_id):\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_MARKET_BANNED,\n                  market_id=intent.market_id)\n    RETURN\n\n  // --- 2. Banned-counterparty registry check ---\n  bannedCounterparties = FETCH internal.registry.banned_counterparties\n  IF bannedCounterparties IS NULL:\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_DATA_UNAVAILABLE,\n                  detail=\"banned_counterparties registry unavailable\")\n    RETURN\n  IF bannedCounterparties.contains(intent.counterparty):\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_COUNTERPARTY_BANNED,\n                  counterparty=intent.counterparty)\n    RETURN\n\n  // --- 3. Fetch Gamma market metadata ---\n  market = FETCH gamma.getMarketByConditionId(intent.market_id)\n  IF market IS NULL OR market.fetched_at_ms < now_ms() - 300000:\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_DATA_UNAVAILABLE,\n                  detail=\"gamma market metadata unavailable or stale\")\n    RETURN\n\n  // --- 4. Time-to-resolution check ---\n  htr = (market.end_date_ms - now_ms()) / 3600000\n  IF htr < params.min_hours_to_resolution:\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_NEAR_RESOLUTION,\n                  hours_to_resolution=htr)\n    RETURN\n  IF htr < 4:\n    annotations.append(WARN(BLACKLIST_KEEPER_NEAR_RESOLUTION, hours_to_resolution=htr))\n\n  // --- 5. Single-source resolution check ---\n  IF params.block_single_source AND market.single_source:\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_SINGLE_SOURCE)\n    RETURN\n\n  // --- 6. Ambiguity keyword scan ---\n  FOR keyword IN params.ambiguity_keywords:\n    IF keyword IN market.resolution_rules.lower():\n      EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_AMBIGUOUS_RULES,\n                    keyword=keyword)\n      RETURN\n\n  // --- 7. Prior dispute history check ---\n  IF market.prior_disputes > 0:\n    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_PRIOR_DISPUTE,\n                  prior_disputes=market.prior_disputes)\n    RETURN\n\n  // --- 8. All checks passed ---\n  EMIT RiskVote(decision=APPROVE, reason=BLACKLIST_KEEPER_PASS,\n                inputs_used=[\"internal.registry.banned_markets\",\n                             \"internal.registry.banned_counterparties\",\n                             \"gamma.market.resolution_rules\",\n                             \"gamma.market.single_source\",\n                             \"gamma.market.end_date_ms\",\n                             \"gamma.market.prior_disputes\"],\n                annotations=annotations,\n                checked_at=now_ms())",
    "sdk_calls": [
      "gamma.getMarketByConditionId(market_id)",
      "internal.registry.banned_markets.contains(condition_id)",
      "internal.registry.banned_counterparties.contains(wallet)",
      "internal.killswitch.status()",
      "toPusdUnits(amount)"
    ],
    "complexity": "O(1) \u2014 constant number of registry lookups and linear keyword scan over resolution_rules text (bounded by max text length ~2 KB); all backed by in-memory Redis cache"
  },
  "wire_examples": {
    "input": [
      {
        "label": "OrderIntent from strategy \u2014 market on banned list",
        "source": "internal",
        "payload": {
          "intent_id": "int_a1b2c3d4e5f6a7b8",
          "trace_id": "trc_0011223344556677",
          "market_id": "0x3f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
          "side": "BUY",
          "outcome": "YES",
          "size_pusd": 250,
          "price": 0.6,
          "counterparty": "0xFa9E1234567890AbCdEf1234567890AbCdEf1234",
          "wallet": "0x1aBcDeF0987654321AbCdEf0987654321AbCdEf09",
          "generated_at_ms": 1746780000000
        }
      },
      {
        "label": "Gamma market metadata for the target market",
        "source": "gamma",
        "payload": {
          "condition_id": "0x3f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
          "resolution_rules": "This market resolves YES if the primary government agency publishes an official statement before end date.",
          "single_source": false,
          "end_date_ms": 1746866400000,
          "prior_disputes": 0,
          "neg_risk": false,
          "fetched_at_ms": 1746779980000
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 HARD_REJECT (market banned)",
        "payload": {
          "guard_id": "risk.blacklist_keeper",
          "decision": "HARD_REJECT",
          "severity": "HARD",
          "reason_code": "BLACKLIST_KEEPER_MARKET_BANNED",
          "message": "Market 0x3f7a...7f8a is on the banned-market registry. Order rejected.",
          "constraints": {},
          "inputs_used": [
            "internal.registry.banned_markets",
            "internal.registry.banned_counterparties",
            "gamma.market.resolution_rules",
            "gamma.market.single_source",
            "gamma.market.end_date_ms"
          ],
          "trace_id": "trc_0011223344556677",
          "checked_at": "2026-05-09T11:05:00Z"
        }
      },
      {
        "label": "RiskVote \u2014 APPROVE (all checks pass)",
        "payload": {
          "guard_id": "risk.blacklist_keeper",
          "decision": "APPROVE",
          "severity": "INFO",
          "reason_code": "BLACKLIST_KEEPER_PASS",
          "message": "All blacklist and structural checks passed for market 0xb2c3...e4f5.",
          "constraints": {},
          "inputs_used": [
            "internal.registry.banned_markets",
            "internal.registry.banned_counterparties",
            "gamma.market.resolution_rules",
            "gamma.market.single_source",
            "gamma.market.end_date_ms",
            "gamma.market.prior_disputes"
          ],
          "trace_id": "trc_aabbccddeeff0011",
          "checked_at": "2026-05-09T11:05:02Z"
        }
      }
    ]
  },
  "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 the registry or Gamma API.",
      "user_message": "Trading is currently paused. Please try again later."
    },
    {
      "code": "BLACKLIST_KEEPER_MARKET_BANNED",
      "severity": "HARD_REJECT",
      "meaning": "The target market condition ID appears in the operator-maintained banned-market registry.",
      "action": "Return HARD_REJECT; log market_id and registry entry for audit trail.",
      "user_message": "This market is not available for trading on this platform."
    },
    {
      "code": "BLACKLIST_KEEPER_COUNTERPARTY_BANNED",
      "severity": "HARD_REJECT",
      "meaning": "The counterparty wallet address appears in the operator-maintained banned-counterparty registry.",
      "action": "Return HARD_REJECT; log counterparty address (redacted in user-facing messages) for audit trail.",
      "user_message": "This transaction cannot be completed due to a platform restriction on the counterparty."
    },
    {
      "code": "BLACKLIST_KEEPER_NEAR_RESOLUTION",
      "severity": "HARD_REJECT",
      "meaning": "The market resolves within the configured min_hours_to_resolution window, which is below the UMA 2-hour challenge period. Emitted as WARN when hours_to_resolution is between 2 and 4h.",
      "action": "Return HARD_REJECT when below hard threshold; emit WARN annotation and APPROVE when between warning and hard thresholds.",
      "user_message": "This market is too close to resolution to accept new orders."
    },
    {
      "code": "BLACKLIST_KEEPER_SINGLE_SOURCE",
      "severity": "HARD_REJECT",
      "meaning": "The market resolves on a single data source and block_single_source is enabled, indicating unacceptable feed manipulation and outage risk.",
      "action": "Return HARD_REJECT; log market_id and single_source flag.",
      "user_message": "This market cannot be traded due to its resolution source configuration."
    },
    {
      "code": "BLACKLIST_KEEPER_AMBIGUOUS_RULES",
      "severity": "HARD_REJECT",
      "meaning": "The market resolution rules text contains one or more ambiguity keywords (e.g. 'substantial', 'primary') that indicate high dispute risk.",
      "action": "Return HARD_REJECT; log the matched keyword and market_id for the risk team to review registry addition.",
      "user_message": "This market has ambiguous resolution rules and is not available for trading."
    },
    {
      "code": "BLACKLIST_KEEPER_PRIOR_DISPUTE",
      "severity": "HARD_REJECT",
      "meaning": "The market has a recorded prior dispute in its history, indicating structural resolution instability.",
      "action": "Return HARD_REJECT; log market_id and prior_disputes count. Risk team should review whether to add to banned-market registry.",
      "user_message": "This market has a history of resolution disputes and is not available for trading."
    },
    {
      "code": "BLACKLIST_KEEPER_DATA_UNAVAILABLE",
      "severity": "HARD_REJECT",
      "meaning": "One or more required data sources (banned-market registry, banned-counterparty registry, or Gamma market metadata) are unavailable or returned stale data beyond TTL.",
      "action": "Return HARD_REJECT (fail-closed). Log which source failed. Alert on-call if sustained > 60s.",
      "user_message": "We could not verify this market at this time. Please try again shortly."
    },
    {
      "code": "BLACKLIST_KEEPER_PASS",
      "severity": "INFO",
      "meaning": "All registry and structural checks passed for this market and counterparty.",
      "action": "Emit APPROVE and continue to next guardrail.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_risk_blacklistkeeper_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision",
          "reason_code"
        ],
        "meaning": "Total RiskVote decisions emitted by BlacklistKeeper, broken down by decision type and reason code. Primary signal for ban-list hit rate and structural rejection trends."
      },
      {
        "name": "polytraders_risk_blacklistkeeper_registry_hits_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "registry",
          "action"
        ],
        "meaning": "Number of banned-market and banned-counterparty registry hits, labeled by registry type ('market' or 'counterparty') and action ('reject'). Used to track how often the lists are triggering."
      },
      {
        "name": "polytraders_risk_blacklistkeeper_registry_size",
        "type": "gauge",
        "unit": "count",
        "labels": [
          "registry"
        ],
        "meaning": "Current number of entries in each registry (banned_markets, banned_counterparties). Tracks list growth over time and alerts on unexpected shrinkage."
      },
      {
        "name": "polytraders_risk_blacklistkeeper_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Wall-clock time from OrderIntent receipt to RiskVote emit. P99 target < 30ms (registry lookups are in-memory Redis; Gamma metadata is cached)."
      },
      {
        "name": "polytraders_risk_blacklistkeeper_data_source_errors_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "source"
        ],
        "meaning": "Number of fail-closed rejections caused by unavailable data sources (banned_markets, banned_counterparties, gamma), by source name. Non-zero triggers immediate alert."
      },
      {
        "name": "polytraders_risk_blacklistkeeper_registry_cache_age_seconds",
        "type": "gauge",
        "unit": "seconds",
        "labels": [
          "registry"
        ],
        "meaning": "Age of the in-memory registry cache for banned_markets and banned_counterparties. Alerts if stale beyond configured refresh interval (default 30s)."
      }
    ],
    "alerts": [
      {
        "name": "BlacklistKeeperDataSourceError",
        "condition": "rate(polytraders_risk_blacklistkeeper_data_source_errors_total[5m]) > 0",
        "severity": "page",
        "runbook": "#runbook-blacklistkeeper-data-source-error"
      },
      {
        "name": "BlacklistKeeperRegistryCacheStale",
        "condition": "polytraders_risk_blacklistkeeper_registry_cache_age_seconds > 60",
        "severity": "page",
        "runbook": "#runbook-blacklistkeeper-registry-cache-stale"
      },
      {
        "name": "BlacklistKeeperHighRejectRate",
        "condition": "rate(polytraders_risk_blacklistkeeper_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_blacklistkeeper_decisions_total[5m]) > 0.15",
        "severity": "warn",
        "runbook": "#runbook-blacklistkeeper-reject-rate"
      },
      {
        "name": "BlacklistKeeperRegistryShrinkage",
        "condition": "delta(polytraders_risk_blacklistkeeper_registry_size[1h]) < -5",
        "severity": "warn",
        "runbook": "#runbook-blacklistkeeper-registry-shrinkage"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Risk overview / BlacklistKeeper",
      "Grafana \u2014 Risk compliance / banned-market and banned-counterparty hit rates"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "Two Redis SETs: banned_markets (condition_id strings) and banned_counterparties (wallet address strings). Backed by operator-managed admin entries and auto-populated from ObservationReport signals. Gamma market metadata cached in a Redis HASH keyed by condition_id.",
    "ttl": "Registry SETs: no TTL (persistent, operator-managed); registry in-memory snapshot refreshed every 30s. Gamma market metadata cache: 300s TTL with background refresh.",
    "recovery": "On cold start, registry snapshot is loaded from Redis synchronously before the first evaluation. If Redis is unavailable, all evaluations return HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) until connectivity is restored.",
    "size_estimate": "~5 KB per 100 banned condition IDs; ~3 KB per 100 banned counterparty addresses; ~200 B per cached market metadata entry"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 500,
    "idempotency_key": "intent_id",
    "timeout_ms": 30,
    "backpressure": "drop newest",
    "locking": "registry SETs are read-only at evaluation time; writes (admin additions/removals) use optimistic locking via Redis WATCH/MULTI/EXEC; no per-intent locking required"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Global brake \u2014 checked first before any registry is consulted.",
        "contract": "RiskVote.HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all registry and structural evaluation."
      }
    ],
    "emits_to": [
      {
        "bot_id": "exec.smart_router",
        "why": "Approved RiskVote passes to SmartRouter for ExecutionPlan construction.",
        "contract": "APPROVE passes through; HARD_REJECT causes SmartRouter to discard the intent without retry."
      }
    ],
    "sibling": [
      {
        "bot_id": "risk.compliance_gate",
        "why": "Sibling guardrail; both must APPROVE before SmartRouter runs. ComplianceGate handles user/wallet policy; BlacklistKeeper handles market and counterparty registry."
      },
      {
        "bot_id": "risk.liquidity_guard",
        "why": "Sibling guardrail; runs in parallel with BlacklistKeeper on the same OrderIntent."
      },
      {
        "bot_id": "risk.portfolio_guard",
        "why": "Sibling guardrail; all risk guardrails must APPROVE or RESHAPE before execution proceeds."
      }
    ],
    "external": [
      {
        "service": "Gamma API (market metadata)",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 300ms p99",
        "fallback": "HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) if Gamma is unreachable or returns stale metadata."
      },
      {
        "service": "Redis (registry store)",
        "endpoint": "internal Redis cluster",
        "sla": "99.99% (in-cluster)",
        "fallback": "HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) if Redis is unavailable. In-memory snapshot used up to 30s stale."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Operator registry poisoning: unauthorized modification of the banned-market or banned-counterparty SET in Redis, either to add legitimate markets (denial-of-service) or to remove banned entries (allowing hostile markets through)",
      "Cache timing window: submitting an order for a newly banned market within the 30s registry refresh window before the in-memory snapshot is updated",
      "Resolution-rules text injection: crafting a market with resolution_rules text that avoids ambiguity keywords but is semantically ambiguous, evading keyword-based detection"
    ],
    "mitigations": [
      "Redis registry SETs are write-protected by role-based access control; only the risk-admin service account may modify entries",
      "Registry refresh is triggered both on TTL expiry and on write events (Redis keyspace notifications) to minimize the window between a ban and its enforcement",
      "Fail-closed policy: Redis unavailability \u2192 HARD_REJECT, never APPROVE",
      "All registry modifications are audit-logged with timestamp, operator ID, and before/after state"
    ],
    "contract_calls": []
  },
  "failure_injection": [
    {
      "scenario": "REGISTRY_UNAVAILABLE",
      "how_to_inject": "Block outbound TCP to Redis; wait for in-memory snapshot TTL (30s) to expire",
      "expected_behaviour": "All evaluations return HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) once snapshot expires; BlacklistKeeperDataSourceError alert fires",
      "recovery": "Returns to normal within one snapshot-refresh cycle after Redis connectivity is restored."
    },
    {
      "scenario": "BANNED_MARKET_HIT",
      "how_to_inject": "Add a test condition_id to the banned_markets Redis SET and submit an OrderIntent targeting that market_id",
      "expected_behaviour": "HARD_REJECT(BLACKLIST_KEEPER_MARKET_BANNED) within one registry refresh cycle (<=30s)",
      "recovery": "Returns to APPROVE immediately after the condition_id is removed from the registry and cache refreshes."
    },
    {
      "scenario": "AMBIGUOUS_RESOLUTION_RULES",
      "how_to_inject": "Override Gamma mock to return resolution_rules containing 'substantial' for a target market",
      "expected_behaviour": "HARD_REJECT(BLACKLIST_KEEPER_AMBIGUOUS_RULES) with keyword='substantial' logged in developer_log",
      "recovery": "Returns to APPROVE once Gamma returns clean resolution_rules and metadata cache refreshes."
    },
    {
      "scenario": "NEAR_RESOLUTION_HARD",
      "how_to_inject": "Set Gamma mock market.end_date_ms = now_ms() + 3600000 (1 h from now)",
      "expected_behaviour": "HARD_REJECT(BLACKLIST_KEEPER_NEAR_RESOLUTION) with hours_to_resolution=1.0",
      "recovery": "Returns to APPROVE once end_date_ms is > min_hours_to_resolution in the future or market is replaced with a new one."
    },
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Return 503 from Gamma API mock for the getMarketByConditionId endpoint",
      "expected_behaviour": "HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) for any intent where Gamma metadata is required and cache has expired",
      "recovery": "Returns to APPROVE once Gamma API is reachable and market metadata cache is refreshed."
    },
    {
      "scenario": "BANNED_COUNTERPARTY_HIT",
      "how_to_inject": "Add a test wallet address to the banned_counterparties Redis SET and submit an OrderIntent with that counterparty",
      "expected_behaviour": "HARD_REJECT(BLACKLIST_KEEPER_COUNTERPARTY_BANNED) within one registry refresh cycle",
      "recovery": "Returns to APPROVE immediately after the wallet is removed from the registry and cache refreshes."
    }
  ],
  "runbook": {
    "summary": "BlacklistKeeper incidents typically involve a Redis registry outage causing fail-closed mass rejections, a stale registry cache preventing a newly banned market from being blocked, or an unexpected spike in ambiguity-rule rejections indicating a new market category has been listed without review.",
    "oncall_actions": [
      {
        "alert": "BlacklistKeeperDataSourceError",
        "first_action": "Check the source label on the error counter to identify whether Redis or Gamma is failing. For Redis: check cluster health and replication lag. For Gamma: check API status page and network routing.",
        "escalate_to": "Risk pod lead if sustained > 2 minutes; infra on-call for Redis failure."
      },
      {
        "alert": "BlacklistKeeperRegistryCacheStale",
        "first_action": "Check Redis connectivity from the bot host. Manually trigger a registry snapshot refresh via 'polytraders risk refresh-registry --all'. Verify keyspace notifications are enabled on Redis.",
        "escalate_to": "Risk pod lead if cache age > 120s; newly banned markets may not be enforced."
      },
      {
        "alert": "BlacklistKeeperHighRejectRate",
        "first_action": "Check reason_code distribution on the decisions counter. If dominated by BLACKLIST_KEEPER_DATA_UNAVAILABLE, follow the data-source runbook. If dominated by BLACKLIST_KEEPER_AMBIGUOUS_RULES, a batch of new markets with problematic resolution language may have been listed \u2014 review with the risk team.",
        "escalate_to": "Risk pod lead + market-ops team if ambiguity rejections spike."
      },
      {
        "alert": "BlacklistKeeperRegistryShrinkage",
        "first_action": "Compare current registry size with the last known-good snapshot in audit logs. Identify which entries were removed and by which service account. If unauthorized, immediately re-add the missing entries and rotate the Redis write credentials.",
        "escalate_to": "Security team + risk pod lead immediately if unauthorized shrinkage is confirmed."
      }
    ],
    "manual_overrides": [
      {
        "name": "force_pass",
        "how": "set config.force_verdict = pass for 60s",
        "when": "NEVER \u2014 BlacklistKeeper is fail-closed and protects against hostile markets. Force-pass requires written approval from the risk pod lead and must be time-limited to <= 60s."
      },
      {
        "name": "add_to_banned_markets",
        "how": "polytraders risk ban-market --condition-id <hex> --reason <text>",
        "when": "When a market is identified as structurally hostile (e.g. ambiguous rules detected but not yet in keyword list, or dispute history discovered post-listing)."
      },
      {
        "name": "remove_from_banned_markets",
        "how": "polytraders risk unban-market --condition-id <hex> --reason <text>",
        "when": "When a false-positive ban is confirmed by the risk team after review. Requires two-person approval."
      }
    ],
    "healthcheck": "GET /internal/health/blacklistkeeper \u2192 200 if Redis registry SETs are reachable, snapshot age < 30s, and Gamma metadata cache is populated for all recently seen market_ids; red if Redis is unreachable, registry snapshot age > 60s, Gamma metadata fetch failing for > 60s, or p99 evaluation latency > 100ms."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "All unit tests pass including fail-closed and registry scenarios",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "Registry integration test: ban a test market, confirm rejection within 30s",
        "how_measured": "Integration test suite",
        "threshold": "Pass"
      },
      {
        "gate": "Fail-closed injection test: Redis unavailable \u2192 HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE)",
        "how_measured": "Failure injection test suite",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Shadow reject rate matches expected baseline within 5% over 48h",
        "how_measured": "Grafana shadow vs live comparison dashboard",
        "threshold": "< 5% divergence"
      },
      {
        "gate": "p99 evaluation latency < 30ms (cache-backed)",
        "how_measured": "polytraders_risk_blacklistkeeper_eval_latency_ms histogram",
        "threshold": "p99 < 30ms"
      },
      {
        "gate": "Zero false-positive bans confirmed over 48h shadow run",
        "how_measured": "Manual review of BLACKLIST_KEEPER_MARKET_BANNED rejections by risk team",
        "threshold": "0 confirmed false positives"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero BLACKLIST_KEEPER_DATA_UNAVAILABLE rejections during normal operating hours over 7 days",
        "how_measured": "BlacklistKeeperDataSourceError alert history",
        "threshold": "0 firings"
      },
      {
        "gate": "Registry shrinkage alert has never fired in staging or limited-live",
        "how_measured": "BlacklistKeeperRegistryShrinkage alert history",
        "threshold": "0 firings"
      },
      {
        "gate": "Risk team sign-off on ambiguity keyword list completeness",
        "how_measured": "Written approval from risk pod lead",
        "threshold": "Approved"
      },
      {
        "gate": "Counterparty ban registry integration test confirmed in staging",
        "how_measured": "Integration test suite \u2014 banned counterparty test",
        "threshold": "Pass"
      }
    ]
  },
  "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"
  }
}