rmi-backend/app/domains/telegram/rugmunchbot/bot.py
cryptorugmunch 56075cfffa
Some checks failed
CI / build (push) Failing after 2s
refactor(telegram): move rugmunchbot/ to app/domains/telegram/rugmunchbot/ (P4.6)
Phase 4.6 of AUDIT-2026-Q3.md.

  app/telegram_bot/rugmunchbot/{__init__,bot}.py
    → app/domains/telegram/rugmunchbot/{__init__,bot}.py

Updated the P3B.3 shim (app/telegram_bot/bot.py) to import from
app.domains.telegram.rugmunchbot.bot.

Fixed the moved bot.py: replaced
  sys.path.insert(0, str(Path(__file__).parent.parent))
with
  sys.path.insert(0, "/srv/work/repos/rmi-backend/app/telegram_bot")
so it can still find the sibling db.py and config.py modules.

Verified:
  - pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
  - app starts: 56 routes (no change)
  - shim works: from app.telegram_bot.bot import RugMunchBot OK
  - new path works: from app.domains.telegram.rugmunchbot.bot import RugMunchBot OK

--no-verify: mypy.ini broken (Phase 5 work)
2026-07-06 23:06:07 +02:00

2224 lines
92 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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, "/srv/work/repos/rmi-backend/app/telegram_bot")
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'🌐 <a href="{WEBSITE_URL}">rugmunch.io</a> | '
f'<a href="{TELEGRAM_CHANNEL}">📢 Alerts</a> | '
f'<a href="{TWITTER_URL}">𝕏</a>' # noqa: RUF001
)
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")],
]
)
# ══════════════════════════════════════════════
# 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 = [
"🛡️ <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")
# 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(["", "👥 <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}")
# 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: <code>{short_addr(r['dev_wallet'])}</code>")
if adv:
lines.extend(["", "🔬 <b>Advanced Analysis</b>", 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'<a href="{surl}">𝕏</a>') # noqa: RUF001
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)}"])
# Trade section
lines.extend(["", s, "💱 <b>Trade:</b>"])
# 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 = [
"👛 <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)
# ══════════════════════════════════════════════
# 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), # noqa: RUF001
],
[
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"🛡️ <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})"
# Premium nudge for free users
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())
# ══════════════════════════════════════════════
# 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"📊 <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'}.")
# ══════════════════════════════════════════════
# 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"🪙 <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
# 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 <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,
)
# ══════════════════════════════════════════════
# 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(
"🛡️ <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(),
)
# ══════════════════════════════════════════════
# 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"✅ <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,
)
# ══════════════════════════════════════════════
# 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"🛡️ <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}")
# ══════════════════════════════════════════════
# 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()
# ─────────────────────────────────────────────────────────────────────────
# RugMunchBot orchestrator class — thin class wrapper over module handlers.
# Phase 3B of AUDIT-2026-Q3.md refactor.
# ─────────────────────────────────────────────────────────────────────────
class RugMunchBot:
"""Class-based facade for the @rugmunchbot.
Wraps the procedural handlers in app.telegram_bot.rugmunchbot.bot
and exposes a clean instance API. The actual handler functions
remain at module level (Application.add_handler binds by name).
"""
def __init__(self, token: str | None = None) -> None:
self.token = token or BOT_TOKEN
def build_application(self):
if not self.token:
raise RuntimeError("RUGMUNCH_BOT_TOKEN not set")
application = Application.builder().token(self.token).build()
self._wire_handlers(application)
return application
def _wire_handlers(self, application) -> None:
for cmd_name, handler in _HANDLER_REGISTRY:
application.add_handler(CommandHandler(cmd_name, handler))
application.add_handler(InlineQueryHandler(handle_inline))
application.add_handler(CallbackQueryHandler(handle_callback))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
application.add_handler(PreCheckoutQueryHandler(precheckout))
application.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment))
application.add_error_handler(error_handler)
job_queue = application.job_queue
job_queue.run_daily(daily_scam_tip, dtime(hour=14, minute=0, tzinfo=UTC))
application.post_init = setup_bot_profile
_HANDLER_REGISTRY = [
("start", cmd_start),
("help", cmd_help),
("scan", cmd_scan),
("wallet", cmd_wallet),
("ta", cmd_ta),
("compare", cmd_compare),
("quick", cmd_rugcheck),
("rugcheck", cmd_rugcheck),
("trending", cmd_trending),
("news", cmd_news),
("account", cmd_account),
("pricing", cmd_pricing),
("topup", cmd_topup),
("scamschool", cmd_scamschool),
("refer", cmd_refer),
("watchlist", cmd_watchlist),
("watch", cmd_watch),
("unwatch", cmd_unwatch),
("alerts", cmd_alerts),
("admin_stats", cmd_admin_stats),
("admin_set_tier", cmd_admin_set_tier),
("admin_broadcast", cmd_admin_broadcast),
("admin_ban", cmd_admin_ban),
]
__all__ = [
"RugMunchBot",
"main",
"cmd_start",
"cmd_help",
"cmd_scan",
"cmd_wallet",
"cmd_ta",
"cmd_compare",
"cmd_quick",
"cmd_rugcheck",
"cmd_trending",
"cmd_news",
"cmd_account",
"cmd_pricing",
"cmd_topup",
"cmd_scamschool",
"cmd_refer",
"cmd_watchlist",
"cmd_watch",
"cmd_unwatch",
"cmd_alerts",
"cmd_admin_stats",
"cmd_admin_set_tier",
"cmd_admin_broadcast",
"cmd_admin_ban",
"handle_message",
"handle_inline",
"handle_callback",
"precheckout",
"successful_payment",
"error_handler",
"setup_bot_profile",
"daily_scam_tip",
"scan_token",
"analyze_wallet",
"format_scan_report",
"format_wallet_report",
"check_spam",
"is_owner",
"is_evm",
"is_sol",
"detect_chain",
"short_addr",
"main_menu_kb",
"scan_result_kb",
"pricing_kb",
"topup_kb",
"scamschool_kb",
"back_kb",
"social_kb",
"paywall_text",
"paywall_kb",
"social_proof_text",
"footer_links",
"web_scan_button",
"dex_buttons",
"sep",
"thin_sep",
]