> ## Documentation Index
> Fetch the complete documentation index at: https://285e39fd5e337e58f16290.sightscreen.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Live score → lock screen

> How a live cricket score travels from SportMonks to the user's lock screen via the event-driven pipeline.

```mermaid theme={null}
sequenceDiagram
    participant SP as ScorePoller (5s / 2.5s)
    participant SM as SportMonks API
    participant EB as MatchEventBus
    participant AM as DynamoDB (active-matches)
    participant T0 as Tier0EventDetector
    participant T1 as Tier1EventDetector
    participant CF as CooldownFilter
    participant SF as StalenessFilter
    participant PB as pickBestEvent
    participant DSB as DisplayStateBuilder
    participant LAC as LACoordinator
    participant LA as DynamoDB (live-activities)
    participant APNs as ApnsDeliveryService
    participant Apple as Apple APNs (HTTP/2)
    participant iOS as iOS Device

    SP->>SM: Poll match state
    SM-->>SP: Raw match data
    SP->>EB: Feed raw data per match

    EB->>AM: Read previous state (scores, cooldowns, last 30 balls)
    AM-->>EB: Previous match state

    EB->>T0: Compare current vs previous scores
    T0-->>EB: Tier0 events (wicket, fifty, hundred, innings_end, match_end...)

    EB->>T1: Analyze ball-by-ball data
    T1-->>EB: Tier1 events (six, four, maiden, hattrick...)

    EB->>CF: Filter through cooldown map
    CF-->>EB: Non-duplicate events

    EB->>SF: Check staleness
    SF-->>EB: Non-stale events

    EB->>PB: Priority sort all candidates
    PB-->>EB: Single highest-priority keyEvent

    EB->>DSB: Build UnifiedDisplayState
    DSB-->>EB: displayState + keyEvent

    EB->>AM: Write updated state (scores, cooldowns, balls)
    EB->>LAC: Emit MatchBusEvent

    LAC->>LA: Query tokens where status=active for matchId
    LA-->>LAC: List of active tokens

    LAC->>LAC: Build ONE APNs payload from displayState
    LAC->>LAC: keyEvent + isAlertWorthy → P10 (alert) or P5 (silent)

    LAC->>APNs: sendBatch(tokens, payload)
    APNs->>Apple: HTTP/2 push per token
    Apple-->>iOS: Push notification
    iOS->>iOS: ActivityKit updates Lock Screen + Dynamic Island

    APNs-->>LAC: Invalid token responses
    LAC->>LA: Mark status=ended, endReason=token_invalid
```

## How it works

The live score pipeline is fully event-driven. There is no monolithic job -- instead, a chain of specialized components each do one thing.

### Two pollers, two cadences

| Poller                       | Cadence     | Data                                        | Matches                     |
| ---------------------------- | ----------- | ------------------------------------------- | --------------------------- |
| **ScorePoller** (Poller 1)   | 5 seconds   | Scores, batting/bowling stats, match status | All live matches            |
| **MarqueePoller** (Poller 2) | 2.5 seconds | Everything above + ball-by-ball data        | Current marquee league only |

Both pollers are instances of the same reusable `ScorePoller` class. The marquee poller fetches the `balls` include from SportMonks, which provides ball-by-ball data needed for Tier1 event detection (sixes, fours, maidens, hat-tricks).

For non-marquee matches, only Tier0 (score-based) events are detected.

### MatchEventBus: the detection pipeline

The EventBus receives raw match data and runs it through a four-stage detection pipeline:

<Steps>
  <Step title="Tier0EventDetector (score-based)">
    Compares current scores against previous scores stored in `active-matches`. Detects: `match_end`, `innings_end`, `hundred`, `batsman_on_90s`, `fifty`, `batsman_on_fire`, `bowler_on_fire`, `wicket`, `new_batsman`.
  </Step>

  <Step title="Tier1EventDetector (ball-based)">
    Analyzes the last 30 balls stored in `active-matches`. Detects: `hattrick`, `on_hattrick`, `consecutive_sixes`, `maiden`, `six`, `four`. Only runs for marquee league matches (requires ball data).
  </Step>

  <Step title="CooldownFilter">
    Suppresses duplicate events using a cooldowns map stored on the `active-matches` record. For example, a `fifty` event for the same batsman won't fire again within the cooldown window.
  </Step>

  <Step title="StalenessFilter">
    Ignores events that are stale given the current state. For example, a `fifty` event is stale if the batsman already has 100 -- the `hundred` event takes priority.
  </Step>
</Steps>

After filtering, `pickBestEvent` priority-sorts all remaining candidates and selects the single highest-priority event as the `keyEvent`.

### Event priorities and alert worthiness

Events are sorted by priority (lower number = higher priority):

| Priority | Event               | Alert worthy?  |
| -------- | ------------------- | -------------- |
| P1       | `match_end`         | Yes (P10 push) |
| P2       | `innings_end`       | Yes            |
| P3       | `hattrick`          | Yes            |
| P4       | `on_hattrick`       | Yes            |
| P5       | `hundred`           | Yes            |
| P6       | `batsman_on_90s`    | Yes            |
| P7       | `consecutive_sixes` | Yes            |
| P8       | `fifty`             | Yes            |
| P9       | `batsman_on_fire`   | Yes            |
| P10      | `bowler_on_fire`    | Yes            |
| P11      | `wicket`            | Yes            |
| P12      | `new_batsman`       | Yes            |
| P13      | `maiden`            | Yes            |
| P14      | `six`               | No (P5 silent) |
| P15      | `four`              | No (P5 silent) |

<Note>
  **Alert-worthy events** are sent as P10 (high priority) APNs pushes that wake the screen and show a banner. **Non-alert events** (six, four) are sent as P5 (silent) pushes that update the Live Activity without disturbing the user.
</Note>

### DisplayStateBuilder

`DisplayStateBuilder` takes the raw match data and the selected `keyEvent` and builds a `UnifiedDisplayState`. This is the single payload shape that iOS renders on the Lock Screen and Dynamic Island. It includes: team names, scores, overs, batting/bowling stats, recent balls, run rate, chase info, and the key event label.

### LACoordinator: fan-out to devices

The `LACoordinator` listens for `MatchBusEvent` emissions from the EventBus:

1. Queries `live-activities` table for all records with `matchId` and `status=active`
2. Builds a single APNs payload from the `UnifiedDisplayState`
3. Sets push priority based on `isAlertWorthy` (P10 or P5)
4. Calls `sendBatch` via `ApnsDeliveryService` for all tokens
5. Handles invalid token responses by marking records as `status=ended, endReason=token_invalid`

<Tip>
  The LACoordinator builds ONE payload per match, not per user. Every follower of the same match gets the identical payload. This is a deliberate simplification -- there is no team-centric perspective switching at the push level.
</Tip>

## Resilience

### Circuit breaker

The SportMonks client uses [cockatiel](https://github.com/connor4312/cockatiel) for circuit breaking. If the API returns repeated 5xx errors, the circuit opens and requests are short-circuited for a cooldown period. This prevents hammering a degraded upstream.

### Distributed locking

Each poller acquires a distributed lock before polling to prevent duplicate processing when multiple backend instances are running. If the lock is held by another instance, the poller skips that cycle.

<Warning>
  Both pollers run independently with separate locks. The marquee poller and the main poller can process the same match concurrently for marquee league matches. The EventBus handles this gracefully -- the active-matches record is the single source of truth for previous state, so duplicate raw data just produces no-op detections.
</Warning>

## Key tables

| Table             | Key schema                           | Purpose                                                            |
| ----------------- | ------------------------------------ | ------------------------------------------------------------------ |
| `active-matches`  | PK: `matchId`                        | Tracks polling state, previous scores, cooldown map, last 30 balls |
| `live-activities` | PK: `matchId`, SK: `userId#deviceId` | Maps users + devices to active LA tokens                           |

## Related pages

* [Live Activity lifecycle](/business-flows/live-activity-lifecycle) -- how LAs are created, updated, and ended
* [Match following](/business-flows/match-following) -- what triggers the LA creation that feeds into this pipeline
* [Device registration](/business-flows/device-registration) -- how push tokens get registered in the first place
