{
  "schema_version": "2.0.0",
  "bot_id": "4.4",
  "bot_name": "CrossMarketGraph",
  "slug": "crossmarketgraph",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "beta",
  "readiness": "Limited live",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal Service",
    "authority": "Read-only",
    "runs_before": "Strategy layer, SumToOneArb, LiquidityGuard",
    "runs_after": "Gamma API market list loaded; MarketScanner candidates available",
    "applies_to": "All live Polymarket markets with active conditions",
    "default_mode": "limited_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Intelligence pod"
  },
  "purpose": "CrossMarketGraph builds and maintains a directed graph of semantically equivalent and logically linked Polymarket markets by embedding market titles and rules text, applying cosine-similarity clustering, and supplementing with manual override pairs. Each node is a condition_id; each edge carries a relation_type (SAME_EVENT, COMPLEMENTARY, NEG_RISK_SIBLING, SUPERSEDES) and a confidence score. The graph is the foundation for cross-market correlation signals used by SumToOneArb and liquidity-aware strategies to detect near-duplicate markets and hedge opportunities. CrossMarketGraph is strictly read-only \u2014 it never submits or signs orders.",
  "why_it_matters": [
    {
      "failure": "Near-duplicate markets not detected",
      "consequence": "SumToOneArb cannot identify arbitrage opportunities where two markets resolve the same event; edge goes unexploited and probability discrepancies persist."
    },
    {
      "failure": "Stale graph used during rapid market creation spree",
      "consequence": "New markets linked to a live event are not added to the graph in time; strategy enters both sides of a newly duplicated market, creating an unintended hedge."
    },
    {
      "failure": "Neg-risk siblings not identified",
      "consequence": "Strategy holds positions on multiple outcomes of the same neg-risk event without knowing they are structurally linked, over-exposing the book to a single resolution."
    },
    {
      "failure": "Incorrect SUPERSEDES edge causes stale-market entry",
      "consequence": "A superseded market is incorrectly treated as live; strategy generates an intent for a market that has already been replaced by an updated version."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market titles, rules text, condition IDs, neg_risk flag, enable_neg_risk flag",
      "source": "Gamma API",
      "required": true,
      "use": "Primary input for embedding and metadata-based graph construction."
    },
    {
      "input": "Market active/closed/resolved status",
      "source": "Gamma API",
      "required": true,
      "use": "Filter out closed or resolved markets from graph nodes; mark SUPERSEDES edges for replaced markets."
    },
    {
      "input": "24-hour trading volume and book depth per market",
      "source": "data_api / clob_public",
      "required": false,
      "use": "Annotate graph nodes with liquidity metadata for downstream strategy weighting."
    }
  ],
  "internal_inputs": [
    {
      "input": "Manual pair override list",
      "source": "config / operator overrides",
      "required": false,
      "use": "Inject known-linked pairs with forced confidence=1.0 regardless of embedding similarity."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Continue computing graph updates but suppress ObservationReport emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "cluster_threshold \u00b7 0\u20131",
    "manual_pair_overrides \u00b7 list",
    "logical_relation_types \u00b7 enum"
  ],
  "parameters": [
    {
      "name": "cluster_threshold",
      "default": 0.85,
      "warning": 0.75,
      "hard": 0.65,
      "controls": "Minimum cosine similarity for two market embeddings to be linked with a SAME_EVENT or COMPLEMENTARY edge.",
      "why_default_matters": "0.85 captures near-identical phrasings while rejecting loosely related markets; lower thresholds produce noisy edges that confuse downstream arbitrage detection.",
      "threshold_logic": [
        {
          "condition": "threshold \u2265 0.85",
          "action": "Normal \u2014 high-precision edges"
        },
        {
          "condition": "0.75\u20130.85",
          "action": "WARN \u2014 increased false-positive edges; review graph density"
        },
        {
          "condition": "< 0.65",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.cluster_threshold < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Markets are only linked when their descriptions are highly similar, ensuring only genuine duplicates and complements are grouped."
    },
    {
      "name": "rebuild_interval_s",
      "default": 300,
      "warning": 60,
      "hard": 30,
      "controls": "How often in seconds the full graph is rebuilt from the Gamma API market list.",
      "why_default_matters": "5-minute rebuilds keep the graph fresh for new markets without hammering the Gamma API embedding pipeline.",
      "threshold_logic": [
        {
          "condition": "interval \u2265 300 s",
          "action": "Normal"
        },
        {
          "condition": "60\u2013300 s",
          "action": "WARN \u2014 increased API load"
        },
        {
          "condition": "< 30 s",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.rebuild_interval_s < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "The market relationship map is refreshed regularly to capture newly launched markets."
    },
    {
      "name": "max_edges_per_node",
      "default": 20,
      "warning": 50,
      "hard": 100,
      "controls": "Maximum number of outbound edges per condition_id node. Prevents graph explosion in large event clusters.",
      "why_default_matters": "20 edges per node covers all realistic linked-market scenarios while bounding memory and downstream traversal cost.",
      "threshold_logic": [
        {
          "condition": "edges \u2264 20",
          "action": "Normal"
        },
        {
          "condition": "20\u201350",
          "action": "WARN \u2014 dense cluster; verify no false-positive edges"
        },
        {
          "condition": "> 100",
          "action": "Hard cap enforced \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.max_edges_per_node > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Each market is linked to at most a fixed number of related markets to keep analysis tractable."
    }
  ],
  "default_config": {
    "bot_id": "intel.crossmarketgraph",
    "version": "2.1.0",
    "mode": "limited_live",
    "defaults": {
      "cluster_threshold": 0.85,
      "rebuild_interval_s": 300,
      "max_edges_per_node": 20,
      "logical_relation_types": [
        "SAME_EVENT",
        "COMPLEMENTARY",
        "NEG_RISK_SIBLING",
        "SUPERSEDES"
      ]
    },
    "locked": {
      "cluster_threshold": {
        "min": 0.65
      },
      "rebuild_interval_s": {
        "min": 30
      },
      "max_edges_per_node": {
        "max": 100
      }
    }
  },
  "implementation_flow": [
    "On each rebuild cycle (rebuild_interval_s), FETCH full live market list from Gamma API including title, rules_text, condition_id, neg_risk, enable_neg_risk, active, closed.",
    "For each market, compute embedding vector via local sentence-embedding model over title + ' ' + rules_text[:512].",
    "Apply manual_pair_overrides first: inject edges with confidence=1.0 and the specified relation_type.",
    "For each pair of active markets, compute cosine_similarity(embed_a, embed_b).",
    "If cosine_similarity >= cluster_threshold: add directed edge (condition_id_a -> condition_id_b) with relation_type inferred by heuristic: NEG_RISK_SIBLING if both neg_risk=true and same parent; SUPERSEDES if one is closed and one is the replacement; COMPLEMENTARY if titles match a complement pattern (e.g. 'Yes/No' mirror); SAME_EVENT otherwise.",
    "Cap outbound edges per node at max_edges_per_node, keeping highest-confidence edges.",
    "Check KillSwitch. If active, update graph in memory but suppress ObservationReport emissions.",
    "For each new or changed edge (compared to previous graph snapshot), emit an ObservationReport with: report_id, trace_id, condition_id_a, condition_id_b, relation_type, confidence, edge_direction, graph_version.",
    "Log per-rebuild summary: nodes_total, edges_total, new_edges, dropped_edges, clusters_found, rebuild_latency_ms."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 CrossMarketGraph is read-only; it never approves or submits orders.",
    "reshape_required": "Not applicable.",
    "reject": "ObservationReport emissions are suppressed only when KillSwitch is active (KILL_SWITCH_ACTIVE). Edges below cluster_threshold are silently excluded.",
    "warning_only": "CROSSMARKETGRAPH_DENSE_CLUSTER is included as a warning on ObservationReports when a node accumulates > max_edges_per_node * 0.8 edges, signalling a potential event-cluster explosion."
  },
  "decision_output_schema": "ObservationReport",
  "decision_output_example": {
    "report_id": "rep_cmg_0xabc1_1746702000000",
    "trace_id": "trc_0xcafe0a0b0c0d0e0f",
    "bot_id": "intel.crossmarketgraph",
    "kind": "ObservationReport",
    "condition_id_a": "0xabc1230000000000000000000000000000000000000000000000000000000000",
    "condition_id_b": "0xdef4560000000000000000000000000000000000000000000000000000000000",
    "relation_type": "SAME_EVENT",
    "confidence": 0.92,
    "edge_direction": "bidirectional",
    "graph_version": "cmg_v20260428_301",
    "warnings": [],
    "emitted_at_ms": 1746702000042
  },
  "developer_log": {
    "bot_id": "intel.crossmarketgraph",
    "rebuild_cycle": 301,
    "rebuild_latency_ms": 4820,
    "nodes_total": 318,
    "edges_total": 1104,
    "new_edges": 7,
    "dropped_edges": 2,
    "clusters_found": 38,
    "manual_overrides_applied": 5,
    "killswitch_active": false
  },
  "user_explanations": [
    {
      "situation": "Two similar-looking markets shown as linked",
      "message": "These two markets resolve the same underlying event. The system tracks this relationship to avoid taking conflicting positions on what is effectively the same question."
    },
    {
      "situation": "Market flagged as a neg-risk sibling",
      "message": "This market is one outcome of a multi-outcome event. The system tracks all outcomes together to manage combined exposure across the whole event."
    },
    {
      "situation": "Market flagged as superseded",
      "message": "A newer version of this market is available. The system will prefer the replacement market for new activity."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Embedding model unavailable causes a full rebuild to fail, leaving the graph frozen at the last successful snapshot. Downstream strategies continue using the stale graph; new markets created since the last rebuild are invisible.",
    "false_positive_risk": "Two markets with similar titles but different resolution events (e.g. two different elections with similar question phrasing) incorrectly linked as SAME_EVENT, causing SumToOneArb to attempt cross-market arb on unrelated probabilities.",
    "false_negative_risk": "Markets with very different titles but identical resolution (e.g. paraphrased questions from different operators) score below cluster_threshold and are not linked, missing a valid arbitrage pair.",
    "safe_fallback": "If Gamma API is unavailable, retain last-known graph and emit STALE_DATA WARN. Do not emit ObservationReports based on graph older than 2\u00d7 rebuild_interval_s.",
    "required_dependencies": [
      "Gamma API live market list",
      "Local sentence-embedding model",
      "KillSwitch active flag readable"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Two markets with cosine_similarity=0.92 linked as SAME_EVENT",
        "setup": "Mock two market embeddings with similarity=0.92, cluster_threshold=0.85",
        "expected": "ObservationReport emitted with relation_type=SAME_EVENT, confidence=0.92"
      },
      {
        "test": "Market pair below threshold not linked",
        "setup": "Mock cosine_similarity=0.60, cluster_threshold=0.85",
        "expected": "No edge created; no ObservationReport emitted"
      },
      {
        "test": "Manual override injected with confidence=1.0",
        "setup": "manual_pair_overrides = [{condition_id_a, condition_id_b, relation_type=COMPLEMENTARY}]",
        "expected": "Edge created with confidence=1.0 regardless of embedding similarity"
      },
      {
        "test": "max_edges_per_node cap enforced",
        "setup": "Node with 25 candidate edges, max_edges_per_node=20",
        "expected": "Only top-20 highest-confidence edges retained; excess edges dropped"
      },
      {
        "test": "KillSwitch suppresses ObservationReport emissions",
        "setup": "killswitch.active=true; new edge detected",
        "expected": "Graph updated in memory; no ObservationReport emitted; KILL_SWITCH_ACTIVE logged"
      },
      {
        "test": "NEG_RISK_SIBLING correctly assigned",
        "setup": "Two markets with neg_risk=true, same parent condition, similarity=0.91",
        "expected": "ObservationReport with relation_type=NEG_RISK_SIBLING"
      }
    ],
    "integration": [
      {
        "test": "Full rebuild cycle generates correct graph from live Gamma API",
        "expected": "Graph node count matches live market count; known linked pairs present as edges"
      },
      {
        "test": "New market added to Gamma API appears as graph node on next rebuild cycle",
        "expected": "ObservationReport emitted for any new edges involving the new condition_id"
      },
      {
        "test": "Gamma API outage leaves graph frozen with STALE_DATA warning",
        "expected": "No new ObservationReports emitted; STALE_DATA WARN logged; graph_version unchanged"
      }
    ],
    "property": [
      {
        "property": "CrossMarketGraph never submits, signs, or modifies any order",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when KillSwitch is active",
        "required": "Always true"
      },
      {
        "property": "All edge confidence values are in [0.0, 1.0]",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Group semantically equivalent or logically linked markets.",
  "legacy_pm_signals": [
    "Market title + rules text from Gamma API",
    "Condition-ID metadata"
  ],
  "legacy_external_feeds": [
    "Sentence-embedding model (local)",
    "Operator manual-override pairs"
  ],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma_api",
    "data_api",
    "clob_public",
    "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 \u2014 pUSD denomination and enableNegRisk flag added to Gamma API",
      "action_taken": "Updated Gamma API queries to include enableNegRisk field for NEG_RISK_SIBLING edge classification. Graph node liquidity annotations updated from USDC.e to pUSD denomination. No feeRateBps or signed-order plumbing in this bot."
    }
  ],
  "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": "CrossMarketGraph reads Gamma API neg_risk and enable_neg_risk fields to classify NEG_RISK_SIBLING edges; liquidity annotations on graph nodes use pUSD denomination from data_api and clob_public V2 responses."
  },
  "reference_implementation": {
    "summary": "On each rebuild cycle, fetches all live markets from Gamma API, computes sentence embeddings, applies cosine-similarity clustering above cluster_threshold, injects manual overrides, caps edges per node, and emits ObservationReports for new or changed edges.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output.",
    "pseudocode": "// --- Initialisation ---\nembed_model = load_local_embedding_model()\nlast_graph   = {}        // condition_id -> set of (neighbor_id, relation_type, confidence)\ngraph_version_counter = 0\n\nFUNCTION rebuildCycle():\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n\n  // --- 1. Fetch market list ---\n  markets = FETCH gamma_api.GET('/markets?active=true&closed=false')\n  IF markets IS NULL:\n    LOG WARN 'STALE_DATA \u2014 Gamma API unavailable; graph frozen'\n    RETURN\n\n  // --- 2. Embed all markets ---\n  embeddings = {}\n  FOR market IN markets:\n    text = market.question + ' ' + market.rules_text[:512]\n    embeddings[market.condition_id] = embed_model.encode(text)\n\n  // --- 3. Inject manual overrides ---\n  new_graph = {}\n  FOR override IN params.manual_pair_overrides:\n    addEdge(new_graph, override.condition_id_a, override.condition_id_b,\n            override.relation_type, confidence=1.0)\n\n  // --- 4. Cosine similarity clustering ---\n  cids = LIST(embeddings.keys())\n  FOR i IN range(len(cids)):\n    FOR j IN range(i+1, len(cids)):\n      sim = cosine_similarity(embeddings[cids[i]], embeddings[cids[j]])\n      IF sim < params.cluster_threshold:\n        CONTINUE\n      rel = inferRelationType(markets[cids[i]], markets[cids[j]], sim)\n      addEdge(new_graph, cids[i], cids[j], rel, confidence=sim)\n      addEdge(new_graph, cids[j], cids[i], rel, confidence=sim)\n\n  // --- 5. Cap edges per node ---\n  FOR node IN new_graph:\n    IF len(new_graph[node]) > params.max_edges_per_node:\n      new_graph[node] = topN(new_graph[node], params.max_edges_per_node)\n      LOG WARN 'CROSSMARKETGRAPH_DENSE_CLUSTER node=' + node\n\n  graph_version_counter += 1\n  graph_version = 'cmg_v' + today() + '_' + graph_version_counter\n\n  // --- 6. Diff against last graph ---\n  new_edges     = diff_new(new_graph, last_graph)\n  dropped_edges = diff_dropped(last_graph, new_graph)\n\n  // --- 7. KillSwitch suppress ---\n  IF ks.active:\n    LOG INFO 'KILL_SWITCH_ACTIVE \u2014 suppressing ObservationReports'\n    last_graph = new_graph\n    RETURN\n\n  // --- 8. Emit for new/changed edges ---\n  FOR (cid_a, cid_b, rel, conf) IN new_edges:\n    EMIT ObservationReport {\n      report_id:     'rep_cmg_' + cid_a[:6] + '_' + now_ms(),\n      trace_id:      new_trace_id(),\n      bot_id:        'intel.crossmarketgraph',\n      kind:          'ObservationReport',\n      condition_id_a: cid_a,\n      condition_id_b: cid_b,\n      relation_type:  rel,\n      confidence:     conf,\n      edge_direction: 'bidirectional',\n      graph_version:  graph_version,\n      warnings:       [],\n      emitted_at_ms:  now_ms()\n    }\n\n  last_graph = new_graph\n\nFUNCTION inferRelationType(m_a, m_b, sim):\n  IF m_a.neg_risk AND m_b.neg_risk AND sameParent(m_a, m_b):\n    RETURN 'NEG_RISK_SIBLING'\n  IF m_a.closed AND NOT m_b.closed AND sim > 0.95:\n    RETURN 'SUPERSEDES'\n  IF isComplementPair(m_a.question, m_b.question):\n    RETURN 'COMPLEMENTARY'\n  RETURN 'SAME_EVENT'",
    "sdk_calls": [
      "gamma_api.GET('/markets?active=true&closed=false')",
      "embed_model.encode(text)",
      "cosine_similarity(vec_a, vec_b)"
    ],
    "complexity": "O(M\u00b2) per rebuild cycle where M = live market count; practical with M \u2264 500 at 5-min cadence"
  },
  "wire_examples": {
    "input": {
      "label": "Gamma API market pair (candidate for SAME_EVENT edge)",
      "source": "gamma_api",
      "payload": [
        {
          "condition_id": "0xabc1230000000000000000000000000000000000000000000000000000000000",
          "question": "Will Candidate A win the 2026 election?",
          "active": true,
          "neg_risk": false,
          "enable_neg_risk": false
        },
        {
          "condition_id": "0xdef4560000000000000000000000000000000000000000000000000000000000",
          "question": "Will Candidate A win the 2026 general election?",
          "active": true,
          "neg_risk": false,
          "enable_neg_risk": false
        }
      ]
    },
    "output": {
      "label": "ObservationReport \u2014 SAME_EVENT edge detected",
      "payload": {
        "report_id": "rep_cmg_0xabc1_1746702000000",
        "trace_id": "trc_0xcafe0a0b0c0d0e0f",
        "bot_id": "intel.crossmarketgraph",
        "kind": "ObservationReport",
        "condition_id_a": "0xabc1230000000000000000000000000000000000000000000000000000000000",
        "condition_id_b": "0xdef4560000000000000000000000000000000000000000000000000000000000",
        "relation_type": "SAME_EVENT",
        "confidence": 0.92,
        "edge_direction": "bidirectional",
        "graph_version": "cmg_v20260428_301",
        "warnings": [],
        "emitted_at_ms": 1746702000042
      }
    }
  },
  "reason_codes": [
    {
      "code": "CROSSMARKETGRAPH_SAME_EVENT_EDGE",
      "severity": "INFO",
      "meaning": "Two markets linked as SAME_EVENT: titles or rules text highly similar (confidence >= cluster_threshold).",
      "action": "Emit ObservationReport; SumToOneArb and strategies consume edge for arb detection.",
      "user_message": ""
    },
    {
      "code": "CROSSMARKETGRAPH_NEG_RISK_SIBLING",
      "severity": "INFO",
      "meaning": "Two neg-risk markets share a parent condition; linked as NEG_RISK_SIBLING.",
      "action": "Emit ObservationReport; strategies aggregate combined neg-risk exposure across siblings.",
      "user_message": "These markets are outcomes of the same multi-result event."
    },
    {
      "code": "CROSSMARKETGRAPH_DENSE_CLUSTER",
      "severity": "WARN",
      "meaning": "A node has > 80% of max_edges_per_node edges; potential event-cluster explosion.",
      "action": "Include in ObservationReport warnings; alert Intelligence pod lead for review.",
      "user_message": ""
    },
    {
      "code": "CROSSMARKETGRAPH_SUPERSEDES_EDGE",
      "severity": "WARN",
      "meaning": "A closed market is superseded by a new active market (similarity > 0.95).",
      "action": "Emit ObservationReport with relation_type=SUPERSEDES; downstream strategies prefer the new market.",
      "user_message": "A newer version of this market is available."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "Gamma API unavailable for > 2\u00d7 rebuild_interval_s; graph is frozen at last snapshot.",
      "action": "Halt ObservationReport emissions; log STALE_DATA; alert on-call.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; ObservationReport emissions suppressed.",
      "action": "Continue computing graph updates but suppress all emissions.",
      "user_message": "Market relationship updates are paused while trading is suspended."
    },
    {
      "code": "PARAMETER_CHANGE_REQUIRES_APPROVAL",
      "severity": "HARD_REJECT",
      "meaning": "A parameter change violates a locked bound (e.g. cluster_threshold < 0.65 or rebuild_interval_s < 30).",
      "action": "Reject config change; do not apply.",
      "user_message": ""
    },
    {
      "code": "CROSSMARKETGRAPH_EMBED_FAILURE",
      "severity": "WARN",
      "meaning": "Sentence-embedding model failed to encode one or more markets; those markets excluded from this rebuild.",
      "action": "Log with condition_id; skip affected markets; retry on next cycle.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_crossmarketgraph_nodes_total",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Total condition_id nodes in the current graph snapshot."
      },
      {
        "name": "polytraders_intel_crossmarketgraph_edges_total",
        "type": "gauge",
        "unit": "count",
        "labels": [
          "relation_type"
        ],
        "meaning": "Total directed edges in the graph, broken down by relation_type."
      },
      {
        "name": "polytraders_intel_crossmarketgraph_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "relation_type"
        ],
        "meaning": "ObservationReports emitted for new or changed edges per rebuild cycle."
      },
      {
        "name": "polytraders_intel_crossmarketgraph_rebuild_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Wall-clock latency of a full graph rebuild cycle."
      },
      {
        "name": "polytraders_intel_crossmarketgraph_dense_clusters_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Number of nodes that hit max_edges_per_node cap in rebuild cycles."
      },
      {
        "name": "polytraders_intel_crossmarketgraph_embed_failures_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Embedding failures causing markets to be skipped in a rebuild cycle."
      }
    ],
    "alerts": [
      {
        "name": "CrossMarketGraphStaleGraph",
        "condition": "time_since_last_successful_rebuild > 2 * rebuild_interval_s",
        "severity": "page",
        "runbook": "#runbook-crossmarketgraph-stale-graph"
      },
      {
        "name": "CrossMarketGraphDenseClusterSpike",
        "condition": "rate(polytraders_intel_crossmarketgraph_dense_clusters_total[10m]) > 5",
        "severity": "warn",
        "runbook": "#runbook-crossmarketgraph-dense-cluster"
      },
      {
        "name": "CrossMarketGraphRebuildLatencyHigh",
        "condition": "histogram_quantile(0.99, rate(polytraders_intel_crossmarketgraph_rebuild_latency_ms_bucket[10m])) > 30000",
        "severity": "warn",
        "runbook": "#runbook-crossmarketgraph-latency"
      },
      {
        "name": "CrossMarketGraphEmbedFailures",
        "condition": "rate(polytraders_intel_crossmarketgraph_embed_failures_total[10m]) > 10",
        "severity": "warn",
        "runbook": "#runbook-crossmarketgraph-embed-failures"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / CrossMarketGraph node and edge counts",
      "Grafana \u2014 Intelligence / graph rebuild latency and cluster density"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "in-memory",
    "shape": "graph dict: condition_id -> list of (neighbor_id, relation_type, confidence). Last-snapshot graph retained for diff computation. Manual overrides loaded from config at startup.",
    "ttl": "Refreshed every rebuild_interval_s (300 s default). No durable persistence between restarts.",
    "recovery": "On cold start, graph is empty. First rebuild cycle populates it. Until first rebuild completes, no ObservationReports are emitted.",
    "size_estimate": "~500 bytes per node; ~160 KB for 318 nodes with avg 20 edges each"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 1,
    "idempotency_key": "graph_version",
    "timeout_ms": 30000,
    "backpressure": "drop-after-buffer \u2014 if rebuild takes longer than rebuild_interval_s, the next scheduled rebuild is skipped until current one completes",
    "locking": "none \u2014 single rebuild loop; reads and emits are serialised"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate suppresses ObservationReport emissions."
      }
    ],
    "emits_to": [
      {
        "bot_id": "strat.sum_to_one_arb",
        "what": "ObservationReport with SAME_EVENT and COMPLEMENTARY edges for cross-market arbitrage detection"
      },
      {
        "bot_id": "strat.liquidity_aware_strategies",
        "what": "ObservationReport with NEG_RISK_SIBLING and SUPERSEDES edges for combined exposure management"
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500 ms p99",
        "fallback": "Retain last-known graph; emit STALE_DATA WARN; retry on next rebuild cycle"
      },
      {
        "service": "Local sentence-embedding model",
        "endpoint": "local process",
        "sla": "process-availability",
        "fallback": "Emit CROSSMARKETGRAPH_EMBED_FAILURE for affected markets; skip those markets in current rebuild"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Gamma API returning adversarially crafted market titles to manipulate cosine-similarity scores and inject false SAME_EVENT edges",
      "Manual override list poisoned with a cross-market link that causes downstream arb strategy to take losses"
    ],
    "mitigations": [
      "Embeddings computed on truncated text (512 chars); extreme outliers in similarity space are detectable as anomalies",
      "Manual overrides require operator-level config change with audit trail",
      "All ObservationReports are informational only \u2014 downstream strategies and risk bots independently validate market state before acting"
    ],
    "contract_calls": []
  },
  "failure_injection": [
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block TCP to gamma-api.polymarket.com for 700 s (> 2\u00d7 rebuild_interval_s=300)",
      "expected_behaviour": "Graph frozen at last snapshot; STALE_DATA WARN logged; CrossMarketGraphStaleGraph alert fires; no ObservationReports emitted",
      "recovery": "Automatic on next successful Gamma API response; full rebuild triggered immediately"
    },
    {
      "scenario": "EMBED_MODEL_FAILURE",
      "how_to_inject": "Kill local embedding model process mid-rebuild",
      "expected_behaviour": "CROSSMARKETGRAPH_EMBED_FAILURE logged for affected markets; partial graph built from successful embeds; CrossMarketGraphEmbedFailures alert fires if > 10/min",
      "recovery": "Embedding model restarted automatically; next rebuild cycle processes full market list"
    },
    {
      "scenario": "DENSE_CLUSTER_EXPLOSION",
      "how_to_inject": "Inject 150 mock markets with titles starting 'Will X win...' creating a dense cluster",
      "expected_behaviour": "max_edges_per_node cap applied; CROSSMARKETGRAPH_DENSE_CLUSTER WARN on affected nodes; CrossMarketGraphDenseClusterSpike alert fires",
      "recovery": "Automatic; graph stabilises once cluster is capped"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true; trigger a rebuild cycle with new edges",
      "expected_behaviour": "Graph updated in memory; no ObservationReports emitted; KILL_SWITCH_ACTIVE logged",
      "recovery": "Emissions resume on first rebuild after KillSwitch reset; all new edges from suppressed cycles emitted"
    },
    {
      "scenario": "LOW_THRESHOLD_CONFIG_REJECTED",
      "how_to_inject": "Attempt to set cluster_threshold=0.50",
      "expected_behaviour": "PARAMETER_CHANGE_REQUIRES_APPROVAL raised; config change rejected; threshold unchanged at 0.85",
      "recovery": "Operator must submit approved config change through governance workflow"
    }
  ],
  "runbook": {
    "summary": "CrossMarketGraph incidents are usually Gamma API outages or embedding model failures causing the graph to freeze. Since the graph is read-only infrastructure, stale graphs affect only new edge discovery \u2014 existing positions are unaffected.",
    "oncall_actions": [
      {
        "alert": "CrossMarketGraphStaleGraph",
        "first_action": "Check Gamma API reachability (curl https://gamma-api.polymarket.com/markets?limit=1). Verify embedding model process is running.",
        "escalate_to": "Intelligence pod lead if both API and model are healthy but rebuild still failing"
      },
      {
        "alert": "CrossMarketGraphDenseClusterSpike",
        "first_action": "Identify the dense condition_id(s) from dense_clusters_total metric. Inspect market titles for a new event cluster. Verify edges are legitimate.",
        "escalate_to": "Intelligence pod lead if cluster size > 50 edges on a single node"
      },
      {
        "alert": "CrossMarketGraphRebuildLatencyHigh",
        "first_action": "Check rebuild_latency_ms p99. Large M\u00b2 computation may indicate market count spike. Increase rebuild_interval_s temporarily if needed.",
        "escalate_to": "Infra on-call if latency > 60 s"
      },
      {
        "alert": "CrossMarketGraphEmbedFailures",
        "first_action": "Check embed_failures_total logs for condition_ids. Verify embedding model process health.",
        "escalate_to": "Intelligence pod lead if > 10% of markets failing to embed"
      }
    ],
    "manual_overrides": [
      {
        "name": "add_manual_pair",
        "how": "Add entry to config.manual_pair_overrides with condition_id_a, condition_id_b, relation_type; next rebuild cycle applies with confidence=1.0",
        "when": "Known-linked markets scoring below cluster_threshold due to phrasing differences"
      }
    ],
    "healthcheck": "/internal/health/crossmarketgraph \u2192 200 Last rebuild completed within 2\u00d7 rebuild_interval_s AND nodes_total > 0 AND no STALE_DATA in last cycle; red if No successful rebuild in > 2\u00d7 rebuild_interval_s OR embed_failures_total rate > 10/min OR Gamma API unreachable"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for SAME_EVENT, NEG_RISK_SIBLING, SUPERSEDES classifications and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "Gamma API integration test: market list fetch and embedding pipeline completes within 30 s for 300 markets",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Graph rebuild p99 latency < 10 s over 48 h at live market count",
        "how_measured": "polytraders_intel_crossmarketgraph_rebuild_latency_ms histogram",
        "threshold": "p99 < 10000 ms"
      },
      {
        "gate": "Known linked pairs (from manual test set) all present in graph with correct relation_type",
        "how_measured": "Post-rebuild graph inspection script",
        "threshold": "100% recall on test set"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero graph-freeze incidents over 14 days",
        "how_measured": "CrossMarketGraphStaleGraph alert history",
        "threshold": "0 firings"
      },
      {
        "gate": "False-positive edge rate < 5% as assessed by manual review of sampled edges",
        "how_measured": "Manual weekly review of 50 sampled edges",
        "threshold": "< 5% false positives"
      },
      {
        "gate": "KillSwitch suppression: zero ObservationReports when KillSwitch active",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ]
  },
  "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": "sample-1/10 for unchanged edges re-confirmed on rebuild; emit-every for new or changed edges",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "no",
    "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"
  }
}