On the wire · 04:54:09 UTC Live 549 · Prematch 14,380 · Markets 56,916 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Tutorial · Supabase · REST

Odds da Pinnacle no Supabase com uma Edge Function

Atualizado em · PinnFeed · 4 min de leitura

Resposta: guarde a chave da API como segredo do Supabase, escreva uma Edge Function que chama GET /v1/drops (ou /v1/odds) e faz upsert das linhas com o cliente admin de @supabase/server, e deixe pg_cron + pg_net chamarem a função a cada minuto com uma chave secreta guardada no Supabase Vault. Edge Functions duram pouco, então isto é um job de polling REST, não um listener de WebSocket. Funciona no Free (30 s de atraso, 2 esportes) e em tempo real a partir do Hobby.

1. Tabela#

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. Segredo#

supabase secrets set PINNFEED_KEY=ps_live_your_key

3. Função 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 });
  }),
};

A função é chamada por um cron com uma chave secreta, não por um usuário logado, então desative a checagem de JWT da plataforma em supabase/config.toml e deixe o withSupabase checar a chave:

[functions.pinnfeed-drops]
verify_jwt = false

Publique com supabase functions deploy pinnfeed-drops. Confira uma vez a resposta de /v1/drops para confirmar o formato de movedAt antes de confiar na chave primária.

4. Agendamento#

Ative as extensões pg_cron e pg_net. Guarde uma única vez no Supabase Vault a URL do projeto e uma chave secreta do Supabase (sb_secret_...), para que elas nunca fiquem no SQL do cron:

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

Depois agende a chamada lendo os dois valores de vault.decrypted_secrets e enviando a chave no 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
  );
$$);

Notas:

  • Use a chave secreta, não a publicável. A chave publicável é pública: qualquer pessoa com ela poderia disparar a função.
  • apikey versus Authorization. A documentação atual do Supabase envia as novas chaves sb_secret_ em apikey; elas não são JWT, então não dependa de Authorization: Bearer para elas. Se o seu projeto ainda usa o JWT legado service_role (o Supabase vai descontinuar as chaves legadas até o fim de 2026), mantenha verify_jwt ativado e envie essa chave em apikey e em Authorization: Bearer, lendo-a do Vault do mesmo jeito.
  • Limite do Vault. O pg_net grava a chave descriptografada em net.http_request_queue até enviar a requisição, então qualquer papel que consiga entrar no seu banco poderia lê-la enquanto espera. Restrinja os logins no banco.
  • A chave do PinnFeed continua nos segredos da Edge Function (passo 2), nunca no SQL.

A cada minuto são 1.440 chamadas por dia: tudo bem a partir do Hobby (sem limite diário), mas acima das 1.000 por dia do Free, então no Free use */2 * * * *.

Próximos passos#

Precisa de todos os preços, não só das quedas? Guarde snapshots de /v1/odds: padrão Postgres. Quer push em vez de polling? Rode um pequeno worker sempre ligado com o WebSocket do Builder (€99/mês). Teste de 3 dias; cartão obrigatório, cobrado após o dia 3 a menos que você cancele. Chave grátis.

Conteúdo educativo para maiores de 18 anos. Sem palpites nem lucro garantido. No Brasil, aposte apenas em operadores autorizados pela SPA/MF.

Perguntas frequentes#

Uma Edge Function consegue manter o WebSocket aberto?#

Não de forma confiável: elas duram o tempo da requisição. Para o stream, use um processo de longa duração (uma VM, Fly, Railway, um servidor em casa).

A chave secreta do Supabase está segura aqui?#

Sim, no servidor: no Vault para a chamada do cron e checada dentro da função. Nunca exponha essa chave, nem a do PinnFeed, ao navegador.

Posso usar o Supabase Realtime para enviar dados ao meu app?#

Sim: quando as linhas chegam na tabela, o Supabase Realtime pode transmitir as inserções para o seu front end.

O plano WebSocket: Builder, €99/mês.

Odds da Pinnacle em tempo real por um único WebSocket: os 13 esportes, todos os mercados, odds justas sem margem e odds de fechamento. 3 dias grátis, cancele com um clique.

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.