---
name: project-pin-line-staleness
description: Our "Pinnacle" feed is API-Football's relay updating every ~5.8h; xlow loses on stale lines (-42%) and wins on fresh ones (+3% / censored +40.6%)
metadata:
  type: project
---

Measured 2026-07-28. NOT YET WIRED — user chose to hold off. This is the queued
experiment.

**The feed is not Pinnacle direct.** `live_monitor.fetch_prematch_odds()` (~line
665) hits API-Football's `/odds` endpoint and picks the bookmaker named
"Pinnacle". Measured update frequency across 147 fixtures (median 91 pre-match
snapshots each): **API-Football "Pinnacle" changes once every ~5.8h; Kalshi's own
mid changes every ~2.3h.** The sharpest book in the world repricing half as often
as a retail prediction market is a relay artifact, not a market fact.

**Consequence, measured on the OOS forward test (150 bets),
`research/xlow_vs_line_age.py`.** Line age = hours since that fixture's
(pin_home, pin_draw, pin_away) triple last changed.

| line age at entry | n | ROI |
|---|---|---|
| 0-1h | 41 | -5.5% |
| 1-3h | 40 | -21.4% |
| 3-6h | 9 | -45.5% |
| 6h+ | 5 | -48.7% |
| censored (never seen move; median 24h to KO) | 55 | **+40.6%, z=+2.14** |

Monotone dose-response. **Controlling for time-to-kickoff** (both groups entered
>3h out, n=91): fresh **+3.0%** vs stale **-42.0%**. Age buckets do not track
hours-to-KO (the 6h+ bucket sits at 11.6h to KO, same as 1-3h), so this is not a
kickoff-timing effect.

**Reading:** xlow is two strategies stapled together. Against a fresh line it is
plausibly positive; against a stale one the "CLV" is just lag — the exchange has
repriced and Pinnacle has not — and it loses ~40%. The whole +3.3% headline is
the blend. This likely also explains [[project-kalshi-drift-to-fair]] finding no
general drift toward "fair": a 6h-old anchor cannot lead anything.

**Two queued actions, in order:**
1. **Free, no feed needed:** gate xlow on line age (refuse to fire when Pinnacle
   has not moved in ~1.3h) and log line age on every signal for forward
   confirmation. On this sample that removes the entire losing half.
2. **Paid:** real-time Pinnacle. Pinnacle closed its public API 2025-07-23, so
   only relays remain. Capture script is already written and waiting for a key:
   `main/pin_fresh_capture.py` (pinnapi free tier = 100 req/day; one call returns
   the whole soccer slate, so POLL_MINUTES=20 → 72 calls/day). Analysis harness
   ready at `research/pin_fresh_vs_stale.py`. Needs `PINNAPI_KEY` in `.env`.
   Alternatives with Pinnacle: pinnodds, SharpAPI, SportsGameOdds ($99-499/mo),
   OddsPapi (historical included). **The Odds API does NOT carry Pinnacle.**

**Calibrate expectations before paying:** even the freshest *watched* bucket is
only +3.0%. A live feed plausibly turns this from losing into marginal, not into
a machine. The +40.6% censored group's freshness is inferred, not observed.

**Caveat:** subgroup split on 150 bets; only the censored bucket is individually
significant. Stronger than a fished result — both the firing pattern and the
performance gradient were predicted from the staleness measurement before the
test was run — but it needs forward confirmation.

**Also relevant:** the API-Football plan expires **2026-08-07**, so a feed
decision is due regardless.
