On the wire · 04:49:11 UTC Live 550 · Prematch 14,382 · Markets 56,950 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Respuesta · Postgres · Almacenamiento

Guardar cuotas de Pinnacle en Postgres

Actualizado el · PinnFeed · 2 min de lectura

Respuesta: mantén una tabla de solo inserción con los ticks de precio, con clave por id de mercado, resultado y marca de tiempo, y escribe en ella cada upsert de mercado que llegue por el WebSocket de PinnFeed. Los snapshots dan el estado inicial y los frames delta, cada cambio. Más adelante puedes añadir filas de /v1/closing para el CLV. Funciona con cualquier Postgres (autoalojado, Supabase, Neon).

Esquema#

create table if not exists odds_ticks (
  ts          timestamptz not null default now(),
  event_id    bigint      not null,
  market_id   text        not null,
  market      text        not null,   -- moneyline | spread | total | team_total
  period      int,
  line        numeric,
  outcome     text        not null,
  decimal     numeric     not null,
  fair        numeric,
  max_stake   numeric,
  primary key (market_id, outcome, ts)
);
create index if not exists odds_ticks_event on odds_ticks (event_id, ts);

Worker en Node (Builder)#

// npm i ws pg   ·   PINNFEED_KEY=ps_live_... DATABASE_URL=postgres://... node worker.js
import WebSocket from 'ws';
import pg from 'pg';

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const sql = `insert into odds_ticks (event_id, market_id, market, period, line, outcome, decimal, fair, max_stake)
             values ($1,$2,$3,$4,$5,$6,$7,$8,$9) on conflict do nothing`;

async function save(m) {
  for (const o of m.outcomes || []) {
    await db.query(sql, [m.eventId, m.id, m.type, m.period, m.line ?? null, o.name, o.decimal, o.fair ?? null, m.maxStake ?? null]);
  }
}

function connect() {
  const ws = new WebSocket('wss://pinnfeed.io/v1/stream', { headers: { 'x-api-key': process.env.PINNFEED_KEY } });
  ws.on('open', () => ws.send(JSON.stringify({ type: 'subscribe', sports: ['soccer'], markets: ['moneyline', 'total'], main: true })));
  ws.on('message', async (raw) => {
    const msg = JSON.parse(raw);
    if (msg.type === 'ping') return ws.send(JSON.stringify({ type: 'pong' }));
    if (msg.type === 'snapshot') for (const m of msg.markets || []) await save(m);
    if (msg.type === 'delta') for (const ch of msg.changes) if (ch.kind === 'market' && ch.op === 'upsert') await save(ch.market);
  });
  ws.on('close', (code) => { console.log('closed', code); setTimeout(connect, 2000); });
}
connect();

Si las inserciones van por detrás del stream, el servidor puede cerrar con 4008 slow_consumer: agrupa las inserciones (o ponlas en cola) y suscríbete a menos mercados. Reconexión completa: reconexión en Node.

Consultas de ejemplo#

-- last price per outcome
select distinct on (market_id, outcome) market_id, outcome, decimal, ts
from odds_ticks order by market_id, outcome, ts desc;

Usa la tabla para backtests contra cierres y gráficos de movimiento de línea.

Planes#

El streaming necesita Builder (€99/mes) o Pro; el historial de cierres va incluido en Builder. En Free o Hobby puedes consultar /v1/odds por polling y guardar en la misma tabla. Prueba de 3 días; se pide tarjeta y se cobra después del día 3 salvo que canceles.

Contenido educativo para mayores de 18 años. Datos para investigación, no consejos de apuestas. En España, apuesta solo con operadores con licencia de la DGOJ.

Preguntas frecuentes#

¿Debo guardar el snapshot?#

Sí, como primera fila de cada resultado. Si no, la tabla solo tendrá los precios que cambiaron después de conectarte.

¿Cómo evito duplicados tras una reconexión?#

El snapshot tras reconectar repite los precios actuales; on conflict do nothing con una clave de tiempo los guarda como ticks nuevos. Si solo quieres cambios, compara con el último precio guardado antes de insertar.

¿Y TimescaleDB?#

Funciona igual: convierte odds_ticks en hypertable sobre ts.

El plan WebSocket: Builder, 99 €/mes.

Cuotas de Pinnacle en tiempo real por un único WebSocket: los 13 deportes, todos los mercados, cuotas justas sin margen y cuotas de cierre. 3 días gratis y cancelas con un clic.

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.