Xpulse — Real-Time Social Signal Scanner
Overview
Xpulse is a real-time X/Twitter signal scanner optimized for prediction market traders. It detects market-relevant signals on configurable topics using a three-stage intelligence pipeline:
- Stage 1: Signal Detection — Scans X via DuckDuckGo (site:x.com queries), analyzes posts with local Qwen for tradeable signals
- Stage 2: Materiality Gate — Compares against 48h signal history, filters out noise and repeat signals (fail-closed)
- Stage 3: Position Matching — Only alerts on signals that match active Kalshi positions (keyword overlap, 2+ keywords required)
Key philosophy: Zero API costs (DuckDuckGo + local Qwen), fail-closed design (silence over noise), position-aware filtering (no irrelevant alerts).
When to Use This Skill
- You're actively trading prediction markets on Kalshi
- You want real-time social signals for your active positions
- You prefer local LLM analysis over expensive API calls
- You need to prevent notification fatigue from generic social signals
- You want signals cached for morning brief consumption
Requirements
Python & Dependencies
- Python 3.10 or higher
- Required packages:
pip install ddgs pyyaml
Local Infrastructure
- Ollama (https://ollama.ai) with Qwen model
- Install from https://ollama.ai (macOS, Linux, Windows)
- Then:
ollama pull qwen3:latest - Must be running:
ollama serve(background process)
Kalshi API (for position matching)
- Free Kalshi account with API credentials (https://kalshi.com)
- API key ID
- Private key file (ES256 format)
- Cost: Free (read-only access to positions)
Configuration
Create or update your config.yaml:
kalshi:
enabled: true
api_key_id: "your-key-id"
private_key_file: "/path/to/private.key"
ollama:
enabled: true
model: "qwen3:latest"
timeout_seconds: 30
xpulse:
enabled: true
check_interval_minutes: 30
topics:
- "tariff"
- "Ukraine"
- "inflation"
- "Fed rate"
- "Trump policy"
min_confidence: 0.7 # Stage 1: signal confidence threshold
materiality_gate: true # Stage 2: enable 48h deduplication
position_gate: true # Stage 3: only alert on matched positions
max_history_entries: 200
Topics Configuration
topics list should contain:
- Market-relevant keywords you're interested in
- DuckDuckGo will search:
site:x.com {topic} - Examples: "Trump tariff", "inflation CPI", "Fed rate", "crypto SEC", "presidential election"
Thresholds
| Parameter | Default | Meaning |
|---|---|---|
min_confidence | 0.7 | Stage 1: Only keep signals with ≥70% AI confidence of being tradeable |
check_interval_minutes | 30 | Minimum minutes between scans (prevent API hammering) |
materiality_gate | true | Stage 2: Enable/disable deduplication filter |
position_gate | true | Stage 3: Enable/disable Kalshi position matching |
max_history_entries | 200 | Max entries in 48h signal history (auto-trimmed) |
The Three-Stage Pipeline
Stage 1: Signal Detection
Input: List of topics (e.g., ["tariff", "inflation", "Ukraine"])
Process:
- For each topic, search X/Twitter via DuckDuckGo:
site:x.com {topic} - Fetch up to 5 recent posts
- Feed posts to local Qwen with this prompt:
You are a prediction market analyst. Given these recent X/Twitter posts about '{topic}',
determine if there's a tradeable signal.
{combined_posts}
Respond in JSON: {"has_signal": true/false, "confidence": 0.0-1.0,
"direction": "bullish/bearish/neutral", "summary": "one line"}
Thresholding: Only keep signals where:
has_signal == trueconfidence >= min_confidence(default 0.7)
Output: List of candidate signals with direction + confidence
Stage 2: Materiality Gate (Fail-Closed)
Input: Candidate signals from Stage 1 + 48h signal history
Purpose: Prevent alert fatigue by filtering out:
- Repeat signals about the same story (different wording)
- Ongoing background noise (tariffs in news for days)
- Speculation with no concrete new event
- Minor updates to previously alerted developments
Process:
- Load recently sent signal history (last 20 alerts, 48h window)
- Build prompt with:
- Candidate new signals
- What the user already knows (recent alerts)
- Send to Qwen:
You are a personal alert filter for a prediction market trader.
Your job is to PREVENT notification fatigue.
RECENTLY SENT ALERTS (what the user already knows):
- [3h ago] tariff: Trump announces new steel tariffs (confidence: 0.82)
- [6h ago] inflation: Core CPI steady at 3.2% (confidence: 0.75)
CANDIDATE NEW SIGNALS:
- [tariff] Trump discusses additional tariffs (confidence: 0.68)
- [inflation] Fed signals no near-term cuts (confidence: 0.79)
RULES:
- REJECT if same story as recent alert (even different wording)
- REJECT if ongoing background noise
- REJECT if no concrete new event
- ACCEPT only if genuinely new development or significant escalation
- When in doubt, REJECT. The user prefers silence over noise.
Respond in JSON: {"keep": [list of topic strings to keep], "reasoning": "..."}
Fail-Closed Design:
- If Qwen fails (timeout, error, unparseable): drop all signals
- This prevents spam of uncertain signals when the filter breaks
- Better to miss a signal than spam the user
Output: Filtered list (subset of Stage 1 candidates)
Stage 3: Position Matching Gate
Input: Filtered signals from Stage 2 + active Kalshi positions
Purpose: Only alert the user about signals that match their active positions
Process:
- Fetch active Kalshi positions (unsettled)
- Extract keywords from ticker + market title (>2 char words)
- Example: "POTUS-2028-DEM" → keywords: {potus, 2028, dem}
- For each signal, extract keywords from topic + summary
- Match if 2+ keyword overlap
Example:
Signal: topic="Trump tariff", summary="Trump announces new steel tariffs"
Signal words: {trump, tariff, steel, announces, new}
Kalshi position: ticker="TRUMP-TARIFF-2025", title="Will Trump enact steel tariffs?"
Position keywords: {trump, tariff, 2025, steel, enact}
Overlap: {trump, tariff, steel} = 3 keywords ✓ MATCH
→ Signal passes to iMessage alert
Suppressed Signals:
- Signals without matching positions are logged silently
- Available in cache (
.x_signal_cache.json) for morning brief - Not sent to user
Output: Critical signals matched to positions → iMessage alert
Signal Caching
All signals (pre-filter) are cached in .x_signal_cache.json:
{
"signals": [
{
"topic": "tariff",
"confidence": 0.82,
"direction": "bearish",
"summary": "Trump announces new steel tariffs",
"post_count": 3
}
],
"topics_scanned": 5,
"timestamp": 1709990400.0
}
Used for:
- Morning brief consumption (all signals, even suppressed ones)
- On-demand signal review (what was detected but not alerted)
- Debugging signal pipeline
Running Xpulse
Command Line
# Single run
python -m xpulse.xpulse
# With full logging
DEBUG=1 python -m xpulse.xpulse
# Dry run (logs but doesn't send alerts)
python -m xpulse.xpulse --dry-run
# Force a run regardless of interval
python -m xpulse.xpulse --force
Scheduled (Every 30 Minutes)
Via OpenClaw config:
skills:
xpulse:
enabled: true
schedule: "*/30 * * * *"
timeout_seconds: 120
Via crontab:
# Add to crontab -e:
*/30 * * * * cd /path/to/xpulse && python -m xpulse.xpulse >> /tmp/xpulse.log 2>&1
Via launchd (macOS):
<!-- ~/.launchd/com.xpulse.scanner.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.xpulse.scanner</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/path/to/xpulse.py</string>
</array>
<key>StartInterval</key>
<integer>1800</integer>
<key>StandardOutPath</key>
<string>/tmp/xpulse.log</string>
<key>StandardErrorPath</key>
<string>/tmp/xpulse.log</string>
</dict>
</plist>
Example Output
Alert to iMessage (Stage 3 Pass)
⚠️ X signal — affects your Kalshi positions:
📈 Trump tariff: Trump announces new 25% steel tariffs (82% conf) [TRUMP-TARIFF-2025]
📉 Inflation: Core CPI stays sticky, Fed unlikely to cut (75% conf) [FED-RATE-2026]
➡️ Ukraine: Peace talks accelerate, settlement probability rising (68% conf) [UKRAINE-2026]
Signal Cache (Stage 1 → Morning Brief)
{
"signals": [
{
"topic": "Trump tariff",
"confidence": 0.82,
"direction": "bearish",
"summary": "Trump announces new 25% steel tariffs",
"post_count": 3
},
{
"topic": "inflation",
"confidence": 0.75,
"direction": "bearish",
"summary": "Core CPI stays sticky, Fed unlikely to cut further",
"post_count": 2
}
],
"topics_scanned": 5,
"timestamp": 1709990400.0
}
Materiality Gate — Technical Details
See references/materiality-gate.md for detailed explanation of:
- How the 48h rolling window works
- Example filtering scenarios (signal vs noise)
- Fail-closed behavior
Position Matching — Technical Details
See references/position-matching.md for:
- Keyword extraction algorithm (>2 char threshold)
- Overlap matching logic (2+ keyword requirement)
- Example position-signal pairs
Troubleshooting
Ollama Not Running
Error: Connection refused or ollama: command not found
Fix:
# Start Ollama in background
ollama serve &
# Or install if missing:
# Install from https://ollama.ai, then:
ollama pull qwen:latest
Qwen Model Not Found
Error: ollama run qwen:latest fails
Fix:
ollama pull qwen:latest
# Or use qwen3 if available:
# ollama pull qwen3:latest
DuckDuckGo Returning No Results
Likely causes:
- Topic is too specific or niche
- No recent posts on X about that topic
- DuckDuckGo rate limiting (try again later)
Debug:
from ddgs import DDGS
d = DDGS()
results = list(d.text("site:x.com Trump tariff", max_results=5))
print(results)
No Kalshi Positions (Stage 3 Suppresses All)
Expected behavior: If you have no active Kalshi positions, all signals are suppressed (logged silently). This is intentional — position-aware alerting is the goal.
To test: Either:
- Create a test Kalshi position matching your topic
- Disable Stage 3: set
position_gate: falsein config (alerts all non-materiality signals)
Signal History Growing Too Large
Automatic: History is capped at 200 entries (oldest trimmed first)
Manual reset:
rm ~/.openclaw/state/x_signal_history.json
Qwen Prompts (Core IP)
Stage 1 Prompt: Signal Detection
You are a prediction market analyst. Given these recent X/Twitter posts about '{topic}',
determine if there's a tradeable signal.
{combined_posts}
Respond in JSON: {"has_signal": true/false, "confidence": 0.0-1.0,
"direction": "bullish/bearish/neutral", "summary": "one line"}
Stage 2 Prompt: Materiality Gate
You are a personal alert filter for a prediction market trader.
Your job is to PREVENT notification fatigue. Only let through signals that are genuinely NEW and MATERIAL.
RECENTLY SENT ALERTS (what the user already knows):
{history_block}
CANDIDATE NEW SIGNALS:
{candidate_block}
RULES:
- REJECT if the signal covers the same story/development as a recent alert (even with different wording)
- REJECT if it's ongoing background noise (e.g. 'Trump discusses tariffs' when tariffs have been in the news for days)
- REJECT if there's no concrete new event, just commentary or speculation
- ACCEPT only if: (a) a genuinely new development occurred (vote, announcement, emergency, data release, market move),
OR (b) a significant escalation/reversal of something previously reported
- When in doubt, REJECT. The user prefers silence over noise.
Respond in JSON: {"keep": [list of topic strings to keep], "reasoning": "one line explaining why"}
OpenClaw Ecosystem Integration
Social signal intelligence layer for the Prediction Market Trading Stack.
| Connected Skill | How It Connects |
|---|---|
| Market Morning Brief | X signals appear in your daily morning digest |
| Kalshalyst | Social signals validate or challenge contrarian edges |
| Prediction Market Arbiter | Correlate social sentiment with cross-platform divergences |
| Kalshi Command Center | Act on position-matched signals via trade execution |
Install the complete stack:
clawhub install kalshalyst kalshi-command-center polymarket-command-center prediction-market-arbiter xpulse portfolio-drift-monitor market-morning-brief personality-engine
Implementation Notes
Battle-tested in production trading environments. Design principles:
- Standalone implementation — zero external dependencies beyond listed packages
- Direct alerts — sends signals straight to you via your OpenClaw agent
- All thresholds, filters, and prompts — refined through live social signal monitoring
- Fail-closed design — no signal is better than a false positive
- Scripts are standalone — works with any OpenClaw setup
Performance & Cost
Typical Run
- Runtime: 1-2 minutes (5 topics, Qwen Stage 1+2)
- API calls:
- DuckDuckGo: 5 calls (1 per topic)
- Kalshi: 1 call (fetch positions)
- Ollama (local): 2 calls (Stage 1 + 2)
- Cost: $0 (DuckDuckGo free, Ollama local, Kalshi free read-only)
Scaling
10 runs per day = ~10-20 minutes total runtime + zero API cost. Minimal resource usage.
Further Reading
references/materiality-gate.md— Signal deduplication + 48h windowreferences/position-matching.md— Keyword overlap logic
API Reference
Main Function
from xpulse import check_x_signals
# Single run
result = check_x_signals(state={}, dry_run=False, force=False)
# Returns: bool (True if alert sent, False otherwise)
State Management
# State dict tracks:
state = {
"last_x_signal_check": 1709990400.0, # Unix timestamp of last scan
"last_x_signals_silent": [...] # Suppressed signals (logged only)
}
Support & Iteration
Xpulse is designed for iteration:
- Topic Tuning: Add/remove topics as your trading focus changes
- Confidence Threshold: Lower
min_confidenceto catch more signals (noisier), raise to reduce false positives - Materiality Gate: Disable if you're missing important signals, enable if too noisy
- Position Gate: Disable
position_gate: falseif you want ALL signals (not recommended) - Qwen Prompts: Customize Stage 1/2 prompts for your trading style
License & Attribution
Author: KingMadeLLC
Feedback & Issues
Found a bug? Have a feature request? Want to share results?
- GitHub Issues: github.com/kingmadellc/openclaw-prediction-stack/issues
- X/Twitter: @KingMadeLLC
Part of the OpenClaw Prediction Stack — the first prediction market skill suite on ClawHub.