On the wire · 04:56:51 UTC Live 567 · Prematch 14,380 · Markets 56,913 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Tutorial · Next.js · TypeScript

Pinnacle odds in Next.js (App Router)

Updated · PinnFeed · 2 min read

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#

NeedWhere it runs
Board refreshed every few secondsRoute handler + REST (this page)
Every price change pushed as it happensLong-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").

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.