← back to davidgscott__telegram-wager-bot

Function bodies 23 total

All specs Real LLM only Function bodies
getMonthKey function · javascript · L59-L63 (5 LOC)
index.js
function getMonthKey(date = new Date()) {
  const y = date.getUTCFullYear();
  const m = String(date.getUTCMonth() + 1).padStart(2, '0');
  return `${y}-${m}`; // e.g., "2025-11"
}
getAllTimeScores function · javascript · L65-L70 (6 LOC)
index.js
function getAllTimeScores(chatId) {
  if (!allTimeScoresByChat.has(chatId)) {
    allTimeScoresByChat.set(chatId, new Map());
  }
  return allTimeScoresByChat.get(chatId);
}
getMonthlyScores function · javascript · L72-L81 (10 LOC)
index.js
function getMonthlyScores(chatId, monthKey) {
  if (!monthlyScoresByChat.has(chatId)) {
    monthlyScoresByChat.set(chatId, new Map());
  }
  const monthsMap = monthlyScoresByChat.get(chatId);
  if (!monthsMap.has(monthKey)) {
    monthsMap.set(monthKey, new Map());
  }
  return monthsMap.get(monthKey);
}
addPointsToScores function · javascript · L83-L88 (6 LOC)
index.js
function addPointsToScores(scoresMap, userId, name, delta) {
  const current = scoresMap.get(userId) || { name, points: 0 };
  current.name = name; // keep latest name/username
  current.points += delta;
  scoresMap.set(userId, current);
}
getDisplayName function · javascript · L90-L94 (5 LOC)
index.js
function getDisplayName(from) {
  if (from.username) return '@' + from.username;
  const name = [from.first_name, from.last_name].filter(Boolean).join(' ');
  return name || `User ${from.id}`;
}
updateLeaderboardRow function · javascript · L96-L160 (65 LOC)
index.js
async function updateLeaderboardRow({
  chatId,
  userId,
  name,
  scope,    // 'all' or 'month'
  monthKey, // 'YYYY-MM' for month; ignored for 'all'
  winDelta = 0,
  lossDelta = 0,
}) {
  // If nothing changed, skip the DB hit
  if (winDelta === 0 && lossDelta === 0) return;

  const compositeKey = { 
    chatId: String(chatId),
    userId: String(userId),
    scope,
    monthKey: scope === 'all' ? 'ALL' : monthKey,
  };

  try {
    const existing = await prisma.leaderboardScore.findUnique({
      where: {
        chat_user_scope_month_unique: compositeKey,
      },
    });

    let wins = existing?.wins ?? 0;
    let losses = existing?.losses ?? 0;

    wins += winDelta;
    losses += lossDelta;

    const total = wins + losses;
    const winRate = total > 0 ? wins / total : 0;
    const powerScore = total > 0 ? winRate * 100 * Math.sqrt(total) : 0;

    if (existing) {
      await prisma.leaderboardScore.update({
        where: {
          chat_user_scope_month_unique: composite
formatUtc function · javascript · L167-L172 (6 LOC)
index.js
function formatUtc(date) {
  const iso = date.toISOString();
  const [ymd, hms] = iso.split('T');
  const [hh, mm] = hms.split(':');
  return `${ymd} ${hh}:${mm} UTC`;
}
About: code-quality intelligence by Repobility · https://repobility.com
formatRelative function · javascript · L175-L195 (21 LOC)
index.js
function formatRelative(date, now = new Date()) {
  const diffMs = date.getTime() - now.getTime();
  if (diffMs <= 0) return 'now';

  const minutes = Math.round(diffMs / 60000);
  if (minutes < 1) return 'less than a minute';
  if (minutes === 1) return 'about 1 minute';
  if (minutes < 60) return `about ${minutes} minutes`;

  const hours = Math.floor(minutes / 60);
  const remainMins = minutes % 60;
  if (hours < 24) {
    if (remainMins === 0) return `about ${hours} hour${hours > 1 ? 's' : ''}`;
    return `about ${hours}h ${remainMins}m`;
  }

  const days = Math.floor(hours / 24);
  const remainHours = hours % 24;
  if (remainHours === 0) return `about ${days} day${days > 1 ? 's' : ''}`;
  return `about ${days}d ${remainHours}h`;
}
parseResolution function · javascript · L198-L235 (38 LOC)
index.js
function parseResolution(raw, now = new Date()) {
  const trimmed = raw.trim();

  // ----- 1) Absolute UTC: YYYY-MM-DD HH:MM -----
  const abs = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})$/);
  if (abs) {
    const [, year, month, day, hour, minute] = abs;
    return new Date(Date.UTC(
      Number(year),
      Number(month) - 1,
      Number(day),
      Number(hour),
      Number(minute)
    ));
  }

  // ----- 2) Relative pattern: "in 2 hours", "in 90m", "in 3 days" -----
  const rel = trimmed.match(/^in\s+(\d+)\s*(hours?|hrs?|h|minutes?|mins?|m|days?|d)$/i);
  if (rel) {
    const amount = Number(rel[1]);
    const unit = rel[2].toLowerCase();

    const result = new Date(now);

    if (unit.startsWith('h')) {
      result.setUTCHours(result.getUTCHours() + amount);
    } else if (unit.startsWith('m')) {
      result.setUTCMinutes(result.getUTCMinutes() + amount);
    } else if (unit.startsWith('d')) {
      result.setUTCDate(result.getUTCDate() + amount);
    }

   
parseCondition function · javascript · L239-L253 (15 LOC)
index.js
function parseCondition(raw) {
  const trimmed = raw.trim();
  // e.g. "BTC > 100000" or "DIVI <= 0.01"
  const match = trimmed.match(/^([A-Za-z0-9]+)\s*(>=|<=|>|<)\s*([0-9]+(?:\.[0-9]+)?)$/);
  if (!match) return null;

  const [, symbolRaw, operator, thresholdStr] = match;
  const threshold = Number(thresholdStr);
  if (!Number.isFinite(threshold)) return null;

  const assetSymbol = symbolRaw.toUpperCase();
  const assetId = SYMBOL_TO_ID[assetSymbol] || null;

  return { assetSymbol, assetId, operator, threshold };
}
evaluateCondition function · javascript · L256-L269 (14 LOC)
index.js
function evaluateCondition(price, operator, threshold) {
  switch (operator) {
    case '>':
      return price > threshold;
    case '<':
      return price < threshold;
    case '>=':
      return price >= threshold;
    case '<=':
      return price <= threshold;
    default:
      return false;
  }
}
buildWagerText function · javascript · L272-L294 (23 LOC)
index.js
function buildWagerText(wager, now = new Date()) {
  const remainingMs = wager.voteDeadline.getTime() - now.getTime();
  let statusLine;

  if (remainingMs <= 0) {
    statusLine = 'Voting is CLOSED.';
  } else {
    const remainingSeconds = Math.ceil(remainingMs / 1000);
    statusLine = `Voting closes in ${remainingSeconds} second(s).`;
  }

  const conditionLine = `Condition: ${wager.assetSymbol} ${wager.operator} ${wager.threshold} USD`;

  return (
    `Wager #${wager.id}\n` +
    `${conditionLine}\n\n` +
    `Resolves: ${formatUtc(wager.resolutionTime)} (${formatRelative(wager.resolutionTime, now)})\n` +
    `Voting closes at: ${formatUtc(wager.voteDeadline)}\n\n` +
    `YES: ${wager.yes.size} user(s)\n` +
    `NO: ${wager.no.size} user(s)\n\n` +
    statusLine
  );
}
buildResolvedText function · javascript · L297-L323 (27 LOC)
index.js
function buildResolvedText(wager) {
  const conditionLine = `Condition: ${wager.assetSymbol} ${wager.operator} ${wager.threshold} USD`;
  const winnerSide = wager.outcomeYes ? 'YES' : 'NO';

  const winnersText =
    wager.winners && wager.winners.length
      ? wager.winners.join('\n')
      : 'No winners 😬';

  const losersText =
    wager.losers && wager.losers.length
      ? wager.losers.join('\n')
      : 'No losers 😌';

  return (
    `Wager #${wager.id} (RESOLVED)\n` +
    `${conditionLine}\n\n` +
    `Resolved at: ${formatUtc(wager.resolutionTime)}\n` +
    `Final price: ${wager.finalPrice} USD\n` +
    `Winning side: ${winnerSide}\n\n` +
    `YES: ${wager.yes.size} user(s)\n` +
    `NO: ${wager.no.size} user(s)\n\n` +
    `Winners (${wager.winners?.length || 0}):\n${winnersText}\n\n` +
    `Losers (${wager.losers?.length || 0}):\n${losersText}\n\n` +
    `Voting is CLOSED.`
  );
}
startCountdown function · javascript · L326-L384 (59 LOC)
index.js
function startCountdown(wager) {
  
  // Clear any existing timer just in case
  if (wager.countdownIntervalId) {
    clearInterval(wager.countdownIntervalId);
    wager.countdownIntervalId = null;
  }

  const id = wager.id;

  wager.countdownIntervalId = setInterval(async () => {
    const current = new Date();
    const stored = wagers.get(id);

    // If wager disappeared from memory, stop the timer
    if (!stored) {
      clearInterval(wager.countdownIntervalId);
      wager.countdownIntervalId = null;
      return;
    }

    const remainingMs = stored.voteDeadline.getTime() - current.getTime();
    const done = remainingMs <= 0;
    const newText = buildWagerText(stored, current);

    try {
      await bot.telegram.editMessageText(
        stored.chatId,
        stored.messageId,
        undefined,
        newText,
        {
          reply_markup: {
            inline_keyboard: [
              [
                { text: '👍 YES', callback_data: `join:yes:${id}` },
             
getCurrentPriceUsd function · javascript · L387-L405 (19 LOC)
index.js
async function getCurrentPriceUsd(coinId) {
  if (!coinId) throw new Error('No coinId configured for this symbol');

  const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(
    coinId
  )}&vs_currencies=usd`;

  const res = await fetch(url);
  if (!res.ok) {
    throw new Error(`Price API error: ${res.status} ${res.statusText}`);
  }

  const data = await res.json();
  if (!data[coinId] || typeof data[coinId].usd !== 'number') {
    throw new Error('Unexpected price data shape from CoinGecko');
  }

  return data[coinId].usd;
}
If a scraper extracted this row, it came from Repobility (https://repobility.com)
fetchTop function · javascript · L430-L455 (26 LOC)
index.js
  async function fetchTop(scope, monthKey) {
    const where = {
      chatId: chatIdStr,
      scope,
      monthKey: scope === 'all' ? 'ALL' : monthKey,
    };

    const rows = await prisma.leaderboardScore.findMany({
      where,
      orderBy: { powerScore: 'desc' }, // <-- sort by powerScore
      take: 10,
    });

    if (!rows.length) {
      return 'No scores yet. Resolve a wager first.';
    }

    return rows
      .map((row, i) => {
        const ps = row.powerScore ?? 0;
        const wins = row.wins ?? 0;
        const losses = row.losses ?? 0;
        return `${i + 1}. ${row.name} — PS ${ps.toFixed(1)} (W:${wins} L:${losses})`;
      })
      .join('\n');
  }
tryStrictParse function · javascript · L625-L644 (20 LOC)
index.js
function tryStrictParse(input, now, isDebug) {
  const parts = input.split('|').map((s) => s.trim());
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
    return { success: false };
  }

  const condition = parseCondition(parts[0]);
  if (!condition) return { success: false };

  const resolutionTime = parseResolution(parts[1], now);
  if (!resolutionTime || isNaN(resolutionTime.getTime())) return { success: false };

  const diffMs = resolutionTime.getTime() - now.getTime();
  const minResolutionMs = 10 * 60 * 1000;
  if (!isDebug && diffMs < minResolutionMs) {
    return { success: false, error: `Resolution time must be at least 10 minutes from now.` };
  }

  return { success: true, condition, resolutionTime };
}
validateLLMResult function · javascript · L647-L685 (39 LOC)
index.js
function validateLLMResult(result, now, isDebug) {
  const { symbol, operator, threshold, resolution_time } = result;

  if (!symbol || !operator || threshold == null || !resolution_time) {
    return { error: 'Missing required wager fields. Please try again.' };
  }

  const assetId = SYMBOL_TO_ID[symbol];
  if (!assetId) {
    return { error: `Unsupported symbol: ${symbol}. Supported: ${Object.keys(SYMBOL_TO_ID).join(', ')}` };
  }

  // Parse resolution time — try relative ("2 hours"), then our strict format, then ISO timestamp
  let resolutionTime = parseResolution(`in ${resolution_time}`, now);
  if (!resolutionTime || isNaN(resolutionTime.getTime())) {
    resolutionTime = parseResolution(resolution_time, now);
  }
  if (!resolutionTime || isNaN(resolutionTime.getTime())) {
    // Try parsing as ISO timestamp (e.g. "2026-03-25T07:31:03.442Z")
    const isoAttempt = new Date(resolution_time);
    if (!isNaN(isoAttempt.getTime())) {
      resolutionTime = isoAttempt;
    }
  }
  if
parseWagerWithLLM function · javascript · L760-L780 (21 LOC)
index.js
async function parseWagerWithLLM(messages) {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: WAGER_SYSTEM_PROMPT },
      ...messages,
    ],
    tools: [WAGER_EXTRACT_TOOL],
    tool_choice: { type: 'function', function: { name: 'extract_wager' } },
    temperature: 0,
  });

  const toolCall = response.choices[0]?.message?.tool_calls?.[0];
  if (!toolCall) return null;

  try {
    return JSON.parse(toolCall.function.arguments);
  } catch {
    return null;
  }
}
createWager function · javascript · L783-L867 (85 LOC)
index.js
async function createWager(ctx, condition, resolutionTime, now = new Date()) {
  // Voting window: 60 seconds from creation
  const voteDeadline = new Date(now.getTime() + 60 * 1000);

  // Simple unique ID for the wager
  const id = Date.now().toString();

  const conditionText = `${condition.assetSymbol} ${condition.operator} ${condition.threshold}`;

  // Create wager in memory (with structured condition)
  const wager = {
    id,
    text: conditionText,
    assetSymbol: condition.assetSymbol,
    assetId: condition.assetId,
    operator: condition.operator,
    threshold: condition.threshold,
    yes: new Set(),
    no: new Set(),
    participantNames: new Map(),
    chatId: ctx.chat.id,
    messageId: null,
    createdAt: now,
    resolutionTime,
    voteDeadline,
    countdownIntervalId: null,
    resolved: false,
    finalPrice: null,
    outcomeYes: null,
  };

  wagers.set(id, wager);

  // ---- Persist wager to Postgres (best-effort) ----
  try {
    await prisma.wager.creat
rebuildActiveWagersFromDb function · javascript · L869-L983 (115 LOC)
index.js
async function rebuildActiveWagersFromDb({ graceHours = 24 } = {}) {
  const now = new Date();
  const graceStart = new Date(now.getTime() - graceHours * 60 * 60 * 1000);

  console.log(
    `Rebuilding active wagers from DB (resolved = false, resolutionTime >= ${graceStart.toISOString()})`
  );

  // Clear any existing in-memory wagers
  wagers.clear();

  let dbWagers;
  try {
    dbWagers = await prisma.wager.findMany({
      where: {
        resolved: false,
        resolutionTime: { gte: graceStart },
      },
    });
  } catch (err) {
    console.error(
      'Failed to load unresolved wagers from Postgres:',
      err.message || err
    );
    return;
  }

  if (dbWagers.length === 0) {
    console.log('No unresolved wagers found in DB for rebuild.');
    return;
  }

  // Load all votes for these wagers in a single query
  const wagerIds = dbWagers.map((w) => w.id);

  let dbVotes;
  try {
    dbVotes = await prisma.wagerVote.findMany({
      where: {
        wagerId: { in: wag
rebuildLeaderboardsFromDb function · javascript · L985-L1042 (58 LOC)
index.js
async function rebuildLeaderboardsFromDb() {
  console.log('Rebuilding leaderboards from DB...');

  // Clear in-memory leaderboards
  allTimeScoresByChat.clear();
  monthlyScoresByChat.clear();

  let rows;
  try {
    rows = await prisma.leaderboardScore.findMany();
  } catch (err) {
    console.error(
      'Failed to load leaderboard scores from Postgres:',
      err.message || err
    );
    return;
  }

  if (!rows.length) {
    console.log('No leaderboard rows found in DB.');
    return;
  }

  for (const row of rows) {
    const chatKey = Number(row.chatId) || row.chatId;
    const userKey = Number(row.userId) || row.userId;
    const points = (row.wins || 0) * 10; // matches +10 per win in memory

    if (row.scope === 'all') {
      if (!allTimeScoresByChat.has(chatKey)) {
        allTimeScoresByChat.set(chatKey, new Map());
      }
      const chatMap = allTimeScoresByChat.get(chatKey);
      const existing = chatMap.get(userKey) || { name: row.name, points: 0 };

      // I
resolveDueWagers function · javascript · L1183-L1447 (265 LOC)
index.js
async function resolveDueWagers() {
  const now = new Date();
  console.log('Resolver tick at', now.toISOString());

  let dueWagers;
  try {
    dueWagers = await prisma.wager.findMany({
      where: {
        resolved: false,
        resolutionTime: { lte: now },
      },
      include: {
        votes: true,
      },
    });
  } catch (err) {
    console.error('Failed to load due wagers from Postgres:', err.message || err);
    return;
  }

  if (!dueWagers.length) {
    console.log('No due unresolved wagers found.');
    return;
  }

  for (const dbW of dueWagers) {
    if (!dbW.assetId) {
      console.log('Skipping wager (no assetId):', dbW.id);
      continue;
    }

    const chatId = Number(dbW.chatId) || dbW.chatId;

    // Rebuild yes/no sets and participant names from DB votes
    const yesSet = new Set();
    const noSet = new Set();
    const participantNames = new Map();

    for (const v of dbW.votes) {
      const userIdNum = Number(v.userId);
      const uid = Number.