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.5 SportsFeed-Adapter

4.5 SportsFeed-Adapter

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

SportsFeed-Adapter ingests structured sports data from league APIs (NBA, NFL, EPL, ATP/WTA, MLB) and odds-feed providers, normalises it into a canonical SportsFeedEvent schema, and emits an ObservationReport for each qualifying update (lineup changes, injury reports, score updates, and pre-game odds shifts). It supplements primary API feeds with ws_sports for low-latency in-play state and falls back to web extraction for sports without direct API coverage. Output feeds sports-model strategies with the event data they need to price Polymarket sports markets. SportsFeed-Adapter 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 beforeSports-model strategies, LiquidityGuard
Runs afterws_sports subscription established; Gamma API sports market list loaded
Applies toAll live Polymarket sports markets matched to enabled_sports feed coverage
Default modelimited_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Intelligence pod

2. Purpose

SportsFeed-Adapter ingests structured sports data from league APIs (NBA, NFL, EPL, ATP/WTA, MLB) and odds-feed providers, normalises it into a canonical SportsFeedEvent schema, and emits an ObservationReport for each qualifying update (lineup changes, injury reports, score updates, and pre-game odds shifts). It supplements primary API feeds with ws_sports for low-latency in-play state and falls back to web extraction for sports without direct API coverage. Output feeds sports-model strategies with the event data they need to price Polymarket sports markets. SportsFeed-Adapter is strictly read-only — it never submits or signs orders.

3. Why This Bot Matters

  • Injury report not ingested before market open

    Sports-model strategy prices a market without a key player absence; takes a position at a probability that is stale by a significant amount and gets adversely selected.

  • Score update delayed by > refresh_interval_s

    In-play market probability estimate diverges from ground truth; strategy holds a stale position through a goal or score change, realising avoidable losses.

  • Primary league API unavailable and fallback_to_web disabled

    No sports data flows to the model; strategy falls back to last-known odds and risks trading on stale information for the duration of the outage.

  • Odds from wrong provider used due to preferred_provider misconfiguration

    Systematically biased odds feed primes the model with incorrect pre-game probability estimates, degrading pricing accuracy across all sports markets in the affected sport.

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
Sports market event metadata (event_id, sport, teams, start_time, condition_id)Gamma API (ws_sports supplement)YesMap incoming feed events to Polymarket condition_ids for ObservationReport payloads.
Real-time in-play state updates (score, game_clock, possession)ws_sportsNoLow-latency supplement to league API polling for in-play markets.

5. Required Internal Inputs

InputSourceRequired?Use
KillSwitch active flagKillSwitchYesContinue ingesting feed data but suppress ObservationReport emissions when KillSwitch is active.
Sports model interest listSportsModel configNoPrioritise feed polling for sports and events that the model has active coverage on.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
refresh_interval_s30105How often in seconds the primary league API is polled for updates per tracked event.
stale_feed_threshold_s12060300Seconds since last successful feed update after which a STALE_DATA warning is emitted and the affected sport is flagged as unreliable.
min_odds_shift_bps50205Minimum shift in odds (in basis points) required to trigger an ObservationReport for an odds-update event.

7. Detailed Parameter Instructions

refresh_interval_s

What it means

How often in seconds the primary league API is polled for updates per tracked event.

Default

{ "refresh_interval_s": 30 }

Why this default matters

30 s is sufficient for pre-game data; in-play updates are supplemented by ws_sports which runs continuously.

Threshold logic

ConditionAction
interval ≥ 30 sNormal
10–30 sWARN — increased API load; monitor rate limits
< 5 sReject — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

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

User-facing English

Sports data is refreshed regularly to keep event information current without overloading data providers.

stale_feed_threshold_s

What it means

Seconds since last successful feed update after which a STALE_DATA warning is emitted and the affected sport is flagged as unreliable.

Default

{ "stale_feed_threshold_s": 120 }

Why this default matters

120 s gives two full refresh cycles as tolerance before alerting, avoiding false positives from brief API hiccups.

Threshold logic

ConditionAction
age ≤ 120 sNormal
120–300 sWARN — feed stale; downstream model uses last-known data with STALE_DATA flag
> 300 sHard stale — emit STALE_DATA and halt ObservationReport emissions for affected sport

Developer check

if (feed_age_s > p.hard) emit_stale_and_halt(sport);

User-facing English

If sports data has not been updated within the expected window, affected markets are flagged as using potentially stale information.

min_odds_shift_bps

What it means

Minimum shift in odds (in basis points) required to trigger an ObservationReport for an odds-update event.

Default

{ "min_odds_shift_bps": 50 }

Why this default matters

50 bps filters out micro-fluctuations in odds that carry no meaningful model signal, preventing bus flooding during high-frequency pre-game odds movement.

Threshold logic

ConditionAction
shift ≥ 50 bpsNormal — emit ObservationReport
20–50 bpsWARN — marginal shift; emit with SPORTSFEED_MINOR_ODDS_SHIFT warning
< 5 bpsDiscard — noise floor; do not emit

Developer check

if (abs(odds_shift_bps) < p.hard) return; // noise floor

User-facing English

Only meaningful odds movements generate updates to avoid signal noise.

8. Default Configuration

{
  "bot_id": "intel.sportsfeed-adapter",
  "version": "2.1.0",
  "mode": "limited_live",
  "defaults": {
    "enabled_sports": [
      "NBA",
      "NFL",
      "EPL",
      "ATP",
      "WTA",
      "MLB"
    ],
    "preferred_provider": "primary_league_api",
    "refresh_interval_s": 30,
    "stale_feed_threshold_s": 120,
    "min_odds_shift_bps": 50,
    "fallback_to_web": true
  },
  "locked": {
    "refresh_interval_s": {
      "min": 5
    },
    "stale_feed_threshold_s": {
      "max": 300
    },
    "min_odds_shift_bps": {
      "min": 5
    }
  }
}

9. Implementation Flow

  1. On startup, subscribe to ws_sports for real-time in-play state updates for all enabled_sports.
  2. On each refresh cycle (refresh_interval_s), poll preferred_provider league API for each tracked event: lineups, injuries, odds, score, game_clock.
  3. If preferred_provider unavailable and fallback_to_web=true, attempt web extraction for affected sport/event.
  4. Normalise raw feed payload into canonical SportsFeedEvent schema: {event_id, sport, home_team, away_team, condition_id, event_type, payload, source_provider, feed_age_s}.
  5. Check feed_age_s: if > stale_feed_threshold_s hard (300 s), emit STALE_DATA WARN and halt ObservationReports for that sport.
  6. For odds-update events, compute odds_shift_bps vs last emitted odds. If abs(odds_shift_bps) < min_odds_shift_bps hard (5 bps), discard as noise.
  7. Check KillSwitch. If active, continue ingesting and normalising but suppress ObservationReport emissions.
  8. For qualifying events, emit ObservationReport with: report_id, trace_id, condition_id, event_type, sport, event_id, normalised_payload, source_provider, feed_age_s, odds_shift_bps (if applicable), warnings.
  9. Apply sampling: emit-every for LINEUP_CHANGE, INJURY_UPDATE, GAME_START, GAME_END; sample-1/5 for routine score-tick updates.
  10. Log per-cycle summary: events_polled, events_emitted, events_discarded_noise, stale_sports, fallback_used.

10. Reference Implementation

Polls league APIs and ws_sports for sports events, normalises to canonical SportsFeedEvent schema, applies staleness and noise-floor filters, applies sampling for routine score ticks, and emits ObservationReports to the sports-model strategy layer.

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

// --- Initialisation ---
ws_sports.subscribe(handler=onInPlayEvent)
last_odds   = {}   // event_id -> odds_value
last_feed_ts = {}  // sport -> timestamp_ms
score_tick_counter = {}  // event_id -> int

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

  FOR sport IN params.enabled_sports:
    // --- 1. Poll primary provider ---
    feed = FETCH league_api[sport].getEvents(active=true)

    IF feed IS NULL AND params.fallback_to_web:
      feed = FETCH web_extractor[sport].getEvents()
      provider = 'web_fallback'
    ELSE:
      provider = params.preferred_provider

    IF feed IS NULL:
      feed_age_s = (now_ms() - last_feed_ts.get(sport, 0)) / 1000
      IF feed_age_s > params.stale_feed_threshold_s.hard:
        LOG WARN 'STALE_DATA — ' + sport + ' feed hard stale'
        stale_sports.add(sport)
      CONTINUE

    last_feed_ts[sport] = now_ms()
    stale_sports.discard(sport)

    FOR event IN feed.events:
      condition_id = gamma_api.GET('/sports-event/' + event.event_id + '/condition')
      IF condition_id IS NULL:
        CONTINUE

      // --- 2. Classify event type ---
      FOR update IN event.updates:
        event_type = classifyUpdate(update)  // INJURY_UPDATE | LINEUP_CHANGE | ODDS_UPDATE | SCORE_TICK | GAME_START | GAME_END

        // --- 3. Noise filter for odds updates ---
        IF event_type == 'ODDS_UPDATE':
          shift_bps = abs(update.new_odds - last_odds.get(event.event_id, update.new_odds)) * 10000
          IF shift_bps < params.min_odds_shift_bps.hard:
            events_discarded_noise_counter += 1
            CONTINUE
          last_odds[event.event_id] = update.new_odds

        // --- 4. Staleness check ---
        feed_age_s = (now_ms() - update.provider_ts_ms) / 1000
        warnings = []
        IF feed_age_s > params.stale_feed_threshold_s.default:
          warnings.append('STALE_DATA')
          IF feed_age_s > params.stale_feed_threshold_s.hard:
            CONTINUE  // halt emission

        // --- 5. Sampling for routine ticks ---
        sampling_applied = False
        IF event_type == 'SCORE_TICK':
          score_tick_counter[event.event_id] = score_tick_counter.get(event.event_id, 0) + 1
          IF score_tick_counter[event.event_id] % 5 != 0:
            CONTINUE  // sample-1/5
          sampling_applied = True

        // --- 6. KillSwitch suppress ---
        IF ks.active:
          LOG INFO 'KILL_SWITCH_ACTIVE — suppressing ObservationReport'
          CONTINUE

        // --- 7. Emit ---
        EMIT ObservationReport {
          report_id:         'rep_sfa_' + sport + '_' + condition_id[:6] + '_' + now_ms(),
          trace_id:          new_trace_id(),
          bot_id:            'intel.sportsfeed-adapter',
          kind:              'ObservationReport',
          condition_id:      condition_id,
          event_type:        event_type,
          sport:             sport,
          event_id:          event.event_id,
          normalised_payload: normalise(update),
          source_provider:   provider,
          feed_age_s:        feed_age_s,
          odds_shift_bps:    shift_bps IF event_type == 'ODDS_UPDATE' ELSE None,
          sampling_applied:  sampling_applied,
          warnings:          warnings,
          emitted_at_ms:     now_ms()
        }

FUNCTION onInPlayEvent(ws_event):
  // ws_sports supplement — same normalise + emit pipeline, source_provider='ws_sports'
  condition_id = gamma_api.GET('/sports-event/' + ws_event.event_id + '/condition')
  IF condition_id IS NULL: RETURN
  // ... same emit logic with event_type from ws_sports message type

SDK calls used

  • league_api[sport].getEvents(active=true)
  • ws_sports.subscribe(handler=onInPlayEvent)
  • gamma_api.GET('/sports-event/<event_id>/condition')
  • web_extractor[sport].getEvents()

Complexity: O(S × E) per refresh cycle where S = enabled sports count, E = average events per sport

11. Wire Examples

Input — what arrives on the wire

{
  "label": "League API injury update payload",
  "source": "primary_league_api",
  "payload": {
    "sport": "NBA",
    "event_id": "NBA_2026_PO_G3_TOR_MIA",
    "updates": [
      {
        "type": "injury",
        "player": "Player A",
        "team": "TOR",
        "status": "OUT",
        "reason": "ankle sprain",
        "provider_ts_ms": 1746704000000
      }
    ]
  }
}

Output — what the bot emits

{
  "label": "ObservationReport — INJURY_UPDATE for NBA market",
  "payload": {
    "report_id": "rep_sfa_NBA_0xcc99_1746704000000",
    "trace_id": "trc_0xdead0102030405060708",
    "bot_id": "intel.sportsfeed-adapter",
    "kind": "ObservationReport",
    "condition_id": "0xcc990000000000000000000000000000000000000000000000000000000000000000",
    "event_type": "INJURY_UPDATE",
    "sport": "NBA",
    "event_id": "NBA_2026_PO_G3_TOR_MIA",
    "normalised_payload": {
      "player": "Player A",
      "team": "TOR",
      "status": "OUT",
      "reason": "ankle sprain",
      "updated_at_ms": 1746704000000
    },
    "source_provider": "primary_league_api",
    "feed_age_s": 8,
    "odds_shift_bps": null,
    "sampling_applied": false,
    "warnings": [],
    "emitted_at_ms": 1746704000095
  }
}

12. Decision Logic

APPROVE

Not applicable — SportsFeed-Adapter is read-only; it never approves or submits orders.

RESHAPE_REQUIRED

Not applicable.

REJECT

Observations are suppressed only when KillSwitch is active (KILL_SWITCH_ACTIVE) or when feed data is hard-stale (> stale_feed_threshold_s hard). Noise-floor odds shifts (< 5 bps) are silently discarded.

WARNING_ONLY

SPORTSFEED_MINOR_ODDS_SHIFT is included for shifts between 20–50 bps. STALE_DATA is included when feed age is between 120–300 s.

13. Standard Decision Output

This bot returns a ObservationReport object. See ObservationReport schema.

{
  "report_id": "rep_sfa_NBA_0xcc99_1746704000000",
  "trace_id": "trc_0xdead0102030405060708",
  "bot_id": "intel.sportsfeed-adapter",
  "kind": "ObservationReport",
  "condition_id": "0xcc990000000000000000000000000000000000000000000000000000000000000000",
  "event_type": "INJURY_UPDATE",
  "sport": "NBA",
  "event_id": "NBA_2026_PO_G3_TOR_MIA",
  "normalised_payload": {
    "player": "Player A",
    "team": "TOR",
    "status": "OUT",
    "reason": "ankle sprain",
    "updated_at_ms": 1746704000000
  },
  "source_provider": "primary_league_api",
  "feed_age_s": 8,
  "odds_shift_bps": null,
  "sampling_applied": false,
  "warnings": [],
  "emitted_at_ms": 1746704000095
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
SPORTSFEED_INJURY_UPDATEWARNA player injury or status change was detected for a tracked sports event.Emit ObservationReport emit-every; sports-model strategy reprices the market.An injury report was detected for a player in this market’s event.
SPORTSFEED_LINEUP_CHANGEWARNTeam lineup or starting roster change detected before game start.Emit ObservationReport emit-every; sports-model strategy updates pre-game probability estimate.A lineup change was reported for this market’s event.
SPORTSFEED_MINOR_ODDS_SHIFTWARNOdds shift is between 20–50 bps — marginal but above noise floor.Emit ObservationReport with SPORTSFEED_MINOR_ODDS_SHIFT warning; strategy applies lower weight.
STALE_DATAWARNFeed age is > stale_feed_threshold_s for this sport; data may not reflect current conditions.Include in ObservationReport warnings if between default–hard threshold; halt emissions if > hard threshold.Sports data for this event may be slightly delayed.
KILL_SWITCH_ACTIVEHARD_REJECTKillSwitch active; ObservationReport emissions suppressed.Continue ingesting feed data but suppress all emissions.Sports data updates are paused while trading is suspended.
SPORTSFEED_FALLBACK_ACTIVEWARNPrimary league API unavailable; web fallback in use for this sport.Emit ObservationReport with source_provider=web_fallback; downstream strategy applies lower data-quality weight.
MARKET_CLOSEDEXPLAINFeed event received for a condition_id that is already closed or resolved.Skip emission; log for audit trail only.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTA parameter change violates a locked bound (e.g. refresh_interval_s < 5).Reject config change; do not apply.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_intel_sportsfeedadapter_events_polled_totalcountercountsportTotal sports events polled from all feed sources per refresh cycle, broken down by sport.
polytraders_intel_sportsfeedadapter_observations_emitted_totalcountercountsport, event_typeObservationReports emitted broken down by sport and event_type.
polytraders_intel_sportsfeedadapter_noise_discarded_totalcountercountsportOdds updates discarded as noise (below min_odds_shift_bps hard floor).
polytraders_intel_sportsfeedadapter_feed_age_sgaugesecondssportAge of the most recent successful feed update per sport.
polytraders_intel_sportsfeedadapter_fallback_activations_totalcountercountsportNumber of times web fallback was activated due to primary API unavailability per sport.
polytraders_intel_sportsfeedadapter_stale_sports_gaugegaugecountNumber of sports currently in hard-stale state (feed_age_s > stale_feed_threshold_s hard).

Alerts

AlertConditionSeverityRunbook
SportsFeedAdapterStaleFeedpolytraders_intel_sportsfeedadapter_feed_age_s > 300 for any sport labelpage#runbook-sportsfeedadapter-stale-feed
SportsFeedAdapterFallbackActiverate(polytraders_intel_sportsfeedadapter_fallback_activations_total[10m]) > 0warn#runbook-sportsfeedadapter-fallback
SportsFeedAdapterZeroEmissionsrate(polytraders_intel_sportsfeedadapter_observations_emitted_total[15m]) == 0 AND polytraders_risk_killswitch_active == 0warn#runbook-sportsfeedadapter-zero-emissions
SportsFeedAdapterHighNoiserate(polytraders_intel_sportsfeedadapter_noise_discarded_total[5m]) > 50warn#runbook-sportsfeedadapter-high-noise

Dashboards

  • Grafana — Intelligence / SportsFeed-Adapter feed age per sport
  • Grafana — Intelligence / event type distribution and emissions rate

16. Developer Reporting

{
  "bot_id": "intel.sportsfeed-adapter",
  "refresh_cycle": 2847,
  "events_polled": 42,
  "events_emitted": 6,
  "events_discarded_noise": 11,
  "stale_sports": [],
  "fallback_used": false,
  "killswitch_active": false
}

17. Plain-English Reporting

SituationUser-facing explanation
Market odds adjusted after injury newsA player injury was reported before the game. The system updated its assessment of the market based on this new information.
In-play market showing rapid updatesLive sports data is being processed to keep the market assessment current as the game progresses.
Sports market flagged as using stale dataThe data feed for this sport has not updated recently. The system is using the most recent available information, but it may not reflect the latest conditions.

18. Failure-Mode Block

main_failure_modePrimary league API outage during a live game causes score and in-play state updates to stop flowing; sports-model strategies continue pricing from the last known state, accumulating pricing error until the feed recovers or fallback_to_web activates.
false_positive_riskMinor odds fluctuations below the 50 bps default threshold occasionally carry genuine model signal (e.g. sharp-money moves) that gets filtered as noise, causing the model to miss a meaningful odds shift.
false_negative_riskA major injury or lineup change arrives via a secondary source not covered by the preferred_provider before the primary API is updated; the bot does not emit an ObservationReport until the primary source confirms the update.
safe_fallbackIf all feed sources (primary API and web fallback) are unavailable for > stale_feed_threshold_s hard (300 s), emit STALE_DATA WARN and halt ObservationReport emissions for the affected sport. Downstream strategies treat the last-known SportsFeedEvent as authoritative but annotate positions as STALE_FEED.
required_dependenciesLeague API (or web fallback) for at least one enabled sport, Gamma API for event_id → condition_id mapping, ws_sports subscription for in-play supplements, KillSwitch active flag readable

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
PRIMARY_API_DOWNBlock TCP to primary league API for 200 sPrimary API recovered; primary provider resumes; fallback deactivated
HARD_STALE_FEEDBlock all feed sources (primary + web) for 350 s (> 300 s hard threshold)Automatic on next successful feed response; stale_sports cleared
KILL_SWITCH_ONSet killswitch.active=true; inject INJURY_UPDATE eventEmissions resume on first refresh cycle after KillSwitch reset
NOISE_ODDS_FLOODInject 100 odds updates of 2 bps each for a single eventAutomatic; no action required
WS_SPORTS_DISCONNECTDrop ws_sports TCP connectionAutomatic ws_sports reconnect with exponential back-off

20. State & Persistence

Cold-start recovery

On cold start, Redis state reloaded. First refresh cycle repopulates any missing keys. score_tick_counter resets to 0 for all events.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight6
Idempotency keyevent_id + update_type + provider_ts_ms
Per-call timeout (ms)10000
Backpressure strategydrop-after-buffer — if refresh cycle takes longer than refresh_interval_s, next cycle is skipped
Locking / mutual exclusionnone — per-sport state accessed only from single event loop

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchKillSwitch gate suppresses ObservationReport emissions.

Emits to (downstream consumers)

External services

ServiceEndpointSLA assumedOn failure
League APIs (NBA, NFL, EPL, ATP/WTA, MLB)Sport-specific league API endpointsvariable per provider; assumed 99% / 500 ms p99
Polymarket ws_sportswss://ws-subscriptions-clob.polymarket.com/ws/sportsbest-effort
Gamma API (event_id -> condition_id)https://gamma-api.polymarket.com99.9% / 500 ms p99

23. Security Surfaces

Abuse vectors considered

  • Compromised league API provider injecting false injury or lineup data to manipulate sports-model strategy pricing
  • Web fallback scraping page with adversarially crafted content to produce incorrect normalised_payload values

Mitigations

  • Only preferred league API sources are used by default; web fallback requires explicit fallback_to_web=true config
  • normalised_payload schema is strictly validated; unexpected fields are stripped before emission
  • 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 marketsno
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesSportsFeed-Adapter consumes ws_sports V2 in-play state updates and references condition_ids from Gamma API V2; all model-context amounts are denominated in pUSD.

API surfaces declared

gamma_apiws_sportsws_rtdsinternal

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 — ws_sports V2 message format and pUSD denomination in model contextUpdated ws_sports subscription to CLOB V2 message format. ObservationReport payloads now reference pUSD-denominated market context. Removed legacy nonce from any internal message plumbing. No feeRateBps or signed-order plumbing in this bot.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
INJURY_UPDATE event from league API emits ObservationReport emit-everyMock INJURY_UPDATE payload for NBA event; refresh_interval_s=30ObservationReport emitted with event_type=INJURY_UPDATE, sampling_applied=False
Odds shift below noise floor (3 bps) discarded silentlyodds_shift_bps=3, min_odds_shift_bps hard=5No ObservationReport; events_discarded_noise counter incremented
Stale feed > 300 s halts emissions for affected sportfeed_age_s=350, stale_feed_threshold_s hard=300STALE_DATA WARN logged; no ObservationReport emitted for that sport; stale_sports=['NBA']
KillSwitch suppresses ObservationReport emissionskillswitch.active=true; LINEUP_CHANGE event arrivesEvent ingested and normalised; no ObservationReport emitted; KILL_SWITCH_ACTIVE logged
Primary API down triggers fallback_to_web for affected sportprimary_league_api returns 503; fallback_to_web=trueWeb extraction used; ObservationReport emitted with source_provider=web_fallback
Routine score-tick sampled 1/55 consecutive score-tick events, sampling rule sample-1/5Approximately 1 ObservationReport emitted with sampling_applied=True

Integration Tests

TestExpected result
Full lifecycle: INJURY_UPDATE from league API reaches sports-model strategyStrategy receives ObservationReport with INJURY_UPDATE and correct condition_id; reprices market
ws_sports in-play state update supplements API polling with lower latencyObservationReport emitted within 2 s of ws_sports score event; source_provider=ws_sports
All enabled sports polled correctly on startup without errorsAll sports in enabled_sports list polled; no stale_sports on first cycle

Property Tests

PropertyRequired behaviour
SportsFeed-Adapter never submits, signs, or modifies any orderAlways true
No ObservationReport emitted when KillSwitch is activeAlways true
No ObservationReport emitted when feed_age_s > stale_feed_threshold_s hardAlways true — stale data must never produce a fresh observation

27. Operational Runbook

SportsFeed-Adapter incidents are usually league API outages or hard-stale feeds. Since sports markets can move rapidly around game events, stale feeds during live games should be paged immediately. The fallback_to_web mechanism provides a safety net during primary API outages.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
SportsFeedAdapterStaleFeed
SportsFeedAdapterFallbackActive
SportsFeedAdapterZeroEmissions
SportsFeedAdapterHighNoise

Manual overrides

Healthcheck

GET /internal/health/sportsfeed-adapter -> 200 if All enabled_sports have feed_age_s < stale_feed_threshold_s AND ws_sports connected AND observations_emitted_total rate > 0 in last 5 min. RED if Any sport in stale_sports (hard stale) OR ws_sports disconnected > 60 s AND primary API also down OR observations_emitted_total rate == 0 for > 15 min.

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 INJURY_UPDATE, LINEUP_CHANGE, ODDS_UPDATE noise filter, stale threshold, and KillSwitch suppressionCI test run100% pass
Integration test: all 6 enabled sports polled successfully with correct normalised_payload schemaIntegration test against staging league APIsPass

Promote to Limited live

GateHow measuredThreshold
feed_age_s p99 < 45 s for all enabled sports over 48 hpolytraders_intel_sportsfeedadapter_feed_age_s gaugep99 < 45 s
INJURY_UPDATE and LINEUP_CHANGE correctly classified and emitted for known historical test eventsBack-test against labelled historical feed data100% recall on test set

Promote to General live

GateHow measuredThreshold
Zero hard-stale incidents during live game windows over 14 daysSportsFeedAdapterStaleFeed alert history filtered to game windows0 firings during live game windows
KillSwitch suppression: zero ObservationReports when KillSwitch activeIntegration testPass
Fallback_to_web activates correctly during simulated primary API outageChaos test: block primary API for 60 s; verify web fallback active and emittingPass

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