Pinnacle odds in Next.js (App Router)
Answer: add a route handler at app/api/odds/route.ts that reads PINNFEED_KEY from the server environment, calls GET https://pinnfeed.io/v1/odds with the x-api-key header, revalidates every few seconds and returns only the fields your page needs. The browser calls /api/odds, so the key never ships in client JavaScript. Serverless functions do not stay alive to hold a WebSocket, so for live push run a small long-lived worker on Builder and let Next.js read from it.
app/api/odds/route.ts#
// PINNFEED_KEY=ps_live_... in .env.local (no NEXT_PUBLIC_ prefix: server only)
export async function GET(req: Request) {
const sport = new URL(req.url).searchParams.get("sport") ?? "soccer";
const qs = new URLSearchParams({ sport, market: "moneyline", main: "true", limit: "100" });
const r = await fetch(`https://pinnfeed.io/v1/odds?${qs}`, {
headers: { "x-api-key": process.env.PINNFEED_KEY! },
next: { revalidate: 5 },
});
if (!r.ok) {
const body = await r.json().catch(() => ({}));
return Response.json({ error: body?.error?.message ?? "upstream error" }, { status: r.status });
}
const { events } = await r.json();
return Response.json(events.map((e: any) => ({
id: e.id, home: e.home, away: e.away, start: e.startTime, live: e.live,
prices: Object.fromEntries(e.markets.flatMap((m: any) => m.outcomes.map((o: any) => [o.name, o.decimal]))),
})));
}
In a Server Component, call the same fetch logic directly; in a client component, request /api/odds?sport=tennis (for example with SWR). The next: { revalidate: 5 } cache means many visitors share one upstream request, which keeps you inside your plan's rate limit: 1 req/s on Free, 2 on Hobby, 10 on Builder, 30 on Pro (limits). On a 429, respect Retry-After.
Live push: why a separate worker#
| Need | Where it runs |
|---|---|
| Board refreshed every few seconds | Route handler + REST (this page) |
| Every price change pushed as it happens | Long-running Node process holding wss://pinnfeed.io/v1/stream |
Serverless and edge functions end when the response is sent, so they cannot keep a socket open or answer the server's ping every 20 seconds. Run the Node.js reconnect client on a small VM or container, keep the book in memory or Redis, and have Next.js read from there (or forward updates to the browser over your own SSE or WebSocket). The WebSocket needs Builder (€99/mo, 3 connections).
Plans#
Free REST (30 s delay, 2 sports, 1,000 requests a day) is enough to build the page: get a free key. 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 stacks: FastAPI proxy, Cloudflare Worker, Supabase.
Educational content for adults (18+).
Questions people ask#
Can I call PinnFeed straight from a client component?#
CORS allows it, but the key would be visible to anyone who opens dev tools. Keep it in a route handler or server action: browser JavaScript and CORS.
Does this work on Vercel?#
The route handler does. A WebSocket client does not belong in a Vercel function, because the function stops after each response; host the stream worker elsewhere.
Pages Router?#
Same idea in pages/api/odds.ts with res.setHeader("Cache-Control", "s-maxage=5, stale-while-revalidate").