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
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();
}
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:
- Tariffs -- Will new tariffs stick? What's the economic impact? Polymarket and Kalshi both have active tariff-related contracts.
- Bitcoin -- Price prediction contracts are everywhere. BTC social data is the deepest dataset LunarCrush has.
- 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" },
];
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;
}
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();
Run it
LUNARCRUSH_API_KEY=your_key node index.js
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.
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;
}
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`
);
}
}
Right now this would flag:
Cross-topic divergences:
Bitcoin Price Direction (75%) vs US Recession Odds (54%) -- 21pt gap
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
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");
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:
- Something happens (policy announcement, earnings, geopolitical event)
- Social media reacts -- volume spikes, sentiment shifts, creators amplify
- People form opinions and update their beliefs
- 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
- Get an API key: lunarcrush.com/developers
- Full API docs: lunarcrush.com/developers/api
- MCP server (use with Claude): lunarcrush.com/mcp
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)