On the wire · 04:46:53 UTC Live 550 · Prematch 14,364 · Markets 56,961 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Tutorial · Python · FastAPI

Pinnacle odds proxy with FastAPI

Updated · PinnFeed · 2 min read

Answer: run a tiny FastAPI app that reads PINNFEED_KEY from the environment, calls /v1/odds with httpx, caches the result for a few seconds, and returns only the fields your page needs. The browser talks to your API; the PinnFeed key never leaves the server, and the cache keeps many visitors inside your plan's rate limit.

app.py#

import os, time
import httpx
from fastapi import FastAPI, HTTPException

KEY = os.environ["PINNFEED_KEY"]            # ps_live_...
API = "https://pinnfeed.io/v1"
TTL = 5                                     # seconds
cache: dict[str, tuple[float, list]] = {}
app = FastAPI()
client = httpx.AsyncClient(headers={"x-api-key": KEY}, timeout=10)

@app.get("/odds/{sport}")
async def odds(sport: str, q: str | None = None):
    ck = f"{sport}|{q}"
    hit = cache.get(ck)
    if hit and time.time() - hit[0] < TTL:
        return hit[1]
    params = {"sport": sport, "market": "moneyline", "main": "true", "limit": 100}
    if q:
        params["q"] = q
    r = await client.get(f"{API}/odds", params=params)
    if r.status_code != 200:
        raise HTTPException(r.status_code, r.json().get("error", {}).get("message", "upstream error"))
    rows = [
        {"id": e["id"], "home": e["home"], "away": e["away"], "start": e["startTime"], "live": e["live"],
         "prices": {o["name"]: o["decimal"] for m in e["markets"] for o in m["outcomes"]},
         "maxStake": max((m["maxStake"] or 0) for m in e["markets"]) if e["markets"] else None}
        for e in r.json()["events"]
    ]
    cache[ck] = (time.time(), rows)
    return rows

Run with pip install fastapi uvicorn httpx and uvicorn app:app --port 8000, then call /odds/soccer?q=arsenal. Unknown sports return PinnFeed's invalid_filter message as a 400.

Production notes#

  • Add your own auth or a rate limit on the proxy if it is public.
  • Respect 429 with Retry-After from PinnFeed; plans allow 1 (Free), 2 (Hobby), 10 (Builder) or 30 (Pro) req/s.
  • For live boards, keep one WebSocket on the server (Builder) and fan out yourself instead of polling: Python WebSocket.

Plans#

Free REST is enough to build it (30 s delay, 2 sports, 1,000/day). Builder (€99/mo) for real-time push. 3-day trial; a card is required and it is charged after day 3 unless you cancel. Other hosts: Cloudflare Worker, Supabase.

Educational content for adults (18+).

Related: Pinnacle odds in Next.js (App Router).

Questions people ask#

Why not just call PinnFeed from JavaScript?#

CORS allows it, but the key would be public: browser JavaScript and CORS.

Does the cache make odds stale?#

By up to TTL seconds. Use 1–5 s for prematch and the WebSocket for live.

Can I add Flask instead?#

Yes; the pattern is the same with requests and a dict cache.

The WebSocket plan: Builder, €99/mo.

Real-time Pinnacle odds pushed over one WebSocket, all 13 sports, every market, no-vig fair odds and closing lines. 3 days free, cancel in one click.

Form K-1 · Free API key

Get your free API key.

One field. No password, no card: enter your email, click the link we send, and your key is on screen.

By continuing you agree to the Terms and Privacy Policy.

One free key per email. Link valid for 20 minutes.