🔑 Keyword Monitor & Alerts

Analytics src
v1.0.0 Updated: 2026-02-24 336 lines by @nichxbt

Monitor any X/Twitter search for keyword mentions

How to Use

  1. Go to x.com/search?q=your+keyword
  2. Paste and run

Configuration Options

OptionDefaultDescription
keywords[]Will be auto-detected from search URL
minLikes0Min likes to include in results
maxHistory500Max mentions to keep in history

Default Configuration

const CONFIG = {
    keywords: [],              // Will be auto-detected from search URL
    scrollRounds: 4,
    scrollDelay: 1500,
    minLikes: 0,               // Min likes to include in results
    exportResults: true,
    maxHistory: 500,           // Max mentions to keep in history
  };

Full Script

Copy and paste this entire script into your browser DevTools console on x.com.

/**
 * ============================================================
 * 🔍 Keyword Monitor & Alerts — Production Grade
 * ============================================================
 *
 * @name        keywordMonitor.js
 * @description Monitor any X/Twitter search for keyword mentions
 *              in real-time. Scrapes search results, detects new
 *              tweets matching your keywords, tracks mention
 *              frequency, identifies top mentioners, and persists
 *              history in localStorage. Supports auto-refresh
 *              intervals and engagement threshold filters.
 * @author      nichxbt (https://x.com/nichxbt)
 * @version     1.0.0
 * @date        2026-02-24
 * @repository  https://github.com/nirholas/XActions
 *
 * ============================================================
 * 📋 USAGE:
 *
 * MODE A — Run on existing search page:
 *   1. Go to x.com/search?q=your+keyword
 *   2. Paste and run
 *
 * MODE B — Use the API:
 *   XActions.monitor(['keyword1', 'keyword2'])
 *   XActions.autoRefresh(120000)  // Refresh every 2 min
 *   XActions.stop()
 *   XActions.stats()
 *   XActions.history()
 * ============================================================
 */
(() => {
  'use strict';

  const CONFIG = {
    keywords: [],              // Will be auto-detected from search URL
    scrollRounds: 4,
    scrollDelay: 1500,
    minLikes: 0,               // Min likes to include in results
    exportResults: true,
    maxHistory: 500,           // Max mentions to keep in history
  };

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

  const parseNum = (text) => {
    if (!text) return 0;
    text = text.trim().replace(/,/g, '');
    if (text.endsWith('K')) return Math.round(parseFloat(text) * 1000);
    if (text.endsWith('M')) return Math.round(parseFloat(text) * 1000000);
    return parseInt(text) || 0;
  };

  const loadData = () => {
    try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{"mentions":[],"stats":{}}'); }
    catch { return { mentions: [], stats: {} }; }
  };

  const saveData = (data) => {
    // Trim history
    while (data.mentions.length > CONFIG.maxHistory) data.mentions.shift();
    localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
  };

  let autoRefreshTimer = null;

  // ── Detect keywords from URL ───────────────────────────────
  const detectKeywords = () => {
    const url = new URL(window.location.href);
    const q = url.searchParams.get('q');
    if (q) return [q];
    return CONFIG.keywords;
  };

  // ── Scrape search results ──────────────────────────────────
  const scrapeMentions = async () => {
    const mentions = [];
    const seen = new Set();

    for (let round = 0; round < CONFIG.scrollRounds; round++) {
      const articles = document.querySelectorAll('article[data-testid="tweet"]');

      for (const article of articles) {
        const textEl = article.querySelector('[data-testid="tweetText"]');
        const text = textEl ? textEl.textContent.trim() : '';
        if (!text) continue;

        const fp = text.slice(0, 80);
        if (seen.has(fp)) continue;
        seen.add(fp);

        // Time
        const timeEl = article.querySelector('time');
        const datetime = timeEl ? timeEl.getAttribute('datetime') : null;

        // Author
        const authorLink = article.querySelector('a[href^="/"][role="link"]');
        const authorMatch = authorLink ? (authorLink.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/) : null;
        const author = authorMatch ? authorMatch[1] : 'unknown';

        // Metrics
        const likeBtn = article.querySelector('[data-testid="like"] span') || article.querySelector('[data-testid="unlike"] span');
        const rtBtn = article.querySelector('[data-testid="retweet"] span');
        const replyBtn = article.querySelector('[data-testid="reply"] span');
        const likes = likeBtn ? parseNum(likeBtn.textContent) : 0;
        const rts = rtBtn ? parseNum(rtBtn.textContent) : 0;
        const replies = replyBtn ? parseNum(replyBtn.textContent) : 0;

        if (likes < CONFIG.minLikes) continue;

        // Verified
        const verified = !!article.querySelector('[data-testid="icon-verified"]') || !!article.querySelector('svg[aria-label="Verified"]');

        // Media
        const hasMedia = !!(article.querySelector('[data-testid="tweetPhoto"]') || article.querySelector('video'));

        mentions.push({
          author,
          text: text.slice(0, 200),
          datetime,
          likes, rts, replies,
          totalEng: likes + rts + replies,
          verified,
          hasMedia,
          scrapedAt: new Date().toISOString(),
        });
      }

      console.log(`   📜 Round ${round + 1}: ${mentions.length} mentions`);
      window.scrollTo(0, document.body.scrollHeight);
      await sleep(CONFIG.scrollDelay);
    }

    return mentions;
  };

  // ── Main scan ──────────────────────────────────────────────
  const scan = async () => {
    const keywords = detectKeywords();

    const W = 60;
    console.log('╔' + '═'.repeat(W) + '╗');
    console.log('║  🔍 KEYWORD MONITOR' + ' '.repeat(W - 21) + '║');
    console.log('║  by nichxbt — v1.0' + ' '.repeat(W - 21) + '║');
    console.log('╚' + '═'.repeat(W) + '╝');

    if (keywords.length === 0) {
      console.log('\n❌ No keywords detected. Either:');
      console.log('   1. Navigate to x.com/search?q=keyword');
      console.log('   2. Or use: XActions.monitor(["keyword1", "keyword2"])');
      return;
    }

    console.log(`\n🔍 Monitoring: ${keywords.join(', ')}`);
    console.log('📊 Scraping mentions...\n');

    const mentions = await scrapeMentions();

    if (mentions.length === 0) {
      console.log('❌ No mentions found. Try different keywords or scroll more.');
      return;
    }

    // Load existing data and find new mentions
    const data = loadData();
    const existingFps = new Set(data.mentions.map(m => m.text.slice(0, 80)));
    const newMentions = mentions.filter(m => !existingFps.has(m.text.slice(0, 80)));

    // Add new mentions to history
    data.mentions.push(...newMentions);

    // ── Display results ─────────────────────────────────────
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log(`  📊 SCAN RESULTS (${mentions.length} total, ${newMentions.length} new)`);
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

    // Top mentions by engagement
    const sorted = [...mentions].sort((a, b) => b.totalEng - a.totalEng);
    console.log('\n━━━ 🔥 TOP MENTIONS (by engagement) ━━━');
    for (const m of sorted.slice(0, 10)) {
      const badge = m.verified ? '✅' : '  ';
      console.log(`  ${badge} @${m.author.padEnd(16)} ${String(m.totalEng).padStart(5)} eng — "${m.text.slice(0, 50)}..."`);
    }

    // New mentions alert
    if (newMentions.length > 0) {
      console.log(`\n━━━ 🆕 NEW MENTIONS (${newMentions.length}) ━━━`);
      for (const m of newMentions.slice(0, 8)) {
        console.log(`  🔔 @${m.author}: "${m.text.slice(0, 60)}..." (${m.totalEng} eng)`);
      }
    }

    // ── Top mentioners ──────────────────────────────────────
    console.log('\n━━━ 👥 TOP MENTIONERS ━━━');
    const authorCounts = {};
    for (const m of mentions) {
      authorCounts[m.author] = (authorCounts[m.author] || 0) + 1;
    }
    const topAuthors = Object.entries(authorCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
    for (const [author, count] of topAuthors) {
      const bar = '█'.repeat(Math.round(count / Math.max(...Object.values(authorCounts)) * 20));
      console.log(`  @${author.padEnd(16)} ${String(count).padStart(3)} mentions ${bar}`);
    }

    // ── Sentiment quick scan ────────────────────────────────
    console.log('\n━━━ 📊 MENTION SENTIMENT (quick scan) ━━━');
    const positive = ['love', 'great', 'amazing', 'awesome', 'best', 'excellent', 'perfect', 'incredible', 'thank', 'fire', '🔥', '💯', '❤️', '🙌'];
    const negative = ['hate', 'terrible', 'awful', 'worst', 'bad', 'sucks', 'broken', 'scam', 'fake', 'trash', '👎', '🗑️'];

    let posCount = 0, negCount = 0;
    for (const m of mentions) {
      const lower = m.text.toLowerCase();
      if (positive.some(w => lower.includes(w))) posCount++;
      if (negative.some(w => lower.includes(w))) negCount++;
    }
    const neutral = mentions.length - posCount - negCount;

    console.log(`  ✅ Positive: ${posCount} (${((posCount / mentions.length) * 100).toFixed(0)}%)`);
    console.log(`  ❌ Negative: ${negCount} (${((negCount / mentions.length) * 100).toFixed(0)}%)`);
    console.log(`  ⚖️ Neutral:  ${neutral} (${((neutral / mentions.length) * 100).toFixed(0)}%)`);

    // ── Verified mentions ───────────────────────────────────
    const verifiedMentions = mentions.filter(m => m.verified);
    if (verifiedMentions.length > 0) {
      console.log(`\n━━━ ✅ VERIFIED ACCOUNT MENTIONS (${verifiedMentions.length}) ━━━`);
      for (const m of verifiedMentions.slice(0, 5)) {
        console.log(`  ✅ @${m.author}: "${m.text.slice(0, 60)}..." (${m.totalEng} eng)`);
      }
    }

    // ── Media mentions ──────────────────────────────────────
    const mediaMentions = mentions.filter(m => m.hasMedia);
    console.log(`\n  📸 ${mediaMentions.length}/${mentions.length} mentions include media.`);

    // ── Update stats ────────────────────────────────────────
    const kw = keywords.join(',');
    if (!data.stats[kw]) data.stats[kw] = { scans: 0, totalMentions: 0, firstScan: new Date().toISOString() };
    data.stats[kw].scans++;
    data.stats[kw].totalMentions += newMentions.length;
    data.stats[kw].lastScan = new Date().toISOString();

    saveData(data);

    console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');

    if (CONFIG.exportResults) {
      const exportData = {
        keywords, mentions, newMentions: newMentions.length,
        topAuthors: topAuthors.slice(0, 20),
        sentiment: { positive: posCount, negative: negCount, neutral },
        scanTime: new Date().toISOString(),
      };
      const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
      const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
      a.download = `xactions-keyword-monitor-${new Date().toISOString().slice(0, 10)}.json`;
      document.body.appendChild(a); a.click(); a.remove();
      console.log('📥 Keyword data exported.');
    }
  };

  // ── Controls ───────────────────────────────────────────────
  window.XActions = window.XActions || {};

  window.XActions.monitor = (keywords) => {
    if (!Array.isArray(keywords) || keywords.length === 0) {
      console.log('❌ Usage: XActions.monitor(["keyword1", "keyword2"])');
      return;
    }
    CONFIG.keywords = keywords;
    // Navigate to search
    const q = encodeURIComponent(keywords.join(' OR '));
    window.location.href = `https://x.com/search?q=${q}&src=typed_query&f=live`;
    console.log(`🔍 Navigating to search for: ${keywords.join(', ')}`);
    console.log('⏳ Wait for page to load, then the scan will run automatically.');
    setTimeout(scan, 5000);
  };

  window.XActions.autoRefresh = (intervalMs = 120000) => {
    if (autoRefreshTimer) clearInterval(autoRefreshTimer);
    console.log(`🔄 Auto-refreshing every ${(intervalMs / 60000).toFixed(1)} minutes.`);
    autoRefreshTimer = setInterval(() => {
      console.log('\n🔄 Auto-refresh...');
      window.scrollTo(0, 0);
      setTimeout(scan, 2000);
    }, intervalMs);
  };

  window.XActions.stop = () => {
    if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = null; }
    console.log('⏹️ Auto-refresh stopped.');
  };

  window.XActions.stats = () => {
    const data = loadData();
    const kws = Object.keys(data.stats);
    if (kws.length === 0) { console.log('📭 No stats yet.'); return; }
    console.log(`\n📊 KEYWORD STATS (${kws.length} tracked keywords):\n`);
    for (const kw of kws) {
      const s = data.stats[kw];
      console.log(`  "${kw}" — ${s.scans} scans, ${s.totalMentions} total new mentions`);
      console.log(`    First: ${s.firstScan} | Last: ${s.lastScan}`);
    }
  };

  window.XActions.history = () => {
    const data = loadData();
    if (data.mentions.length === 0) { console.log('📭 No mention history.'); return; }
    console.log(`\n📋 MENTION HISTORY (${data.mentions.length} stored):\n`);
    for (const m of data.mentions.slice(-15)) {
      console.log(`  @${m.author} (${m.totalEng} eng): "${m.text.slice(0, 60)}..."`);
    }
  };

  window.XActions.reset = () => {
    localStorage.removeItem(STORAGE_KEY);
    console.log('🗑️ Keyword history cleared.');
  };

  // Auto-run if on search page
  if (window.location.href.includes('/search')) {
    scan();
  } else {
    console.log('╔════════════════════════════════════════════════════╗');
    console.log('║  🔍 KEYWORD MONITOR — Ready                      ║');
    console.log('╚════════════════════════════════════════════════════╝');
    console.log('\n📋 Commands:');
    console.log('  XActions.monitor(["keyword1", "keyword2"])');
    console.log('  XActions.autoRefresh(120000)');
    console.log('  XActions.stats()');
    console.log('  XActions.history()');
    console.log('  XActions.stop()');
  }
})();

⚡ More XActions Scripts

Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.

Browse All Scripts