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/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 new file mode 100644 index 00000000..5979bb7a --- /dev/null +++ b/community/market-intelligence/main.py @@ -0,0 +1,312 @@ +import json +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" +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 MarketIntelligenceCapability(MatchingCapability): + """Market intelligence via Polymarket prediction markets and CoinGecko.""" + + CAPABILITY_NAME = "market-intelligence" + worker: AgentWorker = None + capability_worker: CapabilityWorker = None + + # {{register capability}} + + 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: + 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 = 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) + if response: + await self.capability_worker.speak(response) + history.append({"role": "user", "content": user_input}) + history.append({"role": "assistant", "content": response}) + else: + 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 = await self.capability_worker.user_response() + + await self.capability_worker.speak("Goodbye!") + self.capability_worker.resume_normal_flow() + + 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] + ) -> 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) + + 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 + ) -> 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." 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