On the wire · 16:01:33 UTC Live 126 · Prematch 8,738 · Markets 36,348 Vol. I · No. 1 · Monday 9 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
ガイド · Telegramボット · Node

ピナクルのライブオッズでTelegramの予想ボットを作る

更新日 · PinnFeed · 約2分で読めます

このボットはPinnFeedのWebSocketでピナクルの価格を監視し、マーケットにスチームが発生するたびにTelegramのチャンネルへメッセージを投稿します。ここでのスチームとは、十分な最大賭け金があるマーケットで、オッズが2分間に5%以上下落することです。約70行の1ファイルで、Telegramのライブラリは使わず、Bot APIを直接呼び出します。

ステップ 1 — ボットを作成する#

  1. Telegramで @BotFather を開き、/newbot を送信して指示に従います。トークンをコピーしてください。
  2. チャンネルを作成し、投稿権限を持つ管理者としてボットを追加します。
  3. チャットIDには、チャンネルの公開名(@mychannel)、または非公開チャットの場合は数値のIDを使います。

ステップ 2 — 環境を設定する#

export PINNFEED_KEY="ps_live_..."        # Builderプラン(WebSocket)
export TELEGRAM_TOKEN="123456:ABC..."    # @BotFatherから取得
export TELEGRAM_CHAT="@mychannel"
npm install ws

TELEGRAM_TOKEN を設定しない場合、ボットはテストモードで動作し、メッセージを送信せずに画面へ出力します。

ステップ 3 — ボット本体#

telegram-picks-bot.mjs として保存します。

// telegram-picks-bot.mjs: ピナクルのスチームを「ピック」としてTelegramのチャンネルに投稿します。
// npm install ws    Node 18+ (global fetch)
// 環境変数: PINNFEED_KEY、TELEGRAM_TOKEN(@BotFatherから取得)、TELEGRAM_CHAT(例: @mychannel またはチャットID)
import WebSocket from 'ws';

const WS_URL = process.env.PINNFEED_WS || 'wss://pinnfeed.io/v1/stream';
const { PINNFEED_KEY, TELEGRAM_TOKEN, TELEGRAM_CHAT } = process.env;
const MIN_DROP = Number(process.env.MIN_DROP || 0.05);   // 5%の下落が...
const WINDOW_MS = Number(process.env.WINDOW_MS || 120000); // ...2分以内に起きた場合
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" -> 最後にアラートを出した時刻

async function post(text) {
  if (!TELEGRAM_TOKEN) return console.log('[dry run]', text);
  const r = await fetch(`https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage`, {
    method: 'POST', headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ chat_id: TELEGRAM_CHAT, text, disable_web_page_preview: true }),
  });
  if (!r.ok) console.error('telegram', r.status, await r.text());
}

function check(m) {
  const ev = events.get(m.eventId);
  if (!ev || (m.maxStake ?? 0) < 100) 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(`📉 ${ev.sport.toUpperCase()} · ${ev.league?.name ?? ''}\n${ev.home} v ${ev.away}\n` +
      `Pick: ${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}`);
  }
}

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

ステップ 4 — 実行してテストする#

node telegram-picks-bot.mjs

メッセージは次のようになります。

📉 SOCCER · Italy - Serie A
Udinese v Fiorentina
Pick: Udinese (spread +0.25)
Pinnacle 1.88 → 1.70 (−9.5%)
Fair now 1.83 · max stake 150

通知の量は MIN_DROP と WINDOW_MS で調整できます。たとえば MIN_DROP=0.08 にすると、アラートの数は減り、より大きな動きだけが通知されます。

ステップ 5 — 常時稼働させる#

  • systemd、pm2、または失敗時に再起動するコンテナで実行してください。ボットはWebSocketに自動で再接続します。
  • Telegramでは、1つのグループまたはチャンネルにつき毎分約20件までのメッセージが許可されています。429 が返される場合は MIN_DROP を上げてください。
  • 実績は正直に公開しましょう。すべてのピックを記録し、クロージングラインバリューで測定してください。

よくある質問#

ボットにはどのPinnFeedプランが必要ですか?#

WebSocketを使うため、Builder(月額99ユーロ)以上が必要です。FreeやHobbyで更新頻度の低いボットを作る場合は、/v1/drops を1分ごとにポーリングしてください。

ピックをDiscordに投稿できますか?#

はい。post 関数を、Discordのwebhook URLに {"content": text} を本文としてPOSTする処理に置き換えてください。

予想を公開することは合法ですか?#

予想の公開や賭博の広告に関するルールは国によって異なります。日本では刑法により賭博は原則として禁止されており、スポーツに関するベッティングで認められているのは、競馬・競輪・競艇・オートレースといった公営競技やスポーツくじ(toto)など、法律で定められたものに限られます。ご自身と閲覧者がいる地域のルールを確認し、18歳以上の方だけを対象とし、ピックを確実なものとして紹介しないでください。これは法的助言ではありません。

WebSocketプラン:Builder、月額99ユーロ

ピナクルのオッズを1本のWebSocketでリアルタイムに。全13競技、全マーケット、マージンなしのフェアオッズとクロージングオッズ。3日間無料、ワンクリックで解約できます。

Form K-1 · Free API key

Get your free API key.

No password. We email you a link; click it and your key is on screen.

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