🧠 Tweet Sentiment Analyzer

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

Analyze the sentiment of your recent tweets,

How to Use

  1. Go to any timeline, profile, or search results on x.com
  2. Open DevTools Console (F12)
  3. Paste and run
  4. Auto-scrolls and analyzes visible tweets

Configuration Options

OptionDefaultDescription
maxTweets100Max tweets to analyze
scrollRounds5Number of scroll rounds to collect

Default Configuration

const CONFIG = {
    maxTweets: 100,                   // Max tweets to analyze
    scrollRounds: 5,                  // Number of scroll rounds to collect
    scrollDelay: 2000,
    exportResults: true,
  };

Full Script

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

/**
 * ============================================================
 * 🧠 Tweet Sentiment Analyzer — Production Grade
 * ============================================================
 *
 * @name        sentimentAnalyzer.js
 * @description Analyze the sentiment of your recent tweets,
 *              mentions, or any visible timeline. Categorizes
 *              posts as positive/negative/neutral using a
 *              battle-tested lexicon-based approach. Shows trends,
 *              toxicity flags, and emotional distribution.
 * @author      nichxbt (https://x.com/nichxbt)
 * @version     1.0.0
 * @date        2026-02-24
 * @repository  https://github.com/nirholas/XActions
 *
 * ============================================================
 * 📋 USAGE:
 *
 * 1. Go to any timeline, profile, or search results on x.com
 * 2. Open DevTools Console (F12)
 * 3. Paste and run
 * 4. Auto-scrolls and analyzes visible tweets
 * ============================================================
 */
(() => {
  'use strict';

  const CONFIG = {
    maxTweets: 100,                   // Max tweets to analyze
    scrollRounds: 5,                  // Number of scroll rounds to collect
    scrollDelay: 2000,
    exportResults: true,
  };

  // ── Sentiment Lexicon ──────────────────────────────────────
  // Carefully curated with intensity weights
  const POSITIVE = {
    // Strong positive (3)
    'amazing': 3, 'incredible': 3, 'fantastic': 3, 'brilliant': 3, 'outstanding': 3,
    'excellent': 3, 'wonderful': 3, 'phenomenal': 3, 'exceptionally': 3, 'masterpiece': 3,
    'extraordinary': 3, 'spectacular': 3, 'magnificent': 3, 'revolutionary': 3, 'legendary': 3,
    // Medium positive (2)
    'great': 2, 'awesome': 2, 'love': 2, 'perfect': 2, 'beautiful': 2, 'impressive': 2,
    'thrilled': 2, 'excited': 2, 'grateful': 2, 'blessed': 2, 'proud': 2, 'inspired': 2,
    'delighted': 2, 'superb': 2, 'remarkable': 2, 'succeed': 2, 'winning': 2, 'victory': 2,
    // Light positive (1)
    'good': 1, 'nice': 1, 'happy': 1, 'like': 1, 'thanks': 1, 'thank': 1, 'helpful': 1,
    'interesting': 1, 'cool': 1, 'agree': 1, 'support': 1, 'hope': 1, 'enjoy': 1,
    'fun': 1, 'smart': 1, 'useful': 1, 'solid': 1, 'strong': 1, 'clean': 1, 'better': 1,
    'congrats': 1, 'well': 1, 'glad': 1, 'fair': 1, 'kind': 1, 'sweet': 1, 'gain': 1,
  };

  const NEGATIVE = {
    // Strong negative (3)
    'terrible': 3, 'horrible': 3, 'disgusting': 3, 'pathetic': 3, 'disastrous': 3,
    'catastrophic': 3, 'abysmal': 3, 'hate': 3, 'despise': 3, 'devastating': 3,
    'outrageous': 3, 'infuriating': 3, 'atrocious': 3, 'repulsive': 3, 'unforgivable': 3,
    // Medium negative (2)
    'bad': 2, 'awful': 2, 'worst': 2, 'angry': 2, 'furious': 2, 'disappointed': 2,
    'frustrated': 2, 'annoying': 2, 'stupid': 2, 'trash': 2, 'garbage': 2, 'broken': 2,
    'scam': 2, 'fraud': 2, 'toxic': 2, 'ridiculous': 2, 'dumb': 2, 'ruined': 2,
    'failed': 2, 'crash': 2, 'ugly': 2, 'waste': 2, 'useless': 2, 'liar': 2,
    // Light negative (1)
    'sad': 1, 'boring': 1, 'wrong': 1, 'slow': 1, 'confusing': 1, 'worried': 1,
    'difficult': 1, 'problem': 1, 'issue': 1, 'miss': 1, 'lost': 1, 'hard': 1,
    'doubt': 1, 'concern': 1, 'risk': 1, 'fear': 1, 'weak': 1, 'lack': 1, 'worse': 1,
  };

  const NEGATORS = new Set(['not', "n't", 'no', 'never', 'neither', 'nobody', 'nothing', 'hardly', 'barely', 'rarely']);
  const INTENSIFIERS = { 'very': 1.5, 'really': 1.5, 'extremely': 2, 'absolutely': 2, 'super': 1.5, 'so': 1.3, 'totally': 1.5, 'incredibly': 2 };

  // ── Emoji sentiment
  const EMOJI_POS = /[😀😃😄😁😆😂🤣😊😇🥰😍🤩😘😗😙😚😋😛😜🤪😝🤑🤗🤭🥳🎉🎊💪🔥✨💯👏❤️💕💖💗💝💘🙏👍👌✅🏆🥇🎯💎🚀⭐🌟]/;
  const EMOJI_NEG = /[😢😭😤😡🤬😠😞😔😟😣😩😫😰😱🤮💀☠️👎❌🚫💩😒😑😐🙄]/;

  // ── Analyze single tweet ──────────────────────────────────
  const analyzeTweet = (text) => {
    const words = text.toLowerCase().replace(/[^\w\s'@#]/g, ' ').split(/\s+/).filter(Boolean);
    let score = 0;
    let posWords = [], negWords = [];

    for (let i = 0; i < words.length; i++) {
      const word = words[i];
      const prevWord = i > 0 ? words[i - 1] : '';
      const negated = NEGATORS.has(prevWord) || (prevWord.endsWith("n't"));
      const intensifier = INTENSIFIERS[prevWord] || 1;

      if (POSITIVE[word]) {
        const val = POSITIVE[word] * intensifier * (negated ? -0.5 : 1);
        score += val;
        if (!negated) posWords.push(word);
      }
      if (NEGATIVE[word]) {
        const val = NEGATIVE[word] * intensifier * (negated ? -0.5 : 1);
        score -= val;
        if (!negated) negWords.push(word);
      }
    }

    // Emoji bonus
    const emojiPosMatches = text.match(new RegExp(EMOJI_POS.source, 'g'));
    const emojiNegMatches = text.match(new RegExp(EMOJI_NEG.source, 'g'));
    if (emojiPosMatches) score += emojiPosMatches.length * 0.5;
    if (emojiNegMatches) score -= emojiNegMatches.length * 0.5;

    // ALL CAPS penalty (shouting)
    const capsRatio = (text.match(/[A-Z]/g) || []).length / Math.max(text.length, 1);
    if (capsRatio > 0.5 && text.length > 10) score *= 1.3; // Amplifies existing emotion

    // Normalize to -1 to 1 range
    const normalized = Math.max(-1, Math.min(1, score / Math.max(words.length * 0.3, 1)));

    let label = 'neutral';
    if (normalized > 0.15) label = 'positive';
    else if (normalized < -0.15) label = 'negative';

    return {
      score: Math.round(normalized * 100) / 100,
      label,
      positiveWords: [...new Set(posWords)],
      negativeWords: [...new Set(negWords)],
    };
  };

  // ── Collect tweets from page ──────────────────────────────
  const collectTweets = () => {
    const articles = document.querySelectorAll('article[data-testid="tweet"]');
    const tweets = [];
    const seen = new Set();

    for (const article of articles) {
      const textEl = article.querySelector('[data-testid="tweetText"]');
      if (!textEl) continue;
      const text = textEl.textContent.trim();
      if (text.length < 5 || seen.has(text)) continue;
      seen.add(text);

      const authorLink = article.querySelector('a[href^="/"][role="link"]');
      const author = authorLink ? (authorLink.getAttribute('href') || '').replace('/', '') : 'unknown';

      tweets.push({ text, author });
    }
    return tweets;
  };

  // ── Main ──────────────────────────────────────────────────
  const run = async () => {
    const W = 60;
    console.log('╔' + '═'.repeat(W) + '╗');
    console.log('║  🧠 TWEET SENTIMENT ANALYZER' + ' '.repeat(W - 31) + '║');
    console.log('║  by nichxbt — v1.0' + ' '.repeat(W - 21) + '║');
    console.log('╚' + '═'.repeat(W) + '╝');

    console.log(`\n📊 Collecting up to ${CONFIG.maxTweets} tweets (${CONFIG.scrollRounds} scroll rounds)...\n`);

    const allTweets = new Map();

    for (let round = 0; round < CONFIG.scrollRounds && allTweets.size < CONFIG.maxTweets; round++) {
      const found = collectTweets();
      for (const t of found) {
        if (allTweets.size >= CONFIG.maxTweets) break;
        if (!allTweets.has(t.text)) allTweets.set(t.text, t);
      }
      console.log(`   📜 Round ${round + 1}: ${allTweets.size} tweets collected`);
      window.scrollTo(0, document.body.scrollHeight);
      await new Promise(r => setTimeout(r, CONFIG.scrollDelay));
    }

    if (allTweets.size === 0) {
      console.error('❌ No tweets found on this page!');
      return;
    }

    // Analyze all tweets
    const results = [];
    let positive = 0, negative = 0, neutral = 0;
    let totalScore = 0;

    for (const [text, tweet] of allTweets) {
      const sentiment = analyzeTweet(text);
      results.push({ ...tweet, ...sentiment });
      totalScore += sentiment.score;
      if (sentiment.label === 'positive') positive++;
      else if (sentiment.label === 'negative') negative++;
      else neutral++;
    }

    // ── Results ─────────────────────────────────────────────
    const total = results.length;
    const avgScore = (totalScore / total).toFixed(3);

    console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('  📊 SENTIMENT ANALYSIS RESULTS');
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log(`\n  Tweets analyzed: ${total}`);
    console.log(`  Average score:   ${avgScore} (${avgScore > 0.1 ? '😊 Positive' : avgScore < -0.1 ? '😠 Negative' : '😐 Neutral'})`);

    // Distribution bar
    const barWidth = 40;
    const posBar = Math.round((positive / total) * barWidth);
    const negBar = Math.round((negative / total) * barWidth);
    const neuBar = barWidth - posBar - negBar;

    console.log(`\n  Distribution:`);
    console.log(`  😊 Positive: ${String(positive).padStart(3)} (${((positive / total) * 100).toFixed(1)}%) ${'█'.repeat(posBar)}`);
    console.log(`  😐 Neutral:  ${String(neutral).padStart(3)} (${((neutral / total) * 100).toFixed(1)}%) ${'░'.repeat(neuBar)}`);
    console.log(`  😠 Negative: ${String(negative).padStart(3)} (${((negative / total) * 100).toFixed(1)}%) ${'▓'.repeat(negBar)}`);

    // Top positive tweets
    const sorted = [...results].sort((a, b) => b.score - a.score);
    console.log('\n  🏆 Most Positive Tweets:');
    for (const t of sorted.slice(0, 3)) {
      console.log(`    [${t.score.toFixed(2)}] @${t.author}: "${t.text.slice(0, 100)}..."`);
    }

    console.log('\n  💀 Most Negative Tweets:');
    for (const t of sorted.slice(-3).reverse()) {
      console.log(`    [${t.score.toFixed(2)}] @${t.author}: "${t.text.slice(0, 100)}..."`);
    }

    // Most common positive/negative words
    const wordFreq = {};
    for (const r of results) {
      for (const w of [...r.positiveWords, ...r.negativeWords]) {
        wordFreq[w] = (wordFreq[w] || 0) + 1;
      }
    }
    const topWords = Object.entries(wordFreq).sort((a, b) => b[1] - a[1]).slice(0, 10);
    if (topWords.length > 0) {
      console.log('\n  📝 Most Frequent Emotional Words:');
      for (const [word, count] of topWords) {
        const type = POSITIVE[word] ? '😊' : '😠';
        console.log(`    ${type} "${word}" — ${count}x`);
      }
    }

    // Toxicity warning
    const toxicCount = results.filter(r => r.score < -0.5).length;
    if (toxicCount > total * 0.2) {
      console.log(`\n  ⚠️ HIGH TOXICITY WARNING: ${((toxicCount / total) * 100).toFixed(0)}% of tweets are strongly negative`);
    }

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

    if (CONFIG.exportResults) {
      const data = {
        summary: { total, positive, negative, neutral, avgScore: parseFloat(avgScore), toxicCount },
        tweets: results,
        analyzedAt: new Date().toISOString(),
        page: window.location.href,
      };
      const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
      const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
      a.download = `xactions-sentiment-${new Date().toISOString().slice(0, 10)}.json`;
      document.body.appendChild(a); a.click(); a.remove();
      console.log('📥 Full results exported as JSON.');
    }
  };

  run();
})();

⚡ More XActions Scripts

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

Browse All Scripts