Skill: kingmadellc/prediction-stack-setup (version 1.1.1)

--- name: Prediction Stack Setup description: "Interactive setup wizard for the OpenClaw Prediction Market Trading Stack. Detects installed skills, walks through API key configuration, creates cron jobs for automated scanning and alerts, enables heartbeat for ambient awareness, and tests iMessage delivery. Wires the 10-skill stack into a connected, proactive trading system in under 5 minutes. Run this after installing the stack — or anytime you need to reconfigure schedules, delivery targets, or alert thresholds." --- # Prediction Stack Setup — One-Command System Activation ## Overview <!-- CODEX: reconciled setup docs with the 10-skill stack and the public Anthropic-backed reference path. --> This skill wires the 10-skill OpenClaw Prediction Market Trading Stack into a connected, proactive system. Without setup, skills work individually when you ask for them. After setup, the runtime skills run autonomously — scanning markets, detecting edges, monitoring positions, and delivering intelligence to your phone on a schedule. **What it does:** 1. Detects which stack skills are installed 2. Configures API keys and credentials 3. **Validates all APIs before creating jobs** (prevents silent failures) 4. Creates scheduled jobs (cron) for each skill 5. Enables heartbeat for ambient awareness 6. Sets up iMessage delivery 7. Tests the full pipeline end-to-end **Time to complete:** Under 5 minutes ## When to Use This Skill - You just installed the Prediction Market Trading Stack (all 8 skills or any subset) - You just installed the Prediction Market Trading Stack (all 10 skills or any subset) - You want to activate automated scanning and alerts - You need to change your delivery target (new phone, new email handle) - You want to adjust scan schedules or alert thresholds - You're migrating from a previous setup or starting fresh - Something broke and you want to rewire from scratch ## Prerequisites ### Required - **OpenClaw** installed and running (gateway active) - **At least one stack skill** installed (any of the 10) - **Kalshi account** with API credentials (free at kalshi.com) - **Anthropic API key** (for Claude Sonnet estimation in Kalshalyst) ### Optional (Enhances the Stack) - **Polygon.io API key** (free tier — adds news context to Kalshalyst) - **Ollama + Qwen** installed locally (free — adds offline fallback + Xpulse materiality gate) - **BlueBubbles** running on your Mac (required for iMessage delivery) ## Setup Procedure ### Phase 1: Skill Detection First, check which stack skills are installed. Run this command: ```bash openclaw skills list | grep -E "kalshalyst|kalshi-command|polymarket-command|prediction-market-arbiter|xpulse|portfolio-drift|market-morning-brief|personality-engine" ``` **The 10 stack skills:** | Skill | Role | Required For | |-------|------|-------------| | Kalshalyst | Edge detection engine | Edge alerts, Morning Brief edges section | | Kalshi Command Center | Trade execution | Executing on Kalshalyst recommendations | | Polymarket Command Center | Market data | Arbiter comparisons, Morning Brief Polymarket section | | Prediction Market Arbiter | Cross-platform divergences | Arbitrage alerts, Morning Brief divergences section | | Xpulse | Social signal scanner | Signal alerts, Morning Brief X signals section | | Portfolio Drift Monitor | Position monitoring | Drift alerts between briefs | | Market Morning Brief | Daily digest | Consolidated morning/evening intelligence | | Personality Engine | Agent behavior | Consistent voice across all output | | Prediction Stack Orchestrator | Premium/experimental pipeline manager | Validation and execution routing | | Prediction Stack Setup | Setup wizard | Validation, scheduling, delivery wiring | **Minimum viable stack:** Kalshalyst + Kalshi Command Center + Market Morning Brief. These three give you edge detection, execution, and daily digest. Everything else enriches the system. **Full stack:** All 10 skills. Morning Brief pulls from every cache and the orchestrator adds a premium/experimental automation path on top. Install missing skills: ```bash clawhub install <skill-name> ``` ### Phase 2: API Key Configuration Create or update `~/.openclaw/config.yaml`: ```yaml # === REQUIRED === kalshi: enabled: true api_key_id: "YOUR_KALSHI_KEY_ID" private_key_file: "~/.openclaw/keys/kalshi-secret.pem" # === REQUIRED FOR KALSHALYST === # Set as environment variable or in config: # export ANTHROPIC_API_KEY="sk-ant-..." # === OPTIONAL — ENHANCES EDGE QUALITY === polygon: [REDACTED_SECRET]" # Free tier at polygon.io ollama: enabled: true # For Xpulse materiality gate + Kalshalyst fallback model: "qwen3:latest" timeout_seconds: 60 # === SKILL-SPECIFIC TUNING (sensible defaults shown) === kalshalyst: enabled: true check_interval_minutes: 60 min_volume: 50 min_days_to_close: 7 max_days_to_close: 365 max_markets_to_analyze: 50 min_edge_pct: 3.0 alert_edge_pct: 6.0 xpulse: enabled: true check_interval_minutes: 30 topics: - "Trump tariff" - "Ukraine" - "inflation CPI" - "Fed rate decision" min_confidence: 0.7 materiality_gate: true position_gate: true portfolio_drift: threshold_pct: 5.0 check_interval_minutes: 60 ``` **Kalshi key setup** (if you don't have one yet): 1. Log in at https://kalshi.com 2. Go to Settings → API Keys 3. Generate a new key pair 4. Save the private key: ```bash mkdir -p ~/.openclaw/keys # Paste your private key into this file: nano ~/.openclaw/keys/kalshi-secret.pem chmod 600 ~/.openclaw/keys/kalshi-secret.pem ``` ### Phase 2.5: Validate API Keys **Before creating cron jobs, verify that all configured APIs are working.** This prevents hours of silent failures where the stack runs but can't reach its data sources. Run the validation script: ```bash python ~/skills/prediction-stack-setup/scripts/validate_setup.py ``` **Output Example:** ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Prediction Stack API Validation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ REQUIRED SERVICES (must pass for stack to function) ────────────────────────────────────────────────────────────────────── ✅ PASS Kalshi API (REQUIRED) (245ms) ✅ PASS Anthropic (Claude) (REQUIRED) (1320ms) OPTIONAL SERVICES (enhance edge detection and resilience) ────────────────────────────────────────────────────────────────────── ✅ PASS Polygon.io API (optional) (180ms) ❌ FAIL Ollama (Qwen) (optional) (15ms) Error: Ollama not running on localhost:11434 ✅ PASS Polymarket API (optional) (95ms) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Required: 2/2 passed Optional: 2/3 passed ✅ All required services validated. Stack is ready to launch. ``` **What each validation tests:** | Service | Required | Test | Failure Handling | |---------|----------|------|------------------| | **Kalshi** | Yes | `get_balance()` call | Blocks setup if auth fails | | **Claude** | Yes | Simple completion call | Blocks setup if key invalid | | **Polygon.io** | No | Public market status endpoint | Kalshalyst works without it | | **Ollama/Qwen** | No | Model availability + inference | Kalshalyst uses Claude fallback | | **Polymarket** | No | Public API endpoint access | Arbiter works without it | **If validation fails on REQUIRED services:** 1. **Kalshi fails** — Check error message: - "Authentication failed": Verify key ID and private key file are correct at https://kalshi.com/settings/api - "Network error": Verify internet connectivity, check Kalshi status at status.kalshi.com - "File not found": Verify `private_key_file` path in config.yaml exists and is readable 2. **Claude fails** — Check error message: - "Invalid or expired API key": Generate a new key at https://console.anthropic.com/account/keys - "Network error": Verify internet connectivity - "Rate limited": Wait 60 seconds and retry 3. **Optional services fail** — This is OK. The stack will work but with reduced functionality: - Polygon.io missing: Kalshalyst estimates will lack news context - Ollama missing: Kalshalyst will use Claude for all estimates (higher cost) - Polymarket missing: Arbiter won't find cross-platform divergences **To validate only specific services:** ```bash # Test only Kalshi (useful for debugging) python ~/skills/prediction-stack-setup/scripts/validate_setup.py --kalshi-only # With detailed output showing live responses python ~/skills/prediction-stack-setup/scripts/validate_setup.py --verbose ``` ### Phase 3: iMessage Delivery Setup The stack delivers alerts via iMessage through BlueBubbles. This requires: 1. **BlueBubbles** running on your Mac (https://bluebubbles.app) 2. **BlueBubbles password** set in OpenClaw config 3. **Your iMessage email handle** (the email registered with your Apple ID for iMessage) **Configure BlueBubbles:** ```bash # Set your BlueBubbles server password openclaw config set channels.bluebubbles.password "YOUR_BB_PASSWORD" # Restart gateway to pick up the change openclaw gateway restart ``` **Test delivery:** ```bash # Replace with YOUR iMessage email handle openclaw message send --channel imessage --target "YOUR_IMESSAGE_EMAIL" --message "Prediction Stack is online." ``` If the message arrives on your phone, delivery is working. If not: **Troubleshooting:** - Verify BlueBubbles is running (check the app on your Mac) - Verify Full Disk Access is granted to Terminal (System Settings → Privacy & Security → Full Disk Access) - On macOS Tahoe+: use email handles, not phone numbers, for proactive sends - Check gateway logs: `tail -50 ~/.openclaw/logs/gateway.log` - Run diagnostics: `openclaw doctor` ### Phase 4: Create Scheduled Jobs **Important:** Complete Phase 2.5 (API validation) before running these commands. Cron jobs will fail silently if APIs are misconfigured. These cron jobs are the nervous system of the stack — they trigger skills on a schedule and deliver results to your phone. **Replace YOUR_IMESSAGE_EMAIL in every command below.** ```bash # ═══════════════════════════════════════════════════════ # DAILY BRIEFS — The bookends of your trading day # ═══════════════════════════════════════════════════════ # Morning Brief — 8:00 AM local time # Compiles: Portfolio P&L + Kalshalyst edges + Arbiter divergences + Xpulse signals + Polymarket trending openclaw cron add \ --name "morning-brief" \ --cron "0 8 * * *" \ --tz "America/Los_Angeles" \ --message "Run the Market Morning Brief skill. Compile the full morning digest — Kalshi portfolio P&L, top edges from Kalshalyst cache, cross-platform divergences from Arbiter cache, X signals from Xpulse cache, Polymarket trending. Format for iMessage (no markdown). Send the brief to the user." \ --announce \ --channel imessage \ --to "YOUR_IMESSAGE_EMAIL" \ --timeout-seconds 120 # Evening Brief — 6:00 PM local time # Lighter summary: notable moves, new edges since morning, drift alerts openclaw cron add \ --name "evening-brief" \ --cron "0 18 * * *" \ --tz "America/Los_Angeles" \ --message "Run the Market Morning Brief evening mode. Lightweight market summary — notable moves, any new Kalshalyst edges since morning, position drift alerts. Format for iMessage (no markdown). Send to the user." \ --announce \ --channel imessage \ --to "YOUR_IMESSAGE_EMAIL" \ --timeout-seconds 120 # ═══════════════════════════════════════════════════════ # EDGE SCANNING — Finding mispricings throughout the day # ═══════════════════════════════════════════════════════ # Kalshalyst Edge Scan — Every 2 hours during market hours (8 AM - 8 PM) # Only alerts on edges >= 6% with confidence >= 0.6. Writes cache silently otherwise. openclaw cron add \ --name "edge-scan" \ --cron "0 8,10,12,14,16,18,20 * * *" \ --tz "America/Los_Angeles" \ --message "Run Kalshalyst edge scan. Only alert the user via iMessage if any market has edge >= 6% with confidence >= 0.6. If nothing meets threshold, write cache silently and do not send a message." \ --announce \ --channel imessage \ --to "YOUR_IMESSAGE_EMAIL" \ --timeout-seconds 180 # ═══════════════════════════════════════════════════════ # SIGNAL DETECTION — Social signals matched to positions # ═══════════════════════════════════════════════════════ # Xpulse Social Signal Scan — Every 30 min during waking hours (8 AM - 10 PM) # Only alerts when signal passes materiality gate AND matches an active position openclaw cron add \ --name "xpulse-scan" \ --cron "*/30 8-22 * * *" \ --tz "America/Los_Angeles" \ --message "Run Xpulse scan on configured topics. Only alert the user via iMessage if a signal passes BOTH the materiality gate AND position matching. Silence is correct when nothing fires." \ --announce \ --channel imessage \ --to "YOUR_IMESSAGE_EMAIL" \ --timeout-seconds 90 # ═══════════════════════════════════════════════════════ # POSITION MONITORING — Catching drift between briefs # ═══════════════════════════════════════════════════════ # Portfolio Drift Monitor — Hourly during market hours (9 AM - 8 PM) # Alerts only when a position moves >= 5% since last snapshot openclaw cron add \ --name "drift-monitor" \ --cron "0 9-20 * * *" \ --tz "America/Los_Angeles" \ --message "Run Portfolio Drift Monitor. Alert the user via iMessage only if any position has moved >= 5% since last snapshot. No alert if everything is stable." \ --announce \ --channel imessage \ --to "YOUR_IMESSAGE_EMAIL" \ --timeout-seconds 60 # ═══════════════════════════════════════════════════════ # CROSS-PLATFORM ARBITRAGE — Kalshi vs Polymarket pricing gaps # ═══════════════════════════════════════════════════════ # Arbiter Cross-Platform Scan — 3x daily (9 AM, 1 PM, 5 PM) # Alerts on divergences >= 10%. Writes all results to cache for Morning Brief. openclaw cron add \ --name "arbiter-scan" \ --cron "0 9,13,17 * * *" \ --tz "America/Los_Angeles" \ --message "Run Prediction Market Arbiter. Scan for Kalshi vs Polymarket divergences. Alert the user via iMessage only if divergence >= 10% on a market with decent volume. Write all results to cache for Morning Brief regardless." \ --announce \ --channel imessage \ --to "YOUR_IMESSAGE_EMAIL" \ --timeout-seconds 120 ``` **Verify all jobs:** ```bash openclaw cron list ``` You should see 6 jobs: morning-brief, evening-brief, edge-scan, xpulse-scan, drift-monitor, arbiter-scan. **Quick reference — what fires when:** | Time | Job | What Happens | |------|-----|-------------| | 8:00 AM | morning-brief + edge-scan | Full digest + first edge scan of the day | | 9:00 AM | drift-monitor + arbiter-scan | Position check + first arbitrage scan | | 9:30 AM | xpulse-scan | Social signal check | | 10:00 AM | edge-scan + drift-monitor + xpulse-scan | Edge + drift + signals | | ... | (continues hourly/half-hourly) | | | 5:00 PM | arbiter-scan | Last arbitrage scan | | 6:00 PM | evening-brief | Evening summary | | 8:00 PM | edge-scan + drift-monitor | Last edge scan + drift check | | 10:00 PM | xpulse-scan | Last signal scan (then quiet until 8 AM) | ### Phase 5: Enable Heartbeat Heartbeat gives your agent ambient awareness between scheduled jobs. Every 30 minutes during active hours, the agent checks if anything needs attention. ```bash openclaw config set agents.defaults.heartbeat.every "30m" openclaw config set agents.defaults.heartbeat.activeHours.start "08:00" openclaw config set agents.defaults.heartbeat.activeHours.end "22:00" openclaw system heartbeat enable openclaw gateway restart ``` **Optional — configure HEARTBEAT.md** for proactive checks between scans: Edit `~/.openclaw/workspace/HEARTBEAT.md`: ```markdown # HEARTBEAT.md Check for new Kalshalyst edges in cache that haven't been alerted yet. Check for portfolio drift events that fired since last heartbeat. If anything is worth telling the user, send it via iMessage. Otherwise, HEARTBEAT_OK. ``` ### Phase 6: Verify Everything ```bash # Check system status openclaw status # List all cron jobs openclaw cron list # Check heartbeat openclaw system heartbeat last # Run a manual test (triggers edge scan immediately) openclaw cron run edge-scan # Check gateway logs for errors tail -20 ~/.openclaw/logs/gateway.log ``` If the manual edge scan delivers results to your phone (or runs silently because nothing met threshold), the stack is fully operational. ## Post-Setup: What to Expect ### First 24 Hours - **Immediately:** Xpulse fires every 30 min. Most runs will be silent (nothing passes materiality gate). This is correct — silence means the filter is working. - **Next morning at 8 AM:** First Morning Brief arrives. If other skill caches are empty (first run), sections will show "unavailable" — they'll populate after the first scan cycle. - **By end of day 1:** All caches populated. Evening Brief will have a full picture. ### Ongoing - **Most alerts are silence.** This is by design. The stack is built to prevent notification fatigue. You'll get 2-5 actionable alerts per day, not 50. - **Morning Brief is your daily touchpoint.** 30-second scan of everything that matters. - **Edge alerts are the money.** When Kalshalyst fires with >= 6% edge, that's your signal to look at the trade. ### Tuning After a week of operation, you may want to adjust: **Alert thresholds** (in cron job messages or config.yaml): - Raise `alert_edge_pct` from 6% to 8% if too many low-quality alerts - Lower to 4% if you're not seeing enough opportunities - Adjust drift threshold from 5% to 3% (more sensitive) or 10% (less noise) **Schedule frequency:** - Reduce edge-scan to 3x daily if API costs are a concern - Increase xpulse to every 15 min if you trade breaking news heavily - Add weekend scanning by changing cron expressions **Xpulse topics** (in config.yaml): - Add topics matching your active positions - Remove topics you're no longer trading To update a cron job: ```bash # List jobs to get the ID openclaw cron list # Update the schedule or message openclaw cron update <JOB_ID> --cron "new expression" ``` ## Reconfiguration ### Change Delivery Target ```bash # Update all cron jobs to a new target openclaw cron list # get all job IDs openclaw cron update <ID> --to "[REDACTED_SECRET]" # Repeat for each job ``` ### Change Timezone ```bash openclaw cron update <ID> --tz "America/New_York" # Repeat for each job ``` ### Disable a Job Temporarily ```bash openclaw cron disable <ID> # Re-enable later: openclaw cron enable <ID> ``` ### Full Reset ```bash # Remove all cron jobs openclaw cron list # note all IDs openclaw cron remove <ID> # for each job # Re-run setup from Phase 4 ``` ## How the Skills Connect (System Architecture) ``` ┌─────────────────────────────────────────────────────────────┐ │ SCHEDULED JOBS (Cron) │ │ edge-scan → xpulse-scan → drift-monitor → arbiter-scan │ │ morning-brief (8AM) ←── reads all caches ──→ evening-brief │ └─────────────┬───────────────────────────────────┬───────────┘ │ │ ▼ ▼ ┌─────────────────────────┐ ┌───────────────────────────────┐ │ INTELLIGENCE LAYER │ │ DELIVERY LAYER │ │ │ │ │ │ Kalshalyst │ │ BlueBubbles → iMessage │ │ └→ .kalshi_research │ │ openclaw send → your phone │ │ _cache.json │ │ │ │ │ │ Heartbeat (30m ambient) │ │ Arbiter │ │ └→ checks caches │ │ └→ .crossplatform │ │ └→ alerts if needed │ │ _divergences.json │ │ │ │ │ └───────────────────────────────┘ │ Xpulse │ │ └→ .x_signal │ ┌───────────────────────────────┐ │ _cache.json │ │ EXECUTION LAYER │ │ │ │ │ │ Portfolio Drift │ │ Kalshi Command Center │ │ └→ snapshot + │ │ └→ trade on edges │ │ threshold alerts │ │ │ │ │ │ Polymarket Command Center │ │ Morning Brief │ │ └→ market data for Arbiter │ │ └→ reads all caches │ │ │ │ └→ consolidated push │ └───────────────────────────────┘ │ │ │ Personality Engine │ ┌───────────────────────────────┐ │ └→ wraps all output │ │ DATA SOURCES │ │ │ │ │ └─────────────────────────┘ │ Kalshi API (free) │ │ Polymarket Gamma API (free) │ │ Polygon.io (free tier) │ │ DuckDuckGo (free, via Xpulse) │ │ Ollama/Qwen (free, local) │ │ Anthropic Claude (paid) │ └───────────────────────────────┘ ``` **Data flow:** Skills write JSON cache files. Morning Brief reads them. Cron jobs trigger everything. BlueBubbles delivers to iMessage. No direct dependencies between skills — install any subset and each works standalone. ## Cost Summary | Component | Cost | Notes | |-----------|------|-------| | Kalshi API | Free | Unlimited reads | | Polymarket API | Free | Public, no auth | | Polygon.io | Free | Free tier sufficient | | DuckDuckGo | Free | Via Xpulse | | Ollama/Qwen | Free | Local inference | | Claude Sonnet | Variable | Depends on model, prompt size, and scan frequency | | BlueBubbles | Free | Self-hosted on Mac | | **Total** | **Variable** | Claude is the only paid dependency in the public reference path | ## Troubleshooting ### No messages arriving 1. Test send: `openclaw message send --channel imessage --target "YOUR_EMAIL" --message "test"` 2. Check BB is running (BlueBubbles app on Mac) 3. Check FDA: System Settings → Privacy & Security → Full Disk Access → Terminal 4. Check gateway: `tail -50 ~/.openclaw/logs/gateway.log` 5. On macOS Tahoe+: use email handles, not phone numbers ### Cron jobs not firing 1. Verify gateway is running: `openclaw status` 2. List jobs: `openclaw cron list` (check "Next" column) 3. Manual trigger: `openclaw cron run <job-name>` 4. Check logs: `~/.openclaw/logs/gateway.log` ### Skills not finding data 1. **First, run the validation script** to catch API issues early: ```bash python ~/skills/prediction-stack-setup/scripts/validate_setup.py --verbose ``` This will identify which API is misconfigured. 2. Check API keys: verify `~/.openclaw/config.yaml` has valid credentials 3. Test Kalshi: `openclaw message send --channel imessage --target "YOUR_EMAIL" --message "Run Kalshi Command Center: show my portfolio"` 4. Check Ollama (for Xpulse): `ollama list` should show qwen3:latest ### Morning Brief shows "unavailable" sections This is normal on first run — caches haven't been populated yet. After one full scan cycle (edge-scan + arbiter-scan + xpulse-scan), all caches will exist and Morning Brief will show full data. ## Stack Skills Reference For detailed documentation on each skill, read its SKILL.md: - **Kalshalyst** — Contrarian edge detection. The intelligence engine. - **Kalshi Command Center** — Portfolio, scanning, trade execution, risk management. - **Polymarket Command Center** — Trending markets, odds, search, watchlists. - **Prediction Market Arbiter** — Cross-platform divergence scanner. - **Xpulse** — X/Twitter social signal scanner with materiality gate. - **Portfolio Drift Monitor** — Position drift alerts. - **Market Morning Brief** — Daily morning/evening intelligence digest. - **Personality Engine** — 6-system behavior framework for consistent agent voice. --- ## Feedback & Issues Found a bug? Have a feature request? Want to share results? - **GitHub Issues**: [github.com/kingmadellc/openclaw-prediction-stack/issues](https://github.com/kingmadellc/openclaw-prediction-stack/issues) - **X/Twitter**: [@KingMadeLLC](https://x.com/KingMadeLLC) Part of the **OpenClaw Prediction Stack** — the first prediction market skill suite on ClawHub.

path: README.md
content: # Prediction Stack Setup

Interactive setup wizard for the [OpenClaw Prediction Market Trading Stack](https://github.com/kingmadellc/openclaw-prediction-stack).

<!-- CODEX: reconciled stack size and cost language with the public reference implementation. -->
Turns the 10-skill stack into a connected, proactive trading system. Creates scheduled scans, enables iMessage delivery, configures API keys, and tests the full pipeline — in under 5 minutes.

The runtime path is the eight core operational skills. `prediction-stack-orchestrator` is documented in the stack as a premium/experimental automation layer.

## Quick Start

1. Install the stack skills you want (minimum: Kalshalyst + Kalshi Command Center + Market Morning Brief)
2. Install this skill: `clawhub install prediction-stack-setup`
3. Tell your agent: **"Set up my prediction stack"**
4. Follow the interactive wizard

## What It Creates

- **6 scheduled jobs**: morning brief, evening brief, edge scan, social signal scan, drift monitor, arbitrage scan
- **Heartbeat config**: 30-minute ambient awareness during waking hours
- **iMessage delivery**: All alerts route to your phone via BlueBubbles
- **Unified config**: Single `config.yaml` with all API keys and thresholds

## Files

- `SKILL.md` — Complete setup procedure and system documentation
- `config.example.yaml` — Configuration template with sensible defaults

## Cost

The reference implementation assumes Anthropic API spend for Claude-based Kalshalyst runs, with local Qwen handling Xpulse and fallback inference. Everything else is free-tier or local.


path: config.example.yaml
content: # OpenClaw Prediction Market Trading Stack — Configuration Template
#
# Copy this file to ~/.openclaw/config.yaml and fill in your credentials.
# Lines marked REQUIRED must be set. Lines marked OPTIONAL enhance the stack.
#
# CODEX: reconciled this template with the current public runtime defaults.

# ═══════════════════════════════════════════════════════
# REQUIRED — Kalshi API (free at kalshi.com)
# ═══════════════════════════════════════════════════════

kalshi:
  enabled: true
  api_key_id: "YOUR_KALSHI_KEY_ID"                    # Settings → API Keys on kalshi.com
  private_key_file: "~/.openclaw/keys/kalshi-secret.pem"  # chmod 600 this file

anthropic:
  [REDACTED_SECRET]"                  # Required for the reference Kalshalyst path

# ═══════════════════════════════════════════════════════
# OPTIONAL — Polygon.io (free tier at polygon.io)
# Adds news context + economic indicators to Kalshalyst
# ═══════════════════════════════════════════════════════

polygon:
  [REDACTED_SECRET]"

# ═══════════════════════════════════════════════════════
# OPTIONAL — Ollama + Qwen (free, local inference)
# Required for: Xpulse materiality gate, Kalshalyst fallback
# Install: https://ollama.ai then: ollama pull qwen3:latest
# ═══════════════════════════════════════════════════════

ollama:
  enabled: true
  model: "qwen3:latest"
  timeout_seconds: 60

# ═══════════════════════════════════════════════════════
# KALSHALYST — Edge detection tuning
# Sensible defaults shown. Adjust after a week of operation.
# ═══════════════════════════════════════════════════════

kalshalyst:
  enabled: true
  check_interval_minutes: 60      # How often the cron job runs
  min_volume: 50                   # Skip illiquid markets
  min_days_to_close: 7             # Skip markets closing too soon
  max_days_to_close: 365           # Skip markets too far out
  max_markets_to_analyze: 50       # Claude calls per run (cost control)
  min_edge_pct: 3.0                # Minimum edge to record
  alert_edge_pct: 6.0              # Minimum edge to alert you
  # Kelly sizing:
  # kelly:
  #   alpha: 0.25                  # Fractional Kelly multiplier (0.25 = conservative)
  #   max_contracts_per_trade: 100
  #   max_cost_per_trade_usd: 25.0
  #   max_portfolio_exposure_usd: 100.0
  #   confidence_exponent: 2.0     # Penalize low-confidence estimates

# ═══════════════════════════════════════════════════════
# XPULSE — Social signal scanning
# Topics should match your active trading positions.
# ═══════════════════════════════════════════════════════

xpulse:
  enabled: true
  check_interval_minutes: 30
  topics:                          # Replace with YOUR topics
    - "Trump tariff"
    - "Ukraine"
    - "inflation CPI"
    - "Fed rate decision"
  min_confidence: 0.7              # Signal confidence threshold
  materiality_gate: true           # Two-stage Qwen filtering (requires Ollama)
  position_gate: true              # Only alert on signals matching your positions
  max_history_entries: 200         # Dedup window

# ═══════════════════════════════════════════════════════
# PORTFOLIO DRIFT — Position monitoring
# ═══════════════════════════════════════════════════════

portfolio_drift:
  threshold_pct: 5.0               # Alert when position moves >= this %
  check_interval_minutes: 60

# ═══════════════════════════════════════════════════════
# PREDICTION MARKET ARBITER — Cross-platform divergence detection
# ═══════════════════════════════════════════════════════

prediction_market_arbiter:
  divergence_threshold_pct: 10.0   # Minimum Kalshi vs Polymarket gap to alert
  min_volume: 100                  # Skip low-volume matches


path: references/validation-troubleshooting.md
content: # API Validation Troubleshooting Guide

Quick reference for fixing validation failures during Prediction Stack setup.

## REQUIRED SERVICES — Must Fix Before Proceeding

### Kalshi API Validation Fails

#### Error: "Authentication failed: Check API key and private key file"

**Diagnosis:** Your API credentials are invalid or mismatched.

**Fix steps:**
1. Log in at https://kalshi.com/settings/api
2. Check if you have an active API key (should show key ID)
3. If not, generate a new one
4. Download the private key file
5. Save it to `~/.openclaw/keys/kalshi-secret.pem`
6. Verify permissions: `chmod 600 ~/.openclaw/keys/kalshi-secret.pem`
7. Update `~/.openclaw/config.yaml`:
   ```yaml
   kalshi:
     enabled: true
     api_key_id: "YOUR_KEY_ID_FROM_KALSHI"
     private_key_file: "~/.openclaw/keys/kalshi-secret.pem"
   ```
8. Rerun: `python validate_setup.py --kalshi-only`

---

#### Error: "Network error: Cannot reach Kalshi API"

**Diagnosis:** Network connectivity issue or Kalshi API is down.

**Fix steps:**
1. Check internet connection: `ping 8.8.8.8`
2. Check if Kalshi is down: https://status.kalshi.com
3. If your network requires a proxy, configure it in config.yaml:
   ```yaml
   kalshi:
     http_proxy: "http://proxy.company.com:8080"
   ```
4. Try from a different network (phone hotspot) to isolate the issue
5. Rerun: `python validate_setup.py --kalshi-only`

---

#### Error: "File not found: Check private_key_file path in config"

**Diagnosis:** The path to your private key doesn't exist.

**Fix steps:**
1. Check file exists: `ls -la ~/.openclaw/keys/kalshi-secret.pem`
2. If not found, download it from https://kalshi.com/settings/api again
3. Paste into file:
   ```bash
   nano ~/.openclaw/keys/kalshi-secret.pem
   # Paste your private key (should start with -----BEGIN PRIVATE KEY-----)
   # Press Ctrl+O, Enter, Ctrl+X to save
   ```
4. Verify: `cat ~/.openclaw/keys/kalshi-secret.pem | head -5`
5. Check permissions: `chmod 600 ~/.openclaw/keys/kalshi-secret.pem`
6. Rerun: `python validate_setup.py --kalshi-only`

---

### Anthropic (Claude) API Validation Fails

#### Error: "Invalid or expired API key"

**Diagnosis:** Your API key is invalid, expired, or incorrectly formatted.

**Fix steps:**
1. Go to https://console.anthropic.com/account/keys
2. Check if you have any active keys (not revoked or expired)
3. If not, click "Create New Key"
4. Copy the full key (looks like `sk-ant-...`)
5. Update `~/.openclaw/config.yaml`:
   ```yaml
   anthropic:
     [REDACTED_SECRET]"
   ```
   OR set as environment variable:
   ```bash
   export ANTHROPIC_API_KEY="[REDACTED_SECRET]"
   ```
6. Verify: `echo $ANTHROPIC_API_KEY` (should show your key, not empty)
7. Rerun: `python validate_setup.py --verbose`

---

#### Error: "Network error: Cannot reach Anthropic API"

**Diagnosis:** Network connectivity issue or Anthropic API is down.

**Fix steps:**
1. Check internet connection: `ping api.anthropic.com`
2. Check if Anthropic is down: https://status.anthropic.com
3. Try from a different network
4. Check for proxy requirements
5. Rerun: `python validate_setup.py --verbose`

---

#### Error: "Rate limited: Try again in a moment"

**Diagnosis:** You've made too many API calls recently.

**Fix steps:**
1. Wait 60 seconds
2. Rerun: `python validate_setup.py`

(This is rarely a problem during initial setup.)

---

## OPTIONAL SERVICES — Can Proceed Without

These services enhance the stack but aren't required. If validation fails, the stack will work with reduced functionality.

### Polygon.io API Validation Fails

**Impact:** Kalshalyst estimates will lack recent news context. Estimates will still work (uses Claude alone).

**To fix (optional):**
1. Sign up at https://polygon.io (free tier works)
2. Navigate to your dashboard → API Keys
3. Copy your API key
4. Add to `~/.openclaw/config.yaml`:
   ```yaml
   polygon:
     [REDACTED_SECRET]"
   ```
5. Rerun: `python validate_setup.py`

**To skip this service:** Remove or set to empty:
```yaml
polygon:
  api_key: ""
```

---

### Ollama (Qwen) Validation Fails

**Impact:** Kalshalyst will use Claude for all estimates (higher cost). No offline fallback.

**To fix (optional):**

If error is "Ollama not running on localhost:11434":
1. Install Ollama: https://ollama.ai
2. Start the server: `ollama serve`
3. In another terminal, download model: `ollama pull qwen3:latest`
4. Rerun: `python validate_setup.py`

If error is "Model 'qwen3:latest' not found":
1. Download the model: `ollama pull qwen3:latest`
2. Verify it installed: `ollama list`
3. Rerun: `python validate_setup.py`

**To skip this service:** Disable in config:
```yaml
ollama:
  enabled: false
```

---

### Polymarket API Validation Fails

**Impact:** Prediction Market Arbiter skill won't find cross-platform divergences. Less useful but still runs.

**To fix (optional):**
1. Polymarket API is public (no auth needed)
2. If it fails, Polymarket infrastructure is likely down
3. Try again in 5 minutes
4. Check: https://gamma-api.polymarket.com/markets (should return JSON)

**To skip this service:** This is read-only/public, so there's nothing to configure. If Polymarket is down, we can't fix it.

---

## Advanced Diagnostics

### Run with verbose output to see actual API responses:
```bash
python validate_setup.py --verbose
```

### Test only one service:
```bash
python validate_setup.py --kalshi-only
```

### Check what config the script is reading:
```python
from scripts.validate_setup import load_config
import json
config = load_config()
print(json.dumps(config, indent=2))
```

### Test Kalshi manually:
```bash
export KALSHI_KEY_ID="your-key-id"
export KALSHI_KEY_PATH="/path/to/private.key"
python -c "from kalshi_python import Configuration, KalshiClient; c = Configuration(); c.api_key_id = os.getenv('KALSHI_KEY_ID'); print('✅ Client created')"
```

### Test Claude manually:
```bash
export ANTHROPIC_API_KEY="sk-ant-..."
python -c "from anthropic import Anthropic; c = Anthropic(); m = c.messages.create(model='claude-3-5-sonnet-20241022', max_tokens=10, messages=[{'role':'user','content':'OK'}]); print('✅ Response:', m.content[0].text)"
```

### Check network connectivity:
```bash
# Kalshi
curl -I https://api.kalshi.com/

# Anthropic
curl -I https://api.anthropic.com/

# Polygon.io
curl -I https://api.polygon.io/

# Ollama
curl -I http://localhost:11434/api/tags

# Polymarket
curl -I https://gamma-api.polymarket.com/markets
```

---

## Summary of Common Fixes

| Service | Most Common Issue | Fix |
|---------|------------------|-----|
| Kalshi | Private key path wrong | Update `private_key_file` path in config |
| Claude | Invalid API key | Get new key from console.anthropic.com |
| Polygon | Not configured | Sign up free at polygon.io, add key to config |
| Ollama | Server not running | Run `ollama serve` in background |
| Polymarket | Usually network | Wait, check status, try again |

---

## Next Steps After Validation Passes

Once all required services show ✅ PASS:

1. **Proceed to Phase 3:** iMessage Delivery Setup
2. **Create cron jobs:** Phase 4 (scheduled tasks)
3. **Launch:** Phase 5 (enable heartbeat)
4. **Monitor:** Phase 6 (verify everything)

If you're still stuck, check the full troubleshooting section in the main SKILL.md.


path: requirements.txt
content: anthropic==0.18.0
kalshi-python==1.0.0
pyyaml==6.0.3
requests==2.32.5


path: scripts/validate_setup.py
content: #!/usr/bin/env python3
"""
API Key Validation Script for Prediction Stack Setup

Validates all configured APIs:
- Kalshi: Test get_balance() call
- Anthropic (Claude): Test messages API with simple prompt
- Polygon.io: Verify public endpoint accessibility
- Ollama: Check model availability and inference

Usage:
    python validate_setup.py                    # Validate all
    python validate_setup.py --kalshi-only     # Validate only Kalshi
    python validate_setup.py --verbose         # Detailed output
"""

import sys
import json
import time
import logging
import argparse
from pathlib import Path
from typing import Dict, Tuple
import os

# Setup logging
logging.basicConfig(
    level=logging.INFO,
    format='%(message)s'
)
logger = logging.getLogger(__name__)


def _resolve_private_key_path(key_path: str) -> Path:
    """Resolve Kalshi private key paths consistently with the skillchain."""
    expanded = Path(key_path).expanduser()
    if expanded.is_absolute():
        return expanded
    return Path.home() / ".openclaw" / "keys" / expanded


class ValidationResult:
    """Encapsulates validation result for a service."""
    # CODEX: default passed=False so validators can progressively fill the result.
    def __init__(self, service_name: str, passed: bool = False, required: bool = False):
        self.service_name = service_name
        self.passed = passed
        self.required = required
        self.error = None
        self.duration_ms = 0

    def __str__(self):
        status = "✅ PASS" if self.passed else "❌ FAIL"
        req = "(REQUIRED)" if self.required else "(optional)"
        return f"{status} {self.service_name:20} {req:15} ({self.duration_ms}ms)"


_CONFIG_TEMPLATE = """\
# OpenClaw Prediction Stack Configuration
# Generated by prediction-stack-setup — fill in your credentials and run again.

kalshi:
  enabled: false
  api_key_id: REPLACE_WITH_YOUR_KEY_ID
  private_key_file: keys/private.key    # relative to ~/.openclaw/

anthropic:
  [REDACTED_SECRET]    # from console.anthropic.com

ollama:
  enabled: false
  model: qwen3:latest
  timeout_seconds: 60

polygon:
  api_key: ""                           # optional — enhances edge detection

xpulse:
  enabled: false
  topics:
    - tariffs
    - federal reserve
"""


def load_config():
    """Load OpenClaw config from ~/.openclaw/config.yaml.

    If the config file doesn't exist, generates a template so the user
    has a clear starting point instead of a cryptic error.
    """
    config_path = Path.home() / ".openclaw" / "config.yaml"

    if not config_path.exists():
        # Generate template config on first run
        config_path.parent.mkdir(parents=True, exist_ok=True)
        config_path.write_text(_CONFIG_TEMPLATE)
        logger.info(f"")
        logger.info(f"📋 First run — generated config template at:")
        logger.info(f"   {config_path}")
        logger.info(f"")
        logger.info(f"Next steps:")
        logger.info(f"  1. Open {config_path}")
        logger.info(f"  2. Add your Kalshi API key + private key path")
        logger.info(f"  3. (Optional) Add Anthropic API key, enable Ollama")
        logger.info(f"  4. Run this again to validate")
        logger.info(f"")
        logger.info(f"Get Kalshi API keys at: https://kalshi.com/account/api")
        logger.info(f"")
        # Return the template so validation can still run and show what's missing
        try:
            import yaml
            return yaml.safe_load(_CONFIG_TEMPLATE) or {}
        except ImportError:
            return {}

    try:
        import yaml
        with open(config_path, 'r') as f:
            return yaml.safe_load(f) or {}
    except ImportError:
        logger.error("PyYAML not installed. Install with: pip install pyyaml")
        return None
    except Exception as e:
        logger.error(f"Error loading config: {e}")
        return None


def validate_kalshi(config: dict, verbose: bool = False) -> ValidationResult:
    """Test Kalshi API connectivity and authentication."""
    result = ValidationResult("Kalshi API", required=True)
    start = time.time()

    try:
        kalshi_cfg = config.get("kalshi", {})

        if not kalshi_cfg.get("enabled", False):
            result.passed = False
            result.error = "Kalshi not enabled in config"
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        key_id = kalshi_cfg.get("api_key_id") or os.getenv("KALSHI_KEY_ID")
        key_path = kalshi_cfg.get("private_key_file") or os.getenv("KALSHI_KEY_PATH")

        if not key_id:
            result.error = "KALSHI_KEY_ID not configured"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        if not key_path:
            result.error = "KALSHI_KEY_PATH not configured"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        key_path = _resolve_private_key_path(key_path)

        if not key_path.exists():
            result.error = f"Private key file not found: {key_path}"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        # Test Kalshi connection
        try:
            from kalshi_python_sync import Configuration, KalshiClient
        except ImportError:
            from kalshi_python import Configuration, KalshiClient

        config_obj = Configuration(host="https://api.elections.kalshi.com/trade-api/v2")
        with open(key_path, 'r') as f:
            config_obj.private_key_pem = f.read()
        config_obj.api_key_id = key_id

        client = KalshiClient(config_obj)

        # Test with get_positions() — public method, avoids _portfolio_api internals
        try:
            resp = client.get_positions(limit=1)
            balance = None  # v3 SDK — balance fetched separately if needed
        except (TypeError, AttributeError):
            balance = client._portfolio_api.get_balance()

        if verbose:
            if balance is not None:
                logger.info(f"  → Kalshi balance: ${balance / 100:.2f}")
            else:
                logger.info("  → Kalshi auth OK (v3 SDK)")

        result.passed = True

    except Exception as e:
        result.passed = False
        error_str = str(e).lower()

        if "401" in error_str or "403" in error_str or "auth" in error_str:
            result.error = f"Authentication failed: Check API key and private key file"
        elif "timeout" in error_str or "connection" in error_str:
            result.error = f"Network error: Cannot reach Kalshi API"
        elif "not found" in error_str or "no such file" in error_str:
            result.error = f"File not found: Check private_key_file path in config"
        else:
            result.error = str(e)[:100]

    result.duration_ms = int((time.time() - start) * 1000)
    return result


def validate_anthropic(config: dict, verbose: bool = False) -> ValidationResult:
    """Test Anthropic Claude API connectivity."""
    result = ValidationResult("Anthropic (Claude)", required=True)
    start = time.time()

    try:
        [REDACTED_SECRET]"anthropic", {}).get("api_key") or os.getenv("ANTHROPIC_API_KEY")

        if not [REDACTED_SECRET] = "ANTHROPIC_API_KEY not configured"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        from anthropic import Anthropic

        client = Anthropic([REDACTED_SECRET])

        # Test with a simple completion
        response = client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=10,
            messages=[
                {
                    "role": "user",
                    "content": "Respond with only the word OK"
                }
            ]
        )

        if response.content:
            result.passed = True
            if verbose:
                logger.info(f"  → Response: {response.content[0].text.strip()}")

    except Exception as e:
        result.passed = False
        error_str = str(e).lower()

        if "401" in error_str or "unauthorized" in error_str or "invalid" in error_str:
            result.error = "Invalid or expired API key"
        elif "timeout" in error_str or "connection" in error_str:
            result.error = "Network error: Cannot reach Anthropic API"
        elif "429" in error_str or "rate" in error_str:
            result.error = "Rate limited: Try again in a moment"
        else:
            result.error = str(e)[:100]

    result.duration_ms = int((time.time() - start) * 1000)
    return result


def validate_polygon(config: dict, verbose: bool = False) -> ValidationResult:
    """Test Polygon.io API connectivity (optional, for enhanced edge detection)."""
    result = ValidationResult("Polygon.io API", required=False)
    start = time.time()

    try:
        polygon_cfg = config.get("polygon", {})
        [REDACTED_SECRET]"api_key") or os.getenv("POLYGON_API_KEY")

        if not [REDACTED_SECRET] = "API key not configured"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        import requests

        # Test with a simple ticker query
        response = requests.get(
            "https://api.polygon.io/v1/marketstatus",
            params={"apikey": api_key},
            timeout=5
        )

        if response.status_code == 200:
            result.passed = True
            if verbose:
                data = response.json()
                logger.info(f"  → Market status: {data.get('status', 'unknown')}")
        elif response.status_code == 401:
            result.error = "Invalid or expired API key"
        else:
            result.error = f"HTTP {response.status_code}"

    except Exception as e:
        result.passed = False
        error_str = str(e).lower()

        if "timeout" in error_str or "connection" in error_str:
            result.error = "Network error: Cannot reach Polygon.io API"
        else:
            result.error = str(e)[:100]

    result.duration_ms = int((time.time() - start) * 1000)
    return result


def validate_ollama(config: dict, verbose: bool = False) -> ValidationResult:
    """Test Ollama model availability (optional, for Xpulse + Kalshalyst fallback)."""
    result = ValidationResult("Ollama (Qwen)", required=False)
    start = time.time()

    try:
        ollama_cfg = config.get("ollama", {})

        if not ollama_cfg.get("enabled", False):
            result.error = "Ollama not enabled in config"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        model_name = ollama_cfg.get("model", "qwen3:latest")
        timeout = ollama_cfg.get("timeout_seconds", 60)

        import requests

        # Test 1: Check if Ollama server is running
        response = requests.get(
            "http://localhost:11434/api/tags",
            timeout=3
        )

        if response.status_code != 200:
            result.error = f"Ollama server not responding (HTTP {response.status_code})"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        # Test 2: Check if model is available
        available_models = response.json().get("models", [])
        model_names = [m["name"] for m in available_models]

        if not any(model_name in name for name in model_names):
            result.error = f"Model '{model_name}' not found. Available: {', '.join(model_names[:3])}"
            result.passed = False
            result.duration_ms = int((time.time() - start) * 1000)
            return result

        # Test 3: Try a simple inference
        response = requests.post(
            "http://localhost:11434/api/generate",
            json={
                "model": model_name,
                "prompt": "respond with OK",
                "stream": False,
            },
            timeout=timeout
        )

        if response.status_code == 200:
            result.passed = True
            if verbose:
                data = response.json()
                logger.info(f"  → Model generated response in {data.get('eval_duration', 0) / 1e9:.2f}s")
        else:
            result.error = f"Model inference failed (HTTP {response.status_code})"

    except requests.exceptions.ConnectionError:
        result.error = "Ollama not running on localhost:11434"
        result.passed = False
    except requests.exceptions.Timeout:
        result.error = "Ollama request timed out"
        result.passed = False
    except Exception as e:
        result.passed = False
        result.error = str(e)[:100]

    result.duration_ms = int((time.time() - start) * 1000)
    return result


def validate_polymarket(config: dict, verbose: bool = False) -> ValidationResult:
    """Test Polymarket public API (optional, no auth needed)."""
    result = ValidationResult("Polymarket API", required=False)
    start = time.time()

    try:
        import requests

        # Test with public endpoint (no auth needed)
        response = requests.get(
            "https://gamma-api.polymarket.com/markets",
            params={"limit": 1},
            timeout=5
        )

        if response.status_code == 200:
            result.passed = True
            if verbose:
                data = response.json()
                market_count = len(data) if isinstance(data, list) else data.get("count", 0)
                logger.info(f"  → Public API reachable, markets available")
        else:
            result.error = f"HTTP {response.status_code}"

    except Exception as e:
        result.passed = False
        error_str = str(e).lower()

        if "timeout" in error_str or "connection" in error_str:
            result.error = "Network error: Cannot reach Polymarket API"
        else:
            result.error = str(e)[:100]

    result.duration_ms = int((time.time() - start) * 1000)
    return result


def main():
    parser = argparse.ArgumentParser(
        description="Validate Prediction Stack API configuration"
    )
    parser.add_argument("--kalshi-only", action="store_true", help="Test only Kalshi")
    parser.add_argument("--verbose", "-v", action="store_true", help="Detailed output")
    args = parser.parse_args()

    # Load config (generates template on first run, returns None only on pyyaml missing)
    config = load_config()
    if config is None:
        logger.error("Cannot proceed without PyYAML. Install with: pip install pyyaml")
        return 1

    logger.info("━" * 70)
    logger.info("Prediction Stack API Validation")
    logger.info("━" * 70)
    logger.info("")

    results = []

    # Required services
    logger.info("REQUIRED SERVICES (must pass for stack to function)")
    logger.info("─" * 70)

    result = validate_kalshi(config, args.verbose)
    results.append(result)
    logger.info(result)
    if result.error:
        logger.info(f"   Error: {result.error}")

    if not args.kalshi_only:
        result = validate_anthropic(config, args.verbose)
        results.append(result)
        logger.info(result)
        if result.error:
            logger.info(f"   Error: {result.error}")

    logger.info("")

    # Optional services
    if not args.kalshi_only:
        logger.info("OPTIONAL SERVICES (enhance edge detection and resilience)")
        logger.info("─" * 70)

        result = validate_polygon(config, args.verbose)
        results.append(result)
        logger.info(result)
        if result.error:
            logger.info(f"   Error: {result.error}")

        result = validate_ollama(config, args.verbose)
        results.append(result)
        logger.info(result)
        if result.error:
            logger.info(f"   Error: {result.error}")

        result = validate_polymarket(config, args.verbose)
        results.append(result)
        logger.info(result)
        if result.error:
            logger.info(f"   Error: {result.error}")

    logger.info("")
    logger.info("━" * 70)

    # Summary
    required_results = [r for r in results if r.required]
    optional_results = [r for r in results if not r.required]

    required_passed = sum(1 for r in required_results if r.passed)
    optional_passed = sum(1 for r in optional_results if r.passed)

    logger.info(f"Required: {required_passed}/{len(required_results)} passed")
    logger.info(f"Optional: {optional_passed}/{len(optional_results)} passed")
    logger.info("")

    # Recommendation
    if required_passed == len(required_results):
        logger.info("✅ All required services validated. Stack is ready to launch.")
        logger.info("")
        return 0
    else:
        failed = [r for r in required_results if not r.passed]
        logger.error(f"❌ {len(failed)} required service(s) failed validation:")
        for r in failed:
            logger.error(f"   - {r.service_name}: {r.error}")
        logger.error("")
        logger.error("Fix these services before proceeding. Check error messages above.")
        logger.error("")
        return 1


if __name__ == "__main__":
    sys.exit(main())


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)