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

Pinnacle-Quoten mit einer Edge Function in Supabase

Aktualisiert am · PinnFeed · 3 Min. Lesezeit

Antwort: lege den API-Key als Supabase-Secret ab, schreibe eine Edge Function, die GET /v1/drops (oder /v1/odds) aufruft und die Zeilen über den Admin-Client von @supabase/server per Upsert speichert, und lass pg_cron + pg_net sie jede Minute mit einem in Supabase Vault gespeicherten Secret Key aufrufen. Edge Functions sind kurzlebig, das ist also ein REST-Polling-Job, kein WebSocket-Listener. Läuft auf Free (30 s Verzögerung, 2 Sportarten) und ab Hobby in Echtzeit.

1. Tabelle#

create table if not exists pinnacle_drops (
  event_id bigint, market_id text, outcome text,
  price_from numeric, price_to numeric, drop_pct numeric,
  moved_at timestamptz, home text, away text,
  primary key (market_id, outcome, moved_at)
);

2. Secret#

supabase secrets set PINNFEED_KEY=ps_live_your_key

3. Funktion supabase/functions/pinnfeed-drops/index.ts#

import { withSupabase } from 'npm:@supabase/server@1';

// auth: 'secret' accepts only callers that send your project's secret key on the apikey header
export default {
  fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) => {
    const r = await fetch('https://pinnfeed.io/v1/drops?sport=soccer&minutes=5&min_pct=3&limit=100', {
      headers: { 'x-api-key': Deno.env.get('PINNFEED_KEY')! },
    });
    if (!r.ok) return new Response(await r.text(), { status: r.status });
    const { drops } = await r.json();
    const rows = drops.map((d: any) => ({
      event_id: d.eventId, market_id: d.marketId, outcome: d.outcome,
      price_from: d.from, price_to: d.to, drop_pct: d.dropPct,
      moved_at: d.movedAt, home: d.home, away: d.away,
    }));
    const { error } = await ctx.supabaseAdmin.from('pinnacle_drops').upsert(rows, { ignoreDuplicates: true });
    return Response.json({ saved: rows.length, error: error?.message ?? null });
  }),
};

Die Funktion wird von einem Cron-Job mit einem Secret Key aufgerufen, nicht von einem angemeldeten Nutzer. Schalte daher die JWT-Prüfung der Plattform in supabase/config.toml ab und lass withSupabase den Key prüfen:

[functions.pinnfeed-drops]
verify_jwt = false

Deploy mit supabase functions deploy pinnfeed-drops. Prüfe einmal die Antwort von /v1/drops, um das Zeitformat von movedAt zu bestätigen, bevor du dich auf den Primärschlüssel verlässt.

4. Zeitplan#

Aktiviere die Erweiterungen pg_cron und pg_net. Speichere die Projekt-URL und einen Supabase Secret Key (sb_secret_...) einmalig in Supabase Vault, damit sie nie direkt im Cron-SQL stehen:

select vault.create_secret('https://YOUR_PROJECT.supabase.co', 'project_url');
select vault.create_secret('YOUR_SUPABASE_SECRET_KEY', 'secret_key');

Plane dann den Aufruf, lies beide Werte aus vault.decrypted_secrets und sende den Key im Header apikey:

select cron.schedule('pinnfeed-drops', '* * * * *', $$
  select net.http_post(
    url := (select decrypted_secret from vault.decrypted_secrets where name = 'project_url')
           || '/functions/v1/pinnfeed-drops',
    headers := jsonb_build_object(
      'Content-Type', 'application/json',
      'apikey', (select decrypted_secret from vault.decrypted_secrets where name = 'secret_key')
    ),
    body := '{}'::jsonb
  );
$$);

Hinweise:

  • Nimm den Secret Key, nicht den Publishable Key. Der Publishable Key ist öffentlich; jeder, der ihn hat, könnte die Funktion auslösen.
  • apikey statt Authorization. Die aktuelle Supabase-Doku sendet die neuen sb_secret_-Keys im Header apikey; sie sind keine JWTs, verlass dich für sie also nicht auf Authorization: Bearer. Nutzt dein Projekt noch den alten service_role-JWT (Supabase schafft die Legacy-Keys bis Ende 2026 ab), lass verify_jwt an und sende diesen Key in apikey und in Authorization: Bearer, ebenfalls aus Vault gelesen.
  • Grenze von Vault. pg_net schreibt den entschlüsselten Key in net.http_request_queue, bis die Anfrage gesendet ist; jede Rolle, die sich an deiner Datenbank anmelden kann, könnte ihn in dieser Zeit lesen. Halte Datenbank-Logins knapp.
  • Der PinnFeed-Key selbst bleibt in den Secrets der Edge Function (Schritt 2), nie im SQL.

Jede Minute sind 1.440 Aufrufe pro Tag: ab Hobby unproblematisch (kein Tageslimit), aber über den 1.000 pro Tag von Free. Nimm auf Free daher */2 * * * *.

Nächste Schritte#

Du brauchst jeden Preis, nicht nur Stürze? Speichere /v1/odds-Snapshots: Postgres-Muster. Du willst Push statt Polling? Betreibe einen kleinen, dauerhaft laufenden Worker mit dem WebSocket auf Builder (99 €/Monat). Testphase 3 Tage; eine Karte ist nötig und wird nach Tag 3 belastet, wenn du nicht kündigst. Kostenloser Key.

Bildungsinhalt für Erwachsene (18+). Keine Tipps, kein Gewinnversprechen. In Deutschland nur bei Anbietern auf der GGL-Whitelist wetten.

Häufige Fragen#

Kann eine Edge Function den WebSocket offen halten?#

Nicht zuverlässig; sie leben nur für die Anfrage. Für den Stream nimm einen langlebigen Prozess (eine VM, Fly, Railway, einen Server zu Hause).

Ist der Supabase Secret Key hier sicher?#

Ja, serverseitig: in Vault für den Cron-Aufruf und in der Funktion geprüft. Gib ihn nie an den Browser weiter, und den PinnFeed-Key ebenso wenig.

Kann ich Supabase Realtime nutzen, um Daten an meine App zu pushen?#

Ja: Sobald Zeilen in der Tabelle landen, kann Supabase Realtime die Inserts an dein Frontend senden.

Der WebSocket-Plan: Builder, 99 €/Monat.

Pinnacle-Quoten in Echtzeit über einen einzigen WebSocket: alle 13 Sportarten, alle Märkte, faire Quoten ohne Marge und Schlussquoten. 3 Tage kostenlos, mit einem Klick kündbar.

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.