[object Object]

← back to Bertha

chore(alshi-dash): update 1 file (.js) [+218/-20]

1d7b7c16eff039f9b7b93ad8096287f9540bba10 · 2026-02-15 16:30:16 +0000 · DW Commit Agent

Files touched

Diff

commit 1d7b7c16eff039f9b7b93ad8096287f9540bba10
Author: DW Commit Agent <commit-agent@dw-agents.com>
Date:   Sun Feb 15 16:30:16 2026 +0000

    chore(alshi-dash): update 1 file (.js) [+218/-20]
---
 kalshi-dash/server.js | 238 +++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 218 insertions(+), 20 deletions(-)

diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 6379d0a..b4c1cd9 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1900,8 +1900,130 @@ async function scrapeMetaculus() {
   }
 }
 
+// ── Discord Bot Intelligence (reads messages from joined servers) ──
+const DISCORD_BOT_TOKEN = 'MTQ3MjYyNDg2OTk3NDg2ODA2Mg.GifeyF.C5l9KqQUia-Ex54Ldg23fuxislxwf2QtmkXYII';
+const DISCORD_API = 'https://discord.com/api/v10';
+
+// Channel names to monitor (matched case-insensitive) — covers all Kalshi/Polymarket categories
+const DISCORD_CHANNEL_KEYWORDS = [
+  // Core
+  'general', 'chat', 'discussion', 'lounge', 'main',
+  // Trading & Markets
+  'trading', 'markets', 'predictions', 'analysis', 'strategy', 'signals', 'alerts',
+  'arbitrage', 'polymarket', 'kalshi', 'bets', 'picks', 'plays', 'odds',
+  // News & Facts
+  'news', 'breaking', 'headlines', 'updates', 'osint', 'intel', 'events',
+  // Weather
+  'weather', 'hurricane', 'tropical', 'storm', 'forecast', 'severe', 'climate',
+  // Politics & Elections
+  'politics', 'election', 'polling', 'congress', 'senate', 'trump', 'biden',
+  'policy', 'legislation', 'vote', 'debate',
+  // Economics & Finance
+  'economics', 'economy', 'finance', 'stocks', 'fed', 'inflation', 'rates',
+  'macro', 'earnings', 'options', 'futures', 'bonds',
+  // Sports
+  'sports', 'nfl', 'nba', 'mlb', 'nhl', 'soccer', 'football', 'basketball',
+  'baseball', 'ufc', 'mma', 'boxing', 'f1', 'golf', 'tennis',
+  // Crypto
+  'crypto', 'bitcoin', 'btc', 'ethereum', 'eth', 'defi', 'altcoin', 'solana',
+  // Geopolitics
+  'geopolitics', 'ukraine', 'russia', 'china', 'military', 'conflict', 'war', 'nato',
+  // Entertainment
+  'entertainment', 'movies', 'film', 'oscars', 'grammys', 'awards', 'box-office',
+  'music', 'tv', 'streaming', 'celebrity',
+  // Science & Space
+  'space', 'spacex', 'nasa', 'launch', 'science', 'rocket',
+  // Tech & AI
+  'tech', 'ai', 'artificial', 'openai', 'gpt', 'machine-learning', 'nvidia',
+];
+
+async function discordFetch(endpoint) {
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), 10000);
+  const res = await fetch(`${DISCORD_API}${endpoint}`, {
+    headers: { Authorization: `Bot ${DISCORD_BOT_TOKEN}`, 'Content-Type': 'application/json' },
+    signal: controller.signal,
+  });
+  clearTimeout(timeout);
+  if (!res.ok) {
+    if (res.status === 429) {
+      const retry = await res.json().catch(() => ({}));
+      const wait = (retry.retry_after || 5) * 1000;
+      console.log(`[Ken] Discord rate limited, waiting ${wait}ms`);
+      await new Promise(r => setTimeout(r, wait));
+      return null;
+    }
+    return null;
+  }
+  return res.json();
+}
+
+async function scrapeDiscord(limit = 50) {
+  const results = [];
+  try {
+    // 1. Get all guilds the bot is in
+    const guilds = await discordFetch('/users/@me/guilds');
+    if (!guilds || !Array.isArray(guilds)) {
+      console.log('[Ken] Discord: no guilds found (bot may not have joined any servers yet)');
+      return [];
+    }
+    console.log(`[Ken] Discord: bot is in ${guilds.length} server(s): ${guilds.map(g => g.name).join(', ')}`);
+
+    for (const guild of guilds) {
+      // 2. Get channels for this guild
+      const channels = await discordFetch(`/guilds/${guild.id}/channels`);
+      if (!channels || !Array.isArray(channels)) continue;
+
+      // 3. Filter to text channels matching keywords
+      const textChannels = channels.filter(ch => {
+        if (ch.type !== 0) return false; // type 0 = text channel
+        const name = (ch.name || '').toLowerCase();
+        return DISCORD_CHANNEL_KEYWORDS.some(kw => name.includes(kw));
+      });
+
+      console.log(`[Ken] Discord [${guild.name}]: monitoring ${textChannels.length}/${channels.filter(c => c.type === 0).length} text channels`);
+
+      for (const channel of textChannels.slice(0, 15)) { // cap at 15 channels per guild
+        try {
+          const messages = await discordFetch(`/channels/${channel.id}/messages?limit=${Math.min(limit, 50)}`);
+          if (!messages || !Array.isArray(messages)) continue;
+
+          // Only messages from last 2 hours (fresh intel)
+          const cutoff = Date.now() - (2 * 60 * 60 * 1000);
+          for (const msg of messages) {
+            const msgTime = new Date(msg.timestamp).getTime();
+            if (msgTime < cutoff) continue;
+            if (!msg.content || msg.content.length < 20) continue;
+            if (msg.author?.bot) continue; // skip bot messages
+
+            results.push({
+              title: msg.content.substring(0, 500),
+              url: `https://discord.com/channels/${guild.id}/${channel.id}/${msg.id}`,
+              source: `Discord ${guild.name}`,
+              channel: channel.name,
+              author: msg.author?.username || 'unknown',
+              score: (msg.reactions || []).reduce((sum, r) => sum + (r.count || 0), 0),
+              num_comments: msg.referenced_message ? 1 : 0, // thread indicator
+              created: new Date(msg.timestamp),
+            });
+          }
+          // Rate limit: 500ms between channel reads
+          await new Promise(r => setTimeout(r, 500));
+        } catch (e) {
+          console.log(`[Ken] Discord channel ${channel.name} error: ${e.message}`);
+        }
+      }
+      await new Promise(r => setTimeout(r, 1000)); // between guilds
+    }
+    console.log(`[Ken] Discord: ${results.length} messages from ${guilds.length} server(s)`);
+  } catch (e) {
+    console.log(`[Ken] Discord scrape failed: ${e.message}`);
+  }
+  return results;
+}
+
 // ══════════════════════════════════════════════
-// INTELLIGENCE LOOP (every 30 min) — Reddit, HN, News, Metaculus
+// INTELLIGENCE LOOP (every 30 min) — Reddit, HN, News, Metaculus, Discord
 // ══════════════════════════════════════════════
 async function redditIntelligenceLoop() {
   console.log('[Ken] Running Reddit intelligence sweep...');
@@ -1934,27 +2056,78 @@ async function redditIntelligenceLoop() {
     }
 
     // Also scrape hot posts from key subreddits for general intelligence
-    const hotSubs = ['politics', 'worldnews', 'economics', 'wallstreetbets', 'news',
-      'Kalshi', 'Polymarket', 'PredictionMarket', 'sportsbook', 'CryptoMarkets',
+    // Categories: Weather | Politics | Economics | Sports | Crypto | Geopolitics | Entertainment | Science/Space | Tech/AI | General Sentiment
+    const hotSubs = [
+      // ── Core prediction markets ──
+      'Kalshi', 'Polymarket', 'PredictionMarket', 'sportsbook',
+      // ── Weather (hurricane/temp/storm markets) ──
+      'TropicalWeather', 'weather', 'tornado', 'hurricane', 'climate',
+      // ── Politics & Elections ──
+      'politics', 'Conservative', 'Liberal', 'NeutralPolitics', 'PoliticalDiscussion',
+      'fivethirtyeight', 'elections', 'supremecourt', 'moderatepolitics', 'law',
+      // ── Economics & Finance ──
+      'economics', 'wallstreetbets', 'economy', 'inflation', 'FederalReserve',
+      'StockMarket', 'investing', 'RealEstate', 'finance', 'commodities',
+      // ── Sports ──
+      'nfl', 'nba', 'baseball', 'soccer', 'MMA', 'CFB', 'CollegeBasketball',
+      'formula1', 'golf', 'hockey', 'tennis', 'boxing',
+      // ── Crypto ──
+      'CryptoMarkets', 'Bitcoin', 'ethereum', 'CryptoCurrency', 'BitcoinMarkets', 'solana',
+      // ── Geopolitics ──
+      'worldnews', 'geopolitics', 'UkrainianConflict', 'ukraine', 'China',
+      'MiddleEastNews', 'foreignpolicy', 'military',
+      // ── Entertainment (awards/box office markets) ──
+      'movies', 'boxoffice', 'television', 'Oscars', 'Music', 'entertainment',
+      // ── Science & Space (launch/milestone markets) ──
+      'SpaceX', 'space', 'nasa', 'science', 'Futurology',
+      // ── Tech & AI ──
+      'technology', 'artificial', 'MachineLearning', 'OpenAI', 'singularity', 'Nvidia',
+      // ── General sentiment ──
+      'news', 'OutOfTheLoop', 'AskReddit', 'TrueReddit', 'dataisbeautiful',
     ];
-    for (const sub of hotSubs) {
-      const hotPosts = await scrapeSubredditHot(sub, 10);
-      for (const post of hotPosts) {
-        if (post.score > 50) {
-          const sentiment = scoreSentiment(post.title);
-          await kenQ(`
-            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-            VALUES ('reddit_hot', $1, 'hot', $2, $3, $4, $5, $6, $7)
-            ON CONFLICT DO NOTHING
-          `, [sub, post.title, post.url, post.score, post.num_comments, sentiment, '']);
-          totalPosts++;
+    // Process subreddits in parallel batches of 5 (75+ subs × 2s serial = too slow)
+    const REDDIT_BATCH = 5;
+    for (let i = 0; i < hotSubs.length; i += REDDIT_BATCH) {
+      const batch = hotSubs.slice(i, i + REDDIT_BATCH);
+      const results = await Promise.allSettled(batch.map(sub => scrapeSubredditHot(sub, 8)));
+      for (let j = 0; j < results.length; j++) {
+        if (results[j].status !== 'fulfilled') continue;
+        const sub = batch[j];
+        for (const post of results[j].value) {
+          if (post.score > 30) { // lower threshold to catch more sentiment signals
+            const sentiment = scoreSentiment(post.title);
+            await kenQ(`
+              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+              VALUES ('reddit_hot', $1, 'hot', $2, $3, $4, $5, $6, $7)
+              ON CONFLICT DO NOTHING
+            `, [sub, post.title, post.url, post.score, post.num_comments, sentiment, '']);
+            totalPosts++;
+          }
         }
       }
-      await new Promise(r => setTimeout(r, 2000));
+      await new Promise(r => setTimeout(r, 1500)); // 1.5s between batches
     }
-
-    // Scrape Google News for top market topics
-    const newsQueries = ['trump cabinet resign', 'federal reserve chair nomination', 'greenland acquisition', 'trump insurrection act', '2028 presidential election'];
+    console.log(`[Ken] Reddit hot: ${hotSubs.length} subreddits across 10 categories`);
+
+    // Scrape Google News for top market topics (covers all Kalshi categories)
+    const newsQueries = [
+      // Politics & elections
+      'trump cabinet resign', 'federal reserve chair nomination', '2028 presidential election',
+      'trump executive order', 'government shutdown', 'supreme court ruling',
+      // Economics
+      'CPI inflation report', 'federal reserve rate decision', 'jobs report unemployment',
+      'GDP growth', 'recession forecast',
+      // Weather
+      'hurricane forecast atlantic', 'severe weather outbreak', 'temperature record',
+      // Geopolitics
+      'ukraine russia ceasefire', 'china taiwan', 'middle east conflict',
+      // Crypto
+      'bitcoin price prediction', 'SEC crypto regulation',
+      // Entertainment
+      'oscar nominations', 'box office weekend',
+      // Space
+      'SpaceX launch', 'NASA artemis',
+    ];
     for (const nq of newsQueries) {
       const articles = await scrapeGoogleNews(nq, 5);
       for (const article of articles) {
@@ -2210,7 +2383,32 @@ async function redditIntelligenceLoop() {
 
     totalPosts += tvTotal + lemmyTotal + lobstersTotal + tildesTotal + bskyTotal;
     totalPosts += manifoldTotal + predMarketRedditTotal;
-    console.log(`[Ken] ═══ FULL SWEEP: ${totalPosts} items (Reddit, HN, ${NEWS_FEEDS.length} feeds, TV, YT, Manifold, r/Kalshi, r/Polymarket, Lemmy, Lobsters, Tildes, Bluesky, Mastodon, Metaculus) ═══`);
+
+    // ── Discord (Kalshi, Polymarket, Prediction Markets servers) ──
+    let discordTotal = 0;
+    try {
+      const discordMessages = await scrapeDiscord(30);
+      for (const msg of discordMessages) {
+        const sentiment = scoreSentiment(msg.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('discord', $1, $2, $3, $4, $5, $6, $7, $8)
+          ON CONFLICT DO NOTHING
+        `, [
+          msg.source, msg.channel || 'general', msg.title.substring(0, 500),
+          msg.url, msg.score || 0, msg.num_comments || 0, sentiment,
+          (msg.author || '').substring(0, 200),
+        ]);
+        discordTotal++;
+      }
+      totalPosts += discordTotal;
+      console.log(`[Ken] Discord: ${discordTotal} messages`);
+    } catch (e) {
+      console.log(`[Ken] Discord failed: ${e.message}`);
+    }
+    if (global.gc) global.gc();
+
+    console.log(`[Ken] ═══ FULL SWEEP: ${totalPosts} items (Reddit, HN, ${NEWS_FEEDS.length} feeds, TV, YT, Manifold, r/Kalshi, r/Polymarket, Lemmy, Lobsters, Tildes, Bluesky, Mastodon, Metaculus, Discord) ═══`);
     return totalPosts;
   } catch (e) {
     console.error('[Ken] Intelligence sweep error:', e.message);
@@ -3146,7 +3344,7 @@ server.listen(PORT, '0.0.0.0', () => {
   console.log(`[Ken] Market scan: every 15 minutes`);
   console.log(`[Ken] Reddit/news: every 30 minutes`);
   console.log(`[Ken] Slack alerts: every 4 hours (high-conf bypass)`);
-  console.log(`[Ken] Data sources: Kalshi | Reddit (70+) | ${NEWS_FEEDS.length} news feeds | HN | GDELT TV | YouTube | Manifold | r/Kalshi | r/Polymarket | Lemmy | Lobsters | Tildes | Bluesky | Mastodon | Metaculus | Polymarket`);
+  console.log(`[Ken] Data sources: Kalshi | Reddit (70+) | ${NEWS_FEEDS.length} news feeds | HN | GDELT TV | YouTube | Manifold | r/Kalshi | r/Polymarket | Lemmy | Lobsters | Tildes | Bluesky | Mastodon | Metaculus | Polymarket | Discord`);
   console.log(`[Ken] ═══════════════════════════════════════`);
 
   // Mutex: prevent concurrent sweeps from overlapping and doubling memory

← ee69ff3 chore(alshi-dash): update 1 file (.js) [+19/-16]  ·  back to Bertha  ·  chore(alshi-dash): update 1 file (.js) [+256/-1] fd178e4 →