{
  "schema_version": "1.0.0",
  "bot_id": "4.11",
  "bot_name": "MarketOntologyBuilder",
  "slug": "marketontologybuilder",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal Service",
    "authority": "Read-only",
    "runs_before": "",
    "runs_after": "",
    "applies_to": "",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Cluster markets into events, themes, and underlying entities for cross-market reasoning.",
  "why_it_matters": [
    {
      "failure": "Cross-market correlation invisible to Risk",
      "consequence": "Without an ontology, PortfolioGuard sees fifty independent markets where the underlying ontology says they all resolve on the same NFL game. Aggregate exposure to a single real-world event is then dramatically under-counted.",
      "worked_example": {
        "setup": "An NFL game has 47 listed markets: moneyline, spread, total points, first scorer, three-way, plus 42 props. Strategies enter independently on 9 of them, total notional 18,000 pUSD.",
        "without_bot": "PortfolioGuard sees nine separate small positions, each within its 5,000 pUSD per-market cap. None breaches the 10,000 pUSD per-event cap because there is no concept of 'event'.",
        "with_bot": "MarketOntologyBuilder maps all 47 markets to event_id=`nfl-2026-w12-buf-mia`. PortfolioGuard sums the 9 positions to 18,000 pUSD against that event, breaches the cap, and rejects the next entry on any of the 47."
      }
    },
    {
      "failure": "Strategies can't compose multi-market positions",
      "consequence": "Cross-market arbitrage and hedging strategies need a structured map of which markets share an underlying. Hand-curated lists drift; a generated ontology stays in sync as new markets list each day."
    },
    {
      "failure": "Search and discovery degrade as the catalogue grows",
      "consequence": "With ten thousand listed markets and no ontology, MarketScanner becomes a substring search. Themed events and entity-level queries \u2014 'all markets resolving on US election' \u2014 require structured clusters."
    },
    {
      "failure": "Reconciliation can't verify outcome correctness",
      "consequence": "When a real-world event fires, gov.reconciler needs to confirm every market that should have resolved did. Without an ontology mapping events to markets, that check is manual."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Full active market list with tags and categories from Gamma API",
      "source": "gamma",
      "required": true,
      "use": "Primary data source for building the market category ontology."
    },
    {
      "input": "Tag taxonomy from Data API",
      "source": "data",
      "required": false,
      "use": "Supplement Gamma tag data with canonical tag hierarchy."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Suppress all ontology build emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "cluster_min_similarity \u00b7 0\u20131",
    "max_cluster_size \u00b7 int",
    "republish_cron \u00b7 cron",
    "respect_curator_overrides \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "build_interval_s",
      "default": 3600,
      "warning": 7200,
      "hard": 86400,
      "controls": "Seconds between full ontology rebuild cycles.",
      "why_default_matters": "3600 s (1 h) captures new market listings and category changes without overloading Gamma API.",
      "threshold_logic": [
        {
          "condition": "interval <= 3600 s",
          "action": "Normal"
        },
        {
          "condition": "3600\u20137200 s",
          "action": "WARN \u2014 reduced freshness of ontology"
        },
        {
          "condition": "> 86400 s",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.build_interval_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Market category structure is rebuilt hourly to reflect new listings."
    },
    {
      "name": "min_market_count",
      "default": 50,
      "warning": 20,
      "hard": 5,
      "controls": "Minimum number of active markets returned by Gamma before ontology build proceeds.",
      "why_default_matters": "50 ensures the build is not based on a truncated Gamma response.",
      "threshold_logic": [
        {
          "condition": "count >= 50",
          "action": "Normal \u2014 proceed with build"
        },
        {
          "condition": "5\u201350",
          "action": "WARN \u2014 MARKETONTOLOGYBUILDER_TAG_COUNT_DROP; retain last ontology"
        },
        {
          "condition": "< 5",
          "action": "Reject \u2014 do not rebuild; emit STALE_DATA"
        }
      ],
      "dev_check": "if (markets.length < p.min_market_count.hard) emit('STALE_DATA');",
      "user_facing": "Ontology is only updated when a sufficient number of markets are available."
    }
  ],
  "default_config": {
    "bot_id": "intel.marketontologybuilder",
    "version": "0.1.0",
    "mode": "planned",
    "defaults": {
      "build_interval_s": 3600,
      "min_market_count": 50
    },
    "locked": {
      "build_interval_s": {
        "max": 86400
      },
      "min_market_count": {
        "min": 5
      }
    }
  },
  "implementation_flow": [],
  "decision_logic": {
    "approve": "",
    "reshape_required": "",
    "reject": "",
    "warning_only": ""
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "report_id": "rep_mob_1746703000000",
    "trace_id": "trc_0xbeef0102030405060712",
    "bot_id": "intel.marketontologybuilder",
    "kind": "ObservationReport",
    "ontology_hash": "0xdeadbeef12345678",
    "category_count": 18,
    "market_count": 312,
    "warnings": [],
    "emitted_at_ms": 1746703020000
  },
  "developer_log": {
    "bot_id": "intel.marketontologybuilder",
    "ontology_hash": "0xdeadbeef12345678",
    "category_count": 18,
    "market_count": 312,
    "build_duration_ms": 4200,
    "ontology_changed": true,
    "killswitch_active": false
  },
  "user_explanations": [
    {
      "situation": "Strategy used category filter to avoid low-quality market segments",
      "message": "The system maintains a live map of market categories to help strategies focus on well-structured markets. Category filters are updated hourly."
    },
    {
      "situation": "Ontology update emitted after new markets added",
      "message": "New markets were detected in a category during the last rebuild cycle. Category-aware strategies will see these markets in their next scan."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Gamma API outage for > 1 h causes MarketOntologyBuilder to serve a stale ontology, reducing category-aware strategy accuracy for markets that were newly listed during the outage.",
    "false_positive_risk": "Gamma API pagination bug returns a truncated market list, causing MARKETONTOLOGYBUILDER_TAG_COUNT_DROP to fire and retaining the previous (potentially stale) ontology.",
    "false_negative_risk": "New market category introduced during a build cycle is missed because the Gamma response is cached, causing the ontology to omit the new category until the next full rebuild.",
    "safe_fallback": "If market_count < min_market_count hard floor, retain last full ontology and emit STALE_DATA WARN. Do not replace a valid ontology with a partial one.",
    "required_dependencies": [
      "Polymarket Gamma API",
      "KillSwitch active flag",
      "Redis for ontology snapshot storage"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Full market list builds ontology and emits ObservationReport on hash change",
        "setup": "312 markets, 18 categories, ontology_hash differs from previous",
        "expected": "ObservationReport emitted with category_count=18, market_count=312"
      },
      {
        "test": "Truncated response triggers WARN and retains last ontology",
        "setup": "Gamma returns 3 markets (below min_market_count hard=5)",
        "expected": "MARKETONTOLOGYBUILDER_TAG_COUNT_DROP WARN; last ontology retained; no rebuild"
      },
      {
        "test": "KillSwitch suppresses emission",
        "setup": "killswitch.active=true; valid ontology built internally",
        "expected": "No ObservationReport; KILL_SWITCH_ACTIVE logged"
      }
    ],
    "integration": [
      {
        "test": "Ontology change detected and consumed by strategy",
        "expected": "Strategy receives updated ontology_hash and category_count after rebuild"
      },
      {
        "test": "Gamma API down: stale ontology retained; STALE_DATA emitted after 2 h",
        "expected": "STALE_DATA WARN after 2 h; last valid ontology snapshot still accessible"
      }
    ],
    "property": [
      {
        "property": "MarketOntologyBuilder never submits or signs orders",
        "required": "Always true"
      },
      {
        "property": "Ontology is never replaced with a build from fewer than min_market_count hard floor markets",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Cluster markets into events, themes, and underlying entities for cross-market reasoning.",
  "legacy_pm_signals": [
    "Market titles, tags, and resolution-rule entities",
    "Embedding-similarity graph across active markets",
    "Manual curator overrides and parent-child links"
  ],
  "legacy_external_feeds": [
    "Local embedding model"
  ],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "data",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q3-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Builds a structured ontology of Gamma market categories and relationships. Read-only. No order signing."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION buildOntology():\n  // 0. KillSwitch check\n  IF FETCH internal.killswitch.status == ACTIVE:\n    RETURN\n\n  // 1. Fetch all active markets from Gamma\n  markets = FETCH gamma.GET('/markets?status=active&limit=500')\n  IF len(markets) == 0:\n    EMIT WARN 'STALE_DATA'\n    RETURN\n\n  // 2. Cluster by tag/category\n  ontology = {}\n  FOR m IN markets:\n    FOR tag IN m.tags:\n      ontology[tag] = ontology.get(tag, []) + [m.condition_id]\n\n  // 3. Build relationship graph\n  graph = buildRelationshipGraph(ontology, markets)\n\n  // 4. Compute ontology_hash\n  new_hash = hash(ontology)\n  IF new_hash == last_ontology_hash:\n    RETURN  // no change\n\n  // 5. Emit ObservationReport\n  EMIT ObservationReport {\n    report_id: gen_id(),\n    kind: 'ObservationReport',\n    ontology_hash: new_hash,\n    category_count: len(ontology),\n    market_count: len(markets),\n    emitted_at_ms: now_ms()\n  }\n  last_ontology_hash = new_hash",
    "sdk_calls": [
      "gamma.GET('/markets?status=active&limit=500')",
      "data.GET('/tags')",
      "internal.killswitch.status"
    ],
    "complexity": "O(M*T) per build cycle where M = markets, T = average tags per market"
  },
  "wire_examples": {
    "input": {
      "label": "Active markets list from Gamma for ontology build",
      "source": "gamma",
      "payload": {
        "market_count": 312,
        "categories": [
          "politics",
          "crypto",
          "sports",
          "economics"
        ],
        "timestamp_ms": 1746703000000
      }
    },
    "output": {
      "label": "ObservationReport \u2014 ontology updated",
      "payload": {
        "report_id": "rep_mob_1746703000000",
        "trace_id": "trc_0xbeef0102030405060712",
        "bot_id": "intel.marketontologybuilder",
        "kind": "ObservationReport",
        "ontology_hash": "0xdeadbeef12345678",
        "category_count": 18,
        "market_count": 312,
        "emitted_at_ms": 1746703020000
      }
    }
  },
  "reason_codes": [
    {
      "code": "MARKETONTOLOGYBUILDER_ONTOLOGY_CHANGED",
      "severity": "INFO",
      "meaning": "Ontology hash changed since last build cycle; new markets or category changes detected.",
      "action": "Emit ObservationReport with updated ontology hash; downstream consumers reload ontology.",
      "user_message": "Market category structure has been updated."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "Gamma API returned empty or zero markets during build cycle.",
      "action": "Skip build; retain last ontology; retry on next poll.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; all ontology build emissions suppressed.",
      "action": "Continue internal builds but suppress ObservationReport emissions.",
      "user_message": "Ontology updates paused while trading is suspended system-wide."
    },
    {
      "code": "MARKETONTOLOGYBUILDER_TAG_COUNT_DROP",
      "severity": "WARN",
      "meaning": "Tag count decreased by more than expected between cycles, possibly due to Gamma API truncation.",
      "action": "Emit with tag_count_drop warning; do not replace last full ontology.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_marketontologybuilder_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "ObservationReports emitted (ontology change events)."
      },
      {
        "name": "polytraders_intel_marketontologybuilder_market_count",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of active markets indexed in the latest ontology build."
      },
      {
        "name": "polytraders_intel_marketontologybuilder_category_count",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of distinct categories in the latest ontology build."
      }
    ],
    "alerts": [
      {
        "name": "MarketOntologyBuilderStale",
        "condition": "rate(polytraders_intel_marketontologybuilder_observations_emitted_total[2h]) == 0",
        "severity": "warn",
        "runbook": "#runbook-marketontologybuilder-stale"
      },
      {
        "name": "MarketOntologyBuilderTagCountDrop",
        "condition": "delta(polytraders_intel_marketontologybuilder_category_count[10m]) < -5",
        "severity": "warn",
        "runbook": "#runbook-marketontologybuilder-tagcount-drop"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / MarketOntologyBuilder market and category count over time"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "redis",
    "shape": "last_ontology_hash, last_category_count, last_market_count, last_build_at_ms. Full ontology snapshot in Redis hash.",
    "ttl": "Ontology snapshot expires after 24 h; rebuilt on next cycle",
    "recovery": "On cold start, trigger full rebuild on first cycle; last snapshot may be stale.",
    "size_estimate": "~50 KB for full ontology snapshot across 500 active markets"
  },
  "concurrency": {
    "execution_model": "single-threaded periodic batch rebuild",
    "max_in_flight": 1,
    "idempotency_key": "ontology_hash",
    "timeout_ms": 30000,
    "backpressure": "skip cycle if previous build still in progress",
    "locking": "Redis SETNX on build_lock key to prevent concurrent rebuilds"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Suppress emissions when KillSwitch is active."
      }
    ],
    "emits_to": [
      {
        "bot_id": "strat.all_strategies",
        "what": "ObservationReport with ontology_hash for category-aware position sizing"
      }
    ],
    "sibling": [
      "intel.contradictiondetector"
    ],
    "external": [
      {
        "service": "Polymarket Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500 ms p99",
        "fallback": "Retain last ontology snapshot; emit STALE_DATA if no successful build for > 1 h"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Gamma API returns a truncated market list causing category count drop and false TAG_COUNT_DROP alerts",
      "Adversary adds misleading tags to markets to skew category ontology"
    ],
    "mitigations": [
      "TAG_COUNT_DROP guard prevents replacing good ontology with truncated data",
      "Ontology is informational only; strategies validate independently before acting on category signals"
    ]
  },
  "failure_injection": [
    {
      "scenario": "GAMMA_TRUNCATED_RESPONSE",
      "how_to_inject": "Return only 10 markets from mock Gamma API when 300+ expected",
      "expected_behaviour": "MARKETONTOLOGYBUILDER_TAG_COUNT_DROP WARN; last full ontology retained; no full replacement",
      "recovery": "Automatic on next successful full build"
    },
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block Gamma API for 2 h",
      "expected_behaviour": "STALE_DATA WARN; no new builds; MarketOntologyBuilderStale alert fires",
      "recovery": "Full rebuild triggered automatically on Gamma recovery"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true during ontology build cycle",
      "expected_behaviour": "Build completes internally; ObservationReport emission suppressed; KILL_SWITCH_ACTIVE logged",
      "recovery": "Emission resumes on KillSwitch reset"
    }
  ],
  "runbook": {
    "summary": "MarketOntologyBuilder incidents are typically Gamma API outages or truncated responses. Stale ontology does not block trading but may reduce category signal quality.",
    "oncall_actions": [
      {
        "alert": "MarketOntologyBuilderStale",
        "first_step": "Check Gamma API health and last_build_at_ms. Trigger manual rebuild via override if API is healthy.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if stale for > 2 h"
      },
      {
        "alert": "MarketOntologyBuilderTagCountDrop",
        "first_step": "Check Gamma API response count. If truncated, do not replace ontology. Investigate Gamma pagination issues.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if drop is > 10 categories"
      }
    ],
    "manual_overrides": [
      {
        "command": "force_rebuild",
        "effect": "POST /internal/marketontologybuilder/rebuild to trigger an immediate full rebuild \u2014 After Gamma API recovery or known category structure changes"
      }
    ],
    "healthcheck": "Endpoint: /internal/health/marketontologybuilder | Green: Last build < 2 h ago AND Redis reachable AND Gamma API returning 200 | Red: No successful build for > 2 h OR Redis unreachable"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for tag-count drop guard, ontology hash change detection, and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Full ontology build completes in < 30 s on staging with 500 active markets",
        "how_measured": "Integration test",
        "threshold": "Build time < 30 s"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero truncation events over 14-day soak with live Gamma API",
        "how_measured": "MarketOntologyBuilderTagCountDrop alert audit",
        "threshold": "0 truncation alerts"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "cadence": "every-event",
    "retention_class": "30d",
    "retention_notes": "Full fidelity for 30 d; rolled-up summary retained for 1 y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "summary-only",
    "consumes_kinds": []
  },
  "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"
  }
}