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
| Metric | What It Means | Signal 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:
| Pattern | Likes | RTs | QTs | Replies | Meaning |
|---|---|---|---|---|---|
| Love | High | High | Low | Low | People agree and amplify |
| Debate | High | Low | High | High | Controversial — people adding opinions |
| Ratio'd | Low | Low | High | High | People disagree — QTs are negative |
| Utility | High | High | Low | Medium | Useful — people save and share |
| Drama | Varies | Low | Very high | Very high | Quote 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
Related Reading
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.