# Competitor tracking (/docs/recipes/competitor-tracking)



Competitor tracking [#competitor-tracking]

Build a competitive dashboard that snapshots every competitor's social footprint on a schedule — follower counts, engagement rates, and what they posted this week — so growth spikes and campaign launches show up in your data, not in your boss's Slack message.

How do you track a competitor's social media accounts? [#how-do-you-track-a-competitors-social-media-accounts]

Fetch the competitor's profile and latest posts from each platform's standard endpoints — `/v1/tiktok/profile` + `/v1/tiktok/profile/videos`, `/v1/instagram/profile` + `/v1/instagram/profile/posts`, `/v1/youtube/channel` + `/v1/youtube/channel/videos`, `/v1/twitter/profile` + `/v1/twitter/user/tweets` — in one `Promise.all`. Store each snapshot and diff against the previous run to detect growth and new posts.

The problem [#the-problem]

Competitors announce nothing. Their follower spike, their sudden posting streak, the video that's doing 10x their normal numbers — that's the real signal, and it's spread across four platforms with four different APIs, none of which you want to integrate and maintain individually.

The solution [#the-solution]

Eight standard-tier endpoints (1 credit each), all behind one key and one envelope:

| Platform  | Profile                                       | Latest posts                                        |
| --------- | --------------------------------------------- | --------------------------------------------------- |
| TikTok    | `GET /v1/tiktok/profile` (param: `handle`)    | `GET /v1/tiktok/profile/videos` (param: `handle`)   |
| Instagram | `GET /v1/instagram/profile` (param: `handle`) | `GET /v1/instagram/profile/posts` (param: `handle`) |
| YouTube   | `GET /v1/youtube/channel` (param: `handle`)   | `GET /v1/youtube/channel/videos` (param: `handle`)  |
| X         | `GET /v1/twitter/profile` (param: `handle`)   | `GET /v1/twitter/user/tweets` (param: `handle`)     |

B2B competitor? Add `GET /v1/linkedin/company` and `GET /v1/linkedin/company/posts` (both take the company page `url`, 1 credit each).

When an endpoint supports `engagement_rate` and the required source inputs are present, read the optional `computed?.engagement_rate` value. Populated values use the canonical calculation, so cross-platform comparisons need no per-platform math; keep `null` when the value is unavailable.

```typescript
// recipe-competitor-tracking.ts
// Snapshots one competitor across 4 platforms; diff snapshots between runs.
// Run with: SOCIALCRAWL_KEY=sc_... npx tsx recipe-competitor-tracking.ts

const KEY = process.env.SOCIALCRAWL_KEY;
if (!KEY) throw new Error("Set SOCIALCRAWL_KEY");

const BASE = "https://www.socialcrawl.dev/v1";
const competitor = "duolingo"; // same handle on all four platforms here

async function get(path: string, params: Record<string, string>) {
  const url = new URL(`${BASE}/${path}`);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers: { "x-api-key": KEY! } });
  return (await res.json()) as {
    success: boolean;
    platform: string;
    data?: {
      author?: { username: string | null; followers: number | null };
      computed?: { engagement_rate: number | null };
      items?: Array<{
        post?: {
          text?: string | null;
          created_at?: string | null;
          engagement?: { views?: number | null; likes?: number | null };
        };
      }>;
    };
  };
}

// ── Step 1: 8 calls in parallel — 4 profiles + 4 post lists ───────────────
const [
  ttProfile,
  igProfile,
  ytProfile,
  twProfile,
  ttPosts,
  igPosts,
  ytPosts,
  twPosts,
] = await Promise.all([
  get("tiktok/profile", { handle: competitor }),
  get("instagram/profile", { handle: competitor }),
  get("youtube/channel", { handle: competitor }),
  get("twitter/profile", { handle: competitor }),
  get("tiktok/profile/videos", { handle: competitor }),
  get("instagram/profile/posts", { handle: competitor }),
  get("youtube/channel/videos", { handle: competitor }),
  get("twitter/user/tweets", { handle: competitor }),
]);

// ── Step 2: Build today's snapshot ─────────────────────────────────────────
const posts = [ttPosts, igPosts, ytPosts, twPosts];
const snapshot = {
  date: new Date().toISOString().slice(0, 10),
  handle: competitor,
  platforms: [ttProfile, igProfile, ytProfile, twProfile]
    .filter((p) => p.success && p.data)
    .map((p, i) => ({
      platform: p.platform,
      followers: p.data!.author?.followers ?? null,
      engagement_rate: p.data!.computed?.engagement_rate ?? null,
      posts_fetched: posts[i]?.data?.items?.length ?? 0,
      latest_post: posts[i]?.data?.items?.[0]?.post?.text?.slice(0, 80) ?? null,
    })),
};

console.table(snapshot.platforms);

// ── Step 3: Diff against your stored snapshot from the previous run ───────
// Persist `snapshot` to a DB table keyed by (handle, date), then:
//   follower_delta   = today.followers - yesterday.followers
//   new_posts        = posts with created_at > yesterday's run time
//   engagement_jump  = today.engagement_rate / trailing 7-day average
// Alert when any delta crosses your threshold.
```

What you get back [#what-you-get-back]

```jsonc
// console.table(snapshot.platforms):
// ┌─────────┬─────────────┬───────────┬─────────────────┬───────────────┬──────────────────────────────┐
// │ (index) │  platform   │ followers │ engagement_rate │ posts_fetched │         latest_post          │
// ├─────────┼─────────────┼───────────┼─────────────────┼───────────────┼──────────────────────────────┤
// │    0    │ "tiktok"    │  17800000 │     0.1124      │      20       │ "the owl has no chill..."    │
// │    1    │ "instagram" │   4900000 │     0.0381      │      12       │ "new feature drop 🦉"        │
// │    2    │ "youtube"   │   8120000 │     0.0512      │      30       │ "How Duo learned Spanish"    │
// │    3    │ "twitter"   │  16200000 │      null       │      100      │ "spanish or vanish"          │
// └─────────┴─────────────┴───────────┴─────────────────┴───────────────┴──────────────────────────────┘
```

Credits cost [#credits-cost]

> **Cost per run:** 8 credits per competitor (4 profiles + 4 post lists × 1 credit). Tracking 10 competitors daily costs 2,400 credits/month. Adding LinkedIn company + posts brings it to 10 credits per competitor.

Take it further [#take-it-further]

* Rank all competitors with comparable rates using [Creator engagement scoring](/docs/recipes/creator-engagement-scoring) — the same `computed.engagement_rate` powers both recipes.
* Catch competitor *mentions* (not just their own posts) with [Brand mention monitoring](/docs/recipes/brand-mention-monitoring), and their paid side with [Ad library aggregation](/docs/recipes/ads-library-aggregation).
* Handles differ per platform in reality — keep a `{ tiktok, instagram, youtube, twitter }` handle map per competitor instead of one string.
* Platform references: [TikTok API](/platforms/tiktok), [Instagram API](/platforms/instagram), [YouTube API](/platforms/youtube), [Twitter API](/platforms/twitter). New here? [Quickstart](/docs/quickstart).
