DEV Community

ton-grinder
ton-grinder

Posted on

Building a Poker Bot for TON Telegram Apps: A Developer's Field Guide

TL;DR: TON-based poker apps on Telegram offer real-time game data that developers can use to build analytics tools, HUDs, or automated bankroll trackers. This guide walks through the API patterns, stake structures, and practical implementation strategies I've discovered while building tools for these platforms.

Understanding the TON Poker Ecosystem

Before writing a single line of code, you need to understand how these apps expose game data. Unlike traditional online poker rooms, TON Telegram poker apps use a hybrid architecture:

  • Game Logic Layer: Handled server-side on TON blockchain smart contracts
  • UI Layer: Telegram Mini Apps (WebApp API)
  • Data Layer: Real-time updates via WebSocket connections

When I started reverse-engineering these apps in early 2025, I found three consistent patterns:

Component Technology Developer Access
Table State WebSocket events Read-only (public tables)
Player Actions Telegram Bot API Restricted
Hand History JSON payloads Downloadable after session
Balance Updates TON Connect With user permission

The Stake Structure You'll Encounter

Here's the practical reality: different apps use different stake formats. Through testing across multiple platforms including ChainPoker, I've mapped the standard denomination system:

const stakeLevels = {
  micro: {
    blinds: [0.01, 0.02],
    minBuyIn: 1,
    maxBuyIn: 5,
    activeHours: "always" // 24/7 liquidity
  },
  low: {
    blinds: [0.10, 0.25],
    minBuyIn: 10,
    maxBuyIn: 40,
    activeHours: "peak+4" // 4 hours around UTC evening
  },
  mid: {
    blinds: [0.50, 1.00],
    minBuyIn: 50,
    maxBuyIn: 200,
    activeHours: "peak" // UTC 18:00-22:00
  },
  high: {
    blinds: [2.00, 5.00],
    minBuyIn: 200,
    maxBuyIn: 1000,
    activeHours: "variable" // Depends on player pool
  }
};
Enter fullscreen mode Exit fullscreen mode

Key insight I learned the hard way: Table liquidity follows a power law distribution. Micro stakes will have 10x more tables than low stakes at any given time. If you're building a bot that needs consistent matchmaking, target the micro pool.

Building a Table Scanner

Here's a minimal WebSocket client that monitors available tables:

const WebSocket = require('ws');

class TONPokerScanner {
  constructor(appEndpoint) {
    this.ws = new WebSocket(appEndpoint);
    this.tables = new Map();
  }

  connect() {
    this.ws.on('message', (data) => {
      const payload = JSON.parse(data.toString());

      if (payload.type === 'table_update') {
        this.processTable(payload.table);
      }

      if (payload.type === 'table_close') {
        this.tables.delete(payload.tableId);
      }
    });

    // Subscribe to all stake levels
    this.ws.send(JSON.stringify({
      action: 'subscribe',
      channels: ['tables:*']
    }));
  }

  processTable(table) {
    const key = `${table.gameType}:${table.blinds}`;

    if (!this.tables.has(key)) {
      this.tables.set(key, {
        count: 0,
        avgPlayers: 0,
        lastSeen: Date.now()
      });
    }

    const entry = this.tables.get(key);
    entry.count++;
    entry.avgPlayers = (entry.avgPlayers + table.players) / 2;
  }

  getStakeReport(stakeLevel) {
    const report = {};
    for (const [key, data] of this.tables) {
      if (key.includes(stakeLevel)) {
        report[key] = data;
      }
    }
    return report;
  }
}

// Usage
const scanner = new TONPokerScanner('wss://api.tonpoker.app/tables');
scanner.connect();
Enter fullscreen mode Exit fullscreen mode

Parsing Hand Histories

Once you've found active tables, you'll want to analyze hand data. Most TON poker apps expose hand histories in a standard JSON format. Here's how I parse them:

class HandHistoryParser {
  constructor(rawData) {
    this.hands = this.parseBatch(rawData);
  }

  parseBatch(data) {
    return data.hands.map(hand => ({
      id: hand.id,
      gameType: hand.game_type, // 'holdem', 'omaha', 'short_deck'
      blinds: hand.blinds,
      players: hand.players.map(p => ({
        id: p.id,
        position: p.seat,
        stack: p.stack_at_start,
        vpip: this.calculateVPIP(p.actions)
      })),
      actions: this.parseActions(hand.actions),
      showdown: hand.showdown || null
    }));
  }

  calculateVPIP(actions) {
    if (!actions || actions.length === 0) return 0;

    const preflopActions = actions.filter(a => a.street === 'preflop');
    const voluntaryCallsOrRaises = preflopActions.filter(a => 
      (a.type === 'call' && a.facing_bet > 0) || 
      a.type === 'raise'
    );

    return (voluntaryCallsOrRaises.length / preflopActions.length) * 100;
  }

  parseActions(actions) {
    return actions.map(action => ({
      street: action.street,
      player: action.player_id,
      type: action.action_type, // 'fold', 'check', 'call', 'raise', 'all_in'
      amount: action.amount || 0,
      timestamp: action.timestamp
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Practical Analytics Dashboard

Here's a real dashboard component I built that tracks game selection efficiency:

class GameSelectionAnalyzer {
  constructor(sessionHistory) {
    this.sessions = sessionHistory;
  }

  calculateWinRateByStake() {
    const stats = {};

    for (const session of this.sessions) {
      const key = `${session.gameType}:${session.blinds}`;

      if (!stats[key]) {
        stats[key] = {
          sessions: 0,
          totalProfit: 0,
          totalHours: 0,
          bbPer100: 0
        };
      }

      const entry = stats[key];
      entry.sessions++;
      entry.totalProfit += session.profit;
      entry.totalHours += session.duration_hours;

      // Calculate big blinds per 100 hands
      const handsPlayed = session.hands_played || 100;
      const bigBlind = session.blinds[1]; // Big blind is second element
      entry.bbPer100 = (session.profit / bigBlind) * (100 / handsPlayed);
    }

    return stats;
  }

  findOptimalStakes() {
    const stats = this.calculateWinRateByStake();

    return Object.entries(stats)
      .filter(([_, data]) => data.sessions >= 10) // Minimum sample
      .sort((a, b) => b[1].bbPer100 - a[1].bbPer100)
      .map(([key, data]) => ({
        game: key,
        winRate: data.bbPer100.toFixed(2),
        confidence: Math.min(data.sessions / 50, 1) // Scale to 0-1
      }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls I've Encountered

  1. WebSocket rate limiting - Most apps limit to 10 connections per IP. I solved this by rotating through a proxy pool.

  2. Stake format inconsistencies - Some apps use TON instead of USD. Always normalize to USD using current TON price before analysis.

  3. Tournament vs Cash differences - Spin & Go tournaments use a completely different data structure. My parser needed separate handlers for each game type.

  4. Empty table detection - Some apps show "ghost tables" that never fill. Add a 5-minute timeout filter.

Putting It All Together

Here's the complete flow I use for nightly analysis:

# 1. Scan for active tables
node scanner.js --output tables.json

# 2. Download hand histories from profitable tables
python fetch_hands.py --tables tables.json --output hands/

# 3. Calculate stats and generate report
node analyze.js --hands hands/ --report report.md

# 4. Send digest via Telegram
python notify.py --report report.md
Enter fullscreen mode Exit fullscreen mode

Pro tip: Start with micro stakes on platforms like ChainPoker where the player pool is largest. The lower variance will give you cleaner data for building and testing your tools.

Resources for Further Development

  • TON Blockchain API docs (for smart contract interaction)
  • Telegram Mini Apps WebSocket documentation
  • Open-source hand history converters (PokerStars format works as reference)

The ecosystem is growing fast. When I started building these tools, only 3 apps had public APIs. Now most major TON poker apps including ChainPoker provide developer endpoints. The key is starting simple - get a table scanner working first, then add analytics. Your bot is only as good as the data pipeline feeding it.

If you're tinkering with the same setup, the ChainPoker Telegram bot is here: https://go.chainpk.top/r/geo_auto_202606_t_20260519_010848_4879

Top comments (0)