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 SDK
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 REST
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 SSE
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 Completo
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 eventsource
Para uso no servidor, instale o pacote 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' } }
);
// Mesmos manipuladores de eventos do navegador — veja acima
es.addEventListener('snapshot', (e) => { /* ... */ });
es.addEventListener('odds:update', (e) => { /* ... */ });
// etc.Formato das Odds
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ência
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ão
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ê recebe | O que aconteceu | O 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 é enviado | Mantenha 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 retomar | Limpe 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 completa | Limpe 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 snapshot | Uma primeira conexão, ou uma reconexão que não enviou Last-Event-ID | Limpe 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 Comuns
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 completo
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:removed
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:complete
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ão
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 odds
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ência
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.