HomeBlog › Arbitrage and +EV Detection with pinnapi: What's Built In, What You Build, and the Code
API & Data

Arbitrage and +EV Detection with pinnapi: What's Built In, What You Build, and the Code

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:

  1. A reference layer — a fair probability to compare everything against.
  2. An event layer — knowing the moment the reference moves, because stale detection is no detection.
  3. 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

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