# Competitor tracking (/docs/recipes/competitor-tracking) Snapshot a competitor's follower counts, engagement rates, and latest posts across TikTok, Instagram, YouTube, and X in one parallel sweep. 8 credits per competitor. You will build a competitive dashboard that snapshots every competitor's social footprint on a schedule: follower counts, engagement rates, and what they posted this week. Growth spikes and campaign launches then show up in your data instead of in someone else's Slack message. **Cost per run:** 8 credits per competitor (4 profiles + 4 post lists, 1 credit each). Tracking 10 competitors daily is 2,400 credits per month. ## How do you track a competitor's social media accounts? Fetch the competitor's profile and latest posts from each platform's standard endpoints in one `Promise.all`, then store each snapshot and diff it against the previous run to detect growth and new posts. Eight calls cover the four platforms that matter for most consumer brands. ## What you need 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`) | 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. Tracking a B2B competitor? Add `GET /v1/linkedin/company` (5 credits) and `GET /v1/linkedin/company/posts` (5 credits). They take **different identifiers**: `company` accepts the company page URL, but `company/posts` needs the numeric `company_id`, which is not in that 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. ## The code ```typescript title="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 ```jsonc title="JSON" // 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" │ // └─────────┴─────────────┴───────────┴─────────────────┴───────────────┴──────────────────────────────┘ ``` ## Can you take the snapshot in one call? Yes. `POST /v1/prism/profiles` takes up to 25 `(platform, handle)` pairs with `include: "posts"` and returns each profile with its first page of posts beside it, from the same four post lists the script calls. `since` trims every page to the posts published since your last run, so `posts.items` is exactly what is new: ```typescript title="TypeScript" // Same 4 profiles + 4 post pages as Step 1, in one request. // Run with: SOCIALCRAWL_KEY=sc_... npx tsx recipe-competitor-batch.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"; const res = await fetch(`${BASE}/prism/profiles`, { method: "POST", headers: { "x-api-key": KEY!, "Content-Type": "application/json" }, body: JSON.stringify({ include: "posts", since: "2026-09-23", // the date of your previous snapshot items: ["tiktok", "instagram", "youtube", "twitter"].map((platform) => ({ platform, handle: competitor, })), }), }); const { data } = await res.json(); for (const row of data.results) { console.log( row.platform, row.status, // ok, not_found, error, ... row.data?.author?.followers ?? null, `${row.posts?.items?.length ?? 0} new posts`, ); } ``` The price is the same 8 credits: 1 per profile found, plus 1 per posts page that came back with posts, even when `since` then trimmed it to nothing. A handle that is not found costs nothing. Each row's `data` is the object the single profile endpoint returns. See [Batch endpoints](/docs/batch.md#profiles-with-their-latest-posts). ## What to change - **`competitor`**: handles differ per platform in reality. Keep a `{ tiktok, instagram, youtube, twitter }` handle map per competitor instead of one string. - **Which platforms**: drop a pair to save 2 credits per run, or add the LinkedIn pair to reach 18 credits per competitor (8 + 5 for `company` + 5 for `company/posts`). - **Where snapshots go**: the script prints a table. Write it to a table keyed by `(handle, date)` so Step 3 has something to diff against. - **Your alert thresholds**: the three deltas in Step 3 are the ones worth watching. Pick the multiples that mean something for your category. ## Related - [Creator engagement scoring](/docs/recipes/creator-engagement-scoring.md): Rank every competitor with comparable engagement rates. - [Brand mention monitoring](/docs/recipes/brand-mention-monitoring.md): Catch what people say about them, not just what they post. - [Ad library aggregation](/docs/recipes/ads-library-aggregation.md): See their paid side across Meta, Google, and LinkedIn. - [Computed fields](/docs/computed-fields.md): When engagement_rate is populated and when it is honestly null. - [Quickstart](/docs/quickstart.md): New here? Get a key and make your first request.