SDK de TypeScript
Inicio rápido del SDK
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 REST
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 SSE
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 TypeScript
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;
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
}
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>;
}
// ─── 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;
// ─── 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 ─────────────────────────────────────────
eventSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
console.log(`Connected: stream ${data.stream_id}`);
// Al reconectar, limpia el estado local — el servidor envía una instantánea nueva
if (data.reconnected) {
oddsMap.clear();
evMap.clear();
arbMap.clear();
lowHoldMap.clear();
isReady = false;
}
});
// ─── Instantánea inicial (en fragmentos) ──────────────────────────────────
eventSource.addEventListener('snapshot', (e) => {
const data = JSON.parse(e.data);
// Instantáneas de cuotas: indexadas por nombre de sportsbook
// p. ej. { "draftkings": [OddsLine, ...], "fanduel": [OddsLine, ...] }
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value) && !['ev', 'arbitrage', 'middles', 'low_hold'].includes(key)) {
// Datos de cuotas — la clave es el nombre del sportsbook
for (const odds of value 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 { books, total_odds } = JSON.parse(e.data);
isReady = true;
console.log(`Ready: ${total_odds} odds from ${books.join(', ')}`);
console.log(`${evMap.size} EV, ${arbMap.size} arb, ${lowHoldMap.size} low-hold opportunities`);
});
// ─── Actualizaciones de cuotas en tiempo real (DELTAS — fusionar en estado local) ─
eventSource.addEventListener('odds:update', (e) => {
const data = JSON.parse(e.data);
for (const [book, odds] of Object.entries(data)) {
if (!Array.isArray(odds)) continue;
for (const line of odds as OddsLine[]) {
oddsMap.set(line.id, line); // upsert
}
}
});
// ─── Cuotas eliminadas (BORRAR del estado local) ──────────────────────────
eventSource.addEventListener('odds:removed', (e) => {
const { ids } = JSON.parse(e.data) as { source: string; ids: string[]; count: number };
for (const id of ids) {
oddsMap.delete(id);
}
});
// ─── Eventos de oportunidades ─────────────────────────────────────────────
eventSource.addEventListener('ev:detected', (e) => {
const opps = JSON.parse(e.data) as EVOpportunity[];
for (const opp of opps) {
// Omitir oportunidades obsoletas
if (opp.possibly_stale) continue;
evMap.set(opp.id, opp);
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 arbs = JSON.parse(e.data) as ArbOpportunity[];
for (const arb of arbs) {
if (arb.possibly_stale) continue;
arbMap.set(arb.id, arb);
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 opps = JSON.parse(e.data) as LowHoldOpportunity[];
for (const opp of opps) {
if (opp.possibly_stale) continue;
lowHoldMap.set(opp.id, opp);
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 eventsource
Para uso en el lado del servidor, instala el paquete eventsource:
npm install eventsourceimport 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 cuotas
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
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 obsolescencia
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. ["POTENTIALLY_STALE_ODDS", "LIVE_STALE_ODDS"]
}
// Filtrar oportunidades obsoletas
eventSource.addEventListener('ev:detected', (e) => {
const opps = JSON.parse(e.data) as EVOpportunity[];
for (const opp of opps) {
if (opp.possibly_stale) {
console.log(`Skipping stale EV: ${opp.id}`);
continue;
}
// Procesar oportunidad válida
}
});Reconexión
Cuando se cae la conexión, EventSource se reconecta automáticamente y envía la cabecera Last-Event-ID. Al reconectarse, el servidor entrega una instantánea completa nueva (no una repetición de los eventos perdidos). Tu cliente debe:
- Detectar la reconexión mediante
data.reconnected === trueen el eventoconnected - Limpiar el estado local (
oddsMap.clear(),evMap.clear(), etc.) - Esperar al nuevo
snapshot:completeantes de actuar sobre los datos
eventSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
if (data.reconnected) {
console.log('Reconnected — clearing stale state');
oddsMap.clear();
evMap.clear();
arbMap.clear();
lowHoldMap.clear();
isReady = false;
}
});Errores habituales
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 completa
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:removed
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:complete
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 reconectar
Al reconectar, el servidor envía una instantánea completa nueva. Si no limpias antes tu estado local, tendrás duplicados y datos obsoletos mezclados.
Solución: limpia todos los Map cuando recibas connected con reconnected: true.
5. Malinterpretar el formato de cuotas
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 obsolescencia
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.