Key Takeaway: Twitter (X) competitor analysis means systematically tracking rivals across four signals: profile growth, content performance, audience composition, and public sentiment. A solid analysis answers three questions: who is winning attention in your niche, what content patterns drive their engagement, and where their audience overlaps with yours. The most efficient way to do it at scale is through an API rather than a SaaS dashboard.

Most "Twitter competitor analysis" guides on the internet are pitch decks for SaaS platforms. They tell you to pay $199 per seat per month for a dashboard that gives you a snapshot of five accounts and zero ability to backfill data older than your subscription start date. This guide is different. It shows you how to build the same intelligence pipeline yourself using a single API, with code you can run today and a cost structure that makes sense even if you are tracking 20 competitors.

I have spent over a decade building social media data pipelines, and since the 2023 Twitter API pricing overhaul, the dominant pattern among teams I have helped has been the same: replace the SaaS competitor tool with a 200-line Python script and an alternative API like Sorsa API. The savings are usually 70-90%, and the depth of analysis you can run is strictly greater, because you own the data.

This guide walks through a complete, repeatable workflow: profile benchmarking, content strategy decomposition, audience composition mapping, public sentiment analysis, and share-of-voice calculation. Every section includes runnable Python code. By the end, you will have a script you can drop into a weekly cron job and a clear sense of what "good" looks like in your industry.

Table of Contents

  1. Why competitor analysis on X still matters in 2026
  2. The five metrics that actually matter
  3. Setup: what you need before writing any code
  4. Phase 1: Profile benchmarking and growth tracking
  5. Phase 2: Content strategy decomposition
  6. Phase 3: Audience composition
  7. Phase 4: Public sentiment and mentions
  8. Phase 5: Share of voice
  9. Putting it together: the weekly automation script
  10. Common mistakes to avoid
  11. Frequently asked questions
  12. Getting started

Why competitor analysis on X still matters in 2026

X remains the platform where companies test messaging before they scale it, where customer complaints arrive faster than support tickets, and where partnership announcements break before press releases. The signal-to-noise ratio is messy but the signal is real: you can see exactly which competitor features get praised, which campaigns flop, and which audience segments are migrating between brands.

The catch is that the default tools for capturing this are either expensive or shallow. Sprout Social pricing starts at around $199 per seat per month in 2026; Hootsuite Advanced sits at $249 per seat per month. Both lock the most useful competitor features behind their top tiers. Worse, both only show data from the moment you start tracking, so if you sign up in May and want to compare a competitor's Q1 strategy to their Q4 strategy from last year, you are stuck.

An API-driven approach inverts this. You pay per request, you backfill as far as the data exists, and you control which metrics actually get calculated. The trade-off is that you write the analysis logic yourself, which is exactly what this guide covers.

Benchmarks worth knowing

Before diving into how to measure, it helps to know what numbers to expect. Based on widely cited 2026 industry data:

  • Average engagement rate per tweet: 1-3% is considered good, 3-5% is strong, and above 5% is exceptional, typically reserved for niche accounts with loyal audiences or viral posts.
  • Median engagement rate across active brand accounts: around 0.59%, according to data aggregated from 7,000+ marketing agencies via AgencyAnalytics.
  • Average organic impressions per month for active brand accounts: 50,000-200,000.
  • Average reply rate on promoted tweets: 0.02-0.05%; retweet rate: 0.5-1%.

These numbers should be treated as rough reference points, not goals. The right benchmark for your account is the median of three direct competitors in your niche, which is exactly what the workflow below will produce.

The five metrics that actually matter

Most analyses fail because they track 15 metrics at the same level of importance. In practice, five matter, and the rest are either downstream consequences or vanity:

  1. Follower growth rate (weekly delta). Raw follower count tells you nothing; rate of change tells you what is working right now.
  2. Engagement rate per tweet. Calculated as (likes + retweets + replies) / impressions, or as a proxy (likes + retweets + replies) / followers when impressions are unavailable.
  3. Content mix ratio. What percentage of a competitor's tweets are original posts vs. replies vs. quotes vs. retweets. This reveals their playbook.
  4. Verified-follower density. What percentage of new followers are verified or high-authority accounts. A growing brand can fake follower count but cannot easily fake the quality of who follows them.
  5. Share of voice. Your mentions divided by total category mentions (yours + all tracked competitors). The single best signal for whether your overall positioning is gaining ground.

Everything else (hashtag performance, posting frequency, top-tweet themes) is supporting context, useful in analysis but not as a top-line KPI.

Setup: what you need before writing any code

You will need:

  • Python 3.8 or newer with requests installed (pip install requests).
  • A Sorsa API key. The Starter plan at $49/month covers 10,000 requests, which is enough for weekly tracking of 3-5 competitors. For larger setups (20+ competitors, daily polling, deep audience analysis), the Pro plan at $199/month covers 100,000 requests. Full pricing is in the Twitter API pricing guide.
  • A way to store snapshots. CSV is fine for hobby projects; Postgres, BigQuery, or DuckDB scale better.

All code below uses the ApiKey header for authentication. Replace YOUR_API_KEY with your actual key. For details on how the auth flow works, see the authentication docs.

python
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY, "Content-Type": "application/json"}

That is the entire setup. No OAuth dance, no callback URLs, no waiting for app approval.

Phase 1: Profile benchmarking and growth tracking

Endpoints used: /info-batch, /info

The first step is a snapshot: where every competitor stands today. Use the batch profile endpoint to pull up to 100 profiles in a single request, which costs you exactly one request from your quota.

Snapshot script

python
import requests
from datetime import date

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY}

def get_profiles(usernames):
    """Fetch profiles for up to 100 accounts in a single API call."""
    resp = requests.get(
        f"{BASE}/info-batch",
        headers=HEADERS,
        params=[("usernames", u) for u in usernames],
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json().get("users", [])


competitors = ["stripe", "wise", "revolutapp"]
profiles = get_profiles(competitors)

print(f"{'Handle':<18} {'Followers':>12} {'Tweets':>10} {'Following':>10} {'Verified':>10}")
print("-" * 64)
for p in profiles:
    print(
        f"@{p['username']:<17} "
        f"{p['followers_count']:>12,} "
        f"{p['tweets_count']:>10,} "
        f"{p['followings_count']:>10,} "
        f"{str(p.get('verified', False)):>10}"
    )

Running this on three fintech accounts in May 2026 will return something like:

Handle             Followers     Tweets  Following   Verified
----------------------------------------------------------------
@stripe              452,103      8,921        421        True
@wise                398,440     12,108        287        True
@revolutapp          612,775      9,544        198        True

This is your baseline. Log it.

Tracking growth over time

A single snapshot is dead weight. To compute growth rate you need at least two snapshots separated by time. Run the script above daily or weekly via cron, save to CSV or a database, and compute deltas:

python
import csv
from pathlib import Path

def log_snapshot(profiles, output_file="snapshots.csv"):
    """Append today's snapshot to a running CSV log."""
    file_exists = Path(output_file).exists()
    today = date.today().isoformat()
    with open(output_file, "a", newline="") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["date", "username", "followers", "tweets", "following"])
        for p in profiles:
            writer.writerow([
                today,
                p["username"],
                p["followers_count"],
                p["tweets_count"],
                p["followings_count"],
            ])


def compute_growth(csv_file, username, days=7):
    """Compute follower growth rate over the last N days."""
    with open(csv_file) as f:
        rows = [r for r in csv.DictReader(f) if r["username"] == username]
    if len(rows) < 2:
        return None
    latest = int(rows[-1]["followers"])
    earlier = int(rows[max(0, len(rows) - days - 1)]["followers"])
    if earlier == 0:
        return None
    return ((latest - earlier) / earlier) * 100


log_snapshot(profiles)
for handle in competitors:
    growth = compute_growth("snapshots.csv", handle, days=7)
    if growth is not None:
        print(f"@{handle}: {growth:+.2f}% weekly follower growth")

After two weeks of daily snapshots you will know exactly who is gaining ground and who is stalling. A competitor growing at 2% per week while you grow at 0.3% is a flag worth investigating. The cost: one API request per snapshot, so daily logging of 100 competitors over a month is 30 requests total. Negligible.

About account age and bio positioning

The /info endpoint returns created_at, description, location, and bio_urls. These are static signals but useful: bios reveal positioning shifts (when a competitor changes "B2B payments" to "embedded finance," that is a strategy change), and bio_urls show where they currently direct traffic. Diff these across snapshots and you get a low-cost change-detection pipeline.

Phase 2: Content strategy decomposition

Endpoints used: /user-tweets, /search-tweets

Numbers tell you that a competitor is growing; their content tells you why. The goal of this phase is to figure out what their tweets actually look like: format mix, posting cadence, top topics, what drives outliers.

Fetching a competitor's recent tweets

The user-tweets endpoint returns up to 20 tweets per request. Unlike the official X API, it has no 3,200-tweet historical cap, so you can paginate all the way back to a competitor's first tweet if needed. For historical analysis going further back, see the historical data guide.

python
import time

def fetch_user_tweets(username, max_pages=10):
    """Pull a competitor's recent tweets via pagination."""
    all_tweets = []
    cursor = None

    for _ in range(max_pages):
        body = {"username": username}
        if cursor:
            body["next_cursor"] = cursor

        resp = requests.post(
            f"{BASE}/user-tweets",
            headers={**HEADERS, "Content-Type": "application/json"},
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        all_tweets.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)

    return all_tweets

Ten pages returns about 200 tweets, which for most active brand accounts covers 4-8 weeks. That is enough to spot patterns; for trend analysis over a year, increase max_pages to 50-100.

Decomposing the content mix

Once you have the raw tweets, compute the metrics that reveal strategy:

python
def analyze_content(tweets, username):
    if not tweets:
        return None

    total = len(tweets)
    likes = [t.get("likes_count", 0) for t in tweets]
    retweets = [t.get("retweet_count", 0) for t in tweets]
    replies = [t.get("reply_count", 0) for t in tweets]

    original = sum(1 for t in tweets if not t.get("is_reply") and not t.get("retweeted_status"))
    reply_count = sum(1 for t in tweets if t.get("is_reply"))
    quote_count = sum(1 for t in tweets if t.get("is_quote_status"))
    with_media = sum(1 for t in tweets if t.get("entities"))

    avg_engagement = (sum(likes) + sum(retweets) + sum(replies)) / total
    top_tweet = max(tweets, key=lambda t: t.get("likes_count", 0))

    return {
        "username": username,
        "sample_size": total,
        "avg_likes": sum(likes) / total,
        "avg_retweets": sum(retweets) / total,
        "avg_replies": sum(replies) / total,
        "avg_engagement_actions": avg_engagement,
        "original_pct": original / total * 100,
        "reply_pct": reply_count / total * 100,
        "quote_pct": quote_count / total * 100,
        "media_pct": with_media / total * 100,
        "top_tweet_likes": top_tweet.get("likes_count", 0),
        "top_tweet_text": top_tweet.get("full_text", "")[:200],
    }


for handle in competitors:
    tweets = fetch_user_tweets(handle, max_pages=10)
    result = analyze_content(tweets, handle)
    if result:
        print(f"\n@{result['username']} (n={result['sample_size']})")
        print(f"  Avg likes/tweet:     {result['avg_likes']:.1f}")
        print(f"  Avg retweets/tweet:  {result['avg_retweets']:.1f}")
        print(f"  Content mix: {result['original_pct']:.0f}% original / "
              f"{result['reply_pct']:.0f}% replies / {result['quote_pct']:.0f}% quotes / "
              f"{result['media_pct']:.0f}% with media")
        print(f"  Top tweet: ({result['top_tweet_likes']} likes) {result['top_tweet_text']}")

What to actually look for

The raw numbers are not the insight. The insight comes from cross-competitor comparison:

PatternWhat it likely means
Original posts > 70%Broadcast strategy; content is the product
Replies > 40%Community-led; building reach through engagement
Quote tweets > 20%Commentary brand; piggybacking on cultural moments
Media-attached > 60%Visual-first; treating X like Instagram
Top tweets are all product launchesNews-driven; high signal weeks during releases
Top tweets are all memes or hot takesPersonality-driven; viral risk-tolerant

When I worked with a fintech client last year tracking three direct competitors, this breakdown was the single most useful thing we produced. One competitor's 80%-original-post strategy was driving 60% lower engagement per follower than another competitor's 50/50 mix with heavy replies. The client immediately shifted their own content split and saw engagement-per-tweet roughly double within six weeks.

Historical comparison

To compare a competitor's Q1 vs Q4 strategy, switch from /user-tweets to search-tweets and use the since: and until: operators. This is one of the biggest advantages over SaaS tools, which usually only show data from the moment you subscribed.

python
def fetch_tweets_in_range(username, since_date, until_date):
    query = f"from:{username} since:{since_date} until:{until_date}"
    resp = requests.post(
        f"{BASE}/search-tweets",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"query": query, "order": "latest"},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json().get("tweets", [])


q1_tweets = fetch_tweets_in_range("stripe", "2026-01-01", "2026-04-01")
q4_tweets = fetch_tweets_in_range("stripe", "2025-10-01", "2026-01-01")

Phase 3: Audience composition

Endpoints used: /followers, /verified-followers, /followers-stats

A competitor's follower list reveals who their audience actually is. There are two angles here, and they have very different costs.

The cheap version: verified followers only

The verified-followers endpoint returns only verified accounts following a given handle. This is the highest-signal slice of an audience: journalists, brands, investors, notable creators. It is also dramatically cheaper than pulling the full follower list. For a competitor with 500K followers, you might have only a few thousand verified followers, paginated 200 per request.

python
def fetch_verified_followers(username, max_pages=10):
    all_users = []
    cursor = None
    for _ in range(max_pages):
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor
        resp = requests.get(
            f"{BASE}/verified-followers",
            headers=HEADERS,
            params=params,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        all_users.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_users


for handle in competitors:
    verified = fetch_verified_followers(handle, max_pages=5)
    top = sorted(verified, key=lambda u: u.get("followers_count", 0), reverse=True)[:10]
    print(f"\n@{handle}: {len(verified)} verified followers fetched")
    print("Top 10 by reach:")
    for u in top:
        print(f"  @{u['username']:<25} {u['followers_count']:>10,} followers")

Diff these lists across snapshots and you get something genuinely useful: a feed of new high-authority followers each competitor picks up. Journalists starting to follow a competitor often precede coverage by 2-4 weeks.

The expensive version: full follower extraction and overlap

If budget allows, the full followers endpoint returns up to 200 profiles per request. For a competitor with 1M followers, that is 5,000 requests, which is half a month on the Pro plan ($199/mo for 100,000 requests). For accounts with 5M+ followers, you should be on Enterprise or a custom plan. This is the realistic cost of doing serious audience analysis at scale, and it is still far cheaper than the equivalent SaaS approach.

Once you have two lists, computing overlap is a one-liner with sets:

python
def fetch_all_followers(username, max_pages=200):
    """Pull all followers via pagination. WARNING: expensive for large accounts."""
    all_users = []
    cursor = None
    for _ in range(max_pages):
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor
        resp = requests.get(
            f"{BASE}/followers",
            headers=HEADERS,
            params=params,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        all_users.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_users


followers_a = {u["id"] for u in fetch_all_followers("competitor_a")}
followers_b = {u["id"] for u in fetch_all_followers("competitor_b")}

overlap = followers_a & followers_b
only_a = followers_a - followers_b
only_b = followers_b - followers_a

print(f"Shared audience: {len(overlap):,}")
print(f"Unique to @competitor_a: {len(only_a):,}")
print(f"Unique to @competitor_b: {len(only_b):,}")

The ratio len(overlap) / min(len(followers_a), len(followers_b)) tells you how much of a competitor's audience is "shared" with another player in the space. High overlap suggests substitutable products; low overlap suggests differentiated positioning, which is itself a strategic insight.

Crypto and Web3 accounts: category breakdown

If your competitive set is in crypto or Web3, the /followers-stats endpoint returns a categorical breakdown specific to that ecosystem: how many of the account's followers are influencers, projects, or VCs, based on Sorsa's internal database. For crypto-focused teams, this is the single fastest way to read the quality of a competitor's audience without pulling and classifying tens of thousands of profiles manually.

python
def get_follower_breakdown(username):
    resp = requests.get(
        f"{BASE}/followers-stats",
        headers=HEADERS,
        params={"username": username},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


for handle in ["VitalikButerin", "saylor"]:
    stats = get_follower_breakdown(handle)
    print(f"\n@{handle}:")
    print(f"  Tracked followers: {stats['followers_count']}")
    print(f"  Influencers:       {stats['influencers_count']}")
    print(f"  Projects:          {stats['projects_count']}")
    print(f"  VCs:               {stats['venture_capitals_count']}")

Note that these counts only include accounts already tracked in Sorsa's crypto database, so they are not a complete picture for non-crypto accounts.

Phase 4: Public sentiment and mentions

Endpoint used: /mentions

What a competitor posts is half the picture. What the public says about them is the other half, and it is often more useful because it reveals complaints, feature requests, and PR vulnerabilities you can act on. For a deeper dive on the mentions endpoint with all its filters, see the track mentions guide.

Pulling high-engagement mentions

The mentions endpoint supports filtering by minimum likes, retweets, and replies, plus date ranges. This matters because a brand's mention stream is mostly low-signal noise (auto-replies, retweets of their own posts, random tags). Filtering by min_likes: 10+ cuts through to mentions that actually moved the conversation.

python
def fetch_mentions(handle, min_likes=10, since_date=None, until_date=None, max_pages=5):
    all_mentions = []
    cursor = None
    for _ in range(max_pages):
        body = {"query": handle, "order": "popular", "min_likes": min_likes}
        if since_date:
            body["since_date"] = since_date
        if until_date:
            body["until_date"] = until_date
        if cursor:
            body["next_cursor"] = cursor

        resp = requests.post(
            f"{BASE}/mentions",
            headers={**HEADERS, "Content-Type": "application/json"},
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        all_mentions.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_mentions

Sentiment: from keyword matching to real NLP

Naive keyword matching (counting "love" vs "hate") works as a first pass but produces a lot of false positives. For production use, swap it out for VADER, an open-source sentiment library tuned specifically for social media text. It handles negation, intensifiers, and emoji reasonably well, and runs locally with no API costs.

python
# pip install vaderSentiment
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer

analyzer = SentimentIntensityAnalyzer()

def classify_sentiment(mentions):
    results = {"positive": [], "negative": [], "neutral": []}
    for m in mentions:
        score = analyzer.polarity_scores(m["full_text"])["compound"]
        if score >= 0.05:
            results["positive"].append((score, m))
        elif score <= -0.05:
            results["negative"].append((score, m))
        else:
            results["neutral"].append((score, m))
    return results


for handle in competitors:
    mentions = fetch_mentions(handle, min_likes=10, max_pages=5)
    s = classify_sentiment(mentions)
    print(f"\n@{handle}: {len(mentions)} mentions analyzed")
    print(f"  Positive: {len(s['positive'])}  Negative: {len(s['negative'])}  Neutral: {len(s['neutral'])}")
    if s["negative"]:
        worst = min(s["negative"], key=lambda x: x[0])
        text = worst[1]["full_text"][:150].replace("\n", " ")
        print(f"  Sharpest negative: {text}...")

For higher accuracy on nuanced cases (sarcasm, technical complaints, mixed sentiment), pipe full_text into an LLM via the OpenAI or Anthropic API. The cost is roughly $0.0001 per mention on a small model, and accuracy on social text typically lands in the 85-92% range, well above VADER's ~70%. The trade-off is latency and per-call cost, so a hybrid (VADER for fast filtering, LLM only on flagged or high-engagement mentions) is often the right architecture.

What to do with the output

The high-engagement negative mentions are the goldmine. They are public complaints that a real audience has rallied around, which means the issue is loud enough to matter. If three different users complain about the same feature with 200+ likes each, that is a market signal stronger than any user research session.

Phase 5: Share of voice

The single best top-line metric for "are we winning this category" is share of voice (SOV): your mention volume divided by the total mention volume across you and your tracked competitors. It rolls up everything from product launches to PR wins to community health into one number.

The formula:

SOV = (your mentions in period) / (your mentions + sum of competitor mentions in period)

Implementation:

python
from datetime import datetime, timedelta

def count_mentions(handle, days=7, min_likes=0):
    until = datetime.utcnow().date().isoformat()
    since = (datetime.utcnow() - timedelta(days=days)).date().isoformat()
    mentions = fetch_mentions(
        handle,
        min_likes=min_likes,
        since_date=since,
        until_date=until,
        max_pages=20,
    )
    return len(mentions)


brand = "your_handle"
your_mentions = count_mentions(brand, days=7, min_likes=5)
competitor_mentions = {h: count_mentions(h, days=7, min_likes=5) for h in competitors}

total = your_mentions + sum(competitor_mentions.values())
print(f"\nShare of voice, last 7 days (min 5 likes):")
print(f"  @{brand:<20} {your_mentions:>5}  ({your_mentions/total*100:.1f}%)")
for h, n in sorted(competitor_mentions.items(), key=lambda x: -x[1]):
    print(f"  @{h:<20} {n:>5}  ({n/total*100:.1f}%)")

A few practical notes on SOV:

  • Filter by minimum engagement (min_likes: 5+ is a reasonable floor). Including zero-like mentions inflates everyone's numbers with bot traffic and spam.
  • Track week-over-week, not absolute. A weekly delta of -3 percentage points means your competitor's launch sucked attention away from you. A weekly delta of +5 points after a campaign tells you the campaign worked.
  • Segment by topic if you can. SOV for "our brand name" is one number; SOV for the category keyword ("embedded finance" or "AI agents" or whatever) is a different and often more useful one. Use /search-tweets with the category keyword to compute the denominator.

Putting it together: the weekly automation script

Here is the consolidated script that runs all five phases for a list of competitors and prints a dashboard. Drop this into a cron job or GitHub Actions schedule and you have a weekly competitor intelligence pipeline.

python
import requests
import csv
import time
from datetime import date, datetime, timedelta
from pathlib import Path

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY}

BRAND = "your_handle"
COMPETITORS = ["competitor1", "competitor2", "competitor3"]
SNAPSHOT_FILE = "snapshots.csv"


def header(text):
    line = "=" * 64
    print(f"\n{line}\n{text}\n{line}")


def run_weekly_report():
    header("PHASE 1: PROFILE BENCHMARKS")
    profiles = get_profiles(COMPETITORS + [BRAND])
    print(f"{'Handle':<18} {'Followers':>12} {'Tweets':>10} {'Verified':>10}")
    for p in profiles:
        print(f"@{p['username']:<17} {p['followers_count']:>12,} "
              f"{p['tweets_count']:>10,} {str(p.get('verified', False)):>10}")
    log_snapshot(profiles)
    for h in COMPETITORS + [BRAND]:
        g = compute_growth(SNAPSHOT_FILE, h, days=7)
        if g is not None:
            print(f"  @{h}: {g:+.2f}% weekly follower growth")

    header("PHASE 2: CONTENT STRATEGY")
    for handle in COMPETITORS:
        tweets = fetch_user_tweets(handle, max_pages=5)
        result = analyze_content(tweets, handle)
        if result:
            print(f"@{result['username']}: avg {result['avg_likes']:.0f} likes/tweet, "
                  f"{result['original_pct']:.0f}% original, "
                  f"{result['media_pct']:.0f}% with media")

    header("PHASE 3: VERIFIED FOLLOWERS")
    for handle in COMPETITORS:
        verified = fetch_verified_followers(handle, max_pages=3)
        print(f"@{handle}: {len(verified)} verified followers in top pages")

    header("PHASE 4: SENTIMENT")
    for handle in COMPETITORS:
        mentions = fetch_mentions(handle, min_likes=10, max_pages=3)
        s = classify_sentiment(mentions)
        print(f"@{handle}: {len(s['positive'])} pos / {len(s['negative'])} neg "
              f"/ {len(s['neutral'])} neutral (n={len(mentions)})")

    header("PHASE 5: SHARE OF VOICE (7d)")
    your_n = count_mentions(BRAND, days=7, min_likes=5)
    comp_n = {h: count_mentions(h, days=7, min_likes=5) for h in COMPETITORS}
    total = your_n + sum(comp_n.values())
    if total:
        print(f"  @{BRAND}: {your_n} ({your_n/total*100:.1f}%)")
        for h, n in sorted(comp_n.items(), key=lambda x: -x[1]):
            print(f"  @{h}: {n} ({n/total*100:.1f}%)")


if __name__ == "__main__":
    run_weekly_report()

For the full helper functions referenced above (get_profiles, log_snapshot, compute_growth, etc.), assemble them from the earlier sections of this guide. The total cost of running this weekly across three competitors is roughly 150-200 requests per run, or under 1,000 requests per month, which fits comfortably in the Starter plan.

For broader patterns on building Twitter-data pipelines in Python with retry logic, error handling, and batch optimization, see the Twitter API Python guide.

Common mistakes to avoid

A few patterns I have seen repeatedly when reviewing competitor analysis setups:

1. Comparing absolute numbers without normalizing. A competitor with 10x your followers will always have more likes per tweet. The honest comparison is engagement rate (per impression, or per follower as a proxy), not raw counts.

2. Using a single snapshot. Growth metrics need two data points minimum. If you are reading this on a Monday and trying to make a decision today, you are too late; the right move was to start logging four weeks ago. Start now anyway.

3. Picking the wrong competitors. Direct competitors are obvious. Aspirational competitors (companies one tier above you in market position) and adjacent competitors (companies serving the same audience with different products) often produce more useful insights than direct rivals. Track a mix.

4. Treating SOV as a vanity metric. SOV moves slowly and is heavily influenced by category events outside anyone's control. Track week-over-week deltas and 4-week rolling averages, not absolute snapshots.

5. Ignoring the qualitative read. Numbers tell you which competitor is winning; reading their top 20 tweets each week tells you why. Both are necessary. Set aside 15 minutes per week to actually read the content, not just the metrics.

Frequently asked questions

How often should I run competitor analysis?

Weekly is the right cadence for most teams. Daily is overkill for anything except active campaign monitoring or PR crisis windows; monthly is too slow to catch tactical shifts. Most patterns (content mix changes, follower growth shifts, sentiment spikes) become legible within 2-4 weeks of consistent weekly tracking.

Can I run this analysis without writing code?

For a quick visual snapshot, the free Sorsa Profile Comparison tool lets you compare two accounts side by side without a single line of code: followers, engagement rate, average likes and retweets per tweet, posting frequency, and account age. The Engagement Calculator handles per-tweet engagement-rate math. For ongoing automated tracking of 3+ competitors, you will eventually want to script it.

Is there an alternative to the official X API for competitor analysis with better pricing?

Yes. The official X API Pro tier costs around $5,000/month for 1M tweet reads, which makes serious competitor analysis prohibitive for most teams. Sorsa API provides equivalent read access (profiles, tweets, mentions, followers, search) starting at $49/month for the Starter plan, with no OAuth setup, no app approval, and a flat 20 requests/second limit on every plan. For a complete cost comparison see the Twitter API alternative guide.

What is a good engagement rate to benchmark against on X in 2026?

For brand accounts: 1-3% is considered good, 3-5% is strong, above 5% is exceptional. The median across active brand accounts is about 0.59%. The most useful benchmark, though, is the median of three direct competitors in your specific niche, since engagement varies massively by industry.

How do I detect when a competitor changes their content strategy?

Run the content mix analysis (Phase 2) weekly and watch for shifts greater than 10 percentage points in any category (original / reply / quote / media). A competitor going from 80% original posts to 50% replies in 3-4 weeks is almost certainly a deliberate strategic shift. Bio changes and pinned-tweet swaps are also strong leading indicators.

How do I get a Twitter API key for this kind of work without going through Twitter's approval process?

You skip the official API entirely. Third-party providers like Sorsa issue keys instantly with no application: sign up, get a key, start making calls. The trade-off is that you only get read access (no posting, no DMs, no likes), which is fine for analysis but means you still need the official API for any write action. For details on the signup flow, see the Twitter API key guide.

Can I analyze a competitor's historical strategy going back years?

Yes, but the cost scales with depth. Use /search-tweets with since: and until: operators to pull tweets from any time window back to 2006. A competitor with 10,000 tweets across two years is roughly 500 paginated requests at 20 tweets per page. On the Pro plan that is 0.5% of your monthly quota; well within budget. See the historical data docs for the full approach.

Does running this script trigger Twitter's "this request looks automated" error?

No. Because Sorsa handles requests on its own infrastructure rather than scraping through your browser session, you never hit the rate-limit or anti-automation walls that DIY scrapers run into. The flat 20 requests/second limit is the only ceiling, and exceeding it just returns a 429 that resolves on retry. For more on this pattern, see why this request looks automated errors happen.

Getting started

If you want to try the workflow in this guide:

  1. Test without code first. Use the free Profile Comparison tool and the Engagement Calculator to get a feel for the data shape. No API key needed.
  2. Get an API key. Sign up at api.sorsa.io and the key is issued immediately. The Starter plan covers most small-team competitor tracking; scale up if your audience analysis needs the full follower extraction described in Phase 3.
  3. Test endpoints in the playground. The API playground lets you run requests against every endpoint without writing code, which is useful for checking response shapes before you build around them.
  4. Read the quickstart. Five minutes from key to first successful call.
  5. Schedule the weekly script. A cron job, GitHub Actions, or any orchestrator works. The whole pipeline runs in under 5 minutes for a 3-competitor setup.

Disclosure: Sorsa API is our product. I've aimed to keep the technical recommendations honest (the workflow above works against any read-access X data provider; you would just replace the endpoints), but the cost and feature framing reflects our pricing, so test any provider against your specific workload before standardizing on one.


Last updated: May 19, 2026. Pricing, rate limits, and endpoint signatures verified against docs.sorsa.io on the same date.

About the author: Daniel Kolbassen is a data engineer and API infrastructure consultant with 12+ years of experience building data pipelines around social media platforms. He has worked with the Twitter/X API since the v1.1 era and has helped over 40 companies restructure their data infrastructure after the 2023 pricing overhaul. Read more from Daniel at api.sorsa.io/blog/daniel-kolbassen.