|
| 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