Skip to content

Commit eb4af9c

Browse files
Mlaz-codeclaude
andcommitted
docs(concepts): clarify Pinnacle odds_changed_at as trading-desk timestamp vs last_seen_at pipeline freshness
Adds a dedicated concept page explaining that odds_changed_at on Pinnacle rows is the book's own trading-desk timestamp — it carries forward when price/line/is_live are unchanged — and that last_seen_at is the pipeline freshness signal. Sharpens schema descriptions for both fields across every endpoint that emits them (odds, events-odds, odds-batch, odds-best, odds-comparison, stream) and adds a callout on the sportsbooks page near the Sharp Books table. odds_changed_at was missing from the REST odds schema entirely — now documented. Addresses repeat customer misreads (Dean Beluga SHA-1992, visavi/Ilya, Adam Esterle) where a stale-looking odds_changed_at on NBA/MLB markets was read as a pipeline gap. Per-sport Pinnacle CDN cadence table (soccer ~94% -> NBA ~9%) on the concept page makes the upstream variance explicit. Follow-up /health cadence panel filed as SHA-2050. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7c7d1d1 commit eb4af9c

9 files changed

Lines changed: 95 additions & 6 deletions

File tree

content/en/api-reference/events-odds.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ All responses include standard rate limit and metadata headers:
6161
| `odds.american` | number | American odds (e.g., -110, +150) |
6262
| `odds.decimal` | number | Decimal odds (e.g., 1.909) |
6363
| `odds.probability` | number | Implied probability (e.g., 0.5238) |
64-
| `last_seen_at` | string | ISO 8601 timestamp of when these odds were last seen/updated |
64+
| `last_seen_at` | string | ISO 8601 timestamp when our pipeline last observed this row. Use this as your pipeline freshness signal. |
65+
| `odds_changed_at` | string | ISO 8601 timestamp of when the price, line, or `is_live` flag last actually changed. Sportsbook-provided when available; on Pinnacle it carries forward across unchanged refreshes — see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at). |
6566

6667
## Example Requests
6768

content/en/api-reference/odds-batch.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ Each item in the `data.events` array is an event object with nested odds:
289289
| `odds_american` | number | American odds |
290290
| `odds_decimal` | number | Decimal odds |
291291
| `line` | number \| null | Line value |
292-
| `last_seen_at` | string | When odds were last seen/updated |
292+
| `last_seen_at` | string | ISO 8601 timestamp when our pipeline last observed this row. Use this as your pipeline freshness signal. |
293+
| `odds_changed_at` | string | ISO 8601 timestamp of when the price, line, or `is_live` flag last actually changed. Sportsbook-provided when available; on Pinnacle it carries forward across unchanged refreshes — see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at). |
293294

294295
## Use Cases
295296

content/en/api-reference/odds-best.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ X-Request-Id: req_best_789xyz
211211
| `all_books[].odds` | object | Odds object (`american`, `decimal`) |
212212
| `all_books[].edge` | number | Edge over the worst available odds (percentage points) |
213213
| `all_books[].line` | number \| null | Line at this sportsbook |
214-
| `all_books[].last_seen_at` | string | When this book's odds were last seen/updated |
214+
| `all_books[].last_seen_at` | string | When our pipeline last observed this book's row — pipeline freshness signal |
215+
| `all_books[].odds_changed_at` | string | When this book's price, line, or `is_live` flag last actually changed. On Pinnacle, carries forward across unchanged refreshes — see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at). |
215216
| `last_seen_at` | string | ISO 8601 timestamp of the best odds determination |
216217
| `player_name` | string\|undefined | Player name (player prop markets only) |
217218
| `stat_category` | string\|undefined | Stat category, e.g. `points`, `rebounds` (player prop markets only) |

content/en/api-reference/odds-comparison.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ Each entry in the `books` object:
253253
|-------|------|-------------|
254254
| `odds_american` | number | American odds |
255255
| `odds_decimal` | number | Decimal odds |
256-
| `last_seen_at` | string | When this book's odds were last seen/updated |
256+
| `last_seen_at` | string | When our pipeline last observed this book's row — pipeline freshness signal |
257+
| `odds_changed_at` | string | When this book's price, line, or `is_live` flag last actually changed. On Pinnacle, carries forward across unchanged refreshes — see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at). |
257258

258259
## Understanding Hold
259260

content/en/api-reference/odds.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ X-Request-Id: req_abc123def456
302302
| `odds_probability` | number | Implied probability (e.g., 0.5238) |
303303
| `line` | number \| null | Spread or total line value (`null` for moneyline) |
304304
| `event_start_time` | string | ISO 8601 event start time |
305-
| `last_seen_at` | string | ISO 8601 timestamp when odds were last seen/updated |
305+
| `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. |
306+
| `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. |
306307
| `is_live` | boolean | Whether the event is currently live |
307308
| `player_name` | string\|undefined | Player name (player prop markets only) |
308309
| `stat_category` | string\|undefined | Stat category, e.g. `points`, `rebounds` (player prop markets only) |

content/en/api-reference/sportsbooks.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,10 @@ The `requires_tier` field indicates the minimum subscription tier needed to acce
383383
| `pinnacle` | Pinnacle | Yes | Yes | **Sharp** |
384384
{/* AUTO:END:sharp-books */}
385385

386+
<Callout type="info">
387+
**Reading Pinnacle timestamps.** `odds_changed_at` on a Pinnacle row is their trading-desk timestamp, not our pipeline's. Pinnacle holds lines steady when the market has not moved — an idle value of 30+ minutes is common on MLB player props and NBA markets. Use `last_seen_at` for pipeline freshness. See [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at) for what "stale" actually means on their feed.
388+
</Callout>
389+
386390
### International
387391

388392
{/* AUTO:START:intl-books */}

content/en/api-reference/stream.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ data: {"odds":[{"id":"123456","odds_american":-150,"odds_decimal":1.667,"odds_pr
129129
| `odds_probability` | number | Updated implied probability (e.g. `0.6`) |
130130
| `line` | number \| null | Updated line/spread (e.g. `-3.5`), or `null` for moneyline |
131131
| `is_live` | boolean | Whether the event is currently live |
132-
| `odds_changed_at` | string | ISO 8601 timestamp of when the odds changed |
132+
| `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 underlying price/line/`is_live` flag are unchanged — see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at). |
133133

134134
**Envelope fields:**
135135

content/en/concepts/_meta.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export default {
44
arbitrage: "Arbitrage",
55
"event-matching": "Event Matching",
66
"live-vs-prematch": "Live vs. Pre-Match",
7+
"pinnacle-odds-changed-at": "Pinnacle `odds_changed_at`",
78
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
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."
3+
---
4+
5+
import { Callout } from 'nextra/components'
6+
7+
# Understanding Pinnacle's `odds_changed_at`
8+
9+
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.
10+
11+
## What `odds_changed_at` Actually Means
12+
13+
On Pinnacle rows, `odds_changed_at` is **Pinnacle's own trading-desk timestamp** — when Pinnacle last repriced this specific line.
14+
15+
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.
16+
17+
<Callout type="warning">
18+
`odds_changed_at` is **not** the last time our pipeline refreshed or touched this row. For pipeline freshness, use `last_seen_at`.
19+
</Callout>
20+
21+
## Why It Can Look Stale
22+
23+
Pinnacle is a market-maker. Their trading desk publishes a new price only when actual flow forces a re-price — they do not shade lines around retail action to squeeze margin the way soft books do. That discipline is the reason Pinnacle is used as the sharp reference for +EV calculations, but it also means lines can sit unchanged for long stretches.
24+
25+
Observed over a 24-hour window of Pinnacle's own CDN responses, the rate at which Pinnacle published *new* data (rather than a cached `304 Not Modified`) varies enormously by sport:
26+
27+
| Sport | Pinnacle CDN "new data" rate |
28+
|--------------|------------------------------|
29+
| Soccer | ~94% |
30+
| Tennis | ~66% |
31+
| NHL | ~51% |
32+
| MLB | ~18% |
33+
| NBA | ~9% |
34+
35+
NBA and MLB player props commonly show long idle windows — 30+ minutes is not unusual — because Pinnacle's trading desk is not moving the line. If you are seeing an old `odds_changed_at` on an NBA or MLB market, it is almost always Pinnacle's own publish cadence, not a gap in our pipeline.
36+
37+
## How to Read the Fields Together
38+
39+
Every odds row exposes two timestamps. They answer different questions:
40+
41+
| Field | What it tells you |
42+
|--------------------|----------------------------------------------------------------------|
43+
| `odds_changed_at` | The last time Pinnacle's trading desk moved this line |
44+
| `last_seen_at` | The last time our pipeline observed this row |
45+
46+
For pipeline freshness checks, use `last_seen_at` — this updates every time we ingest the row, regardless of whether the price moved.
47+
48+
For "when did Pinnacle last move this line," use `odds_changed_at`.
49+
50+
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.
51+
52+
```json
53+
{
54+
"sportsbook": "pinnacle",
55+
"market_type": "player_total_bases",
56+
"selection": "Edmundo Sosa Over",
57+
"line": 0.5,
58+
"odds_american": -129,
59+
"last_seen_at": "2026-04-21T21:35:02Z",
60+
"odds_changed_at": "2026-04-21T18:49:00Z"
61+
}
62+
```
63+
64+
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.
65+
66+
<Callout type="info">
67+
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.
68+
</Callout>
69+
70+
## Why Pinnacle Is Different
71+
72+
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.
73+
74+
## Related
75+
76+
- [Odds Snapshot](/en/api-reference/odds)`odds_changed_at` and `last_seen_at` fields on the REST odds response
77+
- [Streaming](/en/api-reference/stream)`odds_changed_at` on `odds:update` deltas
78+
- [Live vs. Pre-Match](/en/concepts/live-vs-prematch) — publish cadence and book delivery mechanisms
79+
- [EV Calculation](/en/concepts/ev-calculation) — why Pinnacle is the sharp reference

0 commit comments

Comments
 (0)