On the wire · 04:50:43 UTC Live 550 · Prematch 14,382 · Markets 56,992 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Tutorial · Cloudflare · REST

Pinnacle odds on a Cloudflare Worker

Updated · PinnFeed · 2 min read

Answer: store the key with wrangler secret put PINNFEED_KEY. The Worker's fetch handler proxies /v1/odds for your front end with a short edge cache, so the key never reaches the browser, and its scheduled handler runs on a cron trigger to call /v1/drops and post new drops to a Discord webhook. All REST, so it works from Free (30 s delayed) upwards.

wrangler.toml#

name = "pinnfeed-worker"
main = "src/index.js"
compatibility_date = "2026-11-01"

[triggers]
crons = ["*/2 * * * *"]
npx wrangler secret put PINNFEED_KEY      # ps_live_...
npx wrangler secret put DISCORD_WEBHOOK   # https://discord.com/api/webhooks/...
npx wrangler deploy

src/index.js#

const API = 'https://pinnfeed.io/v1';

export default {
  async fetch(req, env, ctx) {
    const url = new URL(req.url);
    if (url.pathname !== '/odds') return new Response('not found', { status: 404 });
    const sport = url.searchParams.get('sport') || 'soccer';
    const cache = caches.default;
    const cacheKey = new Request(`${url.origin}/odds?sport=${sport}`);
    let res = await cache.match(cacheKey);
    if (!res) {
      const r = await fetch(`${API}/odds?sport=${sport}&market=moneyline&main=true&limit=50`, {
        headers: { 'x-api-key': env.PINNFEED_KEY },
      });
      res = new Response(r.body, { status: r.status, headers: { 'content-type': 'application/json', 'cache-control': 'max-age=5' } });
      if (r.ok) ctx.waitUntil(cache.put(cacheKey, res.clone()));
    }
    return res;
  },

  async scheduled(_event, env) {
    const r = await fetch(`${API}/drops?sport=soccer&minutes=2&min_pct=5&limit=10`, {
      headers: { 'x-api-key': env.PINNFEED_KEY },
    });
    if (!r.ok) return;
    const { drops } = await r.json();
    if (!drops.length) return;
    const lines = drops.map((d) => `${d.home} v ${d.away}: ${d.market} ${d.outcome} ${d.from} → ${d.to} (${d.dropPct}%)`);
    await fetch(env.DISCORD_WEBHOOK, {
      method: 'POST', headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ content: lines.join('\n').slice(0, 1900) }),
    });
  },
};

Because the cron window and the minutes window are both 2, each drop is posted about once; store ids in KV if you need strict de-duplication.

Limits to respect#

Free allows 1 req/s and 1,000 requests/day, Hobby 2 req/s, Builder 10 req/s. The edge cache keeps traffic spikes from turning into API calls. For true push, keep the WebSocket on a long-running server on Builder (€99/mo). 3-day trial; a card is required and it is charged after day 3 unless you cancel.

Related: browser apps and CORS, Discord picks bot, dropping odds API.

Educational content for adults (18+).

Questions people ask#

Can a Worker keep the PinnFeed WebSocket open?#

A plain Worker is request-scoped. A Durable Object can hold sockets, but a small always-on server is simpler for the stream.

Will the edge cache serve stale odds?#

For at most the max-age you set (5 s here). Lower it, or skip caching for live views.

Does the cron count toward my daily quota?#

Yes, each run is one request: every 2 minutes is 720 a day, inside Free's 1,000.

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.