1. Bot Identity
| Layer | Intelligence Intelligence |
|---|
| Bot class | Signal Service |
|---|
| Authority | Read-only |
|---|
| Status | BETA |
|---|
| Readiness | Limited live |
|---|
| Runs before | Sports-model strategies, LiquidityGuard |
|---|
| Runs after | ws_sports subscription established; Gamma API sports market list loaded |
|---|
| Applies to | All live Polymarket sports markets matched to enabled_sports feed coverage |
|---|
| Default mode | limited_live |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders 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.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| refresh_interval_s | 30 | 10 | 5 | How often in seconds the primary league API is polled for updates per tracked event. |
| stale_feed_threshold_s | 120 | 60 | 300 | Seconds since last successful feed update after which a STALE_DATA warning is emitted and the affected sport is flagged as unreliable. |
| min_odds_shift_bps | 50 | 20 | 5 | Minimum 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
| Condition | Action |
|---|
| interval ≥ 30 s | Normal |
| 10–30 s | WARN — increased API load; monitor rate limits |
| < 5 s | Reject — 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
| Condition | Action |
|---|
| age ≤ 120 s | Normal |
| 120–300 s | WARN — feed stale; downstream model uses last-known data with STALE_DATA flag |
| > 300 s | Hard 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
| Condition | Action |
|---|
| shift ≥ 50 bps | Normal — emit ObservationReport |
| 20–50 bps | WARN — marginal shift; emit with SPORTSFEED_MINOR_ODDS_SHIFT warning |
| < 5 bps | Discard — 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
- On startup, subscribe to ws_sports for real-time in-play state updates for all enabled_sports.
- On each refresh cycle (refresh_interval_s), poll preferred_provider league API for each tracked event: lineups, injuries, odds, score, game_clock.
- If preferred_provider unavailable and fallback_to_web=true, attempt web extraction for affected sport/event.
- 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}.
- Check feed_age_s: if > stale_feed_threshold_s hard (300 s), emit STALE_DATA WARN and halt ObservationReports for that sport.
- 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.
- Check KillSwitch. If active, continue ingesting and normalising but suppress ObservationReport emissions.
- 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.
- Apply sampling: emit-every for LINEUP_CHANGE, INJURY_UPDATE, GAME_START, GAME_END; sample-1/5 for routine score-tick updates.
- 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
| Code | Severity | Meaning | Action | User-facing message |
|---|
SPORTSFEED_INJURY_UPDATE | WARN | A 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_CHANGE | WARN | Team 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_SHIFT | WARN | Odds shift is between 20–50 bps — marginal but above noise floor. | Emit ObservationReport with SPORTSFEED_MINOR_ODDS_SHIFT warning; strategy applies lower weight. | |
STALE_DATA | WARN | Feed 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_ACTIVE | HARD_REJECT | KillSwitch active; ObservationReport emissions suppressed. | Continue ingesting feed data but suppress all emissions. | Sports data updates are paused while trading is suspended. |
SPORTSFEED_FALLBACK_ACTIVE | WARN | Primary 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_CLOSED | EXPLAIN | Feed event received for a condition_id that is already closed or resolved. | Skip emission; log for audit trail only. | |
PARAMETER_CHANGE_REQUIRES_APPROVAL | HARD_REJECT | A parameter change violates a locked bound (e.g. refresh_interval_s < 5). | Reject config change; do not apply. | |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_intel_sportsfeedadapter_events_polled_total | counter | count | sport | Total sports events polled from all feed sources per refresh cycle, broken down by sport. |
polytraders_intel_sportsfeedadapter_observations_emitted_total | counter | count | sport, event_type | ObservationReports emitted broken down by sport and event_type. |
polytraders_intel_sportsfeedadapter_noise_discarded_total | counter | count | sport | Odds updates discarded as noise (below min_odds_shift_bps hard floor). |
polytraders_intel_sportsfeedadapter_feed_age_s | gauge | seconds | sport | Age of the most recent successful feed update per sport. |
polytraders_intel_sportsfeedadapter_fallback_activations_total | counter | count | sport | Number of times web fallback was activated due to primary API unavailability per sport. |
polytraders_intel_sportsfeedadapter_stale_sports_gauge | gauge | count | | Number of sports currently in hard-stale state (feed_age_s > stale_feed_threshold_s hard). |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
SportsFeedAdapterStaleFeed | polytraders_intel_sportsfeedadapter_feed_age_s > 300 for any sport label | page | #runbook-sportsfeedadapter-stale-feed |
SportsFeedAdapterFallbackActive | rate(polytraders_intel_sportsfeedadapter_fallback_activations_total[10m]) > 0 | warn | #runbook-sportsfeedadapter-fallback |
SportsFeedAdapterZeroEmissions | rate(polytraders_intel_sportsfeedadapter_observations_emitted_total[15m]) == 0 AND polytraders_risk_killswitch_active == 0 | warn | #runbook-sportsfeedadapter-zero-emissions |
SportsFeedAdapterHighNoise | rate(polytraders_intel_sportsfeedadapter_noise_discarded_total[5m]) > 50 | warn | #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
| Situation | User-facing explanation |
|---|
| Market odds adjusted after injury news | A 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 updates | Live sports data is being processed to keep the market assessment current as the game progresses. |
| Sports market flagged as using stale data | The 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_mode | Primary 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_risk | Minor 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_risk | A 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_fallback | If 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_dependencies | League 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
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
PRIMARY_API_DOWN | Block TCP to primary league API for 200 s | | Primary API recovered; primary provider resumes; fallback deactivated |
HARD_STALE_FEED | Block all feed sources (primary + web) for 350 s (> 300 s hard threshold) | | Automatic on next successful feed response; stale_sports cleared |
KILL_SWITCH_ON | Set killswitch.active=true; inject INJURY_UPDATE event | | Emissions resume on first refresh cycle after KillSwitch reset |
NOISE_ODDS_FLOOD | Inject 100 odds updates of 2 bps each for a single event | | Automatic; no action required |
WS_SPORTS_DISCONNECT | Drop ws_sports TCP connection | | Automatic 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
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop |
| Max in-flight | 6 |
| Idempotency key | event_id + update_type + provider_ts_ms |
| Per-call timeout (ms) | 10000 |
| Backpressure strategy | drop-after-buffer — if refresh cycle takes longer than refresh_interval_s, next cycle is skipped |
| Locking / mutual exclusion | none — per-sport state accessed only from single event loop |
22. Dependencies
Depends on (must run first)
Emits to (downstream consumers)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| League APIs (NBA, NFL, EPL, ATP/WTA, MLB) | Sport-specific league API endpoints | variable per provider; assumed 99% / 500 ms p99 | |
| Polymarket ws_sports | wss://ws-subscriptions-clob.polymarket.com/ws/sports | best-effort | |
| Gamma API (event_id -> condition_id) | https://gamma-api.polymarket.com | 99.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
| Aspect | Value |
|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | no |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | SportsFeed-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
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | v1 | v2 | CLOB V2 cutover — ws_sports V2 message format and pUSD denomination in model context | Updated 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
| Test | Setup | Expected result |
|---|
| INJURY_UPDATE event from league API emits ObservationReport emit-every | Mock INJURY_UPDATE payload for NBA event; refresh_interval_s=30 | ObservationReport emitted with event_type=INJURY_UPDATE, sampling_applied=False |
| Odds shift below noise floor (3 bps) discarded silently | odds_shift_bps=3, min_odds_shift_bps hard=5 | No ObservationReport; events_discarded_noise counter incremented |
| Stale feed > 300 s halts emissions for affected sport | feed_age_s=350, stale_feed_threshold_s hard=300 | STALE_DATA WARN logged; no ObservationReport emitted for that sport; stale_sports=['NBA'] |
| KillSwitch suppresses ObservationReport emissions | killswitch.active=true; LINEUP_CHANGE event arrives | Event ingested and normalised; no ObservationReport emitted; KILL_SWITCH_ACTIVE logged |
| Primary API down triggers fallback_to_web for affected sport | primary_league_api returns 503; fallback_to_web=true | Web extraction used; ObservationReport emitted with source_provider=web_fallback |
| Routine score-tick sampled 1/5 | 5 consecutive score-tick events, sampling rule sample-1/5 | Approximately 1 ObservationReport emitted with sampling_applied=True |
Integration Tests
| Test | Expected result |
|---|
| Full lifecycle: INJURY_UPDATE from league API reaches sports-model strategy | Strategy receives ObservationReport with INJURY_UPDATE and correct condition_id; reprices market |
| ws_sports in-play state update supplements API polling with lower latency | ObservationReport emitted within 2 s of ws_sports score event; source_provider=ws_sports |
| All enabled sports polled correctly on startup without errors | All sports in enabled_sports list polled; no stale_sports on first cycle |
Property Tests
| Property | Required behaviour |
|---|
| SportsFeed-Adapter never submits, signs, or modifies any order | Always true |
| No ObservationReport emitted when KillSwitch is active | Always true |
| No ObservationReport emitted when feed_age_s > stale_feed_threshold_s hard | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate 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.
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|
| 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 |