Skip to Content
SDKsTypeScript

SDK TypeScript

O SDK TypeScript oficial @sharp-api/client oferece acesso tipado a todos os endpoints da SharpAPI, com streaming SSE, validação Zod e autocomplete completo na IDE desde o início.

Instale o SDK oficial: npm install @sharp-api/client — GitHub 

Início Rápido com o SDKPermalink for this section

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

API RESTPermalink for this section

Use fetch para chamar qualquer 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(); } // Exemplos 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

O streaming SSE entrega atualizações de odds em tempo real e alertas de oportunidades. Esta seção cobre como construir um cliente correto.

Crítico: eventos odds:update são deltas — eles contêm apenas as odds que mudaram. Seu cliente deve manter o estado local e mesclar as atualizações nele. Tratar cada evento como um snapshot completo é a causa #1 de dados incorretos.

Cliente TypeScript CompletoPermalink 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; // Apenas mercados de player props stat_category?: string; // Apenas mercados de player props } // Uma linha de `odds:update`: `id` mais os campos que podem mudar (odds_american, // odds_decimal, odds_probability, line, is_live, timestamp, ...). Uma linha de um id // que você ainda não tem chega completa, com todos os 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 de 3 vias (futebol, hóquei) 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. // Um item é uma oportunidade nova OU uma versão atualizada de uma já enviada (mesmo `id`). interface DetectedEnvelope<T> { opportunities: T[]; count: number; type: 'ev' | 'arbitrage' | 'middles' | 'low_hold'; } // ─── Gerenciamento de Estado ────────────────────────────────────────────── // Indexado pelo ID da linha de odds (ex: "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; // definido por `connected` com `resumed: true` — veja Reconexão abaixo // ─── Conectar ───────────────────────────────────────────────────────────── 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 da conexão ───────────────────────────────────────────── 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; // Mantenha o estado local só quando o servidor retomou o stream (`resumed: true`): // ele reproduz os eventos de odds que você perdeu em vez de enviar um snapshot. // Qualquer outra conexão é seguida de um snapshot completo, então limpe agora. // Só o canal `odds` é retomado — neste stream `channel=all` toda reconexão // recebe um snapshot. Não use `reconnected`: ele também é `true` num resume. resuming = data.resumed === true; if (!resuming) clearState(); }); // ─── Snapshot inicial (em chunks) ───────────────────────────────────────── eventSource.addEventListener('snapshot', (e) => { const data = JSON.parse(e.data); // Um snapshot depois de `resumed: true` significa que o servidor desistiu do resume: // este snapshot substitui seu estado (o snapshot:complete dele indica `full_resync`) if (resuming) { clearState(); resuming = false; } // Chunks de odds trazem suas linhas em `odds`; chunks de oportunidades // trazem `ev` / `arbitrage` / `middles` / `low_hold` no lugar for (const odds of (data.odds ?? []) as OddsLine[]) { oddsMap.set(odds.id, odds); } // Snapshots 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" ou ausente na primeira conexão // Um full resync pode chegar sem nenhum chunk de `snapshot` (nada mais combina // com seus filtros), então a limpeza do handler `snapshot` nunca rodou. A // verificação de `resuming` evita apagar um snapshot já armazenado. 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`); }); // ─── Atualizações de odds em tempo real (DELTAS — mesclar no estado local) ─ // Campos do protocolo, conforme a referência de eventos de streaming: `odds:update` // traz { odds, count, book, partial }, `odds:removed` traz { ids, count, book }, // e as linhas de odds usam `odds_probability`. // Veja /pt-BR/api-reference/stream/#oddsupdate para a referência 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) { // Mesclar: um delta traz só os campos que podem mudar, então atribuí-lo sobre // a linha armazenada preserva sportsbook, selection, times e o resto Object.assign(row, delta); } else if (delta.sportsbook !== undefined) { // Uma linha aberta depois do seu snapshot chega como linha completa — armazene-a oddsMap.set(delta.id, delta as OddsLine); } // Um delta compacto de um id que você não tem não tem onde ser mesclado — ignore-o } }); // ─── Odds removidas (DELETAR do 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) { // Pular oportunidades obsoletas — e descartar a versão guardada com este `id` if (opp.possibly_stale) { evMap.delete(opp.id); continue; } const isNew = !evMap.has(opp.id); evMap.set(opp.id, opp); // upsert — um id conhecido é uma atualização (novo preço / 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 — um id conhecido é uma atualização (novas odds das pernas) 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 — um id conhecido é uma atualização (novas odds / 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); } }); // ─── Monitoramento de saúde ─────────────────────────────────────────────── let lastHeartbeat = Date.now(); eventSource.addEventListener('heartbeat', () => { lastHeartbeat = Date.now(); }); // Verificar conexões obsoletas a cada 60 segundos setInterval(() => { if (Date.now() - lastHeartbeat > 60_000) { console.warn('No heartbeat for 60s — reconnecting'); eventSource.close(); // Recriar EventSource (o navegador reconectará automaticamente, // mas fechar explicitamente + reconectar reseta o estado de forma limpa) } }, 60_000); // ─── Tratamento de erros ────────────────────────────────────────────────── 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 com o pacote eventsourcePermalink for this section

Para uso no servidor, instale o pacote 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' } } ); // Mesmos manipuladores de eventos do navegador — veja acima es.addEventListener('snapshot', (e) => { /* ... */ }); es.addEventListener('odds:update', (e) => { /* ... */ }); // etc.

Formato das OddsPermalink for this section

Todos os valores de odds são retornados no formato americano como representação primária, com decimal e probabilidade implícita inclusos:

// Cada OddsLine inclui os três formatos: { odds_american: -110, // Odds americanas odds_decimal: 1.909, // Odds decimais odds_probability: 0.524 // Probabilidade implícita (0-1) }

Se você precisar converter entre formatos por conta própria:

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); }

Metadados de ObsolescênciaPermalink for this section

As respostas de oportunidades de EV, arbitragem e low-hold incluem informações de obsolescência para ajudar você a filtrar oportunidades baseadas em odds desatualizadas:

interface EVOpportunity { // ... outros campos ... possibly_stale: boolean; // true se quaisquer odds subjacentes podem estar obsoletas oldest_odds_age_seconds: number | null; // idade da leg de odds mais antiga warnings: string[]; // ex: ["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; } // Processar oportunidade válida } });

O motor de EV emite exatamente três avisos — LIVE_STALE_ODDS, SINGLE_SHARP_REF e SINGLE_SHARP_PERIOD; o endpoint de arbitragem tem sua própria taxonomia, incluindo POTENTIALLY_STALE_ODDS (veja Oportunidades de Arbitragem).

ReconexãoPermalink for this section

Quando a conexão cai, o EventSource reconecta sozinho e envia o id do último evento que recebeu no cabeçalho Last-Event-ID. No canal odds, o servidor então tenta um resume: reproduz os eventos de odds que você perdeu em vez de enviar um snapshot novo. Quando não consegue, envia uma ressincronização completa: um snapshot novo que substitui seu estado. Ele informa qual você recebeu em resumed no evento connected e em mode no evento snapshot:complete que encerra a reconexão:

Você recebeO que aconteceuO que seu cliente faz
connected com resumed: true, eventos odds:update / odds:removed reproduzidos e marcados com "replay": true, depois snapshot:complete com mode: "resume"Resume: os eventos que você perdeu foram reproduzidos. Nenhum snapshot é enviadoMantenha seu estado e mescle os eventos reproduzidos como os eventos ao vivo
connected com resumed: false e um fallback_reason, chunks de snapshot, depois snapshot:complete com mode: "full_resync"Ressincronização completa: o servidor não conseguiu retomarLimpe seu estado e reconstrua-o a partir do snapshot
connected com resumed: true, alguns eventos reproduzidos, depois chunks de snapshot e snapshot:complete com mode: "full_resync"O servidor começou um resume, não conseguiu concluí-lo e passou para uma ressincronização completaLimpe seu estado quando chegar o primeiro chunk de snapshot — ou, se a ressincronização não trouxer nenhum chunk, quando snapshot:complete indicar mode: "full_resync" — e reconstrua-o a partir do snapshot
connected sem o campo resumed, depois chunks de snapshotUma primeira conexão, ou uma reconexão que não enviou Last-Event-IDLimpe o que tiver e construa a partir do snapshot

O resume é best-effort, não um log durável. Ele cobre só o canal odds. Uma reconexão em gamestate ou opportunities não envia nenhuma Last-Event-ID, então ela simplesmente reinicia a partir de um snapshot novo e seu evento connected não traz nem resumed nem fallback_reason. Em all, os eventos de odds definem o id do evento, então a reconexão o apresenta e o servidor o recusa com resumed: false e fallback_reason: "channel_unsupported". Ele também cobre só uma janela curta de eventos recentes, então uma queda mais longa, uma manutenção do servidor ou uma mudança de filtros também podem terminar numa ressincronização completa. O descritor resume do evento connected informa esses limites em toda conexão, e o contrato de confiabilidade lista cada fallback_reason. Trate a ressincronização completa como o caminho normal de recuperação.

Não decida com base em reconnected. Ele é true em toda reconexão que enviou um Last-Event-ID, inclusive num resume bem-sucedido, então limpar com base nele descarta o estado em que os eventos reproduzidos são mesclados.

// `resuming`, `isReady` e `clearState()` como no cliente acima eventSource.addEventListener('connected', (e) => { const { resumed, fallback_reason } = JSON.parse(e.data); isReady = false; resuming = resumed === true; if (!resuming) { // Um snapshot completo vem a seguir: primeira conexão, sem Last-Event-ID, ou resumed: false if (fallback_reason) console.log(`Full resync: ${fallback_reason}`); clearState(); } }); eventSource.addEventListener('snapshot', (e) => { if (resuming) { // O servidor desistiu do resume: este snapshot substitui seu estado clearState(); resuming = false; } // ...armazene o chunk como no cliente acima }); eventSource.addEventListener('snapshot:complete', (e) => { const { mode } = JSON.parse(e.data); // "resume", "full_resync", ou ausente numa primeira conexão // Uma ressincronização completa pode vir sem nenhum chunk, se nada mais corresponde aos seus filtros if (mode === 'full_resync' && resuming) clearState(); resuming = false; isReady = true; });

Armadilhas ComunsPermalink for this section

Estes são os erros mais comuns ao construir um cliente SSE. Errar qualquer um deles pode produzir arbitragens fantasmas ou cálculos de EV incorretos.

1. Tratar odds:update como um snapshot completoPermalink for this section

Eventos odds:update contêm apenas odds que mudaram desde o último evento. Se você substituir todo o seu estado local a cada atualização, verá apenas 1-2 books por vez — fazendo cada mercado parecer uma oportunidade de arbitragem.

Solução: Sempre mescle as atualizações no seu Map, nunca o substitua.

2. Ignorar eventos odds:removedPermalink for this section

Quando um sportsbook retira uma linha (mercado suspenso, evento liquidado), enviamos odds:removed com os IDs a serem deletados. Se você não tratar isso, odds obsoletas se acumulam e criam arbitragens fantasmas entre linhas removidas e novas.

Solução: Delete as odds do seu Map quando receber odds:removed.

3. Calcular antes de snapshot:completePermalink for this section

O snapshot inicial é dividido em vários eventos snapshot. Se você começar a calcular arbs ou EV durante o carregamento do snapshot, terá uma visão incompleta dos mercados disponíveis.

Solução: Defina uma flag em snapshot:complete e só comece os cálculos depois disso.

4. Não limpar o estado na reconexãoPermalink for this section

Se uma reconexão termina num snapshot completo e você não limpou seu estado local, linhas da sessão anterior ficam misturadas com os dados novos, inclusive linhas que foram removidas enquanto você estava desconectado. Limpar em toda reconexão também é errado: um resume não envia snapshot, então você ficaria só com as linhas reproduzidas.

Solução: Limpe todos os Maps quando connected chegar sem resumed: true, e quando um chunk de snapshot chegar depois de resumed: true. Não use reconnected, que também é true num resume. Veja Reconexão.

5. Interpretação incorreta do formato das oddsPermalink for this section

Se você tratar odds americanas (-110) como odds decimais, seus cálculos produzirão resultados extremamente incorretos. Nossa API sempre fornece ambos os formatos — use odds_decimal para os cálculos.

6. Ignorar avisos de obsolescênciaPermalink for this section

Oportunidades de EV e arbitragem incluem os campos possibly_stale e oldest_odds_age_seconds. Oportunidades sinalizadas como obsoletas podem ser baseadas em odds com vários minutos de idade e não mais acionáveis.

Solução: Verifique possibly_stale antes de agir sobre qualquer oportunidade.

Last updated on