#!/usr/bin/env python3 """ CryptoRugMunch Bot v6 — The Bloomberg Terminal of Shitcoins ============================================================ Production-grade: multi-chain scanning, wallet forensics, tiered subs, Telegram Stars payments, DEX ref revenue, referral system, AI chat, inline queries, scheduled tips, trending tokens, spam protection, website integration, social links, bot profile setup. Run: cd /root/backend/app/telegram_bot && python3 bot.py """ import asyncio import logging import random import re import sys import time from datetime import UTC, datetime from datetime import time as dtime from pathlib import Path from urllib.parse import quote import httpx from telegram import ( BotCommand, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputTextMessageContent, Update, ) from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackQueryHandler, CommandHandler, ContextTypes, InlineQueryHandler, MessageHandler, PreCheckoutQueryHandler, filters, ) # ── Local imports ── sys.path.insert(0, str(Path(__file__).parent)) import contextlib import db from config import ( BACKEND_URL, BOT_DESCRIPTION, BOT_SHORT_DESCRIPTION, BOT_TOKEN, BOT_USERNAME, CHAIN_IDS, CHANNELS, DEX_REF_LINKS, DEXSCREENER, GOPLUS, HONEYPOT, OWNER_IDS, SCAM_SCHOOL_TOPICS, SUPPORT_EMAIL, TELEGRAM_CHANNEL, TELEGRAM_GROUP, TIERS, TOP_UP_PACKS, TWITTER_URL, WEB_APP_URL, WEBSITE_URL, fmt_chain, fmt_number, fmt_pct, risk_bar, threat_indicator, ) # ══════════════════════════════════════════════ # LOGGING # ══════════════════════════════════════════════ logging.basicConfig( format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", level=logging.INFO, ) logger = logging.getLogger("rmi_bot") # ══════════════════════════════════════════════ # SPAM PROTECTION # ══════════════════════════════════════════════ _user_last_action: dict[int, float] = {} SPAM_COOLDOWN = 3 def check_spam(user_id: int) -> bool: now = time.time() last = _user_last_action.get(user_id, 0) if now - last < SPAM_COOLDOWN and not is_owner(user_id): return True _user_last_action[user_id] = now return False # ══════════════════════════════════════════════ # HELPERS # ══════════════════════════════════════════════ EVM_RE = re.compile(r"^0x[a-fA-F0-9]{40}$") SOL_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$") TOKEN_RE = re.compile(r"\$([A-Za-z]{2,10})\b") def is_evm(addr: str) -> bool: return bool(EVM_RE.match(addr)) def is_sol(addr: str) -> bool: return bool(SOL_RE.match(addr)) def detect_chain(addr: str) -> str: if is_evm(addr): return "ethereum" if is_sol(addr): return "solana" return "unknown" def short_addr(addr: str) -> str: if len(addr) > 12: return f"{addr[:6]}...{addr[-4:]}" return addr def is_owner(uid: int) -> bool: return uid in OWNER_IDS def sep(char: str = "━", length: int = 28) -> str: return char * length def thin_sep(char: str = "─", length: int = 24) -> str: return char * length def footer_links() -> str: """Standard footer with website + social links.""" return ( f"\n{thin_sep('·', 28)}\n" f'🌐 rugmunch.io | ' f'📢 Alerts | ' f'𝕏' ) def web_scan_button(address: str, chain: str = "") -> InlineKeyboardButton: """Button to view full scan on the website.""" url = f"{WEB_APP_URL}?token={address}" if chain: url += f"&chain={chain}" return InlineKeyboardButton("🌐 Full Report on RugMunch.io", url=url) def dex_buttons(token: str, chain: str) -> list[InlineKeyboardButton]: buttons = [] for _key, dex in DEX_REF_LINKS.items(): if chain.lower() in dex.get("chains", []): url = dex["url"].format( token=token, chain=chain, chain_id=CHAIN_IDS.get(chain.lower(), 1), ) buttons.append(InlineKeyboardButton(f"{dex['emoji']} {dex['name']}", url=url)) return buttons def social_proof_text() -> str: """Dynamic social proof from DB stats.""" try: conn = db.get_db() total_users = conn.execute("SELECT COUNT(*) as c FROM users").fetchone()["c"] total_scans = conn.execute("SELECT SUM(total_scans) as c FROM users").fetchone()["c"] or 0 conn.close() users_str = f"{total_users:,}" if total_users else "1,000+" scans_str = f"{total_scans:,}" if total_scans else "50,000+" return f"👥 {users_str} users | 🔍 {scans_str} scans completed" except Exception: return "👥 1,000+ users | 🔍 50,000+ scans completed" def paywall_text(user_id: int, action: str = "scan") -> str: tier = db.get_user_tier(user_id) scans, ai = db.get_weekly_usage(user_id) user = db.get_or_create_user(user_id) tc = TIERS.get(tier, TIERS["free"]) limit = tc.get("scans_per_week", 25) if action == "scan" else tc.get("ai_msgs_per_week", 15) used = scans if action == "scan" else ai bonus = user.get("bonus_scans", 0) if action == "scan" else user.get("bonus_ai_msgs", 0) return ( f"⚡ Weekly {action.title()} Limit Reached\n\n" f"📊 {tc['name']} Tier: {used}/{limit} used this week\n" f"🎁 Bonus Balance: {bonus} extra\n\n" f"Unlock more power:\n\n" f"🔍 Scout ($29.99/mo) — 150 scans/wk + bundle detection\n" f"🎯 Hunter ($49.99/mo) — 400 scans/wk + wallet forensics\n" f"👑 Pro ($99.99/mo) — 1,000 scans/wk + smart money alerts\n\n" f"Or buy one-time top-ups — never expire!\n" f"🔋 10 scans = ⭐75 | 50 scans = ⭐300 | 100 scans = ⭐500" ) def paywall_kb() -> InlineKeyboardMarkup: return InlineKeyboardMarkup( [ [InlineKeyboardButton("⭐ View Plans", callback_data="menu_pricing")], [InlineKeyboardButton("🔋 Buy Top-Up", callback_data="menu_topup")], [InlineKeyboardButton("🌐 rugmunch.io", url=WEBSITE_URL)], [InlineKeyboardButton("◀️ Menu", callback_data="menu_main")], ] ) # ══════════════════════════════════════════════ # SCAN ENGINE # ══════════════════════════════════════════════ async def scan_token(address: str, chain: str | None = None) -> dict: result = { "address": address, "chain": chain or detect_chain(address), "name": "Unknown", "symbol": "???", "price": 0, "mcap": 0, "fdv": 0, "volume_24h": 0, "liquidity": 0, "price_change_1h": 0, "price_change_24h": 0, "price_change_7d": 0, "holders": 0, "pair_address": "", "dex": "", "created_at": None, "risk_score": 0, "threats": [], "contract_verified": None, "honeypot": None, "buy_tax": None, "sell_tax": None, "lp_locked": None, "mintable": None, "can_blacklist": None, "owner_renounced": None, "top_holders": [], "bundle_detected": False, "fresh_wallet_pct": 0, "exchange_pct": 0, "volume_real_ratio": None, "dev_wallet": None, "dev_other_tokens": [], "errors": [], "socials": {}, } async with httpx.AsyncClient(timeout=15) as client: # ── DexScreener ── try: r = await client.get(f"{DEXSCREENER}/tokens/{address}") if r.status_code == 200: data = r.json() pairs = data.get("pairs", []) if pairs: p = pairs[0] result["name"] = p.get("baseToken", {}).get("name", "Unknown") result["symbol"] = p.get("baseToken", {}).get("symbol", "???") result["price"] = float(p.get("priceUsd", 0)) result["mcap"] = float(p.get("marketCap") or p.get("fdv") or 0) result["fdv"] = float(p.get("fdv") or result["mcap"]) vol = p.get("volume", {}) result["volume_24h"] = float(vol.get("h24") or 0) liq = p.get("liquidity", {}) result["liquidity"] = float(liq.get("usd") or 0) pc = p.get("priceChange", {}) result["price_change_1h"] = pc.get("h1") or 0 result["price_change_24h"] = pc.get("h24") or 0 result["price_change_7d"] = pc.get("d7") or 0 result["pair_address"] = p.get("pairAddress", "") result["dex"] = p.get("dexId", "") result["chain"] = p.get("chainId", result["chain"]) result["created_at"] = p.get("pairCreatedAt") socials = p.get("info", {}).get("socials", []) result["socials"] = {s.get("type"): s.get("url") for s in socials} except Exception as e: result["errors"].append(f"DexScreener: {e}") # ── GoPlus Security (EVM) ── if is_evm(address): try: chain_id_map = { "ethereum": "1", "bsc": "56", "polygon": "137", "arbitrum": "42161", "avalanche": "43114", "base": "8453", "optimism": "10", "fantom": "250", } cid = chain_id_map.get(result["chain"].lower(), "1") r = await client.get(f"{GOPLUS}/token_security/{cid}?contract_addresses={address}") if r.status_code == 200: sec = r.json().get("result", {}).get(address.lower(), {}) if sec: result["contract_verified"] = sec.get("is_open_source") == "1" result["honeypot"] = sec.get("is_honeypot") == "1" result["buy_tax"] = float(sec.get("buy_tax") or 0) result["sell_tax"] = float(sec.get("sell_tax") or 0) result["mintable"] = sec.get("is_mintable") == "1" result["can_blacklist"] = ( sec.get("is_blacklisted") == "1" or sec.get("can_take_back_ownership") == "1" ) result["owner_renounced"] = sec.get("is_owner_renounced") != "0" lp_locks = sec.get("lp_holders", []) locked_pct = sum(float(h.get("percent", 0)) for h in lp_locks if h.get("is_locked") == 1) result["lp_locked"] = locked_pct holders_data = sec.get("holders", []) result["holders"] = len(holders_data) if holders_data else 0 for h in holders_data[:10]: result["top_holders"].append( { "address": h.get("address", ""), "pct": float(h.get("percent") or 0) * 100, "is_contract": h.get("is_contract") == 1, "tag": h.get("tag", ""), } ) if result["honeypot"]: result["threats"].append("HONEYPOT DETECTED") result["risk_score"] += 40 if not result["contract_verified"]: result["threats"].append("Contract not verified") result["risk_score"] += 15 if result["mintable"]: result["threats"].append("Mintable supply") result["risk_score"] += 20 if result["can_blacklist"]: result["threats"].append("Owner can blacklist") result["risk_score"] += 15 if not result["owner_renounced"]: result["threats"].append("Ownership not renounced") result["risk_score"] += 10 if result["buy_tax"] and result["buy_tax"] > 0.1: result["threats"].append(f"High buy tax: {result['buy_tax'] * 100:.1f}%") result["risk_score"] += 10 if result["sell_tax"] and result["sell_tax"] > 0.1: result["threats"].append(f"High sell tax: {result['sell_tax'] * 100:.1f}%") result["risk_score"] += 15 if locked_pct < 0.5 and result["mcap"] > 10000: result["threats"].append(f"Low LP lock: {locked_pct * 100:.0f}%") result["risk_score"] += 15 except Exception as e: result["errors"].append(f"GoPlus: {e}") # ── Honeypot.is (EVM fallback) ── if is_evm(address) and result["honeypot"] is None: try: r = await client.get(f"{HONEYPOT}/IsHoneypot?address={address}") if r.status_code == 200: hp = r.json() result["honeypot"] = hp.get("isHoneypot", False) if result["honeypot"]: result["threats"].append("HONEYPOT (honeypot.is)") result["risk_score"] += 40 except Exception as e: result["errors"].append(f"Honeypot.is: {e}") # ── RMI Backend ── try: r = await client.get(f"{BACKEND_URL}/api/v1/databus/token/{address}", timeout=10) if r.status_code == 200: rmi = r.json() if rmi.get("bundle_detected"): result["bundle_detected"] = True result["threats"].append("Bundle activity detected") result["risk_score"] += 20 if rmi.get("fresh_wallet_pct"): result["fresh_wallet_pct"] = rmi["fresh_wallet_pct"] if rmi["fresh_wallet_pct"] > 50: result["threats"].append(f"High fresh wallet ratio: {rmi['fresh_wallet_pct']:.0f}%") result["risk_score"] += 10 if rmi.get("dev_wallet"): result["dev_wallet"] = rmi["dev_wallet"] except Exception: pass # ── Volume authenticity ── if result["volume_24h"] > 0 and result["mcap"] > 0: ratio = result["volume_24h"] / result["mcap"] result["volume_real_ratio"] = ratio if ratio > 5: result["threats"].append(f"Suspicious volume/mcap ratio: {ratio:.1f}x") result["risk_score"] += 15 # ── Liquidity check ── if result["mcap"] > 0 and result["liquidity"] > 0: liq_ratio = result["liquidity"] / result["mcap"] if liq_ratio < 0.02: result["threats"].append("Very low liquidity vs mcap") result["risk_score"] += 20 result["risk_score"] = min(result["risk_score"], 100) return result def format_scan_report(r: dict) -> str: chain_str = fmt_chain(r["chain"]) risk = r["risk_score"] d = thin_sep() s = sep() # Age calculation age_str = "" if r.get("created_at"): try: created = datetime.fromtimestamp(r["created_at"] / 1000, tz=UTC) age = datetime.now(UTC) - created if age.days > 365: age_str = f" | Age: {age.days // 365}y {age.days % 365 // 30}mo" elif age.days > 0: age_str = f" | Age: {age.days}d" else: age_str = f" | Age: {age.seconds // 3600}h" except Exception: pass lines = [ "🛡️ RMI Token Scan", s, f"{r['name']} ({r['symbol']}) {chain_str}{age_str}", f"{r['address']}", "", "📊 Market Data", d, f"💰 Price: {fmt_number(r['price'])}", f"📈 Market Cap: {fmt_number(r['mcap'])}", f"💧 Liquidity: {fmt_number(r['liquidity'])}", f"📊 24h Volume: {fmt_number(r['volume_24h'])}", f"🔄 1h: {fmt_pct(r['price_change_1h'])} | 24h: {fmt_pct(r['price_change_24h'])} | 7d: {fmt_pct(r['price_change_7d'])}", "", "🔒 Security", d, risk_bar(risk), ] if r["threats"]: lines.append("") for t in r["threats"]: lines.append(f" 🔴 {t}") else: lines.append(" ✅ No threats detected") # Contract details cl = [] if r["honeypot"] is not None: cl.append(threat_indicator("Honeypot", r["honeypot"])) if r["contract_verified"] is not None: cl.append(threat_indicator("Verified", not r["contract_verified"])) if r["mintable"] is not None: cl.append(threat_indicator("Mintable", r["mintable"])) if r["owner_renounced"] is not None: cl.append(threat_indicator("Ownership Renounced", not r["owner_renounced"])) if r["lp_locked"] is not None: cl.append(f"🔐 LP Locked: {r['lp_locked'] * 100:.0f}%") if r["buy_tax"] is not None: cl.append(f"📥 Buy Tax: {r['buy_tax'] * 100:.1f}%") if r["sell_tax"] is not None: cl.append(f"📤 Sell Tax: {r['sell_tax'] * 100:.1f}%") if cl: lines.append("") lines.extend(cl) # Holders if r["top_holders"]: lines.extend(["", "👥 Top Holders", d]) for i, h in enumerate(r["top_holders"][:5], 1): tag = f" [{h['tag']}]" if h.get("tag") else "" cf = " 📄" if h.get("is_contract") else "" lines.append(f" {i}. {short_addr(h['address'])} — {h['pct']:.1f}%{tag}{cf}") # Advanced adv = [] if r["bundle_detected"]: adv.append("🕸️ Bundle activity detected") if r["fresh_wallet_pct"] > 0: adv.append(f"🆕 Fresh wallets: {r['fresh_wallet_pct']:.0f}%") if r["volume_real_ratio"] is not None: adv.append(f"📊 Vol/MCap ratio: {r['volume_real_ratio']:.2f}x") if r["dev_wallet"]: adv.append(f"👨‍💻 Dev: {short_addr(r['dev_wallet'])}") if adv: lines.extend(["", "🔬 Advanced Analysis", d]) lines.extend(adv) # Social links from DexScreener socials = r.get("socials", {}) if socials: soc_parts = [] for stype, surl in socials.items(): if stype == "twitter": soc_parts.append(f'𝕏') elif stype == "telegram": soc_parts.append(f'📱') elif stype == "website": soc_parts.append(f'🌐') if soc_parts: lines.extend(["", f"🔗 Socials: {' | '.join(soc_parts)}"]) # Trade section lines.extend(["", s, "💱 Trade:"]) # Footer lines.append(footer_links()) return "\n".join(lines) # ══════════════════════════════════════════════ # WALLET FORENSICS ENGINE # ══════════════════════════════════════════════ async def analyze_wallet(address: str, chain: str | None = None) -> dict: result = { "address": address, "chain": chain or detect_chain(address), "balance_usd": 0, "tx_count": 0, "first_seen": None, "wallet_age_days": 0, "top_tokens": [], "is_fresh": False, "is_exchange": False, "is_contract": False, "risk_score": 0, "flags": [], "recent_txs": [], "funding_source": None, "connected_wallets": [], "pnl_estimate": None, "errors": [], } async with httpx.AsyncClient(timeout=15) as client: try: r = await client.get(f"{BACKEND_URL}/api/v1/databus/wallet/{address}", timeout=10) if r.status_code == 200: data = r.json() for k in [ "balance_usd", "tx_count", "first_seen", "is_exchange", "is_contract", "risk_score", "flags", "funding_source", "pnl_estimate", ]: if k in data: result[k] = data[k] result["top_tokens"] = data.get("top_tokens", [])[:10] result["recent_txs"] = data.get("recent_txs", [])[:5] result["connected_wallets"] = data.get("connected_wallets", [])[:5] if result["first_seen"]: try: first = datetime.fromisoformat(result["first_seen"].replace("Z", "+00:00")) result["wallet_age_days"] = (datetime.now(UTC) - first).days result["is_fresh"] = result["wallet_age_days"] < 7 except Exception: pass return result except Exception: pass if is_sol(address): try: r = await client.get(f"https://api.solana.fm/v0/accounts/{address}") if r.status_code == 200: data = r.json() result["balance_usd"] = float(data.get("balance", 0)) / 1e9 * 150 result["tx_count"] = data.get("transaction_count", 0) except Exception as e: result["errors"].append(f"Solana.fm: {e}") return result def format_wallet_report(r: dict) -> str: d = thin_sep() s = sep() age_str = f"{r['wallet_age_days']} days" if r["wallet_age_days"] else "Unknown" lines = [ "👛 Wallet Analysis", s, f"{r['address']}", f"{fmt_chain(r['chain'])} | Age: {age_str}", "", "📊 Overview", d, f"💰 Balance: {fmt_number(r['balance_usd'])}", f"📝 Transactions: {r['tx_count']:,}" if r["tx_count"] else "📝 Transactions: N/A", f"🏷️ Type: {'Exchange' if r['is_exchange'] else 'Contract' if r['is_contract'] else 'EOA (Wallet)'}", ] if r["is_fresh"]: lines.append("🆕 FRESH WALLET (<7 days) — ⚠️ Higher risk") if r["flags"]: lines.extend(["", "⚠️ Flags", d]) for flag in r["flags"]: lines.append(f" 🔴 {flag}") if r["pnl_estimate"] is not None: pnl_e = "📈" if r["pnl_estimate"] >= 0 else "📉" lines.extend(["", f"{pnl_e} Estimated P&L: {fmt_number(r['pnl_estimate'])}"]) if r["top_tokens"]: lines.extend(["", "🪙 Top Holdings", d]) for i, t in enumerate(r["top_tokens"][:5], 1): lines.append(f" {i}. {t.get('symbol', '???')} — {fmt_number(t.get('value_usd', 0))}") if r["funding_source"]: lines.extend(["", f"🏦 Funding Source: {r['funding_source']}"]) if r["connected_wallets"]: lines.extend(["", "🔗 Connected Wallets", d]) for w in r["connected_wallets"][:5]: lines.append(f" • {short_addr(w)}") if r["risk_score"] > 0: lines.extend(["", risk_bar(r["risk_score"])]) lines.append(footer_links()) return "\n".join(lines) # ══════════════════════════════════════════════ # KEYBOARD BUILDERS # ══════════════════════════════════════════════ def main_menu_kb() -> InlineKeyboardMarkup: return InlineKeyboardMarkup( [ [ InlineKeyboardButton("🔍 Scan Token", callback_data="menu_scan"), InlineKeyboardButton("👛 Wallet Check", callback_data="menu_wallet"), ], [ InlineKeyboardButton("📊 Technical Analysis", callback_data="menu_ta"), InlineKeyboardButton("🔥 Trending", callback_data="menu_trending"), ], [ InlineKeyboardButton("👀 Watchlist", callback_data="menu_watchlist"), InlineKeyboardButton("🔔 Alerts", callback_data="menu_alerts"), ], [ InlineKeyboardButton("📋 My Account", callback_data="menu_account"), InlineKeyboardButton("⭐ Upgrade", callback_data="menu_pricing"), ], [ InlineKeyboardButton("📚 Scam School", callback_data="menu_scamschool"), InlineKeyboardButton("🤝 Refer & Earn", callback_data="menu_refer"), ], [ InlineKeyboardButton("🌐 rugmunch.io", url=WEBSITE_URL), InlineKeyboardButton("📢 Alerts Channel", url=TELEGRAM_CHANNEL), ], ] ) def scan_result_kb(address: str, chain: str) -> InlineKeyboardMarkup: rows = [ [web_scan_button(address, chain)], [ InlineKeyboardButton("🔬 Deep Scan (250★)", callback_data=f"deep_{address}_{chain}"), InlineKeyboardButton("⭐ Watch", callback_data=f"watch_{address}_{chain}"), ], ] dex_btns = dex_buttons(address, chain) for i in range(0, len(dex_btns), 2): rows.append(dex_btns[i : i + 2]) rows.append([InlineKeyboardButton("◀️ Menu", callback_data="menu_main")]) return InlineKeyboardMarkup(rows) def pricing_kb() -> InlineKeyboardMarkup: rows = [] for key in ["scout", "hunter", "pro"]: t = TIERS[key] rows.append( [ InlineKeyboardButton( f"{t['emoji']} {t['name']} — ${t['price_monthly']}/mo", callback_data=f"sub_{key}", ) ] ) rows.append([InlineKeyboardButton("🌐 Compare Plans on Website", url=f"{WEBSITE_URL}/pricing")]) rows.append([InlineKeyboardButton("📧 Enterprise", url=f"mailto:{SUPPORT_EMAIL}")]) rows.append([InlineKeyboardButton("◀️ Back", callback_data="menu_main")]) return InlineKeyboardMarkup(rows) def topup_kb() -> InlineKeyboardMarkup: rows = [] for key, pack in TOP_UP_PACKS.items(): rows.append( [ InlineKeyboardButton( f"{pack['emoji']} {pack['name']} — ⭐{pack['stars']}", callback_data=f"topup_{key}", ) ] ) rows.append([InlineKeyboardButton("◀️ Back", callback_data="menu_main")]) return InlineKeyboardMarkup(rows) def scamschool_kb() -> InlineKeyboardMarkup: rows = [] for key, topic in SCAM_SCHOOL_TOPICS.items(): rows.append([InlineKeyboardButton(topic["title"], callback_data=f"scam_{key}")]) rows.append([InlineKeyboardButton("◀️ Back", callback_data="menu_main")]) return InlineKeyboardMarkup(rows) def back_kb() -> InlineKeyboardMarkup: return InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Main Menu", callback_data="menu_main")]]) def social_kb() -> InlineKeyboardMarkup: return InlineKeyboardMarkup( [ [ InlineKeyboardButton("🌐 Website", url=WEBSITE_URL), InlineKeyboardButton("𝕏 Twitter", url=TWITTER_URL), ], [ InlineKeyboardButton("📢 Alerts", url=TELEGRAM_CHANNEL), InlineKeyboardButton("💬 Chat", url=TELEGRAM_GROUP), ], ] ) # ══════════════════════════════════════════════ # COMMAND HANDLERS # ══════════════════════════════════════════════ async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user u = db.get_or_create_user(user.id, user.username, user.first_name) # Referral handling if ctx.args and ctx.args[0].startswith("ref_"): ref_code = ctx.args[0][4:] if ref_code != u.get("referral_code") and not u.get("referred_by"): conn = db.get_db() referrer = conn.execute("SELECT user_id FROM users WHERE referral_code = ?", (ref_code,)).fetchone() if referrer: conn.execute( "UPDATE users SET referred_by = ?, bonus_scans = bonus_scans + 5 WHERE user_id = ?", (ref_code, user.id), ) conn.execute( "UPDATE users SET bonus_scans = bonus_scans + 5 WHERE user_id = ?", (referrer["user_id"],), ) conn.commit() conn.close() proof = social_proof_text() text = ( f"🛡️ RugMunch Intelligence\n" f"The Bloomberg Terminal of Shitcoins\n\n" f"{proof}\n\n" f"What I can do for you:\n\n" f"🔍 Token Scanning\n" f" Honeypots, rug pulls, bundles, fake volume\n" f" across 77+ chains in seconds\n\n" f"👛 Wallet Forensics\n" f" Track smart money, dev wallets, funding sources\n\n" f"📊 Market Intelligence\n" f" Technical analysis, trending tokens, alerts\n\n" f"🤖 AI-Powered Risk Scoring\n" f" Advanced detection that goes beyond surface-level\n\n" f"Quick Start:\n" f" 📌 Paste any contract address → auto-scan\n" f" 📌 Type $TOKEN → quick price lookup\n" f" 📌 /scan address → full analysis\n" f" 📌 /help → see all commands\n\n" f"🆓 Free: {TIERS['free']['scans_per_week']} scans + {TIERS['free']['ai_msgs_per_week']} AI msgs/week\n" f"⭐ Premium from $29.99/mo — unlock everything\n\n" f'🌐 rugmunch.io — full web scanner' ) kb = InlineKeyboardMarkup( [ [InlineKeyboardButton("🔍 Scan a Token", callback_data="menu_scan")], [ InlineKeyboardButton("⭐ View Premium Plans", callback_data="menu_pricing"), InlineKeyboardButton("📚 Learn About Scams", callback_data="menu_scamschool"), ], [ InlineKeyboardButton("🌐 rugmunch.io", url=WEBSITE_URL), InlineKeyboardButton("📢 Join Alerts", url=TELEGRAM_CHANNEL), ], ] ) await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb, disable_web_page_preview=True) async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE): d = thin_sep() text = ( f"📖 RugMunch Intelligence — Command Guide\n" f"{sep()}\n\n" f"🔍 TOKEN ANALYSIS\n{d}\n" f"/scan address — Full security scan\n" f"/ta address — Technical analysis & signals\n" f"/compare addr1 addr2 — Side-by-side\n" f"/quick symbol — Quick price check (same as $TOKEN)\n\n" f"👛 WALLET INTELLIGENCE\n{d}\n" f"/wallet address — Forensic wallet analysis\n" f"/watch address [chain] — Add to watchlist\n" f"/unwatch address — Remove from watchlist\n" f"/watchlist — View your watchlist\n" f"/alerts — Price & activity alerts\n\n" f"📊 MARKET DATA\n{d}\n" f"/trending — Hot tokens right now\n" f"/news — Latest crypto news\n\n" f"💳 ACCOUNT & BILLING\n{d}\n" f"/account — Dashboard & usage stats\n" f"/pricing — Subscription plans\n" f"/topup — Buy extra scans (no expiry)\n" f"/refer — Invite friends, earn scans\n\n" f"📚 EDUCATION\n{d}\n" f"/scamschool — Learn about crypto scams\n" f"/rugcheck — Quick rug pull checklist\n\n" f"💡 PRO TIPS\n{d}\n" f"• Paste any 0x... address to auto-scan\n" f"• Type $PEPE for instant price check\n" f"• Use inline mode: @RugMunchBot address in any chat\n" f"• Upgrade for bundle detection, wallet forensics & more\n" f"• Use /refer to earn free scans\n\n" f'🌐 rugmunch.io — Full web scanner with charts' ) kb = InlineKeyboardMarkup( [ [ InlineKeyboardButton("🌐 rugmunch.io", url=WEBSITE_URL), InlineKeyboardButton("💬 Support", url=TELEGRAM_GROUP), ], [InlineKeyboardButton("◀️ Menu", callback_data="menu_main")], ] ) await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb, disable_web_page_preview=True) async def cmd_scan(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user db.get_or_create_user(user.id, user.username, user.first_name) if check_spam(user.id): return if not ctx.args: await update.message.reply_text( "🔍 Token Scanner\n\n" "Usage: /scan address [chain]\n\n" "Examples:\n" " /scan 0x6982508145454Ce325dDbE47a25d4ec3d2311933\n" " /scan EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v solana\n\n" "💡 Or just paste a contract address directly!", parse_mode=ParseMode.HTML, ) return target = ctx.args[0] chain = ctx.args[1] if len(ctx.args) > 1 else None if not is_owner(user.id): allowed, _used, _limit = db.check_rate_limit(user.id, "scan") if not allowed: await update.message.reply_text( paywall_text(user.id, "scan"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb() ) return msg = await update.message.reply_text( f"🔍 Scanning token...\n" f"{short_addr(target)}\n\n" f"⏳ Querying DexScreener, GoPlus, Honeypot.is, RMI...", parse_mode=ParseMode.HTML, ) try: result = await scan_token(target, chain) if result["name"] == "Unknown" and not result["errors"]: await msg.edit_text( f"❌ Token Not Found\n\n" f"No data found for {short_addr(target)}.\n\n" f"• Check the address is correct\n" f"• Ensure the token has a trading pair\n" f"• Try specifying the chain: /scan addr solana", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) return report = format_scan_report(result) db.increment_usage(user.id, scan=True) db.log_scan(user.id, target, result["chain"], "basic", result["risk_score"]) await msg.edit_text( report, parse_mode=ParseMode.HTML, reply_markup=scan_result_kb(target, result["chain"]), disable_web_page_preview=True, ) except Exception as e: logger.error(f"Scan error: {e}") await msg.edit_text( f"❌ Scan Failed\n\nError: {str(e)[:200]}\n\nPlease check the address and try again.", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) async def cmd_wallet(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user if not ctx.args: await update.message.reply_text( "👛 Wallet Forensics\n\n" "Usage: /wallet address\n\n" "Example:\n" " /wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\n\n" "Hunter+ tiers get full P&L, funding source tracing, and connected wallet analysis.", parse_mode=ParseMode.HTML, ) return if not is_owner(user.id): allowed, _, _ = db.check_rate_limit(user.id, "scan") if not allowed: await update.message.reply_text( paywall_text(user.id, "scan"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb() ) return addr = ctx.args[0] chain = ctx.args[1] if len(ctx.args) > 1 else None msg = await update.message.reply_text( "👛 Analyzing wallet...\n⏳ Querying on-chain data...", parse_mode=ParseMode.HTML ) try: result = await analyze_wallet(addr, chain) report = format_wallet_report(result) db.increment_usage(user.id, scan=True) await msg.edit_text(report, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True) except Exception as e: logger.error(f"Wallet error: {e}") await msg.edit_text( f"❌ Wallet Analysis Failed\n\n{str(e)[:200]}", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) async def cmd_ta(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user if not ctx.args: await update.message.reply_text( "📊 Technical Analysis\n\nUsage: /ta address_or_symbol", parse_mode=ParseMode.HTML, ) return if not is_owner(user.id): allowed, _, _ = db.check_rate_limit(user.id, "scan") if not allowed: await update.message.reply_text( paywall_text(user.id, "scan"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb() ) return target = ctx.args[0] msg = await update.message.reply_text("📊 Running Technical Analysis...", parse_mode=ParseMode.HTML) try: async with httpx.AsyncClient(timeout=15) as client: r = await client.get(f"{DEXSCREENER}/tokens/{target}") if r.status_code != 200: await msg.edit_text("❌ Token not found on DexScreener.", parse_mode=ParseMode.HTML) return pairs = r.json().get("pairs", []) if not pairs: await msg.edit_text("❌ No trading pairs found.", parse_mode=ParseMode.HTML) return p = pairs[0] name = p.get("baseToken", {}).get("name", "???") symbol = p.get("baseToken", {}).get("symbol", "???") price = float(p.get("priceUsd", 0)) pc = p.get("priceChange", {}) vol = p.get("volume", {}) txns = p.get("txns", {}) signals = [] h1 = pc.get("h1") or 0 h24 = pc.get("h24") or 0 d7 = pc.get("d7") or 0 if h1 > 5 and h24 > 10: signals.append("🟢 Strong short-term momentum") elif h1 < -5 and h24 < -10: signals.append("🔴 Strong downtrend") if h24 > 0 > h1: signals.append("🟡 Pullback in uptrend") if h24 < 0 < h1: signals.append("🟡 Bounce in downtrend") v24 = float(vol.get("h24") or 0) v6 = float(vol.get("h6") or 0) if v6 > 0 and v24 > 0: vol_trend = v6 / (v24 / 4) if vol_trend > 1.5: signals.append("📈 Volume increasing (acceleration)") elif vol_trend < 0.5: signals.append("📉 Volume declining (loss of interest)") buys = txns.get("h24", {}).get("buys", 0) sells = txns.get("h24", {}).get("sells", 0) total_tx = buys + sells if total_tx > 0: buy_ratio = buys / total_tx if buy_ratio > 0.7: signals.append(f"🟢 Buy pressure dominant ({buy_ratio * 100:.0f}% buys)") elif buy_ratio < 0.3: signals.append(f"🔴 Sell pressure dominant ({(1 - buy_ratio) * 100:.0f}% sells)") else: signals.append(f"⚪ Balanced buy/sell ({buy_ratio * 100:.0f}% buys)") text = ( f"📊 Technical Analysis\n{sep()}\n" f"{name} ({symbol})\n{target}\n\n" f"💰 Price: {fmt_number(price)}\n" f"🔄 1h: {fmt_pct(h1)} | 24h: {fmt_pct(h24)} | 7d: {fmt_pct(d7)}\n\n" f"📈 Volume\n{thin_sep()}\n" f"6h: {fmt_number(v6)} | 24h: {fmt_number(v24)}\n" f"Txns 24h: {total_tx:,} (B:{buys:,} S:{sells:,})\n\n" f"🧭 Signals\n{thin_sep()}\n" ) if signals: text += "\n".join(signals) else: text += "⚪ No strong signals detected" text += "\n\nTA is not financial advice. Always /scan for security." text += footer_links() db.increment_usage(user.id, scan=True) await msg.edit_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True) except Exception as e: logger.error(f"TA error: {e}") await msg.edit_text(f"❌ TA failed: {str(e)[:200]}", parse_mode=ParseMode.HTML, reply_markup=back_kb()) async def cmd_compare(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user if len(ctx.args) < 2: await update.message.reply_text( "⚖️ Compare Tokens\n\nUsage: /compare addr1 addr2", parse_mode=ParseMode.HTML, ) return if not is_owner(user.id): allowed, _, _ = db.check_rate_limit(user.id, "scan") if not allowed: await update.message.reply_text( paywall_text(user.id, "scan"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb() ) return msg = await update.message.reply_text("⚖️ Comparing tokens...", parse_mode=ParseMode.HTML) try: r1, r2 = await asyncio.gather(scan_token(ctx.args[0]), scan_token(ctx.args[1])) def mini(r): return ( f"{r['name']} ({r['symbol']})\n" f"💰 {fmt_number(r['price'])} | MCap: {fmt_number(r['mcap'])}\n" f"💧 Liq: {fmt_number(r['liquidity'])} | Vol: {fmt_number(r['volume_24h'])}\n" f"🔄 24h: {fmt_pct(r['price_change_24h'])}\n" f"⚠️ Risk: {r['risk_score']}% | Threats: {len(r['threats'])}" ) text = ( f"⚖️ Token Comparison\n{sep()}\n\n1️⃣ {mini(r1)}\n\n2️⃣ {mini(r2)}\n\n{thin_sep()}\n🏆 Verdict: " ) if r1["risk_score"] < r2["risk_score"]: text += f"{r1['name']} has lower risk ({r1['risk_score']}% vs {r2['risk_score']}%)" elif r2["risk_score"] < r1["risk_score"]: text += f"{r2['name']} has lower risk ({r2['risk_score']}% vs {r1['risk_score']}%)" else: text += "Both have similar risk scores" text += footer_links() db.increment_usage(user.id, scan=True) await msg.edit_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True) except Exception as e: await msg.edit_text(f"❌ Compare failed: {str(e)[:200]}", parse_mode=ParseMode.HTML, reply_markup=back_kb()) async def cmd_quick(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """Quick price check — same as $TOKEN but as a command.""" if not ctx.args: await update.message.reply_text( "🪙 Usage: /quick SYMBOL\nExample: /quick PEPE", parse_mode=ParseMode.HTML ) return symbol = ctx.args[0].upper().lstrip("$") try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{DEXSCREENER}/search/?q={symbol}") if r.status_code == 200: pairs = r.json().get("pairs", []) if pairs: p = pairs[0] addr = p.get("baseToken", {}).get("address", "") chain = p.get("chainId", "") price = float(p.get("priceUsd", 0)) h24 = p.get("priceChange", {}).get("h24") or 0 vol = float(p.get("volume", {}).get("h24") or 0) mcap = float(p.get("marketCap") or 0) name = p.get("baseToken", {}).get("name", symbol) text = ( f"🪙 {name} ({symbol}) {fmt_chain(chain)}\n" f"{addr}\n\n" f"💰 {fmt_number(price)} | 🔄 24h: {fmt_pct(h24)}\n" f"📈 MCap: {fmt_number(mcap)} | 💧 Vol: {fmt_number(vol)}" f"{footer_links()}" ) kb = InlineKeyboardMarkup( [ [InlineKeyboardButton("🔍 Full Scan", callback_data=f"scan_{addr}_{chain}")], [web_scan_button(addr, chain)], ] ) await update.message.reply_text( text, parse_mode=ParseMode.HTML, reply_markup=kb, disable_web_page_preview=True, ) return except Exception: pass await update.message.reply_text(f"❌ Could not find token: {symbol}", parse_mode=ParseMode.HTML) async def cmd_rugcheck(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """Quick rug pull checklist — educational.""" text = ( f"🚩 Rug Pull Checklist\n{sep()}\n\n" f"Before buying ANY token, check these:\n\n" f"🔴 RED FLAGS (Run!)\n{thin_sep()}\n" f" ❌ Honeypot detected (can't sell)\n" f" ❌ Unrenounced ownership\n" f" ❌ Mintable supply (infinite tokens)\n" f" ❌ Blacklist function exists\n" f" ❌ LP not locked (< 50%)\n" f" ❌ Buy/sell tax > 10%\n\n" f"🟡 YELLOW FLAGS (Caution)\n{thin_sep()}\n" f" ⚠️ Bundle activity detected\n" f" ⚠️ >50% fresh wallets (<7 days)\n" f" ⚠️ Dev wallet holds >20%\n" f" ⚠️ Suspicious volume/mcap ratio\n" f" ⚠️ Very low liquidity vs mcap\n\n" f"🟢 GREEN FLAGS (Safer)\n{thin_sep()}\n" f" ✅ Contract verified\n" f" ✅ Ownership renounced\n" f" ✅ LP locked > 90%\n" f" ✅ No blacklist/mint functions\n" f" ✅ Diverse holder distribution\n" f" ✅ Organic volume patterns\n\n" f"🛡️ Always run /scan before buying!" f"{footer_links()}" ) await update.message.reply_text( text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True ) async def cmd_trending(update: Update, ctx: ContextTypes.DEFAULT_TYPE): msg = await update.message.reply_text("🔥 Fetching trending tokens...", parse_mode=ParseMode.HTML) try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{DEXSCREENER}/search/?q=trending") if r.status_code == 200: pairs = r.json().get("pairs", [])[:8] if not pairs: await msg.edit_text("❌ No trending data available.", parse_mode=ParseMode.HTML) return lines = ["🔥 Trending Tokens", sep(), thin_sep()] for i, p in enumerate(pairs, 1): name = p.get("baseToken", {}).get("name", "???")[:15] symbol = p.get("baseToken", {}).get("symbol", "???") price = float(p.get("priceUsd", 0)) h24 = p.get("priceChange", {}).get("h24") or 0 vol = float(p.get("volume", {}).get("h24") or 0) chain = p.get("chainId", "?") addr = p.get("baseToken", {}).get("address", "") emoji = "🟢" if h24 >= 0 else "🔴" lines.append( f"{i}. {emoji} {name} ({symbol}) {chain}\n {fmt_number(price)} | {fmt_pct(h24)} | Vol: {fmt_number(vol)}\n {short_addr(addr)}" ) lines.append( f'\n{thin_sep()}\nTap address to /scan | View all on rugmunch.io' ) await msg.edit_text( "\n".join(lines), parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True, ) else: await msg.edit_text("❌ Could not fetch trending data.", parse_mode=ParseMode.HTML) except Exception as e: await msg.edit_text(f"❌ Error: {str(e)[:200]}", parse_mode=ParseMode.HTML) async def cmd_news(update: Update, ctx: ContextTypes.DEFAULT_TYPE): try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{BACKEND_URL}/api/v1/news/latest?limit=5") if r.status_code == 200: articles = r.json() if articles: lines = ["📰 Crypto News", sep(), thin_sep()] for a in articles[:5]: title = a.get("title", "Untitled")[:60] source = a.get("source", "News") lines.append(f"• {title}\n {source}") lines.append(f'\n{thin_sep()}\n🌐 More on rugmunch.io') await update.message.reply_text( "\n".join(lines), parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True, ) return except Exception: pass await update.message.reply_text( f"📰 Crypto News\n\n" f"News feed loading...\n\n" f'📢 Follow @RugMunchAlerts for real-time alerts!\n' f'🌐 rugmunch.io', parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True, ) async def cmd_account(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user u = db.get_or_create_user(user.id, user.username, user.first_name) tier = db.get_user_tier(user.id) tc = TIERS.get(tier, TIERS["free"]) scans, ai = db.get_weekly_usage(user.id) stats = db.get_user_stats(user.id) tier_badge = f"{tc['emoji']} {tc['name']}" if u.get("tier_expires") and u["tier_expires"] > 0: exp = datetime.fromtimestamp(u["tier_expires"], tz=UTC).strftime("%b %d, %Y") tier_badge += f" (expires {exp})" # Premium nudge for free users nudge = "" if tier == "free": nudge = ( f"\n{thin_sep()}\n" f"⭐ Upgrade to unlock:\n" f" • Bundle detection & fake volume alerts\n" f" • Wallet forensics & smart money tracking\n" f" • Real-time price alerts & watchlist\n" f" • Up to 1,000 scans/week\n" ) text = ( f"👤 Account Dashboard\n{sep()}\n" f"User: @{user.username or user.first_name}\n" f"Tier: {tier_badge}\n\n" f"📊 This Week\n{thin_sep()}\n" f"🔍 Scans: {scans}/{tc.get('scans_per_week', 25)}\n" f"🤖 AI Messages: {ai}/{tc.get('ai_msgs_per_week', 15)}\n\n" f"🎁 Bonus Balance\n{thin_sep()}\n" f"🔬 Extra Scans: {u.get('bonus_scans', 0)}\n" f"🤖 Extra AI Msgs: {u.get('bonus_ai_msgs', 0)}\n" f"⭐ Stars: {u.get('stars_balance', 0)}\n\n" f"📈 All-Time\n{thin_sep()}\n" f"Total Scans: {u.get('total_scans', 0)}\n" f"Watchlist: {stats.get('watchlist_count', 0)} tokens\n" f"{nudge}" f"{footer_links()}" ) kb = InlineKeyboardMarkup( [ [ InlineKeyboardButton("⭐ Upgrade Plan", callback_data="menu_pricing"), InlineKeyboardButton("🔋 Top Up", callback_data="menu_topup"), ], [InlineKeyboardButton("🌐 rugmunch.io", url=WEBSITE_URL)], [InlineKeyboardButton("◀️ Menu", callback_data="menu_main")], ] ) await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb, disable_web_page_preview=True) async def cmd_pricing(update: Update, ctx: ContextTypes.DEFAULT_TYPE): d = thin_sep() text = f"⭐ Subscription Plans\n{sep()}\n\n" for key, t in TIERS.items(): if key == "free": text += f"{t['emoji']} {t['name']} — $0\n{d}\n" else: text += f"{t['emoji']} {t['name']}${t['price_monthly']}/mo\n{d}\n" for f in t["features"]: text += f" • {f}\n" text += "\n" text += ( f"{sep()}\n" f"💳 Pay with Telegram Stars\n" f"🔄 Weekly reset Monday 00:00 UTC\n\n" f'🌐 Compare plans on rugmunch.io\n' f"📧 Enterprise: {SUPPORT_EMAIL}" ) await update.message.reply_text( text, parse_mode=ParseMode.HTML, reply_markup=pricing_kb(), disable_web_page_preview=True ) async def cmd_topup(update: Update, ctx: ContextTypes.DEFAULT_TYPE): text = ( f"🔋 Top Up — Buy Extra Usage\n{sep()}\n\n" f"Ran out of weekly scans? Buy bonus usage!\n\n" f"✅ No expiry — carries over forever\n" f"✅ Stackable — buy multiple times\n" f"✅ Instant — available immediately\n\n" f"Scan Packs:\n" f" 🔬 10 scans — ⭐75\n" f" 🔬 50 scans — ⭐300\n" f" 🔬 100 scans — ⭐500\n\n" f"AI Message Packs:\n" f" 🤖 10 messages — ⭐100\n" f" 🤖 50 messages — ⭐400\n" f" 🤖 100 messages — ⭐700\n\n" f"💳 Pay with Telegram Stars ⭐" ) await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=topup_kb()) async def cmd_scamschool(update: Update, ctx: ContextTypes.DEFAULT_TYPE): text = ( f"📚 Scam School\n{sep()}\n\n" f"Learn how to spot crypto scams before you get rugged.\n\n" f'Free education from RugMunch Intelligence.\n\n' f"Select a topic below 👇" ) await update.message.reply_text( text, parse_mode=ParseMode.HTML, reply_markup=scamschool_kb(), disable_web_page_preview=True ) async def cmd_refer(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user u = db.get_or_create_user(user.id, user.username, user.first_name) ref = u.get("referral_code", "UNKNOWN") link = f"https://t.me/{BOT_USERNAME}?start=ref_{ref}" conn = db.get_db() count = conn.execute("SELECT COUNT(*) as c FROM users WHERE referred_by = ?", (ref,)).fetchone()["c"] conn.close() milestones = {5: "🥉", 10: "🥈", 25: "🥇", 50: "💎", 100: "👑"} next_ms = next((k for k in sorted(milestones.keys()) if k > count), None) ms_text = ( f"\n🎯 Next milestone: {milestones[next_ms]} {next_ms} referrals" if next_ms else "\n👑 You've hit all milestones!" ) text = ( f"🤝 Refer & Earn\n{sep()}\n\n" f"Invite friends and both get 5 bonus scans!\n\n" f"🔗 Your Link:\n{link}\n\n" f"📊 Your Stats:\n" f" Friends Referred: {count}\n" f" Bonus Earned: {count * 5} scans{ms_text}\n\n" f"How it works:\n" f" 1. Share your referral link\n" f" 2. Friend starts the bot via your link\n" f" 3. Both of you get 5 bonus scans instantly\n" f" 4. No limits — refer as many as you want!" f"{footer_links()}" ) share_text = quote(f"Check out RugMunch Intelligence — the best crypto scam detector on Telegram! 🛡️\n\n{link}") await update.message.reply_text( text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup( [ [InlineKeyboardButton("📤 Share Link", url=f"https://t.me/share/url?url={link}&text={share_text}")], [InlineKeyboardButton("🌐 rugmunch.io", url=WEBSITE_URL)], [InlineKeyboardButton("◀️ Menu", callback_data="menu_main")], ] ), disable_web_page_preview=True, ) async def cmd_watchlist(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user items = db.get_watchlist(user.id) if not items: await update.message.reply_text( "👀 Watchlist Empty\n\n" "Add tokens to track:\n" " /watch address [chain]\n\n" "Scout: 10 slots | Hunter: 50 | Pro: unlimited", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) return tier = db.get_user_tier(user.id) text = f"👀 Watchlist ({TIERS[tier]['emoji']} {TIERS[tier]['name']})\n{sep()}\n\n" for i, w in enumerate(items, 1): alert = "" if w.get("alert_price"): arrow = "↑" if w.get("alert_direction") == "above" else "↓" alert = f" 🔔{arrow}{fmt_number(w['alert_price'])}" text += f"{i}. {w.get('symbol') or '???'} {short_addr(w['token_address'])} ({w['chain']}){alert}\n" text += f"\n{footer_links()}" await update.message.reply_text( text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True ) async def cmd_watch(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user if not ctx.args: await update.message.reply_text("👀 Usage: /watch address [chain]", parse_mode=ParseMode.HTML) return addr = ctx.args[0] chain = ctx.args[1] if len(ctx.args) > 1 else detect_chain(addr) symbol = ctx.args[2] if len(ctx.args) > 2 else None ok = db.add_watchlist(user.id, addr, chain, symbol) if ok: await update.message.reply_text( f"✅ Added {short_addr(addr)} to watchlist.\n/watchlist to view all.", parse_mode=ParseMode.HTML, ) else: tier = db.get_user_tier(user.id) if tier == "free": await update.message.reply_text( "❌ Watchlists require Scout tier or higher.\n\n" "🔍 Scout ($29.99/mo) — 10 watchlist slots\n" "🎯 Hunter ($49.99/mo) — 50 slots + alerts\n" "👑 Pro ($99.99/mo) — unlimited", parse_mode=ParseMode.HTML, reply_markup=pricing_kb(), ) else: await update.message.reply_text( "❌ Could not add. Limit reached or already added.", parse_mode=ParseMode.HTML ) async def cmd_unwatch(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user if not ctx.args: await update.message.reply_text("Usage: /unwatch address", parse_mode=ParseMode.HTML) return addr = ctx.args[0] ok = db.remove_watchlist(user.id, addr) if ok: await update.message.reply_text( f"✅ Removed {short_addr(addr)} from watchlist.", parse_mode=ParseMode.HTML ) else: await update.message.reply_text("❌ Token not found in your watchlist.", parse_mode=ParseMode.HTML) async def cmd_alerts(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user tier = db.get_user_tier(user.id) if tier in ("free",): await update.message.reply_text( f"🔔 Price Alerts\n\n" f"Get notified when tokens hit your target price.\n\n" f"⚠️ Requires Scout tier or higher.\n" f"You're on {TIERS[tier]['emoji']} {TIERS[tier]['name']}.\n\n" f"🎯 Hunter+ gets real-time exchange flow alerts too!", parse_mode=ParseMode.HTML, reply_markup=pricing_kb(), ) return items = db.get_watchlist(user.id) alerts = [w for w in items if w.get("alert_price")] if not alerts: await update.message.reply_text( "🔔 Price Alerts\n\nNo alerts set yet. Add a token with /watch first.", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) return text = f"🔔 Active Alerts\n{sep()}\n\n" for a in alerts: arrow = "↑" if a.get("alert_direction") == "above" else "↓" text += f"• {a.get('symbol', '???')} {arrow} {fmt_number(a['alert_price'])}\n" await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb()) # ══════════════════════════════════════════════ # OWNER COMMANDS # ══════════════════════════════════════════════ async def cmd_admin_stats(update: Update, ctx: ContextTypes.DEFAULT_TYPE): if not is_owner(update.effective_user.id): return conn = db.get_db() total = conn.execute("SELECT COUNT(*) as c FROM users").fetchone()["c"] tiers = conn.execute("SELECT tier, COUNT(*) as c FROM users GROUP BY tier").fetchall() scans_today = ( conn.execute( "SELECT SUM(scans) as c FROM weekly_usage WHERE week_start = ?", (db._current_week_start(),), ).fetchone()["c"] or 0 ) total_scans = conn.execute("SELECT SUM(total_scans) as c FROM users").fetchone()["c"] or 0 banned = conn.execute("SELECT COUNT(*) as c FROM users WHERE is_banned = 1").fetchone()["c"] conn.close() tier_str = "\n".join(f" {t['tier']}: {t['c']}" for t in tiers) await update.message.reply_text( f"📊 Bot Stats\n{thin_sep()}\n" f"👥 Total Users: {total}\n🚫 Banned: {banned}\n\n" f"📈 Tiers:\n{tier_str}\n\n" f"🔍 This Week: {scans_today}\n🔍 All-Time: {total_scans}", parse_mode=ParseMode.HTML, ) async def cmd_admin_set_tier(update: Update, ctx: ContextTypes.DEFAULT_TYPE): if not is_owner(update.effective_user.id): return if len(ctx.args) < 2: await update.message.reply_text("Usage: /admin_set_tier [days]") return uid, tier = int(ctx.args[0]), ctx.args[1].lower() days = int(ctx.args[2]) if len(ctx.args) > 2 else 30 if tier not in TIERS: await update.message.reply_text(f"Invalid tier. Options: {', '.join(TIERS.keys())}") return db.set_user_tier(uid, tier, days) await update.message.reply_text(f"✅ Set user {uid} to {tier} ({days} days)") async def cmd_admin_broadcast(update: Update, ctx: ContextTypes.DEFAULT_TYPE): if not is_owner(update.effective_user.id): return if not ctx.args: await update.message.reply_text("Usage: /admin_broadcast ") return msg_text = " ".join(ctx.args).replace("\\n", "\n") conn = db.get_db() users = conn.execute("SELECT user_id FROM users WHERE is_banned = 0").fetchall() conn.close() sent = failed = 0 for u in users: try: await ctx.bot.send_message(u["user_id"], msg_text, parse_mode=ParseMode.HTML) sent += 1 await asyncio.sleep(0.05) except Exception: failed += 1 await update.message.reply_text(f"📢 Broadcast complete.\n✅ Sent: {sent}\n❌ Failed: {failed}") async def cmd_admin_ban(update: Update, ctx: ContextTypes.DEFAULT_TYPE): if not is_owner(update.effective_user.id): return if not ctx.args: await update.message.reply_text("Usage: /admin_ban [0|1]") return uid, state = int(ctx.args[0]), int(ctx.args[1]) if len(ctx.args) > 1 else 1 conn = db.get_db() conn.execute("UPDATE users SET is_banned = ? WHERE user_id = ?", (state, uid)) conn.commit() conn.close() await update.message.reply_text(f"{'🚫' if state else '✅'} User {uid} {'banned' if state else 'unbanned'}.") # ══════════════════════════════════════════════ # MESSAGE HANDLER (Auto-detect + AI Chat) # ══════════════════════════════════════════════ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): user = update.effective_user if not user or not update.message or not update.message.text: return text = update.message.text.strip() u = db.get_or_create_user(user.id, user.username, user.first_name) if u.get("is_banned"): return if check_spam(user.id): return # Auto-detect contract address if is_evm(text) or is_sol(text): ctx.args = [text] await cmd_scan(update, ctx) return # Token symbol detection ($TOKEN) token_match = TOKEN_RE.search(text) if token_match: symbol = token_match.group(1).upper() await update.message.chat.send_action("typing") try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{DEXSCREENER}/search/?q={symbol}") if r.status_code == 200: pairs = r.json().get("pairs", []) if pairs: p = pairs[0] addr = p.get("baseToken", {}).get("address", "") chain = p.get("chainId", "") price = float(p.get("priceUsd", 0)) h24 = p.get("priceChange", {}).get("h24") or 0 vol = float(p.get("volume", {}).get("h24") or 0) mcap = float(p.get("marketCap") or 0) name = p.get("baseToken", {}).get("name", symbol) quick_text = ( f"🪙 {name} ({symbol}) {fmt_chain(chain)}\n" f"{addr}\n\n" f"💰 {fmt_number(price)} | 🔄 24h: {fmt_pct(h24)}\n" f"📈 MCap: {fmt_number(mcap)} | 💧 Vol: {fmt_number(vol)}" f"{footer_links()}" ) if addr: await update.message.reply_text( quick_text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup( [ [InlineKeyboardButton("🔍 Full Scan", callback_data=f"scan_{addr}_{chain}")], [web_scan_button(addr, chain)], ] ), disable_web_page_preview=True, ) return except Exception: pass # Natural language address extraction evm_match = EVM_RE.search(text) sol_match = SOL_RE.search(text) if evm_match: ctx.args = [evm_match.group()] await cmd_scan(update, ctx) return if sol_match: ctx.args = [sol_match.group()] await cmd_scan(update, ctx) return # AI Chat if not is_owner(user.id): allowed, _used, _limit = db.check_rate_limit(user.id, "ai_msg") if not allowed: await update.message.reply_text( paywall_text(user.id, "ai_msg"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb(), ) return await update.message.chat.send_action("typing") # Try RMI RAG backend try: async with httpx.AsyncClient(timeout=30) as client: r = await client.post( f"{BACKEND_URL}/api/v1/rag/query", json={"query": text, "user_id": user.id, "context": "telegram_bot"}, ) if r.status_code == 200: data = r.json() answer = data.get("answer", "") if answer: db.increment_usage(user.id, ai_msg=True) await update.message.reply_text(answer, parse_mode=ParseMode.HTML, reply_markup=back_kb()) return except Exception: pass # Smart fallback responses text_lower = text.lower() if any(w in text_lower for w in ["honeypot", "rug", "scam", "safe", "check"]): await update.message.reply_text( "🔍 Want me to check a token for scams?\n\n" "Just paste the contract address or use:\n" "/scan address", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif any(w in text_lower for w in ["price", "how much", "worth", "pump", "dump"]): await update.message.reply_text( "📊 Looking for a token price?\n\nSend me the symbol (e.g. $PEPE) or contract address!", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif any(w in text_lower for w in ["help", "how", "what can", "commands"]): await cmd_help(update, ctx) else: db.increment_usage(user.id, ai_msg=True) await update.message.reply_text( f"🤖 I'm here to help with crypto safety!\n\n" f"Try:\n" f"• Paste a contract address → auto-scan\n" f"• $TOKEN → quick price\n" f"• /scan address → full analysis\n" f"• /wallet address → wallet check\n" f"• /scamschool → learn about scams\n" f"• /trending → hot tokens right now" f"{footer_links()}", parse_mode=ParseMode.HTML, reply_markup=main_menu_kb(), disable_web_page_preview=True, ) # ══════════════════════════════════════════════ # INLINE QUERY (Scan from any chat) # ══════════════════════════════════════════════ async def handle_inline(update: Update, ctx: ContextTypes.DEFAULT_TYPE): query = update.inline_query.query.strip() if not query: return results = [] if is_evm(query) or is_sol(query): results.append( InlineQueryResultArticle( id="scan_" + query[:10], title=f"🔍 Scan {short_addr(query)}", description="Run a full security scan on this address", input_message_content=InputTextMessageContent(f"/scan {query}"), ) ) if len(query) >= 2 and len(query) <= 10 and query.isalpha(): results.append( InlineQueryResultArticle( id="sym_" + query, title=f"🪙 Lookup ${query.upper()}", description="Find token price and info", input_message_content=InputTextMessageContent(f"${query.upper()}"), ) ) if results: await update.inline_query.answer(results, cache_time=5, is_personal=True) # ══════════════════════════════════════════════ # CALLBACK HANDLER # ══════════════════════════════════════════════ async def handle_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() data = query.data user = query.from_user db.get_or_create_user(user.id, user.username, user.first_name) if data == "menu_main": await query.edit_message_text( "🛡️ RugMunch Intelligence\n\nWhat would you like to do?", parse_mode=ParseMode.HTML, reply_markup=main_menu_kb(), ) elif data == "menu_scan": await query.edit_message_text( "🔍 Scan a Token\n\nSend me a contract address:\n0x6982...1933\n\nOr use: /scan address", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif data == "menu_wallet": await query.edit_message_text( "👛 Wallet Check\n\nSend a wallet address or use:\n/wallet address", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif data == "menu_ta": await query.edit_message_text( "📊 Technical Analysis\n\nUsage: /ta address\n\nAnalyzes momentum, volume, buy/sell pressure.", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif data == "menu_alerts": await query.edit_message_text( "🔔 Price Alerts\n\nUse /alerts to manage.\n\nScout+ tiers only.", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif data == "menu_account": tier = db.get_user_tier(user.id) tc = TIERS.get(tier, TIERS["free"]) scans, ai = db.get_weekly_usage(user.id) u = db.get_user_stats(user.id) text = ( f"👤 Account\n{thin_sep()}\n" f"Tier: {tc['emoji']} {tc['name']}\n" f"Scans: {scans}/{tc.get('scans_per_week', 25)}\n" f"AI: {ai}/{tc.get('ai_msgs_per_week', 15)}\n" f"Bonus Scans: {u.get('bonus_scans', 0)}\n" f"Bonus AI: {u.get('bonus_ai_msgs', 0)}\n" f"Stars: {u.get('stars_balance', 0)}⭐\n" f"Watchlist: {u.get('watchlist_count', 0)}" ) await query.edit_message_text( text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup( [ [ InlineKeyboardButton("⭐ Upgrade", callback_data="menu_pricing"), InlineKeyboardButton("🔋 Top Up", callback_data="menu_topup"), ], [InlineKeyboardButton("◀️ Menu", callback_data="menu_main")], ] ), ) elif data == "menu_pricing": await query.edit_message_text( "⭐ Upgrade Plan\n\nSelect a tier:", parse_mode=ParseMode.HTML, reply_markup=pricing_kb(), ) elif data == "menu_topup": await query.edit_message_text( "🔋 Top Up\n\nBuy extra usage (no expiry):", parse_mode=ParseMode.HTML, reply_markup=topup_kb(), ) elif data == "menu_scamschool": await query.edit_message_text( "📚 Scam School\n\nSelect a topic:", parse_mode=ParseMode.HTML, reply_markup=scamschool_kb(), ) elif data == "menu_trending": await query.edit_message_text("🔥 Fetching trending...", parse_mode=ParseMode.HTML) try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{DEXSCREENER}/search/?q=trending") if r.status_code == 200: pairs = r.json().get("pairs", [])[:6] lines = ["🔥 Trending", thin_sep()] for _i, p in enumerate(pairs, 1): name = p.get("baseToken", {}).get("name", "???")[:12] sym = p.get("baseToken", {}).get("symbol", "???") h24 = p.get("priceChange", {}).get("h24") or 0 addr = p.get("baseToken", {}).get("address", "") emoji = "🟢" if h24 >= 0 else "🔴" lines.append( f"{emoji} {name} ({sym}) {fmt_pct(h24)}\n {short_addr(addr)}" ) await query.edit_message_text("\n".join(lines), parse_mode=ParseMode.HTML, reply_markup=back_kb()) else: await query.edit_message_text( "❌ Could not fetch trending data.", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) except Exception: await query.edit_message_text("❌ Trending unavailable.", parse_mode=ParseMode.HTML, reply_markup=back_kb()) elif data == "menu_watchlist": items = db.get_watchlist(user.id) if not items: await query.edit_message_text( "👀 Watchlist is empty.\nUse /watch address", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) else: text = "👀 Watchlist\n\n" + "\n".join( f"• {w.get('symbol', '???')} {short_addr(w['token_address'])}" for w in items ) await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb()) elif data == "menu_refer": u = db.get_or_create_user(user.id) link = f"https://t.me/{BOT_USERNAME}?start=ref_{u.get('referral_code', '')}" await query.edit_message_text( f"🤝 Refer Friends\n\nShare your link:\n{link}\n\nBoth get 5 bonus scans!", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif data.startswith("scam_"): topic_key = data[5:] topic = SCAM_SCHOOL_TOPICS.get(topic_key) if topic: await query.edit_message_text(topic["content"], parse_mode=ParseMode.MARKDOWN, reply_markup=scamschool_kb()) elif data.startswith("scan_"): parts = data.split("_", 2) if len(parts) >= 3: await query.edit_message_text( f"🔍 Use /scan {parts[1]} to run a full scan.", parse_mode=ParseMode.HTML, reply_markup=back_kb(), ) elif data.startswith("watch_"): parts = data.split("_", 2) if len(parts) >= 3: addr, chain = parts[1], parts[2] ok = db.add_watchlist(user.id, addr, chain) await query.answer("Added to watchlist!" if ok else "Could not add (limit reached?)", show_alert=True) elif data.startswith("sub_"): tier_key = data[4:] tier = TIERS.get(tier_key) if tier: await query.edit_message_text( f"💳 Subscribe to {tier['name']}\n\n" f"${tier['price_monthly']}/month\n" f"or ⭐{tier.get('price_stars', 0)} Stars\n\n" f"Payment integration coming next update.\n" f"Contact {SUPPORT_EMAIL} for now.", parse_mode=ParseMode.HTML, reply_markup=pricing_kb(), ) elif data.startswith("topup_"): pack_key = data[6:] pack = TOP_UP_PACKS.get(pack_key) if pack: u = db.get_or_create_user(user.id) if u.get("stars_balance", 0) >= pack["stars"]: ok = db.apply_top_up(user.id, pack["type"], pack["amount"], pack["stars"]) if ok: await query.edit_message_text( f"✅ Top Up Applied!\n\n" f"+{pack['amount']} bonus {pack['type'].replace('_', ' ')}\n" f"Cost: ⭐{pack['stars']}\n\n" f"These never expire!", parse_mode=ParseMode.HTML, reply_markup=topup_kb(), ) else: await query.answer("Transaction failed.", show_alert=True) else: await query.edit_message_text( f"❌ Not Enough Stars\n\n" f"You need ⭐{pack['stars']} but have ⭐{u.get('stars_balance', 0)}.\n\n" f"Buy Stars via Telegram or earn through referrals!", parse_mode=ParseMode.HTML, reply_markup=topup_kb(), ) # ══════════════════════════════════════════════ # PAYMENT HANDLER # ══════════════════════════════════════════════ async def precheckout(update: Update, ctx: ContextTypes.DEFAULT_TYPE): await update.pre_checkout_query.answer(ok=True) async def successful_payment(update: Update, ctx: ContextTypes.DEFAULT_TYPE): payment = update.message.successful_payment user = update.effective_user db.add_stars(user.id, payment.total_amount, "payment", payment.telegram_payment_charge_id) await update.message.reply_text( f"✅ Payment Received!\n\n" f"⭐ {payment.total_amount} Stars added to your balance.\n" f"Use /topup to spend them.", parse_mode=ParseMode.HTML, ) # ══════════════════════════════════════════════ # GLOBAL ERROR HANDLER # ══════════════════════════════════════════════ async def error_handler(update: object, ctx: ContextTypes.DEFAULT_TYPE): logger.error(f"Exception: {ctx.error}", exc_info=ctx.error) if isinstance(update, Update) and update.effective_message: with contextlib.suppress(Exception): await update.effective_message.reply_text( "⚠️ Something went wrong. Please try again.\nIf the issue persists, contact support.", parse_mode=ParseMode.HTML, ) # ══════════════════════════════════════════════ # BOT PROFILE SETUP (BotFather API) # ══════════════════════════════════════════════ async def setup_bot_profile(app: Application): """Set bot description, about text, and command list via BotFather API.""" bot = app.bot try: await bot.set_my_description(description=BOT_DESCRIPTION) logger.info("Bot description set") except Exception as e: logger.warning(f"Could not set description: {e}") try: await bot.set_my_short_description(short_description=BOT_SHORT_DESCRIPTION) logger.info("Bot short description set") except Exception as e: logger.warning(f"Could not set short description: {e}") try: commands = [ BotCommand("start", "Welcome & quick start guide"), BotCommand("help", "Full command reference"), BotCommand("scan", "Scan token for scams & risks"), BotCommand("wallet", "Wallet forensics & analysis"), BotCommand("ta", "Technical analysis & signals"), BotCommand("compare", "Compare two tokens side-by-side"), BotCommand("quick", "Quick token price check"), BotCommand("trending", "Hot tokens right now"), BotCommand("rugcheck", "Rug pull red flag checklist"), BotCommand("watch", "Add token to watchlist"), BotCommand("watchlist", "View your watchlist"), BotCommand("alerts", "Price & activity alerts"), BotCommand("account", "Your dashboard & usage"), BotCommand("pricing", "Subscription plans"), BotCommand("topup", "Buy extra scans (no expiry)"), BotCommand("refer", "Invite friends, earn scans"), BotCommand("scamschool", "Learn about crypto scams"), BotCommand("news", "Latest crypto news"), ] await bot.set_my_commands(commands) logger.info("Bot commands registered") except Exception as e: logger.warning(f"Could not set commands: {e}") # ══════════════════════════════════════════════ # SCHEDULED JOBS # ══════════════════════════════════════════════ async def daily_scam_tip(ctx: ContextTypes.DEFAULT_TYPE): if not CHANNELS.get("main"): return topic_key = random.choice(list(SCAM_SCHOOL_TOPICS.keys())) topic = SCAM_SCHOOL_TOPICS[topic_key] text = ( f"🛡️ Daily Scam Tip\n{sep()}\n\n" f"{topic['content']}\n\n" f"📚 Learn more: /scamschool\n" f"🔍 Scan tokens: @RugMunchBot\n" f'🌐 rugmunch.io' ) try: await ctx.bot.send_message(CHANNELS["main"], text, parse_mode=ParseMode.HTML, disable_web_page_preview=True) except Exception as e: logger.error(f"Daily tip failed: {e}") # ══════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════ def main(): if not BOT_TOKEN: logger.error("RUGMUNCH_BOT_TOKEN not set!") sys.exit(1) app = Application.builder().token(BOT_TOKEN).build() # ── Commands ── app.add_handler(CommandHandler("start", cmd_start)) app.add_handler(CommandHandler("help", cmd_help)) app.add_handler(CommandHandler("scan", cmd_scan)) app.add_handler(CommandHandler("wallet", cmd_wallet)) app.add_handler(CommandHandler("ta", cmd_ta)) app.add_handler(CommandHandler("compare", cmd_compare)) app.add_handler(CommandHandler("quick", cmd_quick)) app.add_handler(CommandHandler("rugcheck", cmd_rugcheck)) app.add_handler(CommandHandler("trending", cmd_trending)) app.add_handler(CommandHandler("news", cmd_news)) app.add_handler(CommandHandler("account", cmd_account)) app.add_handler(CommandHandler("pricing", cmd_pricing)) app.add_handler(CommandHandler("topup", cmd_topup)) app.add_handler(CommandHandler("scamschool", cmd_scamschool)) app.add_handler(CommandHandler("refer", cmd_refer)) app.add_handler(CommandHandler("watchlist", cmd_watchlist)) app.add_handler(CommandHandler("watch", cmd_watch)) app.add_handler(CommandHandler("unwatch", cmd_unwatch)) app.add_handler(CommandHandler("alerts", cmd_alerts)) # ── Owner Commands ── app.add_handler(CommandHandler("admin_stats", cmd_admin_stats)) app.add_handler(CommandHandler("admin_set_tier", cmd_admin_set_tier)) app.add_handler(CommandHandler("admin_broadcast", cmd_admin_broadcast)) app.add_handler(CommandHandler("admin_ban", cmd_admin_ban)) # ── Inline Query ── app.add_handler(InlineQueryHandler(handle_inline)) # ── Callbacks & Messages ── app.add_handler(CallbackQueryHandler(handle_callback)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) # ── Payments ── app.add_handler(PreCheckoutQueryHandler(precheckout)) app.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment)) # ── Error Handler ── app.add_error_handler(error_handler) # ── Scheduled Jobs ── job_queue = app.job_queue job_queue.run_daily(daily_scam_tip, dtime(hour=14, minute=0, tzinfo=UTC)) # ── Startup: set bot profile ── app.post_init = setup_bot_profile logger.info("🛡️ CryptoRugMunch Bot v6 starting...") app.run_polling(drop_pending_updates=True) if __name__ == "__main__": main()