# 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?url=…` (5 credits) and `GET /v1/linkedin/company/posts?company_id=…` (5 credits). The two take **different identifiers**: `company` accepts the company page URL, but `company/posts` needs the numeric `company_id`, which is not in the URL — resolve it from the `company` response first, then cache it. Every LinkedIn content endpoint is advanced tier or above; the platform has no 1-credit profile or post surface. 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) { 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 the LinkedIn pair brings it to 18 credits per competitor (8 + 5 for `company` + 5 for `company/posts`). Take it further [#take-it-further] * Rank all competitors with comparable rates using [Creator engagement scoring](/docs/recipes/creator-engagement-scoring.md) — 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.md), and their paid side with [Ad library aggregation](/docs/recipes/ads-library-aggregation.md). * 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.md).