# Live Intelligence

In-play state for football, with momentum, a timeline, and — as an add-on — the
per-minute statistical breakdown.

Requires an active `live_intelligence` subscription.

## Endpoints

| | Requires |
|---|---|
| `GET /live` | `live_intelligence` |
| `GET /live/{id}` | + `live_match` |
| `GET /live/{id}/timeline` | + `live_match` |
| `GET /live/{id}/momentum` | + `momentum` |
| `GET /live/{id}/stats` | + `advanced_stats` **(add-on)** |

`live_match` and `momentum` come with the subscription. `advanced_stats` is sold
separately — it is the per-minute series and the per-period totals.

## How your live data is produced

Your own provider credential fetches the in-play list; your own workspace stores
it; your own process computes over it. Two customers watching the same match get
two independent series.

Each pass replays a bounded window of retained payloads before processing the
newest one. That is what makes momentum reproducible: the result is a function of
stored input, not of how long a process has been running.

## Momentum

```json
{
  "fixture_id": "fx_…",
  "momentum": {
    "momentum": {
      "home": 61, "away": 39, "leader": "home",
      "strength": "clear", "scope": "match", "feed_age_ms": 9400
    }
  },
  "unavailable_reason": null,
  "statistics_url": "https://api.helixodds.com/api/v1/live/fx_…/stats"
}
```

`home` and `away` sum to 100 and describe pressure over a rolling window, not a
probability of winning.

**`scope` always travels with the value.** Momentum is computed within one
fixture and is not comparable between fixtures — 61 in a cup final and 61 in a
reserve-league match are not the same quantity.

### When momentum is not available

```json
{ "momentum": null, "unavailable_reason": "insufficient_samples" }
```

`momentum: null` with a reason, never a neutral number. A momentum of 50 that
means "we do not know" is indistinguishable from one that means "balanced", and a
consumer cannot tell them apart. So we do not produce it.

The first snapshot of a fixture is a **baseline**, not a delta. Seeing
`dangerous_attacks 38-24` for the first time does not mean 38 attacks just
happened; it means the match arrived at that state before we were watching.

## Timeline

```json
{
  "timeline": [
    { "at": "2026-10-08T20:14:02.000Z", "minute": 63, "second": 12,
      "state": "PRESSURE", "certainty": "INFERRED", "side_proven": "home",
      "until": "2026-10-08T20:14:31.000Z" }
  ]
}
```

One entry per **change** of state, not one per sample. `until` extends while the
state holds.

### Three levels of confidence

| `certainty` | Meaning |
|---|---|
| `EXACT` | Anchored to a provider event with an event time. |
| `INFERRED` | Derived from counters moving between samples. |
| `UNKNOWN` / stale | The feed has not moved recently enough to say anything. |

`EXACT` requires an event time from the provider. Without one, the most we can
honestly say is `INFERRED` — something changed between two samples and we do not
know precisely when.

A stale state is reported as stale rather than held as the last known good one. A
match that stops appearing in the feed does not keep its state indefinitely; a
tracker that showed a confident state over a dead feed would be worse than one
that said nothing.

## Statistics (add-on)

```json
{
  "series": { "maxi": 14, "tm_max": 78,
              "kova": [{ "tm": 63, "h": 4, "a": 1, "state": "VLERE" }] },
  "periods": { "pjesa_1": { "state": "VLERE", "home": 21, "away": 9 } }
}
```

Per-minute buckets and per-period totals.

**`state` travels with every bucket**, and it is the field that matters most:

| `state` | Meaning |
|---|---|
| `VLERE` | A real value. |
| `ZERO` | A real zero — this happened zero times. |
| `MUNGON` | The provider did not send the field. |
| `NULL` | The provider sent the field with no value. |

`ZERO` and `MUNGON` are never merged. "Nothing happened" and "we do not know" are
different facts, and a chart that drew both as a zero would be lying about one of
them.

*(These four state names are the pipeline's own vocabulary and are returned
verbatim. They are frozen; renaming them would be a breaking change.)*

## Freshness

```json
{ "provider_fetched_at": "…", "processed_at": "…", "age_ms": 9400 }
```

`feed_age_ms` on momentum is how old the newest sample is. The cadence is around
8 seconds plus latency, so a healthy `age_ms` is in the tens of seconds. Minutes
mean something is wrong upstream.

## Football only

eSoccer is filtered at the entry to the pipeline, not in the response. Its clock
is accelerated and its counters behave differently — measured: `possession_rt`
has no coverage at all, and `dangerous_attack` sits at 50.3% against 55.4% in
real football. Mixing it in would corrupt every comparison the pipeline makes.

Nothing below that filter has ever seen an eSoccer fixture.

## A fixture outside the detail set

The number of fixtures carrying full detail in each snapshot is bounded, ordered
by how far momentum has moved from neutral. A fixture in `/live` but outside that
set returns `DATA_NOT_READY` on `/timeline`, `/momentum` and `/stats`.

That is deliberate: an empty timeline would read as "nothing happened".

## What this is not

It is not a prediction. It produces no probability of a result, no expected
goals model and no recommendation. It reports what the feed shows, how confident
it is, and when it does not know.

See also: [Public API](public-api-v1.md) · [Data contract](data-contract-v1.md)
