Pular para o conteúdo
Tutoriais

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

  1. Abra o Telegram e busque por @BotFather — é o bot oficial pra criar bots
  2. Envie o comando /newbot e siga as instruções: nome do bot e username
  3. Copie o token que o BotFather te dá — é a chave de acesso do seu bot
  4. Crie um grupo ou canal no Telegram e adicione o bot como administrador
  5. Descubra o chat_id do grupo: envie uma mensagem e acesse https://api.telegram.org/bot{TOKEN}/getUpdates
  6. Anote o token e o chat_id — você vai precisar dos dois no código

Estrutura do projeto

bash
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 --init

Usamos 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

  1. Crie uma conta no Railway (railway.app) e conecte com seu GitHub
  2. Crie um novo projeto e selecione o repositório do bot
  3. Adicione as variáveis de ambiente: TELEGRAM_TOKEN, TELEGRAM_CHAT_ID, ODDS_API_KEY
  4. Configure o start command: npx tsx src/bot.ts (ou node dist/bot.js se compilar)
  5. Deploy automático acontece a cada push na main — o Railway detecta o Dockerfile ou usa Nixpacks
  6. 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.