Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions content/en/api-reference/odds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ The example below shows the flat fields only. As of May 2026, every row also car
"line": null,
"event_start_time": "2026-01-26T19:00:00Z",
"last_seen_at": "2026-01-26T02:10:24.125Z",
"wire_received_at": "2026-01-26T02:10:24.087Z",
"odds_changed_at": "2026-01-26T02:09:51.000Z",
"is_live": false
},
{
Expand All @@ -217,6 +219,8 @@ The example below shows the flat fields only. As of May 2026, every row also car
"line": null,
"event_start_time": "2026-01-26T19:00:00Z",
"last_seen_at": "2026-01-26T02:10:24.125Z",
"wire_received_at": "2026-01-26T02:10:24.087Z",
"odds_changed_at": "2026-01-26T02:09:51.000Z",
"is_live": false
}
],
Expand Down Expand Up @@ -314,8 +318,9 @@ X-Request-Id: req_abc123def456
| `odds_probability` | number | Implied probability (e.g., 0.5238) |
| `line` | number \| null | Spread or total line value (`null` for moneyline) |
| `event_start_time` | string | ISO 8601 event start time |
| `last_seen_at` | string | ISO 8601 timestamp of our adapter's last observation of this row. Updates every ingest cycle — this is the pipeline freshness signal. |
| `odds_changed_at` | string | ISO 8601 timestamp of the sportsbook's own source update for this line, when available. On Pinnacle, carries forward while the price/line/`is_live` flag are unchanged (see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at/)). Use `last_seen_at` for pipeline freshness. |
| `last_seen_at` | string | ISO 8601 timestamp of our adapter's last observation of this row. Advances every ingest cycle even when the content is unchanged — use as a heartbeat that the row is still being maintained, not as an ingest-latency benchmark. |
| `wire_received_at` | string\|undefined | ISO 8601 timestamp of when SharpAPI first observed a content change for this row, carried forward across subsequent unchanged refreshes. **Use this field for ingest-latency benchmarking** — it isolates SharpAPI's pipeline contribution from the sportsbook's source-side publish cadence. Omitted on cold-start rows where no prior observation exists. See [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at/#benchmarking-pipeline-latency). |
| `odds_changed_at` | string | ISO 8601 timestamp of the sportsbook's own source update for this line, when available. On Pinnacle, carries forward while the price/line/`is_live` flag are unchanged (see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at/)). Not suitable for SharpAPI pipeline-latency benchmarking — use `wire_received_at` for that. |
| `is_live` | boolean | Whether the event is currently live |
| `player_name` | string\|undefined | Player name (player prop markets only) |
| `stat_category` | string\|undefined | Stat category, e.g. `points`, `rebounds` (player prop markets only) |
Expand Down
47 changes: 31 additions & 16 deletions content/en/concepts/pinnacle-odds-changed-at.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
description: "How Pinnacle's odds_changed_at field behaves — why it is the trading-desk timestamp, why it can look stale on MLB and NBA, and how to read it alongside last_seen_at as a pipeline freshness signal."
description: "How Pinnacle's odds_changed_at field behaves — why it is the trading-desk timestamp, why it can look stale on MLB and NBA, and which field (wire_received_at) to use when benchmarking SharpAPI's pipeline latency."
---

import { Callout } from 'nextra/components'

# Understanding Pinnacle's `odds_changed_at`

Sharp customers building +EV and arbitrage tooling against Pinnacle frequently ask the same question: "why does `odds_changed_at` on this Pinnacle row look 20 minutes old?" The short answer is that it is working as designed — it carries Pinnacle's own trading-desk timestamp, not ours. This page explains what the field actually tracks, why it can sit unchanged for long stretches, and how to pair it with `last_seen_at` for a clean read on pipeline freshness.
Sharp customers building +EV and arbitrage tooling against Pinnacle frequently ask the same question: "why does `odds_changed_at` on this Pinnacle row look 20 minutes old?" The short answer is that it is working as designed — it carries Pinnacle's own trading-desk timestamp, not ours. This page explains what the field actually tracks, why it can sit unchanged for long stretches, and which timestamp to use when benchmarking SharpAPI's pipeline latency (spoiler: it is `wire_received_at`, not `odds_changed_at`).

## What `odds_changed_at` Actually Means

Expand All @@ -15,7 +15,7 @@ On Pinnacle rows, `odds_changed_at` is **Pinnacle's own trading-desk timestamp**
It carries forward unchanged whenever Pinnacle signals that the price, line, and `is_live` flag on a market have not moved. Internally SharpAPI hashes those three fields for every odds row on every refresh; when the hash is identical to the previous snapshot, we keep the previous `odds_changed_at` rather than overwriting it with the observation time. This preserves the "when did this line last move" semantic across repeated polls of an unchanged market.

<Callout type="warning">
`odds_changed_at` is **not** the last time our pipeline refreshed or touched this row. For pipeline freshness, use `last_seen_at`.
`odds_changed_at` is **not** the last time our pipeline refreshed or touched this row, and it is **not** the right field for benchmarking SharpAPI's ingest latency. For "when did SharpAPI first observe this content," use `wire_received_at`. For "is this row still being kept alive by the pipeline," use `last_seen_at`. See [Benchmarking Pipeline Latency](#benchmarking-pipeline-latency) below.
</Callout>

## Why It Can Look Stale
Expand All @@ -36,18 +36,21 @@ NBA and MLB player props commonly show long idle windows — 30+ minutes is not

## How to Read the Fields Together

Every odds row exposes two timestamps. They answer different questions:
Every odds row exposes three timestamps. Each answers a different question:

| Field | What it tells you |
|--------------------|----------------------------------------------------------------------|
| `odds_changed_at` | The last time Pinnacle's trading desk moved this line |
| `last_seen_at` | The last time our pipeline observed this row |

For pipeline freshness checks, use `last_seen_at` — this updates every time we ingest the row, regardless of whether the price moved.
| Field | What it tells you |
|---------------------|----------------------------------------------------------------------------------------------------|
| `odds_changed_at` | The last time Pinnacle's trading desk moved this line (their source timestamp; can be hours old). |
| `wire_received_at` | The last time SharpAPI's pipeline first observed a content change for this row. |
| `last_seen_at` | The last time the pipeline touched this row at all (advances every ingest cycle). |

For "when did Pinnacle last move this line," use `odds_changed_at`.

A large gap between the two (fresh `last_seen_at`, old `odds_changed_at`) means Pinnacle is holding the line steady. This is normal and is the single most common source of confusion when reading Pinnacle data.
For "when did this content arrive at SharpAPI" (ingest-latency benchmarking), use `wire_received_at`.

For a heartbeat-style check ("is the row still being maintained by the pipeline"), use `last_seen_at`.

A large gap between `wire_received_at` and `odds_changed_at` (fresh `wire_received_at`, old `odds_changed_at`) means Pinnacle is holding the line steady — the pipeline has the row fresh, the trading desk just has not moved it. This is normal and is the single most common source of confusion when reading Pinnacle data.

```json
{
Expand All @@ -56,24 +59,36 @@ A large gap between the two (fresh `last_seen_at`, old `odds_changed_at`) means
"selection": "Edmundo Sosa Over",
"line": 0.5,
"odds_american": -129,
"last_seen_at": "2026-04-21T21:35:02Z",
"odds_changed_at": "2026-04-21T18:49:00Z"
"last_seen_at": "2026-04-21T21:35:02Z",
"wire_received_at": "2026-04-21T21:34:58Z",
"odds_changed_at": "2026-04-21T18:49:00Z"
}
```

In this example the pipeline saw this row 4 seconds before the client fetched it (`last_seen_at` is fresh). The price itself last moved 2h 46m earlier (`odds_changed_at`), because Pinnacle's trading desk has not repriced Sosa's total-bases line since pre-market open. Both values are correct.
In this example the pipeline last touched this row 4 seconds before the client fetched it (`last_seen_at`), and last saw a content change 8 seconds before (`wire_received_at`). The price itself last moved 2h 46m earlier (`odds_changed_at`), because Pinnacle's trading desk has not repriced Sosa's total-bases line since pre-market open. All three values are correct.

<Callout type="info">
If `last_seen_at` is stale (more than a minute or two old for a major-league market), that is a pipeline signal worth investigating. If `odds_changed_at` is stale but `last_seen_at` is fresh, Pinnacle is holding the line — treat the displayed price as current.
If `wire_received_at` and `last_seen_at` are both stale (more than a minute or two old for a major-league market), that is a pipeline signal worth investigating. If only `odds_changed_at` is stale, Pinnacle is holding the line — treat the displayed price as current.
</Callout>

## Benchmarking Pipeline Latency

If you are comparing SharpAPI's Pinnacle delivery latency against another vendor, the field to measure against is **`wire_received_at`**, not `odds_changed_at`. The difference matters a lot:

- `odds_changed_at` is Pinnacle's own source timestamp. Computing `now - odds_changed_at` measures **Pinnacle's publish cadence plus our pipeline lag** — including the long idle windows from the cadence table above. On NBA player props this can routinely sit hours old because Pinnacle has not moved the line, not because we are slow.
- `wire_received_at` is stamped by SharpAPI at the moment we first observe a content change for the row, then carried forward across subsequent unchanged refreshes. Computing `now - wire_received_at` (or `wire_received_at - odds_changed_at`) isolates SharpAPI's ingest contribution from Pinnacle's trading-desk cadence.

Vendors do not all expose their internal arrival stamp explicitly, and some surface their own ingest time under the `odds_changed_at` name. Benchmarking SharpAPI's `odds_changed_at` against another vendor's `odds_changed_at` without checking the semantic on each side is comparing the time-since-source-update on one platform to the time-since-ingest on another, which structurally makes the platform that preserves source semantics look slower even when the pipeline is faster.

For an internal counterpart to per-row `wire_received_at`, the `/health` endpoint exposes Pinnacle's CDN publish cadence per sport (`cdn_fetch_pct_1h`, `cdn_fetch_pct_24h`). Low values for a given sport indicate the trading desk is holding lines steady; high values indicate active repricing. This panel lets you separate "Pinnacle is quiet" from "pipeline is stuck" without having to instrument anything yourself.

## Why Pinnacle Is Different

Pinnacle accepts sharp action and re-prices based on real flow rather than shading lines around retail bettors. That market-maker discipline is why we use them as the devig reference for +EV — their lines are the closest thing to a fair price available in the market. The tradeoff is that pre-match lines can sit unchanged for long stretches when nothing in the market has moved, which looks like staleness to anyone expecting the constant micro-adjustment that soft books do.

## Related

- [Odds Snapshot](/en/api-reference/odds/) — `odds_changed_at` and `last_seen_at` fields on the REST odds response
- [Odds Snapshot](/en/api-reference/odds/) — `odds_changed_at`, `wire_received_at`, and `last_seen_at` fields on the REST odds response
- [Streaming](/en/api-reference/stream/) — `odds_changed_at` on `odds:update` deltas
- [Live vs. Pre-Match](/en/concepts/live-vs-prematch/) — publish cadence and book delivery mechanisms
- [EV Calculation](/en/concepts/ev-calculation/) — why Pinnacle is the sharp reference