On the wire · 04:58:09 UTC Live 568 · Prematch 14,378 · Markets 56,837 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Guia · Bot de Discord · Node

Crie um bot de palpites para Discord com odds da Pinnacle ao vivo

Atualizado em · PinnFeed · 7 min de leitura

Este bot acompanha os preços da Pinnacle no WebSocket da PinnFeed e publica um embed num canal do Discord sempre que um mercado tem steam: a odd cai 5% ou mais em dois minutos num mercado com aposta máxima de pelo menos 100. É um único arquivo de umas 90 linhas. Não precisa de biblioteca do Discord nem de conta de bot, porque publica por um webhook de entrada do canal.

É o gêmeo no Discord do bot de palpites para Telegram. A regra de detecção é a explicada no guia de detecção de steam. É uma ferramenta educacional para maiores de 18 anos, não palpites garantidos.

Passo 1 — Crie o webhook#

  1. No seu servidor do Discord, abra Configurações do servidor → Integrações → Webhooks e escolha Novo webhook.
  2. Dê um nome, escolha o canal onde ele vai publicar e use Copiar URL do webhook.
  3. Trate essa URL como uma senha: quem tiver a URL pode publicar no canal. Se vazar, apague o webhook e crie outro.

Passo 2 — Configure o ambiente#

export PINNFEED_KEY="ps_live_..."                          # plano Builder (WebSocket)
export DISCORD_WEBHOOK="https://discord.com/api/webhooks/..." # do passo 1
npm install ws

Se deixar DISCORD_WEBHOOK sem valor, o bot roda em modo de teste: imprime cada alerta em vez de publicar.

O stream exige Builder (€99/mês) ou superior. Chaves Free e Hobby recebem 403 plan_lacks_ws em /v1/stream; veja os preços.

Passo 3 — O bot#

Salve como discord-picks-bot.mjs:

// discord-picks-bot.mjs: post Pinnacle steam moves to a Discord channel through an incoming webhook.
// npm install ws    Node 18+ (global fetch)
// env: PINNFEED_KEY (Builder or higher), DISCORD_WEBHOOK (channel webhook URL; unset = dry run)
import WebSocket from 'ws';

const WS_URL = process.env.PINNFEED_WS || 'wss://pinnfeed.io/v1/stream';
const { PINNFEED_KEY, DISCORD_WEBHOOK } = process.env;
const MIN_DROP = Number(process.env.MIN_DROP || 0.05);    // 5% shortening...
const WINDOW_MS = Number(process.env.WINDOW_MS || 120000); // ...inside 2 minutes
const MIN_STAKE = Number(process.env.MIN_STAKE || 100);   // skip thin markets
const SIDES = new Set(['home', 'away', 'draw', 'over', 'under']);
const events = new Map();
const highs = new Map();   // "marketId|outcome" -> [{t, p}]
const sent = new Map();    // "marketId|outcome" -> last alert time
const queue = [];          // embeds waiting to be posted
let busy = false;

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// One worker drains the queue, so a slow or rate-limited webhook never blocks the stream.
async function drain() {
  if (busy) return;
  busy = true;
  while (queue.length) {
    const embed = queue.shift();
    if (!DISCORD_WEBHOOK) { console.log('[dry run]', embed.title, '|', embed.description.replace(/\n/g, ' | ')); continue; }
    for (let attempt = 0; attempt < 3; attempt++) {
      const r = await fetch(DISCORD_WEBHOOK, {
        method: 'POST', headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ embeds: [embed], allowed_mentions: { parse: [] } }),
      }).catch((e) => ({ ok: false, status: 0, text: async () => e.message }));
      if (r.status === 429) { const b = await r.json().catch(() => ({})); await sleep(1000 * (b.retry_after ?? 1)); continue; }
      if (!r.ok) console.error('discord', r.status, await r.text());
      break;
    }
  }
  busy = false;
}

function post(embed) {
  queue.push(embed);
  if (queue.length > 50) queue.shift(); // an alert from minutes ago is no longer news
  drain();
}

function check(m) {
  const ev = events.get(m.eventId);
  if (!ev || (m.maxStake ?? 0) < MIN_STAKE) return;
  const now = Date.now();
  for (const o of m.outcomes) {
    if (!SIDES.has(o.name)) continue;
    const k = `${m.id}|${o.name}`;
    const h = (highs.get(k) || []).filter((x) => now - x.t < WINDOW_MS);
    h.push({ t: now, p: o.decimal });
    highs.set(k, h);
    const high = Math.max(...h.map((x) => x.p));
    const drop = 1 - o.decimal / high;
    if (drop < MIN_DROP || now - (sent.get(k) || 0) < WINDOW_MS) continue;
    sent.set(k, now);
    const pick = o.name === 'home' ? ev.home : o.name === 'away' ? ev.away : o.name;
    const line = o.points == null ? '' : m.type === 'total' ? ` ${o.points}` : ` ${o.points > 0 ? '+' : ''}${o.points}`;
    post({
      title: `📉 ${ev.home} v ${ev.away}`,
      description: `**${pick}** (${m.type}${line})\nPinnacle ${high.toFixed(2)} → ${o.decimal.toFixed(2)} (−${(drop * 100).toFixed(1)}%)\n` +
        `Fair now ${o.fair.toFixed(2)} · max stake ${m.maxStake}`,
      color: 0xe8590c,
      footer: { text: `${ev.sport.toUpperCase()} · ${ev.league?.name ?? ''} · 18+ · not a tip` },
      timestamp: new Date(now).toISOString(),
    });
  }
}

function connect(delay = 1000) {
  const ws = new WebSocket(WS_URL, { headers: { 'x-api-key': PINNFEED_KEY }, perMessageDeflate: true });
  ws.on('open', () => {
    delay = 1000;
    ws.send(JSON.stringify({ type: 'subscribe', sports: ['soccer', 'tennis', 'basketball'], markets: ['moneyline', 'spread', 'total'], main: true, specials: false }));
  });
  ws.on('message', (raw) => {
    const msg = JSON.parse(raw);
    if (msg.type === 'ping') return ws.send('{"type":"pong"}');
    if (msg.type === 'snapshot') msg.events.forEach((e) => events.set(e.id, e));
    if (msg.type === 'delta') for (const c of msg.changes) {
      if (c.kind === 'event') c.op === 'upsert' ? events.set(c.event.id, c.event) : events.delete(c.id);
      if (c.kind === 'market' && c.op === 'upsert') check(c.market);
    }
  });
  ws.on('close', () => setTimeout(() => connect(Math.min(delay * 2, 30000)), delay));
  ws.on('error', (e) => console.error('ws error:', e.message));
}
connect();

O que ele faz, em ordem:

  • Assina as linhas principais (moneyline, handicap, total) de futebol, tênis e basquete. Troque sports por qualquer um dos 13 esportes do seu plano; o protocolo WebSocket (em inglês) lista todos os filtros.
  • Guarda uma janela de 2 minutos por mercado e resultado e marca uma queda de MIN_DROP em relação ao máximo da janela. Mercados abaixo de MIN_STAKE são ignorados, porque um movimento com limite baixo diz pouco.
  • Enfileira a publicação. Um único worker envia os embeds ao webhook, então um webhook lento ou limitado nunca trava o stream. Se houver mais de 50 alertas esperando, o mais antigo é descartado.
  • Respeita os 429. O Discord responde a rajadas com 429 e um valor retry_after em segundos no corpo JSON. O bot espera esse tempo e tenta de novo, até três tentativas.
  • Sem menções. allowed_mentions: { parse: [] } impede que o nome de um time ou jogador mencione alguém no canal.

Passo 4 — Rode e teste#

node discord-picks-bot.mjs

No modo de teste, um alerta aparece assim:

[dry run] 📉 Udinese v Fiorentina | **Udinese** (spread +0.25) | Pinnacle 1.88 → 1.70 (−9.6%) | Fair now 1.75 · max stake 150

No Discord o mesmo alerta vira um embed: a partida como título, o lado e o movimento da odd no corpo, a odd justa sem margem e a aposta máxima na última linha, e esporte, liga e a nota "18+ · not a tip" no rodapé.

Ajuste o volume com variáveis de ambiente, por exemplo MIN_DROP=0.08 para menos alertas e mais fortes, ou MIN_STAKE=500 para ficar só com mercados mais profundos.

Passo 5 — Mantenha rodando#

  • Rode com systemd, pm2 ou um contêiner que reinicie em caso de falha. O bot se reconecta sozinho ao WebSocket com backoff e reconstrói a lista de eventos a partir do novo snapshot.
  • Um webhook por canal basta. Se quiser futebol e tênis em canais separados, rode duas cópias com outros sports e webhooks; uma chave Builder permite 3 conexões WebSocket.
  • Registre o histórico com honestidade: anote cada alerta e meça com o valor da linha de fechamento, não com a taxa de acerto.
  • Para saber o que publicar e como moderar um servidor, veja o caso de uso de servidores de Discord.

Perguntas frequentes#

Qual plano da PinnFeed o bot de Discord precisa?#

Builder (€99/mês) ou superior, porque lê o WebSocket. Veja a página do preço do WebSocket ao vivo + pré-jogo para comparar. Os planos pagos têm 3 dias de teste grátis; o cartão é obrigatório e a cobrança acontece após o dia 3, a menos que você cancele.

Posso rodar no plano Free?#

Do jeito que está, não. O Free é só REST com 30 segundos de atraso. Para um bot mais lento no Free ou no Hobby, consulte /v1/drops uma vez por minuto e publique o resultado no mesmo webhook; veja a documentação REST (em inglês).

Preciso de uma conta de bot do Discord ou do discord.js?#

Não. Um webhook de entrada aceita um POST JSON simples, então o bot só precisa de fetch. Use uma conta de bot com discord.js só se quiser comandos de barra ou respostas.

Como evito lotar o canal?#

Aumente MIN_DROP ou MIN_STAKE, reduza sports e mantenha o intervalo por resultado (um alerta por lado de mercado a cada WINDOW_MS). O limite da fila descarta alertas velhos em vez de publicá-los atrasados.

As regras sobre palpites e publicidade de apostas mudam de país para país (no Brasil, por exemplo, as apostas de quota fixa e a publicidade delas são regulamentadas), e o Discord tem suas próprias diretrizes da comunidade. Confira as regras de onde você e seus membros estão, mantenha o servidor só para maiores de 18 anos e nunca apresente alertas como ganhos garantidos.

O plano WebSocket: Builder, €99/mês.

Odds da Pinnacle em tempo real por um único WebSocket: os 13 esportes, todos os mercados, odds justas sem margem e odds de fechamento. 3 dias grátis, cancele com um clique.

Form K-1 · Free API key

Get your free API key.

One field. No password, no card: enter your email, click the link we send, and your key is on screen.

By continuing you agree to the Terms and Privacy Policy.

One free key per email. Link valid for 20 minutes.