Pinnacle odds proxy with FastAPI
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
429withRetry-Afterfrom 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.