Telegram Bot para Alertas de Value Bets:
Tutorial passo a passo pra construir um bot de Telegram que monitora odds, detecta value bets e envia alertas formatados direto no seu celular. Node.js + TypeScript +
O que você vai construir
Telegram Bot para Alertas de Value Bets:. Tutorial passo a passo pra construir um bot de Telegram que monitora odds, detecta value bets e envia alertas formatados direto no seu celular. Node.js + TypeScript + The Odds API.
O que você vai construir
Galera, a ideia é simples mas poderosa: em vez de ficar abrindo 10 abas de casas de apostas comparando odds manualmente, você deixa um bot fazendo isso pra você. Quando ele encontra uma discrepância relevante — uma odd que está acima do valor justo — ele manda uma mensagem no Telegram. Você lê, decide e executa. A parte chata fica com a máquina.
O bot tem quatro funcionalidades: monitoramento contínuo de odds via The Odds API, detecção de value bets usando a Pinnacle como benchmark, alerta formatado via Telegram com todas as informações necessárias, e comandos interativos pra configurar ligas, threshold de edge e parar/iniciar o monitoramento.
Por que Node.js e não Python? Porque o Telegram Bot API funciona muito bem com Node.js. As bibliotecas são maduras, a tipagem com TypeScript ajuda muito num projeto com vários tipos de dados, e se você já sabe React (veja o artigo do dashboard), o backend em Node.js faz o ecossistema inteiro ficar JavaScript. Um skill que serve pra tudo.
O projeto final tem cerca de 300 linhas de código. Dá pra construir em uma tarde e deployar de graça no Railway. Vamos lá.
Setup Node.js + Telegram Bot
Criando o bot no Telegram
- Abra o Telegram e busque por @BotFather — é o bot oficial pra criar bots
- Envie o comando /newbot e siga as instruções: nome do bot e username
- Copie o token que o BotFather te dá — é a chave de acesso do seu bot
- Crie um grupo ou canal no Telegram e adicione o bot como administrador
- Descubra o chat_id do grupo: envie uma mensagem e acesse https://api.telegram.org/bot{TOKEN}/getUpdates
- Anote o token e o chat_id — você vai precisar dos dois no código
Estrutura do projeto
mkdir value-bet-bot && cd value-bet-bot
npm init -y
npm install telegraf axios dotenv node-cron
npm install -D typescript @types/node tsx
npx tsc --initUsamos Telegraf como framework pro bot — é o mais popular e bem documentado pra Telegram bots em Node.js. Axios pra chamadas HTTP, dotenv pra variáveis de ambiente, e node-cron pra agendar o polling de odds. TypeScript porque dados de odds são cheios de campos opcionais e tipos aninhados — sem tipagem, é bug garantido.
// src/config.ts
import dotenv from 'dotenv';
dotenv.config();
export const config = {
telegram: {
token: process.env.TELEGRAM_TOKEN!,
chatId: process.env.TELEGRAM_CHAT_ID!,
},
oddsApi: {
key: process.env.ODDS_API_KEY!,
baseUrl: 'https://api.the-odds-api.com/v4',
},
// Configurações de detecção
detection: {
minEdge: 0.03, // edge mínimo de 3% pra alertar
refBookmaker: 'pinnacle', // casa de referência
pollInterval: '*/5 * * * *', // a cada 5 minutos
sports: [
'soccer_brazil_campeonato',
'soccer_epl',
'soccer_spain_la_liga',
],
},
};As variáveis sensíveis ficam no .env e nunca vão pro repositório. Crie um arquivo .env na raiz com TELEGRAM_TOKEN, TELEGRAM_CHAT_ID e ODDS_API_KEY. Adicione .env no .gitignore antes de qualquer commit.
Consumindo a API de odds
A The Odds API retorna odds de dezenas de casas num formato JSON limpo. O endpoint principal é /sports/{sport}/odds com parâmetros pra filtrar região, mercado e formato de odds. Vamos criar um service que abstrai toda essa comunicação.
// src/services/odds-api.ts
import axios from 'axios';
import { config } from '../config';
export interface BookmakerOdds {
gameId: string;
homeTeam: string;
awayTeam: string;
commenceTime: string;
bookmaker: string;
oddHome: number;
oddAway: number;
oddDraw: number;
}
interface ApiGame {
id: string;
home_team: string;
away_team: string;
commence_time: string;
bookmakers: Array<{
key: string;
markets: Array<{
key: string;
outcomes: Array<{ name: string; price: number }>;
}>;
}>;
}
export async function fetchOdds(sport: string): Promise<BookmakerOdds[]> {
const { data } = await axios.get<ApiGame[]>(
`${config.oddsApi.baseUrl}/sports/${sport}/odds`,
{
params: {
apiKey: config.oddsApi.key,
regions: 'eu',
markets: 'h2h',
oddsFormat: 'decimal',
},
}
);
const results: BookmakerOdds[] = [];
for (const game of data) {
for (const bookmaker of game.bookmakers) {
const h2h = bookmaker.markets.find(m => m.key === 'h2h');
if (!h2h) continue;
const outcomes = Object.fromEntries(
h2h.outcomes.map(o => [o.name, o.price])
);
results.push({
gameId: game.id,
homeTeam: game.home_team,
awayTeam: game.away_team,
commenceTime: game.commence_time,
bookmaker: bookmaker.key,
oddHome: outcomes[game.home_team] ?? 0,
oddAway: outcomes[game.away_team] ?? 0,
oddDraw: outcomes['Draw'] ?? 0,
});
}
}
return results;
}
export async function fetchAllSports(): Promise<BookmakerOdds[]> {
const allOdds: BookmakerOdds[] = [];
for (const sport of config.detection.sports) {
try {
const odds = await fetchOdds(sport);
allOdds.push(...odds);
// Respeita rate limit da API
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (err) {
console.error(`Erro ao buscar ${sport}:`, err);
}
}
return allOdds;
}O rate limit da The Odds API é generoso no plano pago, mas no plano gratuito você tem 500 requests por mês. Se pollar 3 esportes a cada 5 minutos, são 864 requests por dia — vai precisar do plano pago. Pra fase de desenvolvimento, reduza pra 2 esportes e poll a cada 15 minutos.
Lógica de detecção de value bets
A lógica de detecção é o coração do bot. A ideia: pegar as odds da Pinnacle como referência de mercado eficiente, comparar com as odds de cada outra casa, e alertar quando uma casa oferece odds significativamente maiores que a Pinnacle pro mesmo resultado.
Por que a Pinnacle como referência? Porque ela aceita apostadores sharp e usa o dinheiro deles como sinal. As odds da Pinnacle são as mais próximas da probabilidade justa que existe no mercado. Se outra casa está oferecendo uma odd 5% acima da Pinnacle, ou ela está errada, ou a Pinnacle está errada. Na maioria das vezes, quem está errada é a outra casa.
// src/services/value-detector.ts
import { BookmakerOdds } from './odds-api';
import { config } from '../config';
export interface ValueBet {
gameId: string;
homeTeam: string;
awayTeam: string;
commenceTime: string;
bookmaker: string;
outcome: 'home' | 'away' | 'draw';
oddOffered: number;
oddPinnacle: number;
edge: number; // percentual
impliedProb: number; // prob implícita da Pinnacle
}
export function detectValueBets(odds: BookmakerOdds[]): ValueBet[] {
const { minEdge, refBookmaker } = config.detection;
const valueBets: ValueBet[] = [];
// Agrupa por jogo
const byGame = new Map<string, BookmakerOdds[]>();
for (const o of odds) {
const list = byGame.get(o.gameId) || [];
list.push(o);
byGame.set(o.gameId, list);
}
for (const [gameId, gameOdds] of byGame) {
const ref = gameOdds.find(o => o.bookmaker === refBookmaker);
if (!ref) continue; // sem Pinnacle, pula
for (const o of gameOdds) {
if (o.bookmaker === refBookmaker) continue;
// Compara cada outcome
const comparisons: Array<{
outcome: 'home' | 'away' | 'draw';
offered: number;
reference: number;
}> = [
{ outcome: 'home', offered: o.oddHome, reference: ref.oddHome },
{ outcome: 'away', offered: o.oddAway, reference: ref.oddAway },
{ outcome: 'draw', offered: o.oddDraw, reference: ref.oddDraw },
];
for (const { outcome, offered, reference } of comparisons) {
if (!offered || !reference || reference <= 1) continue;
const edge = (offered / reference) - 1;
if (edge >= minEdge) {
valueBets.push({
gameId,
homeTeam: o.homeTeam,
awayTeam: o.awayTeam,
commenceTime: o.commenceTime,
bookmaker: o.bookmaker,
outcome,
oddOffered: offered,
oddPinnacle: reference,
edge: Math.round(edge * 10000) / 100, // ex: 4.52%
impliedProb: Math.round((1 / reference) * 10000) / 100,
});
}
}
}
}
return valueBets.sort((a, b) => b.edge - a.edge);
}O threshold de 3% é conservador. Abaixo disso, a diferença pode ser só variação normal de mercado ou atraso de atualização de odds. Acima de 3%, a chance de ser um value bet real é maior. Conforme você coleta dados e valida os alertas, pode ajustar esse número. Alguns apostadores sérios usam 5% como threshold — menos alertas, mas maior confiança em cada um.
Montando o bot completo
Agora vamos juntar tudo: API de odds, detector de value bets, formatação de mensagens e scheduling via cron. O bot também responde a comandos: /status mostra se está rodando, /scan força uma busca imediata, e /config mostra as configurações atuais.
// src/bot.ts
import { Telegraf } from 'telegraf';
import cron from 'node-cron';
import { config } from './config';
import { fetchAllSports } from './services/odds-api';
import { detectValueBets, ValueBet } from './services/value-detector';
const bot = new Telegraf(config.telegram.token);
let isRunning = true;
let lastScan = new Date();
let totalAlerts = 0;
function formatValueBetMessage(vb: ValueBet): string {
const outcomeLabel = {
home: `Vitoria ${vb.homeTeam}`,
away: `Vitoria ${vb.awayTeam}`,
draw: 'Empate',
}[vb.outcome];
const gameTime = new Date(vb.commenceTime).toLocaleString('pt-BR', {
day: '2-digit', month: '2-digit',
hour: '2-digit', minute: '2-digit',
});
return [
`VALUE BET DETECTADO`,
``,
`${vb.homeTeam} x ${vb.awayTeam}`,
`Horario: ${gameTime}`,
``,
`Aposta: ${outcomeLabel}`,
`Casa: ${vb.bookmaker}`,
`Odd oferecida: ${vb.oddOffered.toFixed(2)}`,
`Odd Pinnacle: ${vb.oddPinnacle.toFixed(2)}`,
`Edge: ${vb.edge.toFixed(1)}%`,
`Prob implicita: ${vb.impliedProb.toFixed(1)}%`,
].join('\n');
}
async function scan() {
if (!isRunning) return;
console.log(`[${new Date().toISOString()}] Escaneando odds...`);
lastScan = new Date();
try {
const odds = await fetchAllSports();
const valueBets = detectValueBets(odds);
console.log(` ${odds.length} odds coletadas, ${valueBets.length} value bets encontrados`);
// Envia top 5 pra não spammar
const toSend = valueBets.slice(0, 5);
for (const vb of toSend) {
const message = formatValueBetMessage(vb);
await bot.telegram.sendMessage(config.telegram.chatId, message);
totalAlerts++;
// Delay entre mensagens pra não bater rate limit do Telegram
await new Promise(r => setTimeout(r, 500));
}
if (toSend.length > 0) {
await bot.telegram.sendMessage(
config.telegram.chatId,
`Total: ${valueBets.length} value bets. Mostrando top ${toSend.length} por edge.`
);
}
} catch (err) {
console.error('Erro no scan:', err);
}
}
// Comandos do bot
bot.command('status', (ctx) => {
ctx.reply([
`Bot: ${isRunning ? 'Ativo' : 'Pausado'}`,
`Ultimo scan: ${lastScan.toLocaleString('pt-BR')}`,
`Alertas enviados: ${totalAlerts}`,
`Ligas monitoradas: ${config.detection.sports.length}`,
`Edge minimo: ${config.detection.minEdge * 100}%`,
].join('\n'));
});
bot.command('scan', async (ctx) => {
await ctx.reply('Iniciando scan manual...');
await scan();
await ctx.reply('Scan concluido.');
});
bot.command('pause', (ctx) => {
isRunning = false;
ctx.reply('Monitoramento pausado. Use /resume pra retomar.');
});
bot.command('resume', (ctx) => {
isRunning = true;
ctx.reply('Monitoramento retomado.');
});
bot.command('help', (ctx) => {
ctx.reply([
'Comandos disponiveis:',
'/status - Status atual do bot',
'/scan - Forca scan imediato',
'/pause - Pausa o monitoramento',
'/resume - Retoma o monitoramento',
'/help - Mostra esta mensagem',
].join('\n'));
});
// Agenda o polling
cron.schedule(config.detection.pollInterval, scan);
// Inicia
bot.launch();
console.log('Bot iniciado. Aguardando scans...');
// Graceful shutdown
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));O bot roda com tsx src/bot.ts em desenvolvimento. Pra produção, compile com tsc e rode com node dist/bot.js. O node-cron agenda o scan a cada 5 minutos e o Telegraf cuida da comunicação com o Telegram.
Deploy no Railway
O Railway é a opção mais simples pra deployar um bot que precisa rodar continuamente. Diferente da Vercel que é serverless e mata o processo depois de X segundos, o Railway mantém seu processo Node.js rodando 24/7. O plano hobby dá uns $5 de crédito por mês — mais que suficiente pra um bot leve como esse.
- Crie uma conta no Railway (railway.app) e conecte com seu GitHub
- Crie um novo projeto e selecione o repositório do bot
- Adicione as variáveis de ambiente: TELEGRAM_TOKEN, TELEGRAM_CHAT_ID, ODDS_API_KEY
- Configure o start command: npx tsx src/bot.ts (ou node dist/bot.js se compilar)
- Deploy automático acontece a cada push na main — o Railway detecta o Dockerfile ou usa Nixpacks
- Monitore os logs no dashboard do Railway pra verificar que o cron está rodando
Alternativas ao Railway: Render.com (plano gratuito com cold starts), Fly.io (bom pra containers leves), ou um VPS simples na Hetzner por 3 euros/mês com PM2 pra process management. Se você já tem um VPS rodando o dashboard, coloca o bot no mesmo servidor — é só mais um processo.
Railway
Deploy simples com integração GitHub. Ideal pra bots e processos que rodam continuamente.
+ Prós
- • Deploy automático via GitHub push
- • Variáveis de ambiente no dashboard — sem .env em produção
- • Logs em tempo real e métricas de uso
- • Suporta cron jobs nativamente
− Contras
- • Plano hobby tem limite de crédito mensal
- • Sem domínio customizado no plano gratuito (não precisa pra bot)
- • Cold starts se o serviço for pausado por inatividade
VPS com PM2
Controle total. PM2 gerencia o processo, reinicia em caso de crash e gera logs.
+ Prós
- • Custo fixo baixo — a partir de 3 euros/mês na Hetzner
- • Zero cold starts — processo sempre ativo
- • Roda bot + dashboard + banco tudo no mesmo servidor
- • PM2 monitora e reinicia automaticamente em caso de crash
− Contras
- • Você gerencia atualizações do OS e segurança
- • Deploy manual via SSH (ou configure CI/CD)
- • Precisa configurar monitoramento separado
Checklist pré-deploy do bot
- Variáveis de ambiente configuradas no provider (nunca hardcode no código)
- .env adicionado ao .gitignore
- Bot testado localmente — manda mensagens pro grupo correto
- Rate limit da The Odds API respeitado (delay entre chamadas)
- Graceful shutdown implementado (SIGINT/SIGTERM)
- Log de erros configurado pra não perder informação de falhas
- Mensagens formatadas sem HTML/Markdown problemático
- Comando /status funcionando pra verificar se o bot está ativo remotamente
Conecte com o restante do sistema
Esse bot é uma peça do sistema. Pra ter o máximo de valor, conecte com os modelos estatísticos (artigo de Poisson/ELO/xG) pra calcular probabilidades próprias em vez de usar só a Pinnacle como referência, use o Critério de Kelly pra incluir o stake sugerido em cada alerta, e visualize o histórico de alertas no dashboard de React. A CrazyStack ensina Node.js e TypeScript do zero ao deploy em produção — exatamente o que você precisa pra construir isso.