> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sorsa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Análisis de competidores

# Cómo analizar competidores en Twitter mediante API

Esta guía describe un flujo completo con Sorsa API: comparación de perfiles, estrategia de contenido, composición de audiencias, sentimiento público y cuota de conversación. Cada fase utiliza endpoints concretos y puede incorporarse a un informe semanal programado.

Los ejemplos utilizan Python 3.8 o posterior y `requests`. Sustituye `YOUR_API_KEY` por tu clave. Al final encontrarás el script consolidado con todas las funciones auxiliares.

> **Empieza gratis:** las primeras 100 solicitudes, sin tarjeta ni caducidad, permiten probar el flujo con pocas páginas antes de elegir un plan.

> **Nota:** consulta el contexto estratégico y más ejemplos en la [guía de análisis de competidores para desarrolladores](https://api.sorsa.io/blog/twitter-competitor-analysis).

> **Sin código:** [Compare Users](https://api.sorsa.io/playground/compare-users) compara dos cuentas: seguidores, tasa de interacción, medias de Me gusta y retuits, frecuencia de publicación y antigüedad. [Engagement Calculator](https://api.sorsa.io/playground/engagement-calculator) calcula la interacción de publicaciones de una cuenta.

***

## Configuración

```python theme={null}
import requests
import time
import csv
from pathlib import Path
from datetime import date, datetime, timedelta, timezone

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

`ApiKey` autentica cada solicitud. Consulta [Autenticación](https://docs.sorsa.io/es/authentication).

***

## Fase 1: Comparar perfiles

**Endpoints:** `GET /v3/info`, `GET /v3/info-batch`

Establece una referencia: seguidores, volumen de publicaciones, antigüedad, biografía y verificación. [`/info-batch`](https://docs.sorsa.io/es/api-reference/usuarios/perfiles-de-usuario-por-lotes) permite consultar hasta 100 perfiles en una sola solicitud.

### Script de captura

```python theme={null}
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}"
    )
```

### Medir el crecimiento

Una captura da una referencia; para medir crecimiento necesitas al menos dos observaciones con fecha. Registra capturas diarias o semanales con cron, GitHub Actions u otra herramienta y calcula las variaciones:

```python theme={null}
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):
    with open(csv_file, encoding="utf-8") as f:
        rows = [r for r in csv.DictReader(f) if r["username"].lower() == username.lower()]
    if len(rows) < 2:
        return None
    rows.sort(key=lambda row: row["date"])
    latest_row = rows[-1]
    cutoff = date.fromisoformat(latest_row["date"]) - timedelta(days=days)
    earlier_rows = [r for r in rows if date.fromisoformat(r["date"]) <= cutoff]
    if not earlier_rows:
        return None
    latest = int(latest_row["followers"])
    earlier = int(earlier_rows[-1]["followers"])
    return ((latest - earlier) / earlier) * 100 if earlier else None


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

La función compara la última captura con la observación más reciente en la fecha límite solicitada o antes. Si las capturas son irregulares, el intervalo puede superar `days`; captura a diario si necesitas mayor precisión temporal. En producción, guarda IDs junto a los nombres para que un cambio de nombre no divida el historial de una cuenta.

Fórmula de crecimiento:

```text theme={null}
Growth Rate % = ((Followers Today - Followers N Days Ago) / Followers N Days Ago) * 100
```

### Cambios de biografía y posicionamiento

`/info` devuelve `description`, `location`, `bio_urls` y `created_at`. Compara capturas para detectar cambios de biografía o enlaces sin coste adicional.

***

## Fase 2: Estrategia de contenido

**Endpoints:** `POST /v3/user-tweets`, `POST /v3/search-tweets`

Recupera publicaciones recientes y analiza la combinación de originales, respuestas, citas y retuits, la interacción media y los mejores resultados.

### Recuperar publicaciones recientes

[`/user-tweets`](https://docs.sorsa.io/es/api-reference/publicaciones/publicaciones-del-usuario) devuelve hasta 20 publicaciones por página. A diferencia de los endpoints de cronología de la API oficial de X, no impone un máximo de 3.200. Para periodos muy antiguos, es más fiable utilizar `/search-tweets` con `since:` y `until:`.

```python theme={null}
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=JSON_HEADERS,
            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
```

### Desglosar los tipos de contenido

```python theme={null}
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"))

    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,
        "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']}")
```

Las categorías se solapan: una publicación multimedia también puede ser original. Los porcentajes son proporciones independientes, no partes que sumen 100.

### Comparación histórica

Para comparar dos periodos de una cuenta, como el primer y el cuarto trimestre, utiliza [`/search-tweets`](https://docs.sorsa.io/es/search-tweets) con `since:` y `until:`. Consulta [Operadores de búsqueda](https://docs.sorsa.io/es/search-operators) y [Datos históricos](https://docs.sorsa.io/es/historical-data).

```python theme={null}
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=JSON_HEADERS,
        json={"query": query, "order": "latest"},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json().get("tweets", [])  # First page only; paginate for a full period.


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")
```

***

## Fase 3: Composición de la audiencia

**Endpoints:** `GET /v3/followers`, `GET /v3/verified-followers`, `GET /v3/followers-stats`

La lista de seguidores permite estudiar la audiencia. Hay dos enfoques con costes distintos.

### Seguidores verificados: coste bajo

[`/verified-followers`](https://docs.sorsa.io/es/api-reference/usuarios/seguidores-verificados) devuelve solo cuentas verificadas. Es un segmento de interés que resulta mucho más barato que recuperar toda la red.

```python theme={null}
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")
    for u in top:
        print(f"  @{u['username']:<25} {u['followers_count']:>10,} followers")
```

Compara capturas para detectar nuevos seguidores destacados por competidor. Los seguimientos de periodistas a menudo preceden a la cobertura entre 2 y 4 semanas.

### Lista completa de seguidores: coste alto

[`/followers`](https://docs.sorsa.io/es/api-reference/usuarios/seguidores) devuelve hasta 200 perfiles por página. Un millón de seguidores requiere unas 5.000 solicitudes. Consulta [Precios](https://api.sorsa.io/pricing) y [Optimización del uso de la API](https://docs.sorsa.io/es/optimizing-api-usage) para planificar el presupuesto.

```python theme={null}
def fetch_all_followers(username, max_pages=200):
    """Pull all followers via pagination. Cost scales with account size."""
    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
```

### Audiencia compartida

Calcula la intersección de IDs entre las listas de dos cuentas:

```python theme={null}
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):,}")

denominator = min(len(followers_a), len(followers_b))
overlap_ratio = len(overlap) / denominator if denominator else 0
print(f"Overlap ratio: {overlap_ratio:.1%}")
```

Consulta más patrones en [Seguidores y cuentas seguidas](https://docs.sorsa.io/es/followers-and-following).

### Desglose de seguidores de criptomonedas y Web3

Para cuentas incluidas en la base de criptomonedas de Sorsa, [`/followers-stats`](https://docs.sorsa.io/es/api-reference/sorsa-y-cripto/estad%C3%ADsticas-de-categor%C3%ADas-de-seguidores) devuelve categorías de influencers, proyectos y fondos. Consulta [Sorsa Score y análisis de criptomonedas](https://docs.sorsa.io/es/sorsa-score-and-crypto-analytics).

```python theme={null}
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']}")
```

Los recuentos solo incluyen cuentas que ya figuran en esa base.

***

## Fase 4: Sentimiento público y menciones

**Endpoint:** `POST /v3/mentions`

[`/mentions`](https://docs.sorsa.io/es/api-reference/b%C3%BAsqueda/buscar-menciones) permite filtrar por mínimos de Me gusta, retuits, respuestas y fechas. `min_likes` reduce ruido como respuestas de bots y etiquetas automáticas. Consulta [Seguimiento de menciones](https://docs.sorsa.io/es/search-mentions).

### Recuperar menciones con alta interacción

```python theme={null}
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=JSON_HEADERS,
            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
```

### Clasificar sentimiento con VADER

VADER es una biblioteca abierta para sentimiento en textos de redes sociales. Se ejecuta localmente, sin coste por llamada, y maneja negaciones, intensificadores y emojis.

```python theme={null}
# 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}...")
```

Para sarcasmo, quejas técnicas o sentimiento mixto, puedes enviar `full_text` a una API de LLM, como OpenAI o Anthropic. Un enfoque híbrido, con VADER para filtrar y el LLM para casos señalados o de alta interacción, ayuda a controlar el coste.

***

## Fase 5: Cuota de conversación

La cuota de conversación o share of voice (SOV) compara las menciones de una marca con el total de la categoría.

```text theme={null}
SOV = (your mentions in period) / (your mentions + sum of competitor mentions in period)
```

Implementación:

```python theme={null}
def count_mentions(handle, days=7, min_likes=0):
    until = datetime.now(timezone.utc).date().isoformat()
    since = (datetime.now(timezone.utc) - 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 if total else 0):.1f}%)")
for h, n in sorted(competitor_mentions.items(), key=lambda x: -x[1]):
    print(f"  @{h:<20} {n:>5}  ({(n/total*100 if total else 0):.1f}%)")
```

Ten en cuenta lo siguiente:

* Un mínimo de interacción, como `min_likes=5`, ayuda a reducir bots y spam.
* Compara variaciones semanales. Los acontecimientos de la categoría pueden distorsionar los valores absolutos y ocultar la evolución propia.
* Los recuentos están limitados por `max_pages` y describen la muestra recuperada. Utiliza las mismas fechas y filtros y comprueba que se agotó la paginación de cada marca; de lo contrario, identifica el informe como basado en muestras.
* Para SOV por tema, como «embedded finance», sustituye `/mentions` por `/search-tweets` y utiliza la consulta temática como denominador.

***

## Script consolidado de informe semanal

Combina las cinco fases. Puedes ejecutarlo con cron, GitHub Actions u otro programador.

```python theme={null}
import requests
import csv
import time
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer

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

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

analyzer = SentimentIntensityAnalyzer()


def get_profiles(usernames):
    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", [])


def log_snapshot(profiles, output_file=SNAPSHOT_FILE):
    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):
    with open(csv_file, encoding="utf-8") as f:
        rows = [r for r in csv.DictReader(f) if r["username"].lower() == username.lower()]
    if len(rows) < 2:
        return None
    rows.sort(key=lambda row: row["date"])
    latest_row = rows[-1]
    cutoff = date.fromisoformat(latest_row["date"]) - timedelta(days=days)
    earlier_rows = [r for r in rows if date.fromisoformat(r["date"]) <= cutoff]
    if not earlier_rows:
        return None
    latest = int(latest_row["followers"])
    earlier = int(earlier_rows[-1]["followers"])
    return ((latest - earlier) / earlier) * 100 if earlier else None


def fetch_user_tweets(username, max_pages=5):
    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=JSON_HEADERS, 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


def analyze_content(tweets, username):
    if not tweets:
        return None
    total = len(tweets)
    likes = [t.get("likes_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"))
    with_media = sum(1 for t in tweets if t.get("entities"))
    return {
        "username": username,
        "sample_size": total,
        "avg_likes": sum(likes) / total,
        "original_pct": original / total * 100,
        "media_pct": with_media / total * 100,
    }


def fetch_verified_followers(username, max_pages=3):
    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


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=JSON_HEADERS, 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


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


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


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 if total else 0):.1f}%)")


if __name__ == "__main__":
    run_weekly_report()
```

Con las profundidades predeterminadas, una ejecución para tres competidores consume del orden de 100 solicitudes: algo más para marcas con muchas menciones y algo menos en otros casos. Una ejecución semanal consume unos cientos al mes, dentro de Starter (10.000). Las 100 gratuitas permiten probar primero uno o dos competidores con pocas páginas. Consulta [Precios](https://api.sorsa.io/pricing).

***

## Próximos pasos

* [Búsqueda de publicaciones](https://docs.sorsa.io/es/search-tweets): palabras clave y operadores.
* [Seguimiento de menciones](https://docs.sorsa.io/es/search-mentions): filtros de `/mentions`.
* [Seguidores y cuentas seguidas](https://docs.sorsa.io/es/followers-and-following): recuperación, paginación y CSV.
* [Datos históricos](https://docs.sorsa.io/es/historical-data): análisis de periodos largos.
* [Supervisión en tiempo real](https://docs.sorsa.io/es/real-time-monitoring): detección de nuevas publicaciones.
* [Descubrimiento del público objetivo](https://docs.sorsa.io/es/target-audiences-Discovery): segmentación de seguidores para captar clientes potenciales.
* [Optimización del uso de la API](https://docs.sorsa.io/es/optimizing-api-usage): lotes, cursores y presupuesto.
* [Referencia de la API](https://docs.sorsa.io/es/api-reference-guide): especificaciones.
