DEV Community

Joe Vezzani
Joe Vezzani

Posted on

Track World Cup 2026 Social Buzz With a Simple Node.js Script

The 2026 FIFA World Cup starts June 11 across the US, Canada, and Mexico. 48 teams for the first time ever. More matches, more upsets, more chaos -- and a ridiculous amount of social media activity.

Headlines will tell you who won. Social data tells you who the internet is actually talking about, which fan bases are the loudest, and which underdog is going viral before the pundits catch on.

I built a tracker that pulls real-time social intelligence on World Cup teams using LunarCrush's API. It processes 50M+ social posts per hour across X, Reddit, TikTok, YouTube, Instagram, and 10K+ news sources.

Here's how to build it yourself.

GitHub repo: JoeVezzani/world-cup-tracker

What we're building

A Node.js script that:

  • Tracks social volume, sentiment, and engagement for any World Cup team
  • Compares teams head-to-head (USA vs Mexico, Argentina vs Brazil, etc.)
  • Ranks all 48 teams by social buzz to find who's trending
  • Surfaces the most viral posts driving each team's conversation

Setup

mkdir world-cup-tracker && cd world-cup-tracker
npm init -y
Enter fullscreen mode Exit fullscreen mode

Grab an API key at lunarcrush.com/developers.

The core functions

Two endpoints do all the heavy lifting -- one for metrics, one for posts:

const API_KEY = process.env.LUNARCRUSH_API_KEY;
const BASE = "https://lunarcrush.com/api4/public/topic";

async function getTopicData(keyword) {
  const res = await fetch(
    `${BASE}/${encodeURIComponent(keyword)}/v1`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  return res.json();
}

async function getTopicPosts(keyword) {
  const res = await fetch(
    `${BASE}/${encodeURIComponent(keyword)}/posts/v1`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

Track a single team

Start simple. Pull the social pulse on any team:

async function teamReport(team) {
  const topic = await getTopicData(team);
  const d = topic.data || {};

  console.log(`\nSOCIAL REPORT: ${team.toUpperCase()}`);
  console.log("=".repeat(50));
  console.log(`Posts (24h):       ${(d.num_posts || 0).toLocaleString()}`);
  console.log(`Engagements:       ${(d.interactions || 0).toLocaleString()}`);
  console.log(`Sentiment:         ${d.sentiment || "N/A"}% bullish`);
  console.log(`Contributors:      ${(d.num_contributors || 0).toLocaleString()}`);

  const posts = await getTopicPosts(team);
  console.log(`\nTop Posts:`);
  for (const post of (posts.data || []).slice(0, 3)) {
    const platform = post.network || "unknown";
    const engagement = (post.interactions_total || 0).toLocaleString();
    const text = (post.body || post.title || "").slice(0, 100);
    console.log(`  [${platform}] ${engagement} engagements: ${text}...`);
  }
}

teamReport("USA World Cup");
Enter fullscreen mode Exit fullscreen mode
LUNARCRUSH_API_KEY=your_key node index.js
Enter fullscreen mode Exit fullscreen mode

Head-to-head comparison

This is where it gets fun. Pit two rival fan bases against each other:

async function headToHead(teamA, teamB) {
  const [a, b] = await Promise.all([
    getTopicData(teamA),
    getTopicData(teamB),
  ]);

  const dataA = a.data || {};
  const dataB = b.data || {};

  console.log(`\nHEAD TO HEAD: ${teamA.toUpperCase()} vs ${teamB.toUpperCase()}`);
  console.log("=".repeat(55));

  const metrics = [
    ["Posts (24h)", "num_posts"],
    ["Engagements", "interactions"],
    ["Sentiment", "sentiment"],
    ["Contributors", "num_contributors"],
  ];

  for (const [label, key] of metrics) {
    const valA = dataA[key] || 0;
    const valB = dataB[key] || 0;
    const winner = valA > valB ? teamA : valA < valB ? teamB : "TIE";
    const format = (v) => key === "sentiment" ? `${v}%` : v.toLocaleString();
    console.log(
      `${label.padEnd(18)} ${format(valA).padStart(12)}  vs  ${format(valB).padEnd(12)}  --> ${winner}`
    );
  }
}

headToHead("USA soccer", "Mexico soccer");
Enter fullscreen mode Exit fullscreen mode

Sample output:

HEAD TO HEAD: USA SOCCER vs MEXICO SOCCER
=======================================================
Posts (24h)              48,291  vs  63,817        --> Mexico soccer
Engagements           1,203,441  vs  2,891,003     --> Mexico soccer
Sentiment                   72%  vs  68%           --> USA soccer
Contributors             31,204  vs  44,109        --> Mexico soccer
Enter fullscreen mode Exit fullscreen mode

Run it for any matchup. Argentina vs Brazil. England vs France. Japan vs South Korea. The data is there for all of them.

Rank all 48 teams by buzz

Here's the full leaderboard. Throw every team in, sort by social volume, see who's dominating the conversation:

const TEAMS = [
  "Argentina World Cup", "Brazil World Cup", "France World Cup",
  "England World Cup", "Germany World Cup", "Spain World Cup",
  "USA World Cup", "Mexico World Cup", "Canada World Cup",
  "Japan World Cup", "South Korea World Cup", "Australia World Cup",
  "Netherlands World Cup", "Portugal World Cup", "Belgium World Cup",
  "Croatia World Cup", "Morocco World Cup", "Senegal World Cup",
  "Uruguay World Cup", "Colombia World Cup", "Ecuador World Cup",
  "Saudi Arabia World Cup", "Qatar World Cup", "Iran World Cup",
];

async function buzzLeaderboard(teams) {
  const results = [];

  for (const team of teams) {
    const data = await getTopicData(team);
    const d = data.data || {};
    results.push({
      team,
      posts: d.num_posts || 0,
      engagements: d.interactions || 0,
      sentiment: d.sentiment || 0,
    });
    // Small delay to be polite to the API
    await new Promise((r) => setTimeout(r, 200));
  }

  results.sort((a, b) => b.posts - a.posts);

  console.log(`\nWORLD CUP 2026 - SOCIAL BUZZ LEADERBOARD`);
  console.log("=".repeat(65));
  console.log(
    `${"#".padEnd(4)}${"Team".padEnd(28)}${"Posts".padStart(10)}${"Engagements".padStart(14)}${"Sentiment".padStart(10)}`
  );
  console.log("-".repeat(65));

  results.forEach((r, i) => {
    const name = r.team.replace(" World Cup", "");
    console.log(
      `${String(i + 1).padEnd(4)}${name.padEnd(28)}${r.posts.toLocaleString().padStart(10)}${r.engagements.toLocaleString().padStart(14)}${(r.sentiment + "%").padStart(10)}`
    );
  });
}

buzzLeaderboard(TEAMS);
Enter fullscreen mode Exit fullscreen mode

Add or remove teams from the array as the tournament bracket fills out. You can also swap "World Cup" for more specific queries like "USMNT" or "Les Bleus" depending on what fan communities call their team.

Find trending moments

The most interesting thing during a tournament isn't steady buzz -- it's spikes. An upset, a red card, a last-minute goal. This function checks for teams with unusual social activity:

const fs = require("fs");
const STATE_FILE = "./buzz_state.json";

function loadState() {
  try { return JSON.parse(fs.readFileSync(STATE_FILE)); }
  catch { return {}; }
}

function saveState(state) {
  fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

async function detectSpikes(teams) {
  const state = loadState();
  const spikes = [];

  for (const team of teams) {
    const data = await getTopicData(team);
    const current = data.data?.num_posts || 0;
    const previous = state[team]?.posts || 0;

    if (previous > 0 && current > previous * 2) {
      spikes.push({
        team,
        previous,
        current,
        multiplier: (current / previous).toFixed(1),
      });
    }

    state[team] = { posts: current, timestamp: new Date().toISOString() };
    await new Promise((r) => setTimeout(r, 200));
  }

  saveState(state);

  if (spikes.length === 0) {
    console.log("\nNo major spikes detected. Check back after a match.");
    return;
  }

  spikes.sort((a, b) => b.multiplier - a.multiplier);
  console.log(`\nSPIKE ALERT - Something is happening:`);
  console.log("=".repeat(55));
  for (const s of spikes) {
    const name = s.team.replace(" World Cup", "");
    console.log(
      `${name}: ${s.previous.toLocaleString()} -> ${s.current.toLocaleString()} posts (${s.multiplier}x increase)`
    );
  }
}

detectSpikes(TEAMS);
Enter fullscreen mode Exit fullscreen mode

Run this on a cron every 30 minutes during match days:

# crontab -e
*/30 * * * * cd /path/to/world-cup-tracker && LUNARCRUSH_API_KEY=your_key node spikes.js >> spikes.log
Enter fullscreen mode Exit fullscreen mode

When Japan pulls off an upset against Germany (again), you'll see the spike before ESPN runs the highlight reel.

Why social data matters for sports

Traditional sports coverage tells you who scored. Social data tells you:

  • Which teams have the most passionate fan bases -- not just the biggest
  • Where the conversation is happening -- is it TikTok memes or Reddit analysis?
  • Sentiment shifts in real time -- a team can go from beloved to villain in 90 minutes
  • Underdog stories forming -- viral moments surface in social data before they hit mainstream media

With 48 teams across three countries, this World Cup is going to generate more social activity than any sporting event in history. Having a way to actually measure and compare that activity is more interesting than refreshing a score app.

Try it

Top comments (0)