Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerIntelligence4.4 CrossMarketGraph

4.4 CrossMarketGraph

Intelligence Signal Service Read-only BETA Limited live capital · Indirect P2 · Data normalisation pending stub

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 — it never submits or signs orders.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerIntelligence  Intelligence
Bot classSignal Service
AuthorityRead-only
StatusBETA
ReadinessLimited live
Runs beforeStrategy layer, SumToOneArb, LiquidityGuard
Runs afterGamma API market list loaded; MarketScanner candidates available
Applies toAll live Polymarket markets with active conditions
Default modelimited_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Intelligence pod

2. 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 — it never submits or signs orders.

3. Why This Bot Matters

  • Near-duplicate markets not detected

    SumToOneArb cannot identify arbitrage opportunities where two markets resolve the same event; edge goes unexploited and probability discrepancies persist.

  • Stale graph used during rapid market creation spree

    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.

  • Neg-risk siblings not identified

    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.

  • Incorrect SUPERSEDES edge causes stale-market entry

    A superseded market is incorrectly treated as live; strategy generates an intent for a market that has already been replaced by an updated version.

No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.

4. Required Polymarket Inputs

InputSourceRequired?Use
Market titles, rules text, condition IDs, neg_risk flag, enable_neg_risk flagGamma APIYesPrimary input for embedding and metadata-based graph construction.
Market active/closed/resolved statusGamma APIYesFilter out closed or resolved markets from graph nodes; mark SUPERSEDES edges for replaced markets.
24-hour trading volume and book depth per marketdata_api / clob_publicNoAnnotate graph nodes with liquidity metadata for downstream strategy weighting.

5. Required Internal Inputs

InputSourceRequired?Use
Manual pair override listconfig / operator overridesNoInject known-linked pairs with forced confidence=1.0 regardless of embedding similarity.
KillSwitch active flagKillSwitchYesContinue computing graph updates but suppress ObservationReport emissions when KillSwitch is active.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
cluster_threshold0.850.750.65Minimum cosine similarity for two market embeddings to be linked with a SAME_EVENT or COMPLEMENTARY edge.
rebuild_interval_s3006030How often in seconds the full graph is rebuilt from the Gamma API market list.
max_edges_per_node2050100Maximum number of outbound edges per condition_id node. Prevents graph explosion in large event clusters.

7. Detailed Parameter Instructions

cluster_threshold

What it means

Minimum cosine similarity for two market embeddings to be linked with a SAME_EVENT or COMPLEMENTARY edge.

Default

{ "cluster_threshold": 0.85 }

Why this 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

ConditionAction
threshold ≥ 0.85Normal — high-precision edges
0.75–0.85WARN — increased false-positive edges; review graph density
< 0.65Reject — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.cluster_threshold < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

Markets are only linked when their descriptions are highly similar, ensuring only genuine duplicates and complements are grouped.

rebuild_interval_s

What it means

How often in seconds the full graph is rebuilt from the Gamma API market list.

Default

{ "rebuild_interval_s": 300 }

Why this default matters

5-minute rebuilds keep the graph fresh for new markets without hammering the Gamma API embedding pipeline.

Threshold logic

ConditionAction
interval ≥ 300 sNormal
60–300 sWARN — increased API load
< 30 sReject — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.rebuild_interval_s < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

The market relationship map is refreshed regularly to capture newly launched markets.

max_edges_per_node

What it means

Maximum number of outbound edges per condition_id node. Prevents graph explosion in large event clusters.

Default

{ "max_edges_per_node": 20 }

Why this default matters

20 edges per node covers all realistic linked-market scenarios while bounding memory and downstream traversal cost.

Threshold logic

ConditionAction
edges ≤ 20Normal
20–50WARN — dense cluster; verify no false-positive edges
> 100Hard cap enforced — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.max_edges_per_node > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

Each market is linked to at most a fixed number of related markets to keep analysis tractable.

8. Default Configuration

{
  "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
    }
  }
}

9. Implementation Flow

  1. 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.
  2. For each market, compute embedding vector via local sentence-embedding model over title + ' ' + rules_text[:512].
  3. Apply manual_pair_overrides first: inject edges with confidence=1.0 and the specified relation_type.
  4. For each pair of active markets, compute cosine_similarity(embed_a, embed_b).
  5. 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.
  6. Cap outbound edges per node at max_edges_per_node, keeping highest-confidence edges.
  7. Check KillSwitch. If active, update graph in memory but suppress ObservationReport emissions.
  8. 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.
  9. Log per-rebuild summary: nodes_total, edges_total, new_edges, dropped_edges, clusters_found, rebuild_latency_ms.

10. Reference Implementation

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.

Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output.

// --- Initialisation ---
embed_model = load_local_embedding_model()
last_graph   = {}        // condition_id -> set of (neighbor_id, relation_type, confidence)
graph_version_counter = 0

FUNCTION rebuildCycle():
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status

  // --- 1. Fetch market list ---
  markets = FETCH gamma_api.GET('/markets?active=true&closed=false')
  IF markets IS NULL:
    LOG WARN 'STALE_DATA — Gamma API unavailable; graph frozen'
    RETURN

  // --- 2. Embed all markets ---
  embeddings = {}
  FOR market IN markets:
    text = market.question + ' ' + market.rules_text[:512]
    embeddings[market.condition_id] = embed_model.encode(text)

  // --- 3. Inject manual overrides ---
  new_graph = {}
  FOR override IN params.manual_pair_overrides:
    addEdge(new_graph, override.condition_id_a, override.condition_id_b,
            override.relation_type, confidence=1.0)

  // --- 4. Cosine similarity clustering ---
  cids = LIST(embeddings.keys())
  FOR i IN range(len(cids)):
    FOR j IN range(i+1, len(cids)):
      sim = cosine_similarity(embeddings[cids[i]], embeddings[cids[j]])
      IF sim < params.cluster_threshold:
        CONTINUE
      rel = inferRelationType(markets[cids[i]], markets[cids[j]], sim)
      addEdge(new_graph, cids[i], cids[j], rel, confidence=sim)
      addEdge(new_graph, cids[j], cids[i], rel, confidence=sim)

  // --- 5. Cap edges per node ---
  FOR node IN new_graph:
    IF len(new_graph[node]) > params.max_edges_per_node:
      new_graph[node] = topN(new_graph[node], params.max_edges_per_node)
      LOG WARN 'CROSSMARKETGRAPH_DENSE_CLUSTER node=' + node

  graph_version_counter += 1
  graph_version = 'cmg_v' + today() + '_' + graph_version_counter

  // --- 6. Diff against last graph ---
  new_edges     = diff_new(new_graph, last_graph)
  dropped_edges = diff_dropped(last_graph, new_graph)

  // --- 7. KillSwitch suppress ---
  IF ks.active:
    LOG INFO 'KILL_SWITCH_ACTIVE — suppressing ObservationReports'
    last_graph = new_graph
    RETURN

  // --- 8. Emit for new/changed edges ---
  FOR (cid_a, cid_b, rel, conf) IN new_edges:
    EMIT ObservationReport {
      report_id:     'rep_cmg_' + cid_a[:6] + '_' + now_ms(),
      trace_id:      new_trace_id(),
      bot_id:        'intel.crossmarketgraph',
      kind:          'ObservationReport',
      condition_id_a: cid_a,
      condition_id_b: cid_b,
      relation_type:  rel,
      confidence:     conf,
      edge_direction: 'bidirectional',
      graph_version:  graph_version,
      warnings:       [],
      emitted_at_ms:  now_ms()
    }

  last_graph = new_graph

FUNCTION inferRelationType(m_a, m_b, sim):
  IF m_a.neg_risk AND m_b.neg_risk AND sameParent(m_a, m_b):
    RETURN 'NEG_RISK_SIBLING'
  IF m_a.closed AND NOT m_b.closed AND sim > 0.95:
    RETURN 'SUPERSEDES'
  IF isComplementPair(m_a.question, m_b.question):
    RETURN 'COMPLEMENTARY'
  RETURN 'SAME_EVENT'

SDK calls used

  • gamma_api.GET('/markets?active=true&closed=false')
  • embed_model.encode(text)
  • cosine_similarity(vec_a, vec_b)

Complexity: O(M²) per rebuild cycle where M = live market count; practical with M ≤ 500 at 5-min cadence

11. Wire Examples

Input — what arrives on the wire

{
  "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 — what the bot emits

{
  "label": "ObservationReport — 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
  }
}

12. Decision Logic

APPROVE

Not applicable — 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.

13. Standard Decision Output

This bot returns a ObservationReport object. See ObservationReport schema.

{
  "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
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
CROSSMARKETGRAPH_SAME_EVENT_EDGEINFOTwo markets linked as SAME_EVENT: titles or rules text highly similar (confidence >= cluster_threshold).Emit ObservationReport; SumToOneArb and strategies consume edge for arb detection.
CROSSMARKETGRAPH_NEG_RISK_SIBLINGINFOTwo neg-risk markets share a parent condition; linked as NEG_RISK_SIBLING.Emit ObservationReport; strategies aggregate combined neg-risk exposure across siblings.These markets are outcomes of the same multi-result event.
CROSSMARKETGRAPH_DENSE_CLUSTERWARNA node has > 80% of max_edges_per_node edges; potential event-cluster explosion.Include in ObservationReport warnings; alert Intelligence pod lead for review.
CROSSMARKETGRAPH_SUPERSEDES_EDGEWARNA closed market is superseded by a new active market (similarity > 0.95).Emit ObservationReport with relation_type=SUPERSEDES; downstream strategies prefer the new market.A newer version of this market is available.
STALE_DATAWARNGamma API unavailable for > 2× rebuild_interval_s; graph is frozen at last snapshot.Halt ObservationReport emissions; log STALE_DATA; alert on-call.
KILL_SWITCH_ACTIVEHARD_REJECTKillSwitch active; ObservationReport emissions suppressed.Continue computing graph updates but suppress all emissions.Market relationship updates are paused while trading is suspended.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTA parameter change violates a locked bound (e.g. cluster_threshold < 0.65 or rebuild_interval_s < 30).Reject config change; do not apply.
CROSSMARKETGRAPH_EMBED_FAILUREWARNSentence-embedding model failed to encode one or more markets; those markets excluded from this rebuild.Log with condition_id; skip affected markets; retry on next cycle.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_intel_crossmarketgraph_nodes_totalgaugecountTotal condition_id nodes in the current graph snapshot.
polytraders_intel_crossmarketgraph_edges_totalgaugecountrelation_typeTotal directed edges in the graph, broken down by relation_type.
polytraders_intel_crossmarketgraph_observations_emitted_totalcountercountrelation_typeObservationReports emitted for new or changed edges per rebuild cycle.
polytraders_intel_crossmarketgraph_rebuild_latency_mshistogrammillisecondsWall-clock latency of a full graph rebuild cycle.
polytraders_intel_crossmarketgraph_dense_clusters_totalcountercountNumber of nodes that hit max_edges_per_node cap in rebuild cycles.
polytraders_intel_crossmarketgraph_embed_failures_totalcountercountEmbedding failures causing markets to be skipped in a rebuild cycle.

Alerts

AlertConditionSeverityRunbook
CrossMarketGraphStaleGraphtime_since_last_successful_rebuild > 2 * rebuild_interval_spage#runbook-crossmarketgraph-stale-graph
CrossMarketGraphDenseClusterSpikerate(polytraders_intel_crossmarketgraph_dense_clusters_total[10m]) > 5warn#runbook-crossmarketgraph-dense-cluster
CrossMarketGraphRebuildLatencyHighhistogram_quantile(0.99, rate(polytraders_intel_crossmarketgraph_rebuild_latency_ms_bucket[10m])) > 30000warn#runbook-crossmarketgraph-latency
CrossMarketGraphEmbedFailuresrate(polytraders_intel_crossmarketgraph_embed_failures_total[10m]) > 10warn#runbook-crossmarketgraph-embed-failures

Dashboards

  • Grafana — Intelligence / CrossMarketGraph node and edge counts
  • Grafana — Intelligence / graph rebuild latency and cluster density

16. Developer Reporting

{
  "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
}

17. Plain-English Reporting

SituationUser-facing explanation
Two similar-looking markets shown as linkedThese two markets resolve the same underlying event. The system tracks this relationship to avoid taking conflicting positions on what is effectively the same question.
Market flagged as a neg-risk siblingThis market is one outcome of a multi-outcome event. The system tracks all outcomes together to manage combined exposure across the whole event.
Market flagged as supersededA newer version of this market is available. The system will prefer the replacement market for new activity.

18. Failure-Mode Block

main_failure_modeEmbedding 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_riskTwo 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_riskMarkets 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_fallbackIf Gamma API is unavailable, retain last-known graph and emit STALE_DATA WARN. Do not emit ObservationReports based on graph older than 2× rebuild_interval_s.
required_dependenciesGamma API live market list, Local sentence-embedding model, KillSwitch active flag readable

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
GAMMA_API_DOWNBlock TCP to gamma-api.polymarket.com for 700 s (> 2× rebuild_interval_s=300)Automatic on next successful Gamma API response; full rebuild triggered immediately
EMBED_MODEL_FAILUREKill local embedding model process mid-rebuildEmbedding model restarted automatically; next rebuild cycle processes full market list
DENSE_CLUSTER_EXPLOSIONInject 150 mock markets with titles starting 'Will X win...' creating a dense clusterAutomatic; graph stabilises once cluster is capped
KILL_SWITCH_ONSet killswitch.active=true; trigger a rebuild cycle with new edgesEmissions resume on first rebuild after KillSwitch reset; all new edges from suppressed cycles emitted
LOW_THRESHOLD_CONFIG_REJECTEDAttempt to set cluster_threshold=0.50Operator must submit approved config change through governance workflow

20. State & Persistence

Cold-start recovery

On cold start, graph is empty. First rebuild cycle populates it. Until first rebuild completes, no ObservationReports are emitted.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight1
Idempotency keygraph_version
Per-call timeout (ms)30000
Backpressure strategydrop-after-buffer — if rebuild takes longer than rebuild_interval_s, the next scheduled rebuild is skipped until current one completes
Locking / mutual exclusionnone — single rebuild loop; reads and emits are serialised

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchKillSwitch gate suppresses ObservationReport emissions.

Emits to (downstream consumers)

BotWhyContract
strat.sum_to_one_arb
strat.liquidity_aware_strategies

External services

ServiceEndpointSLA assumedOn failure
Gamma APIhttps://gamma-api.polymarket.com99.9% / 500 ms p99
Local sentence-embedding modellocal processprocess-availability

23. Security Surfaces

Abuse vectors considered

  • 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 — downstream strategies and risk bots independently validate market state before acting

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsyes
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesCrossMarketGraph 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.

API surfaces declared

gamma_apidata_apiclob_publicinternal

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation2.1.0
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1v2CLOB V2 cutover — pUSD denomination and enableNegRisk flag added to Gamma APIUpdated 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.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Two markets with cosine_similarity=0.92 linked as SAME_EVENTMock two market embeddings with similarity=0.92, cluster_threshold=0.85ObservationReport emitted with relation_type=SAME_EVENT, confidence=0.92
Market pair below threshold not linkedMock cosine_similarity=0.60, cluster_threshold=0.85No edge created; no ObservationReport emitted
Manual override injected with confidence=1.0manual_pair_overrides = [{condition_id_a, condition_id_b, relation_type=COMPLEMENTARY}]Edge created with confidence=1.0 regardless of embedding similarity
max_edges_per_node cap enforcedNode with 25 candidate edges, max_edges_per_node=20Only top-20 highest-confidence edges retained; excess edges dropped
KillSwitch suppresses ObservationReport emissionskillswitch.active=true; new edge detectedGraph updated in memory; no ObservationReport emitted; KILL_SWITCH_ACTIVE logged
NEG_RISK_SIBLING correctly assignedTwo markets with neg_risk=true, same parent condition, similarity=0.91ObservationReport with relation_type=NEG_RISK_SIBLING

Integration Tests

TestExpected result
Full rebuild cycle generates correct graph from live Gamma APIGraph node count matches live market count; known linked pairs present as edges
New market added to Gamma API appears as graph node on next rebuild cycleObservationReport emitted for any new edges involving the new condition_id
Gamma API outage leaves graph frozen with STALE_DATA warningNo new ObservationReports emitted; STALE_DATA WARN logged; graph_version unchanged

Property Tests

PropertyRequired behaviour
CrossMarketGraph never submits, signs, or modifies any orderAlways true
No ObservationReport emitted when KillSwitch is activeAlways true
All edge confidence values are in [0.0, 1.0]Always true

27. Operational Runbook

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 — existing positions are unaffected.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
CrossMarketGraphStaleGraph
CrossMarketGraphDenseClusterSpike
CrossMarketGraphRebuildLatencyHigh
CrossMarketGraphEmbedFailures

Manual overrides

Healthcheck

/internal/health/crossmarketgraph → 200 Last rebuild completed within 2× rebuild_interval_s AND nodes_total > 0 AND no STALE_DATA in last cycle; red if No successful rebuild in > 2× rebuild_interval_s OR embed_failures_total rate > 10/min OR Gamma API unreachable

28. Promotion Gates

A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.

Promote to Shadow

GateHow measuredThreshold
Unit tests pass for SAME_EVENT, NEG_RISK_SIBLING, SUPERSEDES classifications and KillSwitch suppressionCI test run100% pass
Gamma API integration test: market list fetch and embedding pipeline completes within 30 s for 300 marketsIntegration testPass

Promote to Limited live

GateHow measuredThreshold
Graph rebuild p99 latency < 10 s over 48 h at live market countpolytraders_intel_crossmarketgraph_rebuild_latency_ms histogramp99 < 10000 ms
Known linked pairs (from manual test set) all present in graph with correct relation_typePost-rebuild graph inspection script100% recall on test set

Promote to General live

GateHow measuredThreshold
Zero graph-freeze incidents over 14 daysCrossMarketGraphStaleGraph alert history0 firings
False-positive edge rate < 5% as assessed by manual review of sampled edgesManual weekly review of 50 sampled edges< 5% false positives
KillSwitch suppression: zero ObservationReports when KillSwitch activeIntegration testPass

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
Purpose defined✓ done
Required inputs listed✓ done
Parameters defined✓ done
Defaults defined✓ done
Warning thresholds defined✓ done
Hard thresholds defined✓ done
Safe fallback defined✓ done
Structured output defined✓ done
Developer log defined✓ done
Plain-English explanation✓ done
Unit tests defined✓ done
Integration tests defined✓ done
Property tests defined✓ done
Failure-mode block complete✓ done
Reference implementation pseudocode✓ done
Wire examples (input + output)✓ done
Reason codes listed✓ done
Metrics & logs defined✓ done
State & persistence defined✓ done
Concurrency & idempotency defined✓ done
Dependencies declared✓ done
Security surfaces declared✓ done
Polymarket V2 compatibility declared✓ done
Version & migration history declared✓ done
Operational runbook defined✓ done
Promotion gates defined✓ done
Failure-injection recipes defined✓ done