""" RugCharts Daily Intelligence Briefing ====================================== THE daily market briefing. AI-researched, AI-written, human-quality. Published 6:30 AM ET to X (@CryptoRugMunch), Telegram, Ghost CMS. Pipeline: 1. Gather — all DataBus sources (prices, news, CT, sentiment, fear/greed, memes) 2. Research — OpenRouter free model analyzes everything 3. Write — OpenRouter free model produces the final report 4. Publish — X/Twitter, Telegram, Ghost CMS Free models used (zero cost): Research: nvidia/nemotron-3-super-120b-a12b:free (1M ctx, 120B MoE) Writing: google/gemma-4-26b-a4b-it:free (262K ctx, excellent prose) """ import logging import os import re import subprocess from datetime import UTC, datetime import httpx logger = logging.getLogger("daily_intel") OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY", "") OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" # Free models for each phase RESEARCH_MODEL = "nvidia/nemotron-3-super-120b-a12b:free" WRITING_MODEL = "google/gemma-4-26b-a4b-it:free" # Fallback if primary unavailable FALLBACK_RESEARCH = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free" FALLBACK_WRITING = "moonshotai/kimi-k2.6:free" # Publishing targets X_ACCOUNT = "CryptoRugMunch" GHOST_URL = os.getenv("GHOST_URL", "http://172.19.0.3:2368") GHOST_KEY = os.getenv("GHOST_ADMIN_API_KEY", "") or os.getenv("GHOST_CONTENT_API_KEY", "") async def _openrouter_chat(model: str, system: str, user: str, max_tokens: int = 1500, temperature: float = 0.5) -> str: """Call OpenRouter with a free model.""" if not OPENROUTER_KEY: return "" try: async with httpx.AsyncClient(timeout=90) as c: r = await c.post( OPENROUTER_URL, headers={ "Authorization": f"Bearer {OPENROUTER_KEY}", "Content-Type": "application/json", "HTTP-Referer": "https://rugmunch.io", "X-Title": "RugCharts Daily Intel", }, json={ "model": model, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], "temperature": temperature, "max_tokens": max_tokens, }, ) if r.status_code == 200: return r.json()["choices"][0]["message"]["content"] else: logger.warning(f"OpenRouter {model}: {r.status_code} {r.text[:200]}") return "" except Exception as e: logger.warning(f"OpenRouter error: {e}") return "" async def _gather_all_data() -> dict: """Gather comprehensive data from ALL DataBus sources.""" data = { "market": {}, "fear_greed": {}, "news": {}, "ct": {}, "social": {}, "prediction_markets": {}, } # Market data try: from app.databus.news_provider import get_market_brief data["market"] = await get_market_brief() except Exception: pass # News intel try: from app.databus.news_intel import aggregate_all_news data["news"] = await aggregate_all_news(limit=20) except Exception: pass # CT Rundown try: from app.databus.x_intel import fetch_ct_rundown data["ct"] = await fetch_ct_rundown(limit=15) except Exception: pass # Social metrics try: from app.databus.social_intel import get_social_metrics data["social"] = await get_social_metrics() except Exception: pass # Fear & Greed try: from app.databus.news_provider import get_fear_greed data["fear_greed"] = await get_fear_greed() except Exception: pass # Prediction markets try: from app.databus.news_provider import get_prediction_markets data["prediction_markets"] = await get_prediction_markets(limit=5) except Exception: pass return data def _build_research_context(data: dict) -> str: """Build comprehensive context for the research model.""" parts = [] # Market snapshot market = data.get("market", {}) if market.get("brief"): parts.append(f"## MARKET SNAPSHOT\n{market['brief']}") # Fear & Greed fg = data.get("fear_greed", {}) if fg.get("value"): parts.append(f"## FEAR & GREED INDEX\n{fg['value']}/100 — {fg.get('classification', 'Neutral')}") # News headlines news = data.get("news", {}) articles = news.get("articles", []) if articles: headlines = "\n".join( f"- [{a.get('sentiment', {}).get('sentiment', '➖')}] {a.get('title', '')}" for a in articles[:15] ) parts.append(f"## TOP HEADLINES\n{headlines}") # CT Pulse ct = data.get("ct", {}) rundown = ct.get("rundown", []) if rundown: ct_pulse = "\n".join(f"- @{s.get('author_handle', '?')}: {s.get('text', '')[:150]}" for s in rundown[:10]) parts.append(f"## CRYPTO TWITTER PULSE\n{ct_pulse}") # Social metrics social = data.get("social", {}) if social.get("trending_topics"): topics = social["trending_topics"] parts.append(f"## TRENDING TOPICS\n{', '.join(list(topics.keys())[:10])}") # Prediction markets pm = data.get("prediction_markets", {}) pmarkets = pm.get("markets", []) if pmarkets: pm_str = "\n".join(f"- {m.get('title', '')[:80]}: ${m.get('volume', 0):,.0f} vol" for m in pmarkets[:3]) parts.append(f"## PREDICTION MARKETS\n{pm_str}") return "\n\n".join(parts) WRITING_STANDARDS = """You are a senior financial writer for RugCharts Daily Intelligence. WRITING STANDARDS: - Human, conversational tone. Like a sharp newsletter, not a robot. - No AI-isms: never use "delve", "tapestry", "landscape", "robust", "moreover", "furthermore" - Lead with the most important story. Hook the reader. - Be specific: use numbers, names, percentages. No vague statements. - Include market sentiment, social mood, and what traders are actually talking about - One section on MEMES/CULTURE — what's trending on CT - One section on RISK RADAR — scams, hacks, regulatory threats to watch - End with BOTTOM LINE — actionable takeaway in 2 sentences FORMAT EXACTLY LIKE THIS: # RUGCHARTS DAILY INTELLIGENCE ## {Date} ### MARKET SNAPSHOT {2-3 sentences on overall market} ### TOP STORIES {3-5 bullet points of most important news with brief context} ### SENTIMENT CHECK {Market mood: fear/greed, social sentiment, what CT is feeling} ### MEMES & CULTURE {What's trending on CT, notable memes, cultural moments} ### RISK RADAR {Scams, hacks, regulatory actions, things to avoid today} ### BOTTOM LINE {1-2 sentence actionable takeaway} --- Published by RugCharts Daily Intelligence Subscribe: https://rugmunch.io/news""" async def generate_daily_intel(publish: bool = False, **kw) -> dict | None: """Generate the complete Daily Intelligence Briefing with quality review. Pipeline: Gather → Research → Write → Review → Fix → Publish All AI calls through model_registry (free models only). Ghost is canonical. X/Telegram are syndication. Args: publish: If True, publish to Ghost (primary) + X/Telegram (syndication) """ from app.databus.model_registry import ai_call, review_content # ── PHASE 0: Gather all data ── logger.info("Daily Intel: gathering data...") data = await _gather_all_data() context = _build_research_context(data) if len(context) < 100: return {"error": "Insufficient data gathered"} # ── PHASE 1: Research ── logger.info("Daily Intel: research phase (free model)...") research_notes = await ai_call( "research", "You are a senior crypto research analyst. Analyze data and produce structured research notes with specific numbers and names.", f"Analyze today's crypto market data. Identify top 3 stories, sentiment drivers, risks, cultural trends, and on-chain signals:\n\n{context}", max_tokens=1200, temperature=0.3, ) if not research_notes: research_notes = "Research phase: raw data analysis (no AI available).\n\n" + context[:2000] # ── PHASE 2: Writing ── logger.info("Daily Intel: writing phase (free model)...") now = datetime.now(UTC) date_str = now.strftime("%A, %B %d, %Y") writing_prompt = f"""Write today's RugCharts Daily Intelligence. Today: {date_str} Research notes: {research_notes} Raw context: {context[:2500]} FORMAT: # RUGCHARTS DAILY INTELLIGENCE ## {date_str} ### MARKET SNAPSHOT 2-3 sentences on overall market direction and key drivers. ### TOP STORIES 3-5 bullet points with specific numbers, names, and context. ### SENTIMENT CHECK Market mood, social sentiment, fear/greed, what CT is saying. ### MEMES & CULTURE What's trending on CT. Notable narratives. Cultural moments. ### RISK RADAR Scams, hacks, regulatory actions. What to avoid today. ### BOTTOM LINE 1-2 sentence actionable takeaway. """ final_report = await ai_call("writing", WRITING_STANDARDS, writing_prompt, max_tokens=2000, temperature=0.7) if not final_report or len(final_report) < 200: headlines = data.get("news", {}).get("articles", []) final_report = f"""# RUGCHARTS DAILY INTELLIGENCE ## {date_str} ### MARKET SNAPSHOT {data.get("market", {}).get("brief", "Market data unavailable")} ### TOP STORIES {chr(10).join("- " + a.get("title", "") for a in headlines[:5])} ### SENTIMENT CHECK Fear & Greed: {data.get("fear_greed", {}).get("value", "?")}/100 ### BOTTOM LINE Stay sharp. Data-driven decisions only.""" # ── PHASE 3: Review ── logger.info("Daily Intel: quality review...") review = await review_content(final_report, "daily_briefing") if not review["pass"] and review.get("fixed_version"): logger.info(f"Daily Intel: auto-fixed (score {review['score']}/100)") final_report = review["fixed_version"] else: logger.info(f"Daily Intel: passed review ({review['score']}/100)") report_data = { "report": final_report, "date": date_str, "research_model": "free_openrouter", "writing_model": "free_openrouter", "review_score": review["score"], "review_issues": review.get("issues", []), "data_sources": sum(1 for v in data.values() if v), "generated_at": datetime.now(UTC).isoformat(), "published": False, "source": "daily_intel_briefing", } # ── PHASE 4: Publish (Ghost first, then syndicate) ── if publish: pub_results = await _publish_briefing(final_report, date_str) report_data["published"] = True report_data["publish_results"] = pub_results return report_data async def _publish_briefing(report: str, date_str: str) -> dict: """Publish the briefing to all channels.""" results = {} # ── X/Twitter via xurl ── x_result = await _publish_to_x(report, date_str) results["x"] = x_result # ── Ghost CMS ── ghost_result = await _publish_to_ghost(report, date_str) results["ghost"] = ghost_result # ── Telegram (via send_message or bot) ── tg_result = await _publish_to_telegram(report, date_str) results["telegram"] = tg_result return results async def _publish_to_x(report: str, date_str: str) -> dict: """Publish briefing summary to X @CryptoRugMunch via xurl.""" # Extract top story + TLDR for tweet thread lines = report.split("\n") headline = "" tldr = "" for line in lines: if line.startswith("### MARKET SNAPSHOT") or line.startswith("##"): continue if not headline and len(line.strip()) > 20: headline = line.strip().lstrip("#- ")[:240] if "BOTTOM LINE" in line: # Grab the next line idx = lines.index(line) if idx + 1 < len(lines): tldr = lines[idx + 1].strip().lstrip("- ")[:240] if not headline: headline = f"RugCharts Daily Intelligence — {date_str}" tweet_text = f"📊 {headline}\n\n{tldr}\n\nFull report: https://rugmunch.io/news" try: result = subprocess.run( ["xurl", "post", tweet_text, "--auth", "oauth2"], capture_output=True, text=True, timeout=20, ) if result.returncode == 0: return {"status": "posted", "platform": "x", "length": len(tweet_text)} else: return {"status": "failed", "platform": "x", "error": result.stderr[:200]} except Exception as e: return {"status": "error", "platform": "x", "error": str(e)[:200]} async def _publish_to_ghost(report: str, date_str: str) -> dict: """Publish briefing to Ghost CMS under 'daily' tag.""" if not GHOST_URL or not GHOST_KEY: return {"status": "skipped", "reason": "Ghost not configured"} try: # Extract title from report report.split("\n") title = f"Daily Intelligence — {date_str}" # Convert markdown to Ghost HTML html = _markdown_to_html(report) async with httpx.AsyncClient(timeout=20) as c: r = await c.post( f"{GHOST_URL}/ghost/api/admin/posts/", headers={ "Authorization": f"Ghost {GHOST_KEY}", "Content-Type": "application/json", "Accept-Version": "v5.0", }, json={ "posts": [ { "title": title, "html": html, "status": "published", "tags": ["daily", "intelligence", "briefing"], "feature_image": "", } ] }, ) if r.status_code in (200, 201): return {"status": "published", "platform": "ghost"} else: return {"status": "failed", "platform": "ghost", "error": r.text[:200]} except Exception as e: return {"status": "error", "platform": "ghost", "error": str(e)[:200]} async def _publish_to_telegram(report: str, date_str: str) -> dict: """Send briefing to Telegram channel.""" bot_token = os.getenv("TELEGRAM_BOT_TOKEN", "") channel = os.getenv("CHANNEL_NEWS", "") or os.getenv("CHANNEL_ALERTS", "") if not bot_token or not channel: return {"status": "skipped", "reason": "Telegram not configured"} # Create a shorter version for Telegram lines = report.split("\n") tg_text = f"📊 *RugCharts Daily Intelligence*\n{date_str}\n\n" # Extract key sections for i, line in enumerate(lines): if line.startswith("### "): tg_text += f"\n*{line.strip('# ')}*\n" elif line.startswith("- ") and len(tg_text) < 3500: tg_text += f"{line}\n" elif "BOTTOM LINE" in line and i + 1 < len(lines): tg_text += f"\n💡 *Bottom Line:* {next_line}\n" break tg_text += "\n🔗 Full report: https://rugmunch.io/news" try: async with httpx.AsyncClient(timeout=15) as c: r = await c.post( f"https://api.telegram.org/bot{bot_token}/sendMessage", json={ "chat_id": channel, "text": tg_text[:4000], "parse_mode": "Markdown", "disable_web_page_preview": False, }, ) if r.status_code == 200: return {"status": "sent", "platform": "telegram"} else: return {"status": "failed", "platform": "telegram", "error": r.text[:200]} except Exception as e: return {"status": "error", "platform": "telegram", "error": str(e)[:200]} def _markdown_to_html(md: str) -> str: """Simple markdown to HTML conversion for Ghost.""" html = md html = re.sub(r"^# (.+)$", r"

\1

", html, flags=re.MULTILINE) html = re.sub(r"^## (.+)$", r"

\1

", html, flags=re.MULTILINE) html = re.sub(r"^### (.+)$", r"

\1

", html, flags=re.MULTILINE) html = re.sub(r"^- (.+)$", r"
  • \1
  • ", html, flags=re.MULTILINE) html = re.sub(r"\*\*(.+?)\*\*", r"\1", html) html = html.replace("\n\n", "

    ").replace("\n", "
    ") html = f"

    {html}

    " html = html.replace("

    ", "").replace("

    ", "") return html