Back to Blog
Tutorial

Quote Tweets & Retweet Analysis: How to Measure True Tweet Virality

April 7, 2026
9 min read
S
By SociaVault Team
TwitterXQuote TweetsRetweetsViralityAnalyticsAPI

Quote Tweets & Retweet Analysis: How to Measure True Tweet Virality

A tweet with 10,000 retweets and 500 quote tweets tells a very different story than a tweet with 10,000 retweets and 0 quote tweets.

Retweets mean "I want my followers to see this." Quote tweets mean "I have something to say about this." The ratio between them — and the sentiment of those quote tweets — tells you whether content is spreading because people love it or because people are roasting it.

This distinction matters for:

  • Brand monitoring: Is your viral tweet going viral for good reasons?
  • Influencer analysis: Does their content generate genuine discussion?
  • Campaign measurement: Are people adding context (good) or dunking (bad)?
  • Competitor intelligence: How does the market react to competitor announcements?

X's native analytics don't break this down. You see a retweet count but can't analyze quote tweet sentiment. This guide shows you how.


Understanding the Virality Signals

MetricWhat It MeansSignal Quality
Likes"I agree/enjoyed this"Weak — low effort
Retweets"My audience should see this"Medium — endorsement
Quote tweets"I have thoughts about this"Strong — generates discussion
Replies"I want to engage with the author"Strong — direct engagement
Bookmarks"I want to save this for later"Strong — utility signal
Views"This was shown to people"Weak — algorithm dependent

The ratio of quote tweets to retweets is one of the most underused virality metrics. A high quote-to-retweet ratio means people aren't just amplifying — they're adding their own perspective.


Pull Quote Tweets for Any Tweet

Analyze what people are saying when they quote a tweet:

const axios = require('axios');

const API_KEY = process.env.SOCIAVAULT_API_KEY;
const BASE_URL = 'https://api.sociavault.com';

async function analyzeQuoteTweets(tweetUrl) {
  // Get the original tweet
  const tweetResponse = await axios.get(`${BASE_URL}/v1/scrape/twitter/tweet`, {
    params: { url: tweetUrl },
    headers: { 'X-API-Key': API_KEY }
  });
  const originalTweet = tweetResponse.data.data;
  const originalStats = originalTweet.legacy || originalTweet.statistics || {};

  // Get quote tweets
  const quotesResponse = await axios.get(`${BASE_URL}/v1/scrape/twitter/quotes`, {
    params: { url: tweetUrl },
    headers: { 'X-API-Key': API_KEY }
  });
  const quotes = quotesResponse.data.data || [];

  console.log(`\n=== QUOTE TWEET ANALYSIS ===`);
  console.log(`Original: "${(originalTweet.full_text || originalTweet.text || '').substring(0, 100)}..."`);
  console.log(`Author: @${originalTweet.author?.screen_name || originalTweet.user?.screen_name}`);
  console.log(`Stats: ${originalStats.favorite_count || 0} likes | ${originalStats.retweet_count || 0} RTs | ${originalStats.quote_count || quotes.length} quotes`);

  // Quote-to-retweet ratio
  const rtCount = originalStats.retweet_count || 1;
  const qtCount = originalStats.quote_count || quotes.length;
  const qtRatio = (qtCount / rtCount * 100).toFixed(1);
  console.log(`\nQuote-to-Retweet ratio: ${qtRatio}%`);

  if (parseFloat(qtRatio) > 50) {
    console.log(`→ HIGH discussion ratio — this tweet sparked debate`);
  } else if (parseFloat(qtRatio) > 20) {
    console.log(`→ MODERATE discussion — mix of amplification and commentary`);
  } else {
    console.log(`→ LOW discussion — mostly amplified without comment`);
  }

  // Analyze quote tweet content
  const positiveWords = ['agree', 'love', 'great', 'exactly', 'perfect', 'this', 'yes', 'true', 'facts', 'brilliant', 'based'];
  const negativeWords = ['disagree', 'wrong', 'bad', 'terrible', 'no', 'false', 'cringe', 'cope', 'ratio', 'awful', 'stupid'];

  let positive = 0;
  let negative = 0;
  let neutral = 0;

  quotes.forEach(qt => {
    const text = (qt.full_text || qt.text || '').toLowerCase();
    const posScore = positiveWords.filter(w => text.includes(w)).length;
    const negScore = negativeWords.filter(w => text.includes(w)).length;

    if (posScore > negScore) positive++;
    else if (negScore > posScore) negative++;
    else neutral++;
  });

  console.log(`\nQuote Tweet Sentiment (${quotes.length} quotes):`);
  console.log(`  Positive: ${positive} (${(positive/Math.max(quotes.length,1)*100).toFixed(0)}%)`);
  console.log(`  Negative: ${negative} (${(negative/Math.max(quotes.length,1)*100).toFixed(0)}%)`);
  console.log(`  Neutral:  ${neutral} (${(neutral/Math.max(quotes.length,1)*100).toFixed(0)}%)`);

  // Show top quote tweets
  const quoteMetrics = quotes.map(qt => {
    const stats = qt.legacy || qt.statistics || {};
    return {
      author: qt.author?.screen_name || qt.user?.screen_name || 'unknown',
      text: (qt.full_text || qt.text || '').substring(0, 120),
      likes: stats.favorite_count || stats.like_count || 0,
      followers: qt.author?.legacy?.followers_count || qt.user?.followers_count || 0
    };
  });

  quoteMetrics.sort((a, b) => b.likes - a.likes);
  console.log(`\nTop quote tweets by engagement:`);
  quoteMetrics.slice(0, 5).forEach((qt, i) => {
    console.log(`  ${i + 1}. @${qt.author} (${qt.followers.toLocaleString()} followers, ${qt.likes} likes)`);
    console.log(`     "${qt.text}..."`);
  });

  return { originalTweet, quotes, positive, negative, neutral, qtRatio };
}

await analyzeQuoteTweets('https://x.com/someuser/status/1234567890');

Cost: 2 credits (1 tweet + 1 quotes).


Analyze Retweet Patterns

Who's amplifying a tweet? Understanding the retweet network reveals how content spreads:

import requests
import os

API_KEY = os.getenv('SOCIAVAULT_API_KEY')
BASE_URL = 'https://api.sociavault.com'
headers = {'X-API-Key': API_KEY}

def analyze_retweet_network(tweet_url):
    """Analyze who retweeted a tweet and their influence"""
    response = requests.get(
        f'{BASE_URL}/v1/scrape/twitter/retweets',
        params={'url': tweet_url},
        headers=headers
    )
    retweeters = response.json().get('data', [])

    # Categorize retweeters by influence
    tiers = {
        'mega': [],    # 1M+ followers
        'macro': [],   # 100K-1M
        'mid': [],     # 10K-100K
        'micro': [],   # 1K-10K
        'nano': [],    # <1K
    }

    total_reach = 0

    for rt in retweeters:
        legacy = rt.get('legacy', rt)
        followers = legacy.get('followers_count', 0)
        screen_name = rt.get('screen_name', legacy.get('screen_name', 'unknown'))
        name = rt.get('name', legacy.get('name', ''))
        verified = rt.get('is_blue_verified', False)

        total_reach += followers

        entry = {
            'handle': screen_name,
            'name': name,
            'followers': followers,
            'verified': verified
        }

        if followers >= 1000000:
            tiers['mega'].append(entry)
        elif followers >= 100000:
            tiers['macro'].append(entry)
        elif followers >= 10000:
            tiers['mid'].append(entry)
        elif followers >= 1000:
            tiers['micro'].append(entry)
        else:
            tiers['nano'].append(entry)

    print(f'\n=== RETWEET NETWORK ANALYSIS ===')
    print(f'Total retweeters analyzed: {len(retweeters)}')
    print(f'Combined reach: {total_reach:,} followers\n')

    print('Retweeter Breakdown:')
    for tier, accounts in tiers.items():
        if accounts:
            tier_reach = sum(a['followers'] for a in accounts)
            print(f'  {tier.upper()} ({len(accounts)} accounts, {tier_reach:,} reach):')
            for a in sorted(accounts, key=lambda x: x['followers'], reverse=True)[:3]:
                badge = ' ✓' if a['verified'] else ''
                print(f'    @{a["handle"]}{badge}{a["followers"]:,} followers')

    # Influence concentration
    if tiers['mega'] or tiers['macro']:
        top_tier_reach = sum(a['followers'] for a in tiers['mega'] + tiers['macro'])
        concentration = (top_tier_reach / max(total_reach, 1)) * 100
        print(f'\n{concentration:.0f}% of reach came from macro/mega accounts')
        if concentration > 70:
            print('→ Virality driven by a few large accounts')
        else:
            print('→ Organic spread across many account sizes')

    return {
        'total_retweeters': len(retweeters),
        'total_reach': total_reach,
        'tiers': {k: len(v) for k, v in tiers.items()}
    }

analyze_retweet_network('https://x.com/someuser/status/1234567890')

Cost: 1 credit.


Compare Virality Across Your Own Tweets

Want to know which of your tweets actually went viral vs just got likes? Analyze the virality quality:

async function compareMyTweetVirality(handle) {
  // Get recent tweets
  const tweetsResponse = await axios.get(`${BASE_URL}/v1/scrape/twitter/user-tweets`, {
    params: { handle },
    headers: { 'X-API-Key': API_KEY }
  });
  const tweets = tweetsResponse.data.data || [];

  const tweetData = tweets.map(t => {
    const stats = t.legacy || t.statistics || {};
    const likes = stats.favorite_count || stats.like_count || 0;
    const retweets = stats.retweet_count || 0;
    const quoteCount = stats.quote_count || 0;
    const replies = stats.reply_count || 0;
    const views = stats.view_count || t.views?.count || 0;

    const totalEngagement = likes + retweets + quoteCount + replies;
    const amplification = retweets + quoteCount;
    const discussion = replies + quoteCount;

    return {
      text: (t.full_text || t.text || '').substring(0, 80),
      likes,
      retweets,
      quotes: quoteCount,
      replies,
      views,
      totalEngagement,
      amplificationScore: amplification / Math.max(likes, 1),
      discussionScore: discussion / Math.max(likes, 1),
      viralityType: amplification > discussion ? 'AMPLIFIED' :
                    discussion > amplification ? 'DISCUSSED' : 'BALANCED'
    };
  });

  // Sort by total engagement
  tweetData.sort((a, b) => b.totalEngagement - a.totalEngagement);

  console.log(`\n=== VIRALITY ANALYSIS: @${handle} ===\n`);
  console.log(`${'Tweet'.padEnd(50)} ${'Likes'.padStart(7)} ${'RTs'.padStart(5)} ${'QTs'.padStart(5)} ${'Type'.padStart(12)}`);
  console.log('-'.repeat(85));

  tweetData.slice(0, 15).forEach(t => {
    const typeEmoji = t.viralityType === 'AMPLIFIED' ? '📢' :
                      t.viralityType === 'DISCUSSED' ? '💬' : '⚖️';
    console.log(`${t.text.padEnd(50)} ${String(t.likes).padStart(7)} ${String(t.retweets).padStart(5)} ${String(t.quotes).padStart(5)} ${typeEmoji} ${t.viralityType}`);
  });

  // Summary
  const amplified = tweetData.filter(t => t.viralityType === 'AMPLIFIED').length;
  const discussed = tweetData.filter(t => t.viralityType === 'DISCUSSED').length;
  const balanced = tweetData.filter(t => t.viralityType === 'BALANCED').length;

  console.log(`\nVirality profile:`);
  console.log(`  📢 Amplified (RT-heavy): ${amplified} tweets`);
  console.log(`  💬 Discussed (reply/QT-heavy): ${discussed} tweets`);
  console.log(`  ⚖️ Balanced: ${balanced} tweets`);

  return tweetData;
}

await compareMyTweetVirality('garyvee');

The Virality Framework

Not all viral content is created equal. Here's how to classify it:

PatternLikesRTsQTsRepliesMeaning
LoveHighHighLowLowPeople agree and amplify
DebateHighLowHighHighControversial — people adding opinions
Ratio'dLowLowHighHighPeople disagree — QTs are negative
UtilityHighHighLowMediumUseful — people save and share
DramaVariesLowVery highVery highQuote tweets are the main engagement

The QT-to-RT ratio is your key metric:

  • under 10%: Pure amplification — content people agree with silently
  • 10-30%: Healthy discussion — people are adding value
  • 30-50%: Controversial — you've hit a nerve
  • >50%: You're being ratio'd — most engagement is negative

Monitoring Campaign Virality

Track how your campaign tweets spread over time:

def campaign_virality_report(tweet_urls, campaign_name):
    """Analyze multiple campaign tweets for virality patterns"""
    print(f'\n=== CAMPAIGN VIRALITY REPORT: {campaign_name} ===\n')

    total_reach = 0
    total_engagement = 0
    virality_types = {'amplified': 0, 'discussed': 0, 'balanced': 0}

    for url in tweet_urls:
        tweet_resp = requests.get(
            f'{BASE_URL}/v1/scrape/twitter/tweet',
            params={'url': url},
            headers=headers
        )
        tweet = tweet_resp.json().get('data', {})
        stats = tweet.get('legacy', tweet.get('statistics', {}))

        likes = stats.get('favorite_count', stats.get('like_count', 0))
        retweets = stats.get('retweet_count', 0)
        quotes = stats.get('quote_count', 0)
        replies = stats.get('reply_count', 0)
        views = stats.get('view_count', 0)

        engagement = likes + retweets + quotes + replies
        total_engagement += engagement
        total_reach += views

        amplification = retweets + quotes
        discussion = replies + quotes

        v_type = 'amplified' if amplification > discussion else 'discussed' if discussion > amplification else 'balanced'
        virality_types[v_type] += 1

        text = (tweet.get('full_text', tweet.get('text', '')))[:60]
        qt_ratio = (quotes / max(retweets, 1) * 100)

        print(f'  "{text}..."')
        print(f'    {likes:,} likes | {retweets:,} RTs | {quotes:,} QTs | {replies:,} replies')
        print(f'    QT ratio: {qt_ratio:.1f}% | Type: {v_type.upper()}')
        print('')

    print(f'CAMPAIGN TOTALS:')
    print(f'  Total impressions: {total_reach:,}')
    print(f'  Total engagement: {total_engagement:,}')
    print(f'  Tweets analyzed: {len(tweet_urls)}')
    print(f'  Amplified: {virality_types["amplified"]} | Discussed: {virality_types["discussed"]} | Balanced: {virality_types["balanced"]}')

    return {
        'total_reach': total_reach,
        'total_engagement': total_engagement,
        'virality_types': virality_types
    }

campaign_virality_report([
    'https://x.com/brand/status/111111111',
    'https://x.com/brand/status/222222222',
    'https://x.com/brand/status/333333333'
], 'Spring 2026 Launch')

Get Started

Sign up free — analyze quote tweets and retweet patterns for any tweet.

Full Twitter/X API docs: docs.sociavault.com/api-reference/twitter


Found this helpful?

Share it with others who might benefit

Ready to Try SociaVault?

Start extracting social media data with our powerful API. No credit card required.