Cuotas de Pinnacle en Supabase con una Edge Function
Respuesta: guarda la clave de la API como secreto de Supabase, escribe una Edge Function que llame a GET /v1/drops (o /v1/odds) y haga upsert de las filas con el cliente admin de @supabase/server, y deja que pg_cron + pg_net la llamen cada minuto con una clave secreta guardada en Supabase Vault. Las Edge Functions duran poco, así que esto es un trabajo de polling REST, no un listener de WebSocket. Funciona en Free (30 s de retraso, 2 deportes) y en tiempo real desde Hobby.
1. Tabla#
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. Secreto#
supabase secrets set PINNFEED_KEY=ps_live_your_key
3. Función 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 la función la llama un cron con una clave secreta, no un usuario con sesión, así que desactiva la comprobación de JWT de la plataforma en supabase/config.toml y deja que withSupabase compruebe la clave:
[functions.pinnfeed-drops]
verify_jwt = false
Despliega con supabase functions deploy pinnfeed-drops. Revisa una vez la respuesta de /v1/drops para confirmar el formato de movedAt antes de fiarte de la clave primaria.
4. Programación#
Activa las extensiones pg_cron y pg_net. Guarda una sola vez en Supabase Vault la URL del proyecto y una clave secreta de Supabase (sb_secret_...), para que nunca aparezcan en el SQL del cron:
select vault.create_secret('https://YOUR_PROJECT.supabase.co', 'project_url');
select vault.create_secret('YOUR_SUPABASE_SECRET_KEY', 'secret_key');
Después programa la llamada leyendo ambos valores de vault.decrypted_secrets y enviando la clave en la cabecera 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:
- Usa la clave secreta, no la publicable. La clave publicable es pública: cualquiera que la tenga podría disparar la función.
apikeyfrente aAuthorization. La documentación actual de Supabase envía las nuevas clavessb_secret_enapikey; no son JWT, así que no dependas deAuthorization: Bearerpara ellas. Si tu proyecto aún usa el JWT heredadoservice_role(Supabase retira las claves heredadas a finales de 2026), dejaverify_jwtactivado y envía esa clave enapikeyy enAuthorization: Bearer, leyéndola de Vault del mismo modo.- Límite de Vault.
pg_netescribe la clave descifrada ennet.http_request_queuehasta enviar la petición, así que cualquier rol que pueda iniciar sesión en tu base de datos podría leerla mientras espera. Limita los accesos a la base de datos. - La clave de PinnFeed sigue en los secretos de la Edge Function (paso 2), nunca en el SQL.
Cada minuto son 1.440 llamadas al día: bien desde Hobby (sin límite diario), pero por encima de las 1.000 diarias de Free, así que en Free usa */2 * * * *.
Siguientes pasos#
¿Necesitas todos los precios y no solo las bajadas? Guarda snapshots de /v1/odds: patrón de Postgres. ¿Quieres push en vez de polling? Ejecuta un pequeño worker siempre encendido con el WebSocket de Builder (€99/mes). Prueba de 3 días; se pide tarjeta y se cobra después del día 3 salvo que canceles. Clave gratis.
Contenido educativo para mayores de 18 años. Sin pronósticos ni ganancias garantizadas. En España, apuesta solo con operadores con licencia de la DGOJ.
Preguntas frecuentes#
¿Puede una Edge Function mantener abierto el WebSocket?#
No de forma fiable: viven lo que dura la petición. Para el stream usa un proceso de larga duración (una VM, Fly, Railway, un servidor en casa).
¿Es segura aquí la clave secreta de Supabase?#
Sí, en el servidor: en Vault para la llamada del cron y comprobada dentro de la función. Nunca la expongas al navegador, ni tampoco la clave de PinnFeed.
¿Puedo usar Supabase Realtime para enviar datos a mi app?#
Sí: cuando las filas llegan a la tabla, Supabase Realtime puede emitir las inserciones a tu front end.