| |
| """ |
| 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, |
| ) |
|
|
| |
| 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.basicConfig( |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", |
| level=logging.INFO, |
| ) |
| logger = logging.getLogger("rmi_bot") |
|
|
| |
| |
| |
| _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 |
|
|
|
|
| |
| |
| |
| 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'π <a href="{WEBSITE_URL}">rugmunch.io</a> | ' |
| f'<a href="{TELEGRAM_CHANNEL}">π’ Alerts</a> | ' |
| f'<a href="{TWITTER_URL}">π</a>' |
| ) |
|
|
|
|
| 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"β‘ <b>Weekly {action.title()} Limit Reached</b>\n\n" |
| f"π <b>{tc['name']} Tier:</b> {used}/{limit} used this week\n" |
| f"π <b>Bonus Balance:</b> {bonus} extra\n\n" |
| f"<b>Unlock more power:</b>\n\n" |
| f"π <b>Scout ($29.99/mo)</b> β 150 scans/wk + bundle detection\n" |
| f"π― <b>Hunter ($49.99/mo)</b> β 400 scans/wk + wallet forensics\n" |
| f"π <b>Pro ($99.99/mo)</b> β 1,000 scans/wk + smart money alerts\n\n" |
| f"Or buy one-time top-ups β <b>never expire!</b>\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")], |
| ] |
| ) |
|
|
|
|
| |
| |
| |
| 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: |
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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_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 = [ |
| "π‘οΈ <b>RMI Token Scan</b>", |
| s, |
| f"<b>{r['name']}</b> ({r['symbol']}) {chain_str}{age_str}", |
| f"<code>{r['address']}</code>", |
| "", |
| "π <b>Market Data</b>", |
| d, |
| f"π° Price: <b>{fmt_number(r['price'])}</b>", |
| f"π Market Cap: <b>{fmt_number(r['mcap'])}</b>", |
| f"π§ Liquidity: <b>{fmt_number(r['liquidity'])}</b>", |
| f"π 24h Volume: <b>{fmt_number(r['volume_24h'])}</b>", |
| f"π 1h: {fmt_pct(r['price_change_1h'])} | 24h: {fmt_pct(r['price_change_24h'])} | 7d: {fmt_pct(r['price_change_7d'])}", |
| "", |
| "π <b>Security</b>", |
| d, |
| risk_bar(risk), |
| ] |
|
|
| if r["threats"]: |
| lines.append("") |
| for t in r["threats"]: |
| lines.append(f" π΄ {t}") |
| else: |
| lines.append(" β
No threats detected") |
|
|
| |
| 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) |
|
|
| |
| if r["top_holders"]: |
| lines.extend(["", "π₯ <b>Top Holders</b>", 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}. <code>{short_addr(h['address'])}</code> β {h['pct']:.1f}%{tag}{cf}") |
|
|
| |
| 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: <code>{short_addr(r['dev_wallet'])}</code>") |
| if adv: |
| lines.extend(["", "π¬ <b>Advanced Analysis</b>", d]) |
| lines.extend(adv) |
|
|
| |
| socials = r.get("socials", {}) |
| if socials: |
| soc_parts = [] |
| for stype, surl in socials.items(): |
| if stype == "twitter": |
| soc_parts.append(f'<a href="{surl}">π</a>') |
| elif stype == "telegram": |
| soc_parts.append(f'<a href="{surl}">π±</a>') |
| elif stype == "website": |
| soc_parts.append(f'<a href="{surl}">π</a>') |
| if soc_parts: |
| lines.extend(["", f"π Socials: {' | '.join(soc_parts)}"]) |
|
|
| |
| lines.extend(["", s, "π± <b>Trade:</b>"]) |
|
|
| |
| lines.append(footer_links()) |
|
|
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
| 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 = [ |
| "π <b>Wallet Analysis</b>", |
| s, |
| f"<code>{r['address']}</code>", |
| f"{fmt_chain(r['chain'])} | Age: {age_str}", |
| "", |
| "π <b>Overview</b>", |
| d, |
| f"π° Balance: <b>{fmt_number(r['balance_usd'])}</b>", |
| f"π Transactions: <b>{r['tx_count']:,}</b>" 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("π <b>FRESH WALLET</b> (<7 days) β β οΈ Higher risk") |
| if r["flags"]: |
| lines.extend(["", "β οΈ <b>Flags</b>", 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} <b>Estimated P&L:</b> {fmt_number(r['pnl_estimate'])}"]) |
| if r["top_tokens"]: |
| lines.extend(["", "πͺ <b>Top Holdings</b>", d]) |
| for i, t in enumerate(r["top_tokens"][:5], 1): |
| lines.append(f" {i}. <b>{t.get('symbol', '???')}</b> β {fmt_number(t.get('value_usd', 0))}") |
| if r["funding_source"]: |
| lines.extend(["", f"π¦ <b>Funding Source:</b> {r['funding_source']}"]) |
| if r["connected_wallets"]: |
| lines.extend(["", "π <b>Connected Wallets</b>", d]) |
| for w in r["connected_wallets"][:5]: |
| lines.append(f" β’ <code>{short_addr(w)}</code>") |
| if r["risk_score"] > 0: |
| lines.extend(["", risk_bar(r["risk_score"])]) |
| lines.append(footer_links()) |
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
| 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), |
| ], |
| ] |
| ) |
|
|
|
|
| |
| |
| |
| 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) |
|
|
| |
| 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"π‘οΈ <b>RugMunch Intelligence</b>\n" |
| f"<i>The Bloomberg Terminal of Shitcoins</i>\n\n" |
| f"{proof}\n\n" |
| f"<b>What I can do for you:</b>\n\n" |
| f"π <b>Token Scanning</b>\n" |
| f" Honeypots, rug pulls, bundles, fake volume\n" |
| f" across 77+ chains in seconds\n\n" |
| f"π <b>Wallet Forensics</b>\n" |
| f" Track smart money, dev wallets, funding sources\n\n" |
| f"π <b>Market Intelligence</b>\n" |
| f" Technical analysis, trending tokens, alerts\n\n" |
| f"π€ <b>AI-Powered Risk Scoring</b>\n" |
| f" Advanced detection that goes beyond surface-level\n\n" |
| f"<b>Quick Start:</b>\n" |
| f" π Paste any contract address β auto-scan\n" |
| f" π Type <code>$TOKEN</code> β quick price lookup\n" |
| f" π /scan <code>address</code> β full analysis\n" |
| f" π /help β see all commands\n\n" |
| f"π <b>Free:</b> {TIERS['free']['scans_per_week']} scans + {TIERS['free']['ai_msgs_per_week']} AI msgs/week\n" |
| f"β <b>Premium from $29.99/mo</b> β unlock everything\n\n" |
| f'π <a href="{WEBSITE_URL}">rugmunch.io</a> β 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"π <b>RugMunch Intelligence β Command Guide</b>\n" |
| f"{sep()}\n\n" |
| f"π <b>TOKEN ANALYSIS</b>\n{d}\n" |
| f"/scan <code>address</code> β Full security scan\n" |
| f"/ta <code>address</code> β Technical analysis & signals\n" |
| f"/compare <code>addr1</code> <code>addr2</code> β Side-by-side\n" |
| f"/quick <code>symbol</code> β Quick price check (same as $TOKEN)\n\n" |
| f"π <b>WALLET INTELLIGENCE</b>\n{d}\n" |
| f"/wallet <code>address</code> β Forensic wallet analysis\n" |
| f"/watch <code>address</code> [chain] β Add to watchlist\n" |
| f"/unwatch <code>address</code> β Remove from watchlist\n" |
| f"/watchlist β View your watchlist\n" |
| f"/alerts β Price & activity alerts\n\n" |
| f"π <b>MARKET DATA</b>\n{d}\n" |
| f"/trending β Hot tokens right now\n" |
| f"/news β Latest crypto news\n\n" |
| f"π³ <b>ACCOUNT & BILLING</b>\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"π <b>EDUCATION</b>\n{d}\n" |
| f"/scamschool β Learn about crypto scams\n" |
| f"/rugcheck β Quick rug pull checklist\n\n" |
| f"π‘ <b>PRO TIPS</b>\n{d}\n" |
| f"β’ Paste any <code>0x...</code> address to auto-scan\n" |
| f"β’ Type <code>$PEPE</code> for instant price check\n" |
| f"β’ Use inline mode: <code>@RugMunchBot address</code> in any chat\n" |
| f"β’ Upgrade for bundle detection, wallet forensics & more\n" |
| f"β’ Use /refer to earn free scans\n\n" |
| f'π <a href="{WEBSITE_URL}">rugmunch.io</a> β 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( |
| "π <b>Token Scanner</b>\n\n" |
| "Usage: /scan <code>address</code> [chain]\n\n" |
| "<b>Examples:</b>\n" |
| " /scan <code>0x6982508145454Ce325dDbE47a25d4ec3d2311933</code>\n" |
| " /scan <code>EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v</code> solana\n\n" |
| "π‘ <i>Or just paste a contract address directly!</i>", |
| 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"π <b>Scanning token...</b>\n" |
| f"<code>{short_addr(target)}</code>\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"β <b>Token Not Found</b>\n\n" |
| f"No data found for <code>{short_addr(target)}</code>.\n\n" |
| f"β’ Check the address is correct\n" |
| f"β’ Ensure the token has a trading pair\n" |
| f"β’ Try specifying the chain: /scan <code>addr</code> 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"β <b>Scan Failed</b>\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( |
| "π <b>Wallet Forensics</b>\n\n" |
| "Usage: /wallet <code>address</code>\n\n" |
| "<b>Example:</b>\n" |
| " /wallet <code>0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045</code>\n\n" |
| "<i>Hunter+ tiers get full P&L, funding source tracing, and connected wallet analysis.</i>", |
| 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( |
| "π <b>Analyzing wallet...</b>\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"β <b>Wallet Analysis Failed</b>\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( |
| "π <b>Technical Analysis</b>\n\nUsage: /ta <code>address_or_symbol</code>", |
| 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("π <b>Running Technical Analysis...</b>", 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"π <b>Technical Analysis</b>\n{sep()}\n" |
| f"<b>{name}</b> ({symbol})\n<code>{target}</code>\n\n" |
| f"π° Price: <b>{fmt_number(price)}</b>\n" |
| f"π 1h: {fmt_pct(h1)} | 24h: {fmt_pct(h24)} | 7d: {fmt_pct(d7)}\n\n" |
| f"π <b>Volume</b>\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"π§ <b>Signals</b>\n{thin_sep()}\n" |
| ) |
| if signals: |
| text += "\n".join(signals) |
| else: |
| text += "βͺ No strong signals detected" |
| text += "\n\n<i>TA is not financial advice. Always /scan for security.</i>" |
| 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( |
| "βοΈ <b>Compare Tokens</b>\n\nUsage: /compare <code>addr1</code> <code>addr2</code>", |
| 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("βοΈ <b>Comparing tokens...</b>", 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"<b>{r['name']}</b> ({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"βοΈ <b>Token Comparison</b>\n{sep()}\n\n1οΈβ£ {mini(r1)}\n\n2οΈβ£ {mini(r2)}\n\n{thin_sep()}\nπ <b>Verdict:</b> " |
| ) |
| 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 <code>SYMBOL</code>\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"πͺ <b>{name}</b> ({symbol}) {fmt_chain(chain)}\n" |
| f"<code>{addr}</code>\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: <b>{symbol}</b>", parse_mode=ParseMode.HTML) |
|
|
|
|
| async def cmd_rugcheck(update: Update, ctx: ContextTypes.DEFAULT_TYPE): |
| """Quick rug pull checklist β educational.""" |
| text = ( |
| f"π© <b>Rug Pull Checklist</b>\n{sep()}\n\n" |
| f"Before buying ANY token, check these:\n\n" |
| f"<b>π΄ RED FLAGS (Run!)</b>\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"<b>π‘ YELLOW FLAGS (Caution)</b>\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"<b>π’ GREEN FLAGS (Safer)</b>\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"<b>π‘οΈ Always run /scan before buying!</b>" |
| 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("π₯ <b>Fetching trending tokens...</b>", 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 = ["π₯ <b>Trending Tokens</b>", 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} <b>{name}</b> ({symbol}) {chain}\n {fmt_number(price)} | {fmt_pct(h24)} | Vol: {fmt_number(vol)}\n <code>{short_addr(addr)}</code>" |
| ) |
| lines.append( |
| f'\n{thin_sep()}\nTap address to /scan | <a href="{WEB_APP_URL}">View all on rugmunch.io</a>' |
| ) |
| 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 = ["π° <b>Crypto News</b>", sep(), thin_sep()] |
| for a in articles[:5]: |
| title = a.get("title", "Untitled")[:60] |
| source = a.get("source", "News") |
| lines.append(f"β’ <b>{title}</b>\n <i>{source}</i>") |
| lines.append(f'\n{thin_sep()}\nπ <a href="{WEBSITE_URL}/news">More on rugmunch.io</a>') |
| 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"π° <b>Crypto News</b>\n\n" |
| f"News feed loading...\n\n" |
| f'π’ Follow <a href="{TELEGRAM_CHANNEL}">@RugMunchAlerts</a> for real-time alerts!\n' |
| f'π <a href="{WEBSITE_URL}">rugmunch.io</a>', |
| 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})" |
|
|
| |
| nudge = "" |
| if tier == "free": |
| nudge = ( |
| f"\n{thin_sep()}\n" |
| f"β <b>Upgrade to unlock:</b>\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"π€ <b>Account Dashboard</b>\n{sep()}\n" |
| f"<b>User:</b> @{user.username or user.first_name}\n" |
| f"<b>Tier:</b> {tier_badge}\n\n" |
| f"π <b>This Week</b>\n{thin_sep()}\n" |
| f"π Scans: <b>{scans}/{tc.get('scans_per_week', 25)}</b>\n" |
| f"π€ AI Messages: <b>{ai}/{tc.get('ai_msgs_per_week', 15)}</b>\n\n" |
| f"π <b>Bonus Balance</b>\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"π <b>All-Time</b>\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"β <b>Subscription Plans</b>\n{sep()}\n\n" |
| for key, t in TIERS.items(): |
| if key == "free": |
| text += f"{t['emoji']} <b>{t['name']}</b> β $0\n{d}\n" |
| else: |
| text += f"{t['emoji']} <b>{t['name']}</b> β <b>${t['price_monthly']}/mo</b>\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'π <a href="{WEBSITE_URL}/pricing">Compare plans on rugmunch.io</a>\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"π <b>Top Up β Buy Extra Usage</b>\n{sep()}\n\n" |
| f"Ran out of weekly scans? Buy bonus usage!\n\n" |
| f"β
<b>No expiry</b> β carries over forever\n" |
| f"β
<b>Stackable</b> β buy multiple times\n" |
| f"β
<b>Instant</b> β available immediately\n\n" |
| f"<b>Scan Packs:</b>\n" |
| f" π¬ 10 scans β β75\n" |
| f" π¬ 50 scans β β300\n" |
| f" π¬ 100 scans β β500\n\n" |
| f"<b>AI Message Packs:</b>\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"π <b>Scam School</b>\n{sep()}\n\n" |
| f"Learn how to spot crypto scams <b>before</b> you get rugged.\n\n" |
| f'Free education from <a href="{WEBSITE_URL}">RugMunch Intelligence</a>.\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"π€ <b>Refer & Earn</b>\n{sep()}\n\n" |
| f"Invite friends and <b>both get 5 bonus scans!</b>\n\n" |
| f"π <b>Your Link:</b>\n<code>{link}</code>\n\n" |
| f"π <b>Your Stats:</b>\n" |
| f" Friends Referred: <b>{count}</b>\n" |
| f" Bonus Earned: <b>{count * 5}</b> scans{ms_text}\n\n" |
| f"<b>How it works:</b>\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( |
| "π <b>Watchlist Empty</b>\n\n" |
| "Add tokens to track:\n" |
| " /watch <code>address</code> [chain]\n\n" |
| "<i>Scout: 10 slots | Hunter: 50 | Pro: unlimited</i>", |
| parse_mode=ParseMode.HTML, |
| reply_markup=back_kb(), |
| ) |
| return |
| tier = db.get_user_tier(user.id) |
| text = f"π <b>Watchlist</b> ({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}. <b>{w.get('symbol') or '???'}</b> <code>{short_addr(w['token_address'])}</code> ({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 <code>address</code> [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 <code>{short_addr(addr)}</code> 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 <b>Scout tier</b> or higher.\n\n" |
| "π <b>Scout ($29.99/mo)</b> β 10 watchlist slots\n" |
| "π― <b>Hunter ($49.99/mo)</b> β 50 slots + alerts\n" |
| "π <b>Pro ($99.99/mo)</b> β 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 <code>address</code>", 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 <code>{short_addr(addr)}</code> 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"π <b>Price Alerts</b>\n\n" |
| f"Get notified when tokens hit your target price.\n\n" |
| f"β οΈ <b>Requires Scout tier</b> or higher.\n" |
| f"You're on {TIERS[tier]['emoji']} {TIERS[tier]['name']}.\n\n" |
| f"π― <b>Hunter+</b> 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( |
| "π <b>Price Alerts</b>\n\nNo alerts set yet. Add a token with /watch first.", |
| parse_mode=ParseMode.HTML, |
| reply_markup=back_kb(), |
| ) |
| return |
| text = f"π <b>Active Alerts</b>\n{sep()}\n\n" |
| for a in alerts: |
| arrow = "β" if a.get("alert_direction") == "above" else "β" |
| text += f"β’ <b>{a.get('symbol', '???')}</b> {arrow} {fmt_number(a['alert_price'])}\n" |
| await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb()) |
|
|
|
|
| |
| |
| |
| 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"π <b>Bot Stats</b>\n{thin_sep()}\n" |
| f"π₯ Total Users: {total}\nπ« Banned: {banned}\n\n" |
| f"π <b>Tiers:</b>\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 <user_id> <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 <message>") |
| 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 <user_id> [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'}.") |
|
|
|
|
| |
| |
| |
| 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 |
|
|
| |
| if is_evm(text) or is_sol(text): |
| ctx.args = [text] |
| await cmd_scan(update, ctx) |
| return |
|
|
| |
| 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"πͺ <b>{name}</b> ({symbol}) {fmt_chain(chain)}\n" |
| f"<code>{addr}</code>\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 |
|
|
| |
| 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 |
|
|
| |
| 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: |
| 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 |
|
|
| |
| 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 <code>address</code>", |
| 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. <code>$PEPE</code>) 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"β’ <code>$TOKEN</code> β quick price\n" |
| f"β’ /scan <code>address</code> β full analysis\n" |
| f"β’ /wallet <code>address</code> β 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, |
| ) |
|
|
|
|
| |
| |
| |
| 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) |
|
|
|
|
| |
| |
| |
| 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( |
| "π‘οΈ <b>RugMunch Intelligence</b>\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( |
| "π <b>Scan a Token</b>\n\nSend me a contract address:\n<code>0x6982...1933</code>\n\nOr use: /scan <code>address</code>", |
| parse_mode=ParseMode.HTML, |
| reply_markup=back_kb(), |
| ) |
| elif data == "menu_wallet": |
| await query.edit_message_text( |
| "π <b>Wallet Check</b>\n\nSend a wallet address or use:\n/wallet <code>address</code>", |
| parse_mode=ParseMode.HTML, |
| reply_markup=back_kb(), |
| ) |
| elif data == "menu_ta": |
| await query.edit_message_text( |
| "π <b>Technical Analysis</b>\n\nUsage: /ta <code>address</code>\n\nAnalyzes momentum, volume, buy/sell pressure.", |
| parse_mode=ParseMode.HTML, |
| reply_markup=back_kb(), |
| ) |
| elif data == "menu_alerts": |
| await query.edit_message_text( |
| "π <b>Price Alerts</b>\n\nUse /alerts to manage.\n\n<i>Scout+ tiers only.</i>", |
| 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"π€ <b>Account</b>\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( |
| "β <b>Upgrade Plan</b>\n\nSelect a tier:", |
| parse_mode=ParseMode.HTML, |
| reply_markup=pricing_kb(), |
| ) |
| elif data == "menu_topup": |
| await query.edit_message_text( |
| "π <b>Top Up</b>\n\nBuy extra usage (no expiry):", |
| parse_mode=ParseMode.HTML, |
| reply_markup=topup_kb(), |
| ) |
| elif data == "menu_scamschool": |
| await query.edit_message_text( |
| "π <b>Scam School</b>\n\nSelect a topic:", |
| parse_mode=ParseMode.HTML, |
| reply_markup=scamschool_kb(), |
| ) |
| elif data == "menu_trending": |
| await query.edit_message_text("π₯ <b>Fetching trending...</b>", 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 = ["π₯ <b>Trending</b>", 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} <b>{name}</b> ({sym}) {fmt_pct(h24)}\n <code>{short_addr(addr)}</code>" |
| ) |
| 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 <code>address</code>", |
| parse_mode=ParseMode.HTML, |
| reply_markup=back_kb(), |
| ) |
| else: |
| text = "π <b>Watchlist</b>\n\n" + "\n".join( |
| f"β’ {w.get('symbol', '???')} <code>{short_addr(w['token_address'])}</code>" 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"π€ <b>Refer Friends</b>\n\nShare your link:\n<code>{link}</code>\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"π³ <b>Subscribe to {tier['name']}</b>\n\n" |
| f"${tier['price_monthly']}/month\n" |
| f"or β{tier.get('price_stars', 0)} Stars\n\n" |
| f"<i>Payment integration coming next update.\n" |
| f"Contact {SUPPORT_EMAIL} for now.</i>", |
| 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"β
<b>Top Up Applied!</b>\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"β <b>Not Enough Stars</b>\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(), |
| ) |
|
|
|
|
| |
| |
| |
| 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"β
<b>Payment Received!</b>\n\n" |
| f"β {payment.total_amount} Stars added to your balance.\n" |
| f"Use /topup to spend them.", |
| parse_mode=ParseMode.HTML, |
| ) |
|
|
|
|
| |
| |
| |
| 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, |
| ) |
|
|
|
|
| |
| |
| |
| 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}") |
|
|
|
|
| |
| |
| |
| 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"π‘οΈ <b>Daily Scam Tip</b>\n{sep()}\n\n" |
| f"{topic['content']}\n\n" |
| f"π Learn more: /scamschool\n" |
| f"π Scan tokens: @RugMunchBot\n" |
| f'π <a href="{WEBSITE_URL}">rugmunch.io</a>' |
| ) |
| 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}") |
|
|
|
|
| |
| |
| |
| def main(): |
| if not BOT_TOKEN: |
| logger.error("RUGMUNCH_BOT_TOKEN not set!") |
| sys.exit(1) |
|
|
| app = Application.builder().token(BOT_TOKEN).build() |
|
|
| |
| 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)) |
|
|
| |
| 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)) |
|
|
| |
| app.add_handler(InlineQueryHandler(handle_inline)) |
|
|
| |
| app.add_handler(CallbackQueryHandler(handle_callback)) |
| app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) |
|
|
| |
| app.add_handler(PreCheckoutQueryHandler(precheckout)) |
| app.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment)) |
|
|
| |
| app.add_error_handler(error_handler) |
|
|
| |
| job_queue = app.job_queue |
| job_queue.run_daily(daily_scam_tip, dtime(hour=14, minute=0, tzinfo=UTC)) |
|
|
| |
| app.post_init = setup_bot_profile |
|
|
| logger.info("π‘οΈ CryptoRugMunch Bot v6 starting...") |
| app.run_polling(drop_pending_updates=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|