Arbitrage and +EV Detection with pinnapi: What's Built In, What You Build, and the Code
Arbitrage and +EV detection on a Pinnacle feed: the no-vig and drop layers run server-side, the comparison loop you own — with Python, Node, and Go starters.
Arbitrage and +EV Detection with pinnapi: What's Built In, What You Build, and the Code
Every arbitrage or +EV pipeline on earth has the same three layers, whoever you buy your data from:
- A reference layer — a fair probability to compare everything against.
- An event layer — knowing the moment the reference moves, because stale detection is no detection.
- A comparison layer — soft-book prices, edge math, and the decision to act.
pinnapi runs the first two server-side, built in. The third is yours — deliberately, and we'll be precise about why, because "built-in arbitrage detection" means different things from different vendors and the difference is your architecture.
Layer 1 (built in): no-vig fair odds — the +EV reference
+EV detection is a comparison against fair probability, and fair probability comes from stripping the margin off Pinnacle's sharp line. pinnapi computes this server-side: every drop alert carries nvp — the no-vig fair price for the outcome that moved, derived from the fresh post-drop market — so the reference half of every +EV check — "the true probability is 51%" — arrives precomputed in the payload. No client-side vig math, no normalization bugs, no mixing up totals lines.
Layer 2 (built in): server-side drop detection
Detection dies when it's late. The naive design — poll, diff snapshots, repeat — puts your polling interval between you and every move that matters. pinnapi inverts it: the SSE drop stream watches every Pinnacle price server-side and pushes an event the instant a move crosses your threshold (min_drop, with an optional prematch recheck that confirms a drop is stable before alerting). Delivery runs ~15–40 ms end-to-end, measured reproducibly. You can't diff snapshots you haven't fetched — server-side detection is the one layer polling architectures structurally cannot replicate.
Layer 3 (yours): the comparison loop
An arbitrage is a relationship between two prices — the sharp reference and a soft counterparty. pinnapi is Pinnacle-only by design, so the soft-book side comes from your slower, wider source (an aggregator works fine — it only needs to be fresh-ish, not fast). Your loop: drop event arrives → fetch/lookup the soft price → compare against no-vig fair value → act if the edge clears your threshold, with the honest execution caveats.
Why don't we do this part too? Because the comparison loop is the strategy — the thresholds, which books, which markets, when to trust a drop. Scanner products (SharpAPI-style) run this loop for you across their book list and sell you the output; the trade is that every subscriber sees the same opportunities and your edge definition is their config, not yours. Builders keep layer 3 in their own repo. That's the line between a detection platform and a detection feed — we're the feed, on purpose.
The code
The same starter in the three languages we get asked about most. Each connects to the drop stream, applies the +EV check against the bundled no-vig price, and leaves a TODO where your soft-book lookup goes.
Python
import json, os, requests
from sseclient import SSEClient
KEY = os.environ["PINNAPI_KEY"]
EDGE_MIN = 0.03 # act at +3% EV — your call, argued in the value-betting post
resp = requests.get(
f"https://pinnapi.com/odds-drop?key={KEY}&min_drop=3", # server-side threshold
stream=True,
)
for event in SSEClient(resp).events():
payload = json.loads(event.data)
if isinstance(payload, dict): # {"type": "connected"} handshake / errors
continue
for drop in payload: # each frame carries an array of alerts
fair_p = 1 / drop["nvp"] # no-vig fair probability, precomputed
soft_odds = get_soft_price(drop) # TODO: your aggregator / book lookup
ev = fair_p * soft_odds - 1
if ev >= EDGE_MIN:
alert(drop, soft_odds, ev)
Node.js
import { EventSource } from "eventsource";
const EDGE_MIN = 0.03;
const es = new EventSource(
`https://pinnapi.com/odds-drop?key=${process.env.PINNAPI_KEY}&min_drop=3`,
);
es.onmessage = async (e) => {
const payload = JSON.parse(e.data);
if (payload.type === "connected") return; // first frame is a handshake
for (const drop of payload) { // frames carry arrays of alerts
const fairP = 1 / drop.nvp; // no-vig fair probability, precomputed
const softOdds = await getSoftPrice(drop); // TODO: your soft-book side
const ev = fairP * softOdds - 1;
if (ev >= EDGE_MIN) alert(drop, softOdds, ev);
}
};
Go
package main
import (
"bufio"
"encoding/json"
"net/http"
"os"
"strings"
)
const edgeMin = 0.03
type Alert struct {
Home string `json:"home"`
Away string `json:"away"`
Outcome string `json:"outcome"`
FromPrice float64 `json:"from_price"`
ToPrice float64 `json:"to_price"`
Nvp float64 `json:"nvp"` // no-vig fair price, computed server-side
}
func main() {
url := "https://pinnapi.com/odds-drop?key=" + os.Getenv("PINNAPI_KEY") + "&min_drop=3"
resp, _ := http.Get(url)
defer resp.Body.Close()
sc := bufio.NewScanner(resp.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var drops []Alert // each frame is an array; the handshake frame won't parse
if err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data:")), &drops); err != nil {
continue
}
for _, d := range drops {
soft := getSoftPrice(d) // TODO: your soft-book side
if (1/d.Nvp)*soft-1 >= edgeMin {
alertDrop(d, soft)
}
}
}
}
A few dozen lines each, and the two hard layers — fair-value math and low-latency move detection — never appear in them, because they already happened server-side. The full Python build, with the execution risks that decide whether it makes money, is the long version.
Honest scope, one more time
- Built in: no-vig fair prices on every drop alert; server-side drop detection with your
min_dropthreshold and prematchrecheck; push delivery at measured latency. - Yours: soft-book prices, the edge threshold, staking, execution — and the variance and limiting realities no API layer can absorb.
- Not us: if you want cross-book opportunities surfaced with zero code across US books, that's a scanner product, and you should buy one knowingly rather than discover the difference mid-build.
FAQ
Does pinnapi have built-in arbitrage detection? It builds in the reference layers — no-vig fair odds and real-time drop detection at your threshold. The cross-book comparison is your loop, by design, because that loop is where your strategy's actual decisions live.
Does pinnapi have +EV detection? The fair-value side, yes — every drop alert arrives with its no-vig fair price computed server-side. Add any soft-book price and +EV detection is the one-line comparison shown in the code above.
What languages can I use with pinnapi? Anything that speaks HTTP/SSE/WebSocket — the examples above are Python, Node.js, and Go, which cover most of what we see; the payloads are plain JSON.
How do I set the drop threshold? Pass min_drop to the stream and the server filters before sending. Choosing the value is a strategy decision — the SSE build post argues through justified thresholds rather than magic numbers.
Get real-time Pinnacle odds in your code
Live & prematch markets with sub-second odds-drop alerts. Free trial key in seconds — no card.
Start free trial