Skip to Content
SDKsTypeScript

SDK de TypeScript

El SDK oficial de TypeScript @sharp-api/client te da acceso tipado a todos los endpoints de SharpAPI, con streaming SSE, validación Zod y autocompletado completo del IDE desde el primer momento.

Instala el SDK oficial: npm install @sharp-api/client — GitHub 

Inicio rápido del SDKPermalink for this section

import { SharpAPI } from '@sharp-api/client' const api = new SharpAPI('sk_live_...') // Obtener cuotas const { data: odds } = await api.odds.get({ league: 'nba' }) // Obtener oportunidades de +EV (Pro+) const { data: ev } = await api.ev.get({ min_ev: 3 }) // Obtener oportunidades de arbitraje (Hobby+) const { data: arbs } = await api.arbitrage.get({ min_profit: 1 }) // Obtener middles (Pro+) const { data: middles } = await api.middles.get({ league: 'nba' }) // Streaming SSE (complemento de WebSocket) const stream = api.stream.odds({ league: 'nba' }) stream.on('update', ({ data }) => console.log(data)) stream.connect() // Streaming WebSocket (~100 ms de latencia) const ws = api.stream.oddsWs({ sportsbook: ['draftkings'] }) ws.on('odds:update', ({ data, source }) => console.log(source, data)) ws.connect()

API RESTPermalink for this section

Usa fetch para llamar a cualquier endpoint REST:

const API_URL = 'https://api.sharpapi.io/api/v1'; const API_KEY = 'YOUR_API_KEY'; async function sharpApi<T>(path: string, params?: Record<string, string>): Promise<T> { const url = new URL(`${API_URL}${path}`); if (params) { for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); } const res = await fetch(url, { headers: { 'X-API-Key': API_KEY } }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); } // Ejemplos const odds = await sharpApi('/odds', { sport: 'basketball', league: 'nba' }); const ev = await sharpApi('/opportunities/ev', { min_ev: '3.0' }); const arbs = await sharpApi('/opportunities/arbitrage', { min_profit: '1.0' });

Streaming SSEPermalink for this section

El streaming SSE entrega actualizaciones de cuotas en tiempo real y alertas de oportunidades. Esta sección cubre cómo construir un cliente correcto.

Crítico: los eventos odds:update son deltas — solo contienen las cuotas que han cambiado. Tu cliente debe mantener un estado local y fusionar las actualizaciones en él. Tratar cada evento como una instantánea completa es la causa número 1 de datos incorrectos.

Cliente completo de TypeScriptPermalink for this section

const API_URL = 'https://api.sharpapi.io/api/v1'; const API_KEY = 'YOUR_API_KEY'; // ─── Tipos ──────────────────────────────────────────────────────────────── interface OddsLine { id: string; sportsbook: string; event_id: string; sport: string; league: string; home_team: string; away_team: string; market_type: string; selection: string; selection_type: string; odds_american: number; odds_decimal: number; odds_probability: number; line?: number; event_start_time: string; is_live: boolean; timestamp: string; player_name?: string; // Solo en mercados de player props stat_category?: string; // Solo en mercados de player props } // Una fila de `odds:update`: `id` más los campos que pueden cambiar (odds_american, // odds_decimal, odds_probability, line, is_live, timestamp, ...). Una fila de un id // que aún no tienes llega completa, con todos los campos de OddsLine. type OddsDelta = Pick<OddsLine, 'id'> & Partial<OddsLine>; interface EVOpportunity { id: string; ev_percentage: number; odds_american: number; odds_decimal: number; selection: string; market: string; sportsbook: string; game: string; sport: string; league: string; is_live: boolean; confidence_score: number; kelly_percent: number | null; possibly_stale: boolean; oldest_odds_age_seconds: number | null; warnings: string[]; detected_at: string; } interface ArbOpportunity { id: string; event_name: string; sport: string; market_type: string; profit_percent: number; possibly_stale: boolean; oldest_odds_age_seconds: number | null; warnings: string[]; legs: Array<{ sportsbook: string; selection: string; odds_american: number; odds_decimal: number; stake_percent: number; }>; detected_at: string; } interface LowHoldOpportunity { id: string; event_id: string; event_name: string; sport: string; league: string; market_type: string; line: number | null; hold_percentage: number; side1: LowHoldSide; side2: LowHoldSide; side3: LowHoldSide | null; // Mercados a 3 vías (fútbol, hockey) is_live: boolean; is_alternate_line: boolean; all_books: string[]; confidence: number; odds_age_seconds: number; possibly_stale: boolean; detected_at: string; } interface LowHoldSide { selection: string; books: string[]; line: number | null; odds: { american: number; decimal: number; implied_probability: number; fair_probability: number; }; deep_links: Record<string, string>; } // Payload de ev:detected / arb:detected / middles:detected / low_hold:detected. // Un elemento es una oportunidad nueva O una versión actualizada de una ya enviada (mismo `id`). interface DetectedEnvelope<T> { opportunities: T[]; count: number; type: 'ev' | 'arbitrage' | 'middles' | 'low_hold'; } // ─── Gestión del estado ─────────────────────────────────────────────────── // Indexado por el ID de la línea de cuotas (p. ej. "draftkings_33483153_moneyline_PHO") const oddsMap = new Map<string, OddsLine>(); const evMap = new Map<string, EVOpportunity>(); const arbMap = new Map<string, ArbOpportunity>(); const lowHoldMap = new Map<string, LowHoldOpportunity>(); let isReady = false; let resuming = false; // lo activa `connected` con `resumed: true` — ver Reconexión más abajo // ─── Conexión ───────────────────────────────────────────────────────────── const url = new URL(`${API_URL}/stream`); url.searchParams.set('channel', 'all'); url.searchParams.set('league', 'nba'); url.searchParams.set('api_key', API_KEY); const eventSource = new EventSource(url.toString()); // ─── Ciclo de vida de la conexión ───────────────────────────────────────── function clearState() { oddsMap.clear(); evMap.clear(); arbMap.clear(); lowHoldMap.clear(); } eventSource.addEventListener('connected', (e) => { const data = JSON.parse(e.data); console.log(`Connected: stream ${data.stream_id}`); isReady = false; // Conserva el estado local solo cuando el servidor reanudó el stream (`resumed: true`): // reproduce los eventos de cuotas que te perdiste en lugar de enviar una instantánea. // Después de cualquier otra conexión llega una instantánea completa, así que limpia ahora. // Solo el canal `odds` se reanuda — en este stream `channel=all` cada reconexión // recibe una instantánea. No uses `reconnected`: también es `true` en una reanudación. resuming = data.resumed === true; if (!resuming) clearState(); }); // ─── Instantánea inicial (en fragmentos) ────────────────────────────────── eventSource.addEventListener('snapshot', (e) => { const data = JSON.parse(e.data); // Una instantánea después de `resumed: true` significa que el servidor abandonó la // reanudación: esta instantánea reemplaza tu estado (su snapshot:complete indica `full_resync`) if (resuming) { clearState(); resuming = false; } // Los fragmentos de cuotas llevan sus filas en `odds`; los fragmentos de // oportunidades llevan `ev` / `arbitrage` / `middles` / `low_hold` en su lugar for (const odds of (data.odds ?? []) as OddsLine[]) { oddsMap.set(odds.id, odds); } // Instantáneas de oportunidades if (data.ev) { for (const opp of data.ev as EVOpportunity[]) { evMap.set(opp.id, opp); } } if (data.arbitrage) { for (const arb of data.arbitrage as ArbOpportunity[]) { arbMap.set(arb.id, arb); } } if (data.low_hold) { for (const lh of data.low_hold as LowHoldOpportunity[]) { lowHoldMap.set(lh.id, lh); } } }); eventSource.addEventListener('snapshot:complete', (e) => { const { mode } = JSON.parse(e.data); // "resume", "full_resync" o ausente en la primera conexión // Una resincronización completa puede llegar sin ningún fragmento `snapshot` // (ya nada coincide con tus filtros), así que la limpieza del handler `snapshot` // nunca se ejecutó. La comprobación de `resuming` evita borrar un snapshot ya guardado. if (mode === 'full_resync' && resuming) clearState(); resuming = false; isReady = true; console.log(`Ready: ${oddsMap.size} odds`); console.log(`${evMap.size} EV, ${arbMap.size} arb, ${lowHoldMap.size} low-hold opportunities`); }); // ─── Actualizaciones de cuotas en tiempo real (DELTAS — fusionar en estado local) ─ // Campos del protocolo, según la referencia de eventos de streaming: `odds:update` // lleva { odds, count, book, partial }, `odds:removed` lleva { ids, count, book }, // y las filas de cuotas usan `odds_probability`. // Consulta /es/api-reference/stream/#oddsupdate para la referencia completa de eventos. eventSource.addEventListener('odds:update', (e) => { const { odds } = JSON.parse(e.data) as { odds: OddsDelta[]; book: string; count: number; partial: boolean }; for (const delta of odds) { const row = oddsMap.get(delta.id); if (row) { // Fusionar: un delta solo trae los campos que pueden cambiar, así que asignarlo // sobre la fila guardada conserva sportsbook, selection, equipos y el resto Object.assign(row, delta); } else if (delta.sportsbook !== undefined) { // Una línea abierta después de tu instantánea llega como fila completa — guárdala oddsMap.set(delta.id, delta as OddsLine); } // Un delta compacto de un id que no tienes no tiene dónde fusionarse — ignóralo } }); // ─── Cuotas eliminadas (BORRAR del estado local) ────────────────────────── eventSource.addEventListener('odds:removed', (e) => { const { ids } = JSON.parse(e.data) as { book: string; ids: string[]; count: number }; for (const id of ids) { oddsMap.delete(id); } }); // ─── Eventos de oportunidades ───────────────────────────────────────────── eventSource.addEventListener('ev:detected', (e) => { const { opportunities } = JSON.parse(e.data) as DetectedEnvelope<EVOpportunity>; for (const opp of opportunities) { // Omitir oportunidades obsoletas — y descartar la versión guardada con este `id` if (opp.possibly_stale) { evMap.delete(opp.id); continue; } const isNew = !evMap.has(opp.id); evMap.set(opp.id, opp); // upsert — un id conocido es una actualización (nuevo precio / EV%) if (isNew) console.log(`+EV: ${opp.selection} ${opp.ev_percentage}% on ${opp.sportsbook}`); } }); eventSource.addEventListener('ev:expired', (e) => { const { expired } = JSON.parse(e.data) as { expired: string[] }; for (const id of expired) { evMap.delete(id); } }); eventSource.addEventListener('arb:detected', (e) => { const { opportunities } = JSON.parse(e.data) as DetectedEnvelope<ArbOpportunity>; for (const arb of opportunities) { if (arb.possibly_stale) { arbMap.delete(arb.id); continue; } const isNew = !arbMap.has(arb.id); arbMap.set(arb.id, arb); // upsert — un id conocido es una actualización (nuevas cuotas de las patas) if (isNew) console.log(`Arb: ${arb.profit_percent}% — ${arb.event_name}`); } }); eventSource.addEventListener('arb:expired', (e) => { const { expired } = JSON.parse(e.data) as { expired: string[] }; for (const id of expired) { arbMap.delete(id); } }); eventSource.addEventListener('low_hold:detected', (e) => { const { opportunities } = JSON.parse(e.data) as DetectedEnvelope<LowHoldOpportunity>; for (const opp of opportunities) { if (opp.possibly_stale) { lowHoldMap.delete(opp.id); continue; } const isNew = !lowHoldMap.has(opp.id); lowHoldMap.set(opp.id, opp); // upsert — un id conocido es una actualización (nuevas cuotas / hold%) if (isNew) console.log(`Low Hold: ${opp.hold_percentage}% — ${opp.event_name} (${opp.market_type})`); } }); eventSource.addEventListener('low_hold:expired', (e) => { const { expired } = JSON.parse(e.data) as { expired: string[] }; for (const id of expired) { lowHoldMap.delete(id); } }); // ─── Monitorización de salud ────────────────────────────────────────────── let lastHeartbeat = Date.now(); eventSource.addEventListener('heartbeat', () => { lastHeartbeat = Date.now(); }); // Comprobar conexiones obsoletas cada 60 segundos setInterval(() => { if (Date.now() - lastHeartbeat > 60_000) { console.warn('No heartbeat for 60s — reconnecting'); eventSource.close(); // Volver a crear EventSource (el navegador reconectará automáticamente, // pero un cierre + reconexión explícitos reinician el estado de forma limpia) } }, 60_000); // ─── Manejo de errores ──────────────────────────────────────────────────── eventSource.addEventListener('error', (e) => { const data = JSON.parse((e as MessageEvent).data); console.warn(`Stream error: ${data.code} — ${data.message}`); }); eventSource.onerror = () => { console.log('Connection lost, auto-reconnecting...'); };

Node.js con el paquete eventsourcePermalink for this section

Para uso en el lado del servidor, instala el paquete eventsource:

npm install eventsource
import EventSource from 'eventsource'; const es = new EventSource( 'https://api.sharpapi.io/api/v1/stream?channel=all&league=nba', { headers: { 'X-API-Key': 'YOUR_KEY' } } ); // Mismos manejadores de eventos que en el navegador — ver más arriba es.addEventListener('snapshot', (e) => { /* ... */ }); es.addEventListener('odds:update', (e) => { /* ... */ }); // etc.

Formato de cuotasPermalink for this section

Todos los valores de cuotas se devuelven en formato americano como representación principal, incluyendo además el formato decimal y la probabilidad implícita:

// Cada OddsLine incluye los tres formatos: { odds_american: -110, // Cuotas americanas odds_decimal: 1.909, // Cuotas decimales odds_probability: 0.524 // Probabilidad implícita (0-1) }

Si necesitas convertir entre formatos por tu cuenta:

function americanToDecimal(american: number): number { return american > 0 ? american / 100 + 1 : 100 / Math.abs(american) + 1; } function americanToProbability(american: number): number { return american > 0 ? 100 / (american + 100) : Math.abs(american) / (Math.abs(american) + 100); }

Metadatos de obsolescenciaPermalink for this section

Las respuestas de oportunidades de EV, arbitraje y low-hold incluyen información de obsolescencia para ayudarte a filtrar oportunidades basadas en cuotas obsoletas:

interface EVOpportunity { // ... otros campos ... possibly_stale: boolean; // true si alguna cuota subyacente puede estar obsoleta oldest_odds_age_seconds: number | null; // antigüedad de la cuota más vieja warnings: string[]; // p. ej. ["SINGLE_SHARP_REF", "LIVE_STALE_ODDS"] } // Filtrar oportunidades obsoletas eventSource.addEventListener('ev:detected', (e) => { const { opportunities } = JSON.parse(e.data) as { opportunities: EVOpportunity[] }; for (const opp of opportunities) { if (opp.possibly_stale) { console.log(`Skipping stale EV: ${opp.id}`); continue; } // Procesar oportunidad válida } });

El motor de EV emite exactamente tres avisos — LIVE_STALE_ODDS, SINGLE_SHARP_REF y SINGLE_SHARP_PERIOD; el endpoint de arbitraje tiene su propia taxonomía, incluido POTENTIALLY_STALE_ODDS (consulta Oportunidades de Arbitraje).

ReconexiónPermalink for this section

Cuando se cae la conexión, EventSource se reconecta por sí solo y envía el id del último evento que recibió en la cabecera Last-Event-ID. En el canal odds, el servidor intenta entonces una reanudación: reproduce los eventos de cuotas que te perdiste en lugar de enviar una instantánea nueva. Si no puede, envía una resincronización completa: una instantánea nueva que reemplaza tu estado. Te indica cuál recibiste en resumed del evento connected y en mode del evento snapshot:complete que cierra la reconexión:

RecibesQué ocurrióQué hace tu cliente
connected con resumed: true, eventos odds:update / odds:removed reproducidos y marcados con "replay": true, y luego snapshot:complete con mode: "resume"Reanudación: se reprodujeron los eventos que te perdiste. No se envía instantáneaConserva tu estado y fusiona los eventos reproducidos igual que los eventos en vivo
connected con resumed: false y un fallback_reason, fragmentos snapshot, y luego snapshot:complete con mode: "full_resync"Resincronización completa: el servidor no pudo reanudarLimpia tu estado y reconstrúyelo desde la instantánea
connected con resumed: true, algunos eventos reproducidos, y luego fragmentos snapshot y snapshot:complete con mode: "full_resync"El servidor empezó una reanudación, no pudo terminarla y pasó a una resincronización completaLimpia tu estado cuando llegue el primer fragmento snapshot — o, si la resincronización no trae ningún fragmento, cuando snapshot:complete indique mode: "full_resync" — y reconstrúyelo desde la instantánea
connected sin campo resumed, y luego fragmentos snapshotUna primera conexión, o una reconexión que no envió Last-Event-IDLimpia lo que tengas y construye desde la instantánea

La reanudación es best-effort, no un log duradero. Solo cubre el canal odds. Una reconexión de gamestate o opportunities no envía ninguna Last-Event-ID, así que simplemente vuelve a arrancar desde un snapshot nuevo y su evento connected no lleva ni resumed ni fallback_reason. En all, los eventos de odds sí fijan el id de evento, por lo que la reconexión lo presenta y el servidor lo rechaza con resumed: false y fallback_reason: "channel_unsupported". Además solo cubre una ventana corta de eventos recientes, así que una caída más larga, un mantenimiento del servidor o un cambio de filtros también pueden terminar en una resincronización completa. El descriptor resume del evento connected indica estos límites en cada conexión, y el contrato de fiabilidad enumera cada fallback_reason. Trata la resincronización completa como el camino normal de recuperación.

No decidas con reconnected. Es true en toda reconexión que envió un Last-Event-ID, incluida una reanudación correcta, así que limpiar con él descarta el estado en el que se fusionan los eventos reproducidos.

// `resuming`, `isReady` y `clearState()` como en el cliente de arriba eventSource.addEventListener('connected', (e) => { const { resumed, fallback_reason } = JSON.parse(e.data); isReady = false; resuming = resumed === true; if (!resuming) { // Llega una instantánea completa: primera conexión, sin Last-Event-ID, o resumed: false if (fallback_reason) console.log(`Full resync: ${fallback_reason}`); clearState(); } }); eventSource.addEventListener('snapshot', (e) => { if (resuming) { // El servidor abandonó la reanudación: esta instantánea reemplaza tu estado clearState(); resuming = false; } // ...guarda el fragmento como en el cliente de arriba }); eventSource.addEventListener('snapshot:complete', (e) => { const { mode } = JSON.parse(e.data); // "resume", "full_resync", o ausente en una primera conexión // Una resincronización completa puede no traer ningún fragmento, si ya nada coincide con tus filtros if (mode === 'full_resync' && resuming) clearState(); resuming = false; isReady = true; });

Errores habitualesPermalink for this section

Estos son los errores más habituales al construir un cliente SSE. Cometer cualquiera de ellos puede producir arbitrajes fantasma o cálculos de EV incorrectos.

1. Tratar odds:update como una instantánea completaPermalink for this section

Los eventos odds:update solo contienen las cuotas que han cambiado desde el último evento. Si reemplazas todo tu estado local con cada actualización, solo verás 1-2 casas de apuestas a la vez, lo que hará que cada mercado parezca una oportunidad de arbitraje.

Solución: fusiona siempre las actualizaciones en tu Map, nunca lo reemplaces.

2. Ignorar los eventos odds:removedPermalink for this section

Cuando un sportsbook retira una línea (mercado suspendido, evento liquidado), enviamos odds:removed con los IDs a eliminar. Si no manejas esto, las cuotas obsoletas se acumulan y crean arbitrajes fantasma entre líneas eliminadas y nuevas.

Solución: elimina las cuotas de tu Map cuando recibas odds:removed.

3. Calcular antes de snapshot:completePermalink for this section

La instantánea inicial se entrega en fragmentos a través de varios eventos snapshot. Si empiezas a calcular arbitrajes o EV durante la carga de la instantánea, tendrás una imagen incompleta de los mercados disponibles.

Solución: establece una bandera al recibir snapshot:complete y empieza los cálculos solo después de eso.

4. No limpiar el estado al reconectarPermalink for this section

Si una reconexión termina en una instantánea completa y no limpiaste tu estado local, las filas de la sesión anterior quedan mezcladas con los datos nuevos, incluidas las líneas que se eliminaron mientras estabas desconectado. Limpiar en cada reconexión también es un error: una reanudación no envía instantánea, así que te quedarías solo con las filas reproducidas.

Solución: limpia todos los Map cuando connected llegue sin resumed: true, y cuando llegue un fragmento snapshot después de resumed: true. No uses reconnected, que también es true en una reanudación. Consulta Reconexión.

5. Malinterpretar el formato de cuotasPermalink for this section

Si tratas las cuotas americanas (-110) como cuotas decimales, tus cálculos producirán resultados tremendamente incorrectos. Nuestra API siempre proporciona ambos formatos — usa odds_decimal para los cálculos.

6. Ignorar las advertencias de obsolescenciaPermalink for this section

Las oportunidades de EV y arbitraje incluyen los campos possibly_stale y oldest_odds_age_seconds. Las oportunidades marcadas como obsoletas pueden basarse en cuotas que tienen varios minutos de antigüedad y que ya no son accionables.

Solución: comprueba possibly_stale antes de actuar sobre cualquier oportunidad.

Last updated on