DEV Community

Joe Vezzani
Joe Vezzani

Posted on • Originally published at lunarcrush.com

Building a Prediction Market Signal Detector with Social Sentiment Data

Prediction markets are having a moment. Polymarket hit 112M engagements in a single day last week. Kalshi is expanding into sports. Bonding curve platforms are popping up everywhere.

But here's the thing about prediction markets: the prices move after the crowd forms an opinion. And the crowd forms its opinion on social media before it places bets.

I built a Node.js tool that compares real-time social sentiment data against prediction market topics to find those early signals -- the moments when social volume spikes or sentiment shifts before the market catches up.

GitHub repo: JoeVezzani/prediction-market-signals

The thesis

Prediction market contracts are priced by supply and demand. When a big event happens -- a policy announcement, an earnings surprise, a geopolitical shift -- social media reacts first. Millions of people start posting, sharing, arguing. That wave of activity carries a measurable sentiment signal.

If you can detect that signal early enough, you know where the prediction market is likely to move next.

LunarCrush processes 50M+ social posts per hour across X, Reddit, TikTok, YouTube, Instagram, and 10K+ news sources. That gives us the raw material to build this.

What we're building

A signal detector that:

  • Tracks social sentiment and volume for prediction market topics in real time
  • Detects anomalies -- sudden spikes in volume, sharp sentiment shifts
  • Compares multiple topics side by side to spot divergences
  • Logs signals over time so you can backtest against actual market moves

Setup

mkdir prediction-market-signals && cd prediction-market-signals
npm init -y
touch index.js
Enter fullscreen mode Exit fullscreen mode

Grab an API key at lunarcrush.com/developers.

The core: pulling social data for any topic

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 getTimeSeries(keyword) {
  const res = await fetch(
    `${BASE}/${encodeURIComponent(keyword)}/time-series/v1`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  return res.json();
}

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

Three endpoints, three views into the same conversation. Topic data gives you the current snapshot. Time series gives you the trend. Posts give you the actual content driving sentiment.

Pick your prediction market topics

The best signals come from topics where prediction markets are active and social conversation is loud. Right now, three topics fit perfectly:

  1. Tariffs -- Will new tariffs stick? What's the economic impact? Polymarket and Kalshi both have active tariff-related contracts.
  2. Bitcoin -- Price prediction contracts are everywhere. BTC social data is the deepest dataset LunarCrush has.
  3. Recession -- "Will the US enter a recession in 2026?" is one of the most traded prediction market questions right now.
const TOPICS = [
  { keyword: "tariffs", label: "Tariffs / Trade Policy" },
  { keyword: "bitcoin", label: "Bitcoin Price Direction" },
  { keyword: "recession", label: "US Recession Odds" },
];
Enter fullscreen mode Exit fullscreen mode

Build the signal detector

Here's where it gets interesting. We pull social data for each topic, calculate whether sentiment or volume is moving abnormally, and flag anything that looks like a leading indicator.

const fs = require("fs");
const STATE_FILE = "./signal_state.json";
const SIGNAL_LOG = "./signals.json";

function loadJSON(path) {
  try { return JSON.parse(fs.readFileSync(path, "utf-8")); }
  catch { return {}; }
}

function saveJSON(path, data) {
  fs.writeFileSync(path, JSON.stringify(data, null, 2));
}

function detectSignals(current, previous, label) {
  const signals = [];

  // Sentiment shift detection
  if (previous?.sentiment && current.sentiment) {
    const delta = current.sentiment - previous.sentiment;
    if (Math.abs(delta) >= 8) {
      signals.push({
        type: "SENTIMENT_SHIFT",
        topic: label,
        direction: delta > 0 ? "BULLISH" : "BEARISH",
        previous: previous.sentiment,
        current: current.sentiment,
        delta,
        strength: Math.abs(delta) >= 15 ? "STRONG" : "MODERATE",
      });
    }
  }

  // Volume spike detection
  if (previous?.posts && current.posts) {
    const ratio = current.posts / previous.posts;
    if (ratio >= 1.5) {
      signals.push({
        type: "VOLUME_SPIKE",
        topic: label,
        previous: previous.posts,
        current: current.posts,
        multiplier: ratio.toFixed(2) + "x",
        strength: ratio >= 3 ? "STRONG" : "MODERATE",
      });
    }
  }

  // Engagement explosion
  if (previous?.engagements && current.engagements) {
    const ratio = current.engagements / previous.engagements;
    if (ratio >= 2) {
      signals.push({
        type: "ENGAGEMENT_SURGE",
        topic: label,
        previous: previous.engagements,
        current: current.engagements,
        multiplier: ratio.toFixed(2) + "x",
      });
    }
  }

  return signals;
}
Enter fullscreen mode Exit fullscreen mode

The main loop

This pulls data for all your tracked topics, compares against the last reading, and logs any signals it finds.

async function scan() {
  const state = loadJSON(STATE_FILE);
  const signalLog = loadJSON(SIGNAL_LOG);
  if (!Array.isArray(signalLog.signals)) signalLog.signals = [];

  const timestamp = new Date().toISOString();
  console.log(`\n--- Prediction Market Signal Scan: ${timestamp} ---\n`);

  for (const { keyword, label } of TOPICS) {
    const data = await getTopicData(keyword);
    const topic = data.data || {};

    const current = {
      sentiment: topic.sentiment,
      posts: topic.num_posts,
      engagements: topic.interactions,
      contributors: topic.num_contributors,
      timestamp,
    };

    console.log(`${label}`);
    console.log(`  Sentiment:    ${current.sentiment}%`);
    console.log(`  Posts (24h):  ${(current.posts || 0).toLocaleString()}`);
    console.log(`  Engagements:  ${(current.engagements || 0).toLocaleString()}`);
    console.log(`  Contributors: ${(current.contributors || 0).toLocaleString()}`);

    const previous = state[keyword];
    const signals = detectSignals(current, previous, label);

    if (signals.length > 0) {
      console.log(`  ** ${signals.length} SIGNAL(S) DETECTED **`);
      for (const sig of signals) {
        console.log(`    [${sig.strength || ""}] ${sig.type}: ${sig.direction || sig.multiplier}`);
        signalLog.signals.push({ ...sig, timestamp });
      }
    } else {
      console.log(`  No signals (within normal range)`);
    }

    state[keyword] = current;
    console.log();
  }

  saveJSON(STATE_FILE, state);
  saveJSON(SIGNAL_LOG, signalLog);
  console.log(`State saved. ${signalLog.signals.length} total signals logged.`);
}

scan();
Enter fullscreen mode Exit fullscreen mode

Run it

LUNARCRUSH_API_KEY=your_key node index.js
Enter fullscreen mode Exit fullscreen mode

First run establishes your baseline. Second run (and every run after) compares against the previous reading and flags anomalies:

--- Prediction Market Signal Scan: 2026-04-06T14:32:00.000Z ---

Tariffs / Trade Policy
  Sentiment:    71%
  Posts (24h):  18,075
  Engagements:  14,264,207
  Contributors: 12,744
  ** 1 SIGNAL(S) DETECTED **
    [MODERATE] SENTIMENT_SHIFT: BULLISH

Bitcoin Price Direction
  Sentiment:    75%
  Posts (24h):  172,155
  Engagements:  135,115,413
  Contributors: 68,883
  No signals (within normal range)

US Recession Odds
  Sentiment:    54%
  Posts (24h):  2,212
  Engagements:  8,258,373
  Contributors: 1,855
  ** 1 SIGNAL(S) DETECTED **
    [STRONG] SENTIMENT_SHIFT: BEARISH

State saved. 2 total signals logged.
Enter fullscreen mode Exit fullscreen mode

That recession signal is exactly the kind of thing that matters. Sentiment dropping while engagement surges means people are increasingly bearish and talking about it more. On a prediction market, that's likely to push "recession" contract prices up.

Add a cross-topic divergence detector

Sometimes the most interesting signal isn't within a single topic -- it's between topics. If Bitcoin sentiment is surging while recession fears are spiking, that's a divergence worth watching.

function detectDivergences(snapshots) {
  const divergences = [];
  const entries = Object.entries(snapshots);

  for (let i = 0; i < entries.length; i++) {
    for (let j = i + 1; j < entries.length; j++) {
      const [labelA, dataA] = entries[i];
      const [labelB, dataB] = entries[j];

      if (dataA.sentiment && dataB.sentiment) {
        const gap = Math.abs(dataA.sentiment - dataB.sentiment);
        if (gap >= 20) {
          divergences.push({
            type: "SENTIMENT_DIVERGENCE",
            topicA: labelA,
            sentimentA: dataA.sentiment,
            topicB: labelB,
            sentimentB: dataB.sentiment,
            gap,
          });
        }
      }
    }
  }

  return divergences;
}
Enter fullscreen mode Exit fullscreen mode

Drop this into your scan loop after collecting all the snapshots:

// After the main loop, check for cross-topic divergences
const snapshots = {};
for (const { keyword, label } of TOPICS) {
  snapshots[label] = state[keyword];
}

const divergences = detectDivergences(snapshots);
if (divergences.length > 0) {
  console.log("Cross-topic divergences:");
  for (const d of divergences) {
    console.log(
      `  ${d.topicA} (${d.sentimentA}%) vs ${d.topicB} (${d.sentimentB}%) -- ${d.gap}pt gap`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Right now this would flag:

Cross-topic divergences:
  Bitcoin Price Direction (75%) vs US Recession Odds (54%) -- 21pt gap
Enter fullscreen mode Exit fullscreen mode

Bitcoin sentiment is bullish while recession sentiment is bearish. That's a real tension -- and exactly the kind of thing prediction market traders are betting on.

Run it on a schedule

Set up a cron to scan every hour and build up your signal log:

# crontab -e
0 * * * * cd /path/to/prediction-market-signals && LUNARCRUSH_API_KEY=your_key node index.js >> scan.log 2>&1
Enter fullscreen mode Exit fullscreen mode

After a few days, you'll have a dataset of timestamped signals you can compare against actual prediction market price movements. That's when it gets really useful -- you can start measuring which signal types actually lead market moves and by how much.

Dig into the posts driving the signal

When a signal fires, you want to know why. The posts endpoint shows you the actual social content behind the numbers:

async function investigateSignal(keyword, label) {
  const posts = await getTopPosts(keyword);
  const topPosts = (posts.data || []).slice(0, 5);

  console.log(`\nTop posts driving "${label}" sentiment:\n`);
  for (const post of topPosts) {
    const platform = post.network || "unknown";
    const engagement = (post.interactions_total || 0).toLocaleString();
    const text = (post.body || post.title || "").slice(0, 120);
    console.log(`  [${platform}] ${engagement} engagements`);
    console.log(`  ${text}...`);
    console.log();
  }
}

// Call it when a signal fires
investigateSignal("recession", "US Recession Odds");
Enter fullscreen mode Exit fullscreen mode

This is the part that turns a quantitative signal into something you can actually reason about. A sentiment drop is just a number until you see that the top 5 posts are all reacting to the same PMI report or the same oil price spike.

Why social data works for this

Prediction markets aggregate opinions into prices. Social media is where those opinions form. The sequence is:

  1. Something happens (policy announcement, earnings, geopolitical event)
  2. Social media reacts -- volume spikes, sentiment shifts, creators amplify
  3. People form opinions and update their beliefs
  4. Those beliefs get priced into prediction market contracts

LunarCrush is sitting at step 2, processing 50M+ posts per hour across X, Reddit, TikTok, YouTube, Instagram, and 10K+ news sources. By the time a prediction market contract moves, the social signal has usually already fired.

The tool we built here is simple on purpose. The point isn't to build an automated trading bot -- it's to build a detection system that tells you when something is happening in the social conversation before the market fully reflects it.

Try it yourself

Swap in any topic you want. Elections, Fed rate decisions, crypto launches, sports outcomes -- if people are talking about it online and betting on it in a prediction market, you can build a signal from it.

Top comments (0)