From fdee96841c50dd94092e604ec5d647c3fe7ade7d Mon Sep 17 00:00:00 2001 From: bishop-commits Date: Fri, 20 Feb 2026 14:06:52 -0600 Subject: [PATCH 1/6] Add market-intelligence community ability Voice-first prediction market + crypto price assistant using Polymarket Gamma API and CoinGecko. Supports multi-turn conversation with automatic query classification across geopolitics, crypto, macro, technology, and corporate categories. Fixes #97 feedback: replaced urllib with requests. --- community/market-intelligence/README.md | 45 ++++ community/market-intelligence/__init__.py | 0 community/market-intelligence/main.py | 312 ++++++++++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 community/market-intelligence/README.md create mode 100644 community/market-intelligence/__init__.py create mode 100644 community/market-intelligence/main.py diff --git a/community/market-intelligence/README.md b/community/market-intelligence/README.md new file mode 100644 index 00000000..602c16ae --- /dev/null +++ b/community/market-intelligence/README.md @@ -0,0 +1,45 @@ +# Market Intelligence + +A voice-first market intelligence assistant powered by **Polymarket** prediction markets and **CoinGecko** crypto data. + +## What It Does + +Ask natural questions about prediction markets, geopolitical events, crypto prices, or macro trends — and get spoken, data-backed answers. + +## Example Conversations + +``` +User: "What's the market saying about Iran?" +AI: "US airstrike on Iran by March 31st: 60% chance of Yes. + US airstrike by February 28th: 26% chance..." + +User: "How's Bitcoin doing?" +AI: "Bitcoin is trading at $67,490.00, up 1.2% in the last 24 hours. + Market cap: $1,337 billion." + +User: "Any predictions on AI?" +AI: "OpenAI valued above $500B by 2026: 72% chance of Yes..." +``` + +## Data Sources + +- **[Polymarket Gamma API](https://gamma-api.polymarket.com)** — Real-time prediction market data (no auth required) +- **[CoinGecko API](https://www.coingecko.com/en/api)** — Crypto prices and market data (free tier, no auth required) + +## Categories + +The ability automatically classifies queries into: +- **Geopolitics** — Conflicts, sanctions, diplomacy +- **Crypto** — Prices, exchanges, DeFi +- **Macro** — Fed, inflation, rates, trade +- **Technology** — AI, semiconductors, IPOs +- **Corporate** — Earnings, M&A, regulation + +## Requirements + +- `requests` (standard in OpenHome runtime) +- No API keys needed — both Polymarket Gamma and CoinGecko free tier work without authentication + +## Author + +[@bishop-commits](https://github.com/bishop-commits) diff --git a/community/market-intelligence/__init__.py b/community/market-intelligence/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/market-intelligence/main.py b/community/market-intelligence/main.py new file mode 100644 index 00000000..1d5ef29c --- /dev/null +++ b/community/market-intelligence/main.py @@ -0,0 +1,312 @@ +import json +import os +import re +from datetime import datetime, timezone +from typing import Dict, List, Optional + +import requests +from src.agent.capability import MatchingCapability +from src.agent.capability_worker import CapabilityWorker +from src.main import AgentWorker + +POLYMARKET_GAMMA_URL = "https://gamma-api.polymarket.com/markets" +COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" + +MATCHING_HOTWORDS = [ + "market intelligence", + "prediction market", + "polymarket", + "what's the market saying", + "market prediction", + "betting odds", + "prediction odds", + "market forecast", + "geopolitical odds", + "crypto market", + "market update", + "market brief", + "what are the odds", + "what does polymarket say", + "election odds", + "strike probability", + "market sentiment", +] + +CATEGORIES = { + "geopolitics": [ + "iran", "israel", "strike", "war", "military", "sanctions", + "china", "taiwan", "russia", "ukraine", "nato", "conflict", + "diplomatic", "ceasefire", "invasion", "troops", + ], + "crypto": [ + "bitcoin", "btc", "ethereum", "eth", "solana", "sol", "crypto", + "token", "defi", "nft", "stablecoin", "exchange", "coinbase", + "binance", "kraken", "mstr", "microstrategy", + ], + "macro": [ + "fed", "interest rate", "inflation", "gdp", "recession", + "unemployment", "treasury", "yield", "dollar", "euro", + "tariff", "trade", "debt ceiling", "deficit", + ], + "technology": [ + "ai", "artificial intelligence", "openai", "google", "apple", + "microsoft", "nvidia", "semiconductor", "chip", "ipo", + "acquisition", "merger", "startup", "valuation", + ], + "corporate": [ + "earnings", "stock", "shares", "revenue", "profit", "ceo", + "board", "lawsuit", "sec", "regulation", "filing", + ], +} + +CRYPTO_IDS = { + "bitcoin": "bitcoin", + "btc": "bitcoin", + "ethereum": "ethereum", + "eth": "ethereum", + "solana": "solana", + "sol": "solana", + "xrp": "ripple", + "cardano": "cardano", + "ada": "cardano", + "dogecoin": "dogecoin", + "doge": "dogecoin", + "avalanche": "avalanche-2", + "avax": "avalanche-2", + "polkadot": "polkadot", + "dot": "polkadot", + "chainlink": "chainlink", + "link": "chainlink", + "polygon": "matic-network", + "matic": "matic-network", +} + + +class WewrwewCapability(MatchingCapability): + """Market intelligence via Polymarket prediction markets and CoinGecko.""" + + CAPABILITY_NAME = "market-intelligence" + + @classmethod + def register_capability(cls) -> "MatchingCapability": + return cls( + hotwords=MATCHING_HOTWORDS, + agent_description=( + "A market intelligence assistant that provides prediction " + "market data from Polymarket and crypto prices from CoinGecko. " + "Ask about geopolitical events, crypto markets, macro trends, " + "or any topic with active prediction markets." + ), + ) + + def call(self, worker: AgentWorker): + user_input = self._best_initial_input() + if not user_input: + worker.speak( + "I'm your market intelligence assistant. Ask me about " + "prediction markets, crypto prices, or geopolitical odds. " + "For example: 'What's the market saying about Iran?' or " + "'How's Bitcoin doing?' Say stop to exit." + ) + user_input = worker.listen() + + history = [] + while user_input and not self._is_exit(user_input): + response = self._handle_query(user_input, history, worker) + if response: + worker.speak(response) + history.append({"role": "user", "content": user_input}) + history.append({"role": "assistant", "content": response}) + else: + worker.speak( + "I couldn't find relevant data for that query. " + "Try asking about a specific topic like Iran, Bitcoin, " + "or interest rates." + ) + user_input = worker.listen() + + worker.speak("Goodbye!") + + def _best_initial_input(self) -> str: + input_text = self.worker_input or "" + if input_text and not self._looks_like_trigger_echo(input_text): + return input_text + return "" + + def _looks_like_trigger_echo(self, text: Optional[str]) -> bool: + if not text: + return False + cleaned = text.strip().lower() + for hw in MATCHING_HOTWORDS: + if cleaned == hw.lower() or cleaned == hw.lower().rstrip("s"): + return True + return False + + def _is_exit(self, text: Optional[str]) -> bool: + if not text: + return True + exit_words = {"stop", "exit", "quit", "bye", "goodbye", "done", "end"} + return text.strip().lower() in exit_words + + def _handle_query( + self, user_input: str, history: List[Dict], worker: AgentWorker + ) -> Optional[str]: + category = self._classify_query(user_input) + + if category == "crypto" and self._wants_price(user_input): + return self._handle_crypto_price(user_input) + + markets = self._search_polymarket(user_input) + if markets: + return self._format_market_response(markets, user_input, worker) + + if category == "crypto": + return self._handle_crypto_price(user_input) + + return None + + def _classify_query(self, text: str) -> str: + text_lower = text.lower() + scores = {} + for cat, keywords in CATEGORIES.items(): + score = sum(1 for kw in keywords if kw in text_lower) + if score > 0: + scores[cat] = score + if scores: + return max(scores, key=scores.get) + return "general" + + def _wants_price(self, text: str) -> bool: + price_words = {"price", "trading", "worth", "cost", "how much", "doing"} + text_lower = text.lower() + return any(w in text_lower for w in price_words) + + def _search_polymarket(self, query: str) -> List[Dict]: + try: + params = { + "limit": 10, + "active": "true", + "closed": "false", + } + clean_query = re.sub( + r"\b(what|the|market|saying|about|does|polymarket|say|" + r"are|odds|of|for|on|is|any|predictions?)\b", + "", + query.lower(), + ).strip() + if clean_query: + params["tag_slug"] = clean_query.replace(" ", "-") + + resp = requests.get( + POLYMARKET_GAMMA_URL, params=params, timeout=15 + ) + resp.raise_for_status() + markets = resp.json() + + if not markets and clean_query: + del params["tag_slug"] + resp = requests.get( + POLYMARKET_GAMMA_URL, + params={**params, "limit": 50}, + timeout=15, + ) + resp.raise_for_status() + all_markets = resp.json() + keywords = clean_query.split() + markets = [ + m for m in all_markets + if any( + kw in m.get("question", "").lower() + or kw in m.get("description", "").lower() + for kw in keywords + if len(kw) > 2 + ) + ][:10] + + return markets + except requests.RequestException: + return [] + + def _format_market_response( + self, markets: List[Dict], query: str, worker: AgentWorker + ) -> str: + if not markets: + return None + + top = markets[:5] + lines = [] + + for m in top: + question = m.get("question", "Unknown") + outcomes = m.get("outcomePrices", "[]") + if isinstance(outcomes, str): + try: + outcomes = json.loads(outcomes) + except (json.JSONDecodeError, TypeError): + outcomes = [] + + if outcomes: + try: + yes_prob = float(outcomes[0]) * 100 + lines.append(f"{question}: {yes_prob:.0f}% chance of Yes.") + except (ValueError, IndexError): + lines.append(f"{question}: odds unavailable.") + else: + lines.append(f"{question}: odds unavailable.") + + if len(markets) > 5: + lines.append( + f"Plus {len(markets) - 5} more related markets on Polymarket." + ) + + return " ".join(lines) + + def _handle_crypto_price(self, text: str) -> Optional[str]: + text_lower = text.lower() + target_id = None + target_name = None + + for name, cg_id in CRYPTO_IDS.items(): + if name in text_lower: + target_id = cg_id + target_name = name.upper() + break + + if not target_id: + target_id = "bitcoin" + target_name = "Bitcoin" + + try: + resp = requests.get( + f"{COINGECKO_BASE_URL}/simple/price", + params={ + "ids": target_id, + "vs_currencies": "usd", + "include_24hr_change": "true", + "include_market_cap": "true", + }, + timeout=15, + ) + resp.raise_for_status() + data = resp.json().get(target_id, {}) + + if not data: + return f"Couldn't fetch price data for {target_name}." + + price = data.get("usd", 0) + change = data.get("usd_24h_change", 0) + mcap = data.get("usd_market_cap", 0) + + direction = "up" if change >= 0 else "down" + mcap_b = mcap / 1e9 if mcap else 0 + + response = ( + f"{target_name} is trading at ${price:,.2f}, " + f"{direction} {abs(change):.1f}% in the last 24 hours." + ) + if mcap_b > 0: + response += f" Market cap: ${mcap_b:,.0f} billion." + + return response + except requests.RequestException: + return f"Having trouble reaching CoinGecko for {target_name} data." From 58f263a907f052935b4ed3f0fff6ad3654117b2e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Feb 2026 20:07:17 +0000 Subject: [PATCH 2/6] style: auto-format Python files with autoflake + autopep8 --- community/market-intelligence/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/community/market-intelligence/main.py b/community/market-intelligence/main.py index 1d5ef29c..6826097e 100644 --- a/community/market-intelligence/main.py +++ b/community/market-intelligence/main.py @@ -1,12 +1,9 @@ import json -import os import re -from datetime import datetime, timezone from typing import Dict, List, Optional import requests from src.agent.capability import MatchingCapability -from src.agent.capability_worker import CapabilityWorker from src.main import AgentWorker POLYMARKET_GAMMA_URL = "https://gamma-api.polymarket.com/markets" From d19cd861690aec1da500e61b97e915154e2fac05 Mon Sep 17 00:00:00 2001 From: bishop-commits Date: Sat, 21 Feb 2026 09:16:12 -0600 Subject: [PATCH 3/6] fix: market-intelligence v2 SDK compliance - Add config.json with unique_name and matching_hotwords - Refactor register_capability() to load config.json per SDK boilerplate - Convert call() to async pattern using CapabilityWorker - Add resume_normal_flow() at all exit points - Remove worker parameter threading (use self.worker) Fixes validation failures on PR #103 --- community/market-intelligence/config.json | 22 +++++++++++ community/market-intelligence/main.py | 45 ++++++++++++++--------- 2 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 community/market-intelligence/config.json diff --git a/community/market-intelligence/config.json b/community/market-intelligence/config.json new file mode 100644 index 00000000..52ebeba4 --- /dev/null +++ b/community/market-intelligence/config.json @@ -0,0 +1,22 @@ +{ + "unique_name": "market-intelligence", + "matching_hotwords": [ + "market intelligence", + "prediction market", + "polymarket", + "what's the market saying", + "market prediction", + "betting odds", + "prediction odds", + "market forecast", + "geopolitical odds", + "crypto market", + "market update", + "market brief", + "what are the odds", + "what does polymarket say", + "election odds", + "strike probability", + "market sentiment" + ] +} diff --git a/community/market-intelligence/main.py b/community/market-intelligence/main.py index 6826097e..ec2717ec 100644 --- a/community/market-intelligence/main.py +++ b/community/market-intelligence/main.py @@ -83,46 +83,57 @@ class WewrwewCapability(MatchingCapability): """Market intelligence via Polymarket prediction markets and CoinGecko.""" CAPABILITY_NAME = "market-intelligence" + worker: AgentWorker = None + capability_worker: CapabilityWorker = None @classmethod def register_capability(cls) -> "MatchingCapability": + with open( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + ) as file: + data = json.load(file) return cls( - hotwords=MATCHING_HOTWORDS, - agent_description=( - "A market intelligence assistant that provides prediction " - "market data from Polymarket and crypto prices from CoinGecko. " - "Ask about geopolitical events, crypto markets, macro trends, " - "or any topic with active prediction markets." - ), + unique_name=data["unique_name"], + matching_hotwords=data["matching_hotwords"], ) def call(self, worker: AgentWorker): + self.worker = worker + self.capability_worker = CapabilityWorker(self.worker) + self.worker.session_tasks.create(self._run()) + + async def _run(self): user_input = self._best_initial_input() if not user_input: - worker.speak( + await self.capability_worker.speak( "I'm your market intelligence assistant. Ask me about " "prediction markets, crypto prices, or geopolitical odds. " "For example: 'What's the market saying about Iran?' or " "'How's Bitcoin doing?' Say stop to exit." ) - user_input = worker.listen() + user_input = await self.capability_worker.user_response() + + if not user_input: + self.capability_worker.resume_normal_flow() + return history = [] while user_input and not self._is_exit(user_input): - response = self._handle_query(user_input, history, worker) + response = self._handle_query(user_input, history) if response: - worker.speak(response) + await self.capability_worker.speak(response) history.append({"role": "user", "content": user_input}) history.append({"role": "assistant", "content": response}) else: - worker.speak( + await self.capability_worker.speak( "I couldn't find relevant data for that query. " "Try asking about a specific topic like Iran, Bitcoin, " "or interest rates." ) - user_input = worker.listen() + user_input = await self.capability_worker.user_response() - worker.speak("Goodbye!") + await self.capability_worker.speak("Goodbye!") + self.capability_worker.resume_normal_flow() def _best_initial_input(self) -> str: input_text = self.worker_input or "" @@ -146,7 +157,7 @@ def _is_exit(self, text: Optional[str]) -> bool: return text.strip().lower() in exit_words def _handle_query( - self, user_input: str, history: List[Dict], worker: AgentWorker + self, user_input: str, history: List[Dict] ) -> Optional[str]: category = self._classify_query(user_input) @@ -155,7 +166,7 @@ def _handle_query( markets = self._search_polymarket(user_input) if markets: - return self._format_market_response(markets, user_input, worker) + return self._format_market_response(markets, user_input) if category == "crypto": return self._handle_crypto_price(user_input) @@ -225,7 +236,7 @@ def _search_polymarket(self, query: str) -> List[Dict]: return [] def _format_market_response( - self, markets: List[Dict], query: str, worker: AgentWorker + self, markets: List[Dict], query: str ) -> str: if not markets: return None From 2dd0c49fc349111ee74c8839c6951d215fc0b264 Mon Sep 17 00:00:00 2001 From: bishop-commits Date: Sun, 22 Feb 2026 08:29:32 -0600 Subject: [PATCH 4/6] fix: add missing os and CapabilityWorker imports --- community/market-intelligence/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/community/market-intelligence/main.py b/community/market-intelligence/main.py index ec2717ec..703f0f9e 100644 --- a/community/market-intelligence/main.py +++ b/community/market-intelligence/main.py @@ -1,9 +1,11 @@ import json +import os import re from typing import Dict, List, Optional import requests from src.agent.capability import MatchingCapability +from src.agent.capability_worker import CapabilityWorker from src.main import AgentWorker POLYMARKET_GAMMA_URL = "https://gamma-api.polymarket.com/markets" From d61366e15087e650065ae188d0550ca4ea39ed75 Mon Sep 17 00:00:00 2001 From: bishop-commits Date: Sun, 22 Feb 2026 11:43:07 -0600 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20align=20with=20SDK=20template=20?= =?UTF-8?q?=E2=80=94=20use=20register=20tag,=20remove=20raw=20open(),=20re?= =?UTF-8?q?name=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- community/market-intelligence/main.py | 14 ++------------ validation_output.txt | 3 +++ 2 files changed, 5 insertions(+), 12 deletions(-) create mode 100644 validation_output.txt diff --git a/community/market-intelligence/main.py b/community/market-intelligence/main.py index 703f0f9e..ce53b238 100644 --- a/community/market-intelligence/main.py +++ b/community/market-intelligence/main.py @@ -1,5 +1,4 @@ import json -import os import re from typing import Dict, List, Optional @@ -81,23 +80,14 @@ } -class WewrwewCapability(MatchingCapability): +class MarketIntelligenceCapability(MatchingCapability): """Market intelligence via Polymarket prediction markets and CoinGecko.""" CAPABILITY_NAME = "market-intelligence" worker: AgentWorker = None capability_worker: CapabilityWorker = None - @classmethod - def register_capability(cls) -> "MatchingCapability": - with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") - ) as file: - data = json.load(file) - return cls( - unique_name=data["unique_name"], - matching_hotwords=data["matching_hotwords"], - ) + #{{register capability}} def call(self, worker: AgentWorker): self.worker = worker diff --git a/validation_output.txt b/validation_output.txt new file mode 100644 index 00000000..3521cb23 --- /dev/null +++ b/validation_output.txt @@ -0,0 +1,3 @@ + +📋 Validating: community/market-intelligence/ + ✅ All checks passed! \ No newline at end of file From ae574d4fdf231a15814300c430bb96a424510a30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Feb 2026 17:43:20 +0000 Subject: [PATCH 6/6] style: auto-format Python files with autoflake + autopep8 --- community/market-intelligence/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community/market-intelligence/main.py b/community/market-intelligence/main.py index ce53b238..5979bb7a 100644 --- a/community/market-intelligence/main.py +++ b/community/market-intelligence/main.py @@ -87,7 +87,7 @@ class MarketIntelligenceCapability(MatchingCapability): worker: AgentWorker = None capability_worker: CapabilityWorker = None - #{{register capability}} + # {{register capability}} def call(self, worker: AgentWorker): self.worker = worker