Pinnacle odds on a Cloudflare Worker
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.