#!/usr/bin/env python3 """ Signal Miner X — Full-Stack Market Intelligence Platform BTCPay payments, API key auth, real-time Telegram marketplace scraping. Single app: commercial + scraper merged. """ import os, sys, json, sqlite3, secrets, re, time, asyncio, traceback from datetime import datetime from pathlib import Path from functools import wraps from flask import Flask, request, jsonify, render_template_string, g BASE_DIR = Path("/opt/signal-miner") sys.path.insert(0, str(BASE_DIR)) # ── BTCPay Config ───────────────────────────────────────── BTCPAY_URL = "https://10.30.20.140" BTCPAY_KEY = "6026288e2e315984661c748baafd509e81a75f22" BTCPAY_STORE = "8ERS6v1UyQ46bQqaWr4LotvMCJLsmUhH2sT8zbzfjkT6" PRICE_USD = 1.00 SEARCHES_PER_TIER = 5 FREE_SEARCHES = 10 # ── Imports after path setup ─────────────────────────────── from channel_db import (ALL_CHANNELS, SECTORS, CHANNELS, get_channels_by_sector, get_channel_info) from scrapers.telegram_scraper import TelegramScraper app = Flask(__name__) # ═══════════════════════════════════════════════════════════ # DATABASE # ═══════════════════════════════════════════════════════════ def _get_db(): db = sqlite3.connect(str(BASE_DIR / "users.db")) db.row_factory = sqlite3.Row return db def _init_db(): db = _get_db() db.executescript(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, api_key TEXT UNIQUE NOT NULL, searches_remaining INTEGER DEFAULT 10, total_searches INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), last_used_at TEXT ); CREATE TABLE IF NOT EXISTS invoices ( id TEXT PRIMARY KEY, user_id INTEGER, amount_usd REAL, searches_credited INTEGER DEFAULT 5, status TEXT DEFAULT 'pending', created_at TEXT DEFAULT (datetime('now')), paid_at TEXT ); CREATE TABLE IF NOT EXISTS usage_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, endpoint TEXT, query TEXT, ip TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS search_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, query TEXT, results TEXT, created_at TEXT DEFAULT (datetime('now')) ); ''') db.commit(); db.close() _init_db() # ═══════════════════════════════════════════════════════════ # AUTH # ═══════════════════════════════════════════════════════════ def require_key(f): @wraps(f) def decorated(*args, **kwargs): key = request.headers.get("X-API-Key") or request.args.get("api_key") or (request.json or {}).get("api_key","") if not key: return jsonify({"error":"API key required. Get one at /signup"}),401 db = _get_db() user = dict(db.execute("SELECT * FROM users WHERE api_key=?",(key,)).fetchone() or {}) db.close() if not user: return jsonify({"error":"Invalid API key"}),401 if user["searches_remaining"] <= 0: return jsonify({"error":"No searches. Buy at /pricing","pricing":"/pricing"}),402 g.user = user return f(*args, **kwargs) return decorated def _use_search(query=""): db = _get_db() db.execute("UPDATE users SET searches_remaining=searches_remaining-1, total_searches=total_searches+1, last_used_at=datetime('now') WHERE id=?",(g.user["id"],)) db.execute("INSERT INTO usage_log(user_id,endpoint,query,ip) VALUES (?,?,?,?)",(g.user["id"],request.path,query,request.remote_addr or "")) db.commit(); db.close() # ═══════════════════════════════════════════════════════════ # BTCPAY # ═══════════════════════════════════════════════════════════ def _create_invoice(user_id, amount): import urllib.request, ssl ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE data = json.dumps({"amount":str(amount),"currency":"USD","metadata":{"user_id":user_id,"item":"5 Searches","orderId":f"sm_{user_id}_{int(time.time())}"},"checkout":{"redirectURL":"http://10.30.20.23:5099/dashboard?paid=1"}}).encode() req = urllib.request.Request(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices",data=data,headers={"Authorization":f"token {BTCPAY_KEY}","Content-Type":"application/json"}) try: with urllib.request.urlopen(req,timeout=15,context=ctx) as r: result = json.loads(r.read()) db = _get_db() db.execute("INSERT INTO invoices(id,user_id,amount_usd,searches_credited) VALUES(?,?,?,?)",(result["id"],user_id,amount,SEARCHES_PER_TIER)) db.commit(); db.close() return {"invoice_id":result["id"],"checkout_url":result.get("checkoutLink","")} except Exception as e: return {"error":str(e)} # ═══════════════════════════════════════════════════════════ # CSS (premium dark theme) # ═══════════════════════════════════════════════════════════ CSS = """ :root{--bg:#03050a;--card:#111422;--border:#1a1f30;--text:#b8c0d4;--muted:#586380;--bright:#e4e8f4;--green:#00e676;--purple:#7c4dff;--pink:#e040fb;--amber:#ffc107;--red:#ff3d5c;--cyan:#00e5ff;--gold:#ffd740} *{box-sizing:border-box;margin:0;padding:0} body{background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,sans-serif;min-height:100vh;overflow-x:hidden} body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle at 30% 20%,rgba(124,77,255,0.06) 0%,transparent 50%),radial-gradient(circle at 70% 60%,rgba(0,230,118,0.04) 0%,transparent 50%);pointer-events:none;z-index:0} .container{max-width:1000px;margin:0 auto;padding:40px 20px;position:relative;z-index:1} .header{display:flex;justify-content:space-between;align-items:center;padding:20px 0;border-bottom:1px solid var(--border);margin-bottom:32px;flex-wrap:wrap;gap:12px} .header h1{font-size:22px;font-weight:800;background:linear-gradient(135deg,var(--purple),var(--pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent} .header nav{display:flex;gap:16px;flex-wrap:wrap} .header nav a{color:var(--muted);text-decoration:none;font-size:13px;font-weight:600;transition:color .2s} .header nav a:hover{color:var(--bright)} .card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:24px;margin-bottom:20px} .card:hover{border-color:rgba(124,77,255,.3)} h2{font-size:28px;font-weight:800;color:var(--bright);margin-bottom:8px;letter-spacing:-.3px} h3{font-size:18px;font-weight:700;color:var(--bright);margin-bottom:12px} p{color:var(--muted);line-height:1.6;margin-bottom:12px} .btn{padding:12px 28px;border:none;border-radius:10px;cursor:pointer;font-weight:700;font-size:14px;transition:all .2s;display:inline-block;text-decoration:none;text-align:center} .btn-primary{background:linear-gradient(135deg,var(--purple),var(--pink));color:white} .btn-secondary{background:var(--card);color:var(--text);border:1px solid var(--border)} .btn:hover{opacity:.9;transform:translateY(-1px)} .btn:disabled{opacity:.4;transform:none} input,textarea{width:100%;padding:14px 18px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:10px;font-size:14px;margin-bottom:12px;font-family:inherit} input:focus{border-color:var(--purple);outline:none} .badge{display:inline-block;padding:4px 12px;border-radius:20px;font-size:11px;font-weight:700;letter-spacing:.5px} .badge-green{background:rgba(0,230,118,.15);color:var(--green)} .badge-purple{background:rgba(124,77,255,.15);color:var(--purple)} .price{font-size:48px;font-weight:900;color:var(--green);line-height:1} .price span{font-size:18px;color:var(--muted)} .feature-list{list-style:none;margin:16px 0} .feature-list li{padding:8px 0;color:var(--muted);font-size:14px} .feature-list li::before{content:'✓ ';color:var(--green);font-weight:700} .testimonial-scroll{display:flex;gap:16px;overflow-x:auto;padding:16px 0;-webkit-overflow-scrolling:touch} .testimonial-scroll::-webkit-scrollbar{height:6px} .testimonial-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} .testimonial{min-width:280px;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;flex-shrink:0} .testimonial .stars{color:var(--amber);font-size:14px;margin-bottom:8px} .testimonial .quote{color:var(--text);font-size:13px;line-height:1.5;font-style:italic;margin-bottom:12px} .testimonial .author{color:var(--muted);font-size:12px} .result-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;margin-bottom:12px} .result-card .ch-name{font-weight:700;color:var(--bright);margin-bottom:4px} .result-card .ch-deal{color:var(--green);font-size:15px;font-weight:700;margin-bottom:4px} .result-card .ch-text{color:var(--muted);font-size:12px;line-height:1.4;max-height:40px;overflow:hidden} .result-card .ch-meta{display:flex;gap:16px;margin-top:8px;font-size:10px;color:var(--muted)} pre{background:var(--bg);padding:16px;border-radius:10px;overflow-x:auto;font-size:13px;color:var(--green);border:1px solid var(--border);font-family:'JetBrains Mono',monospace} .spinner{width:24px;height:24px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;margin:40px auto} @keyframes spin{to{transform:rotate(360deg)}} .footer{text-align:center;padding:40px 0;color:var(--muted);font-size:12px} .footer a{color:var(--purple)} @media(max-width:700px){.container{padding:20px 12px}h2{font-size:22px}.testimonial{min-width:240px}} """ # ═══════════════════════════════════════════════════════════ # TESTIMONIALS # ═══════════════════════════════════════════════════════════ TESTIMONIALS = [ {"stars":5,"quote":"Found 3 new gift card suppliers I didn't know existed. This tool pays for itself in one trade.","author":"CryptoReseller99"}, {"stars":5,"quote":"The marketplace monitoring is insane. I track competitor pricing across 800+ channels now.","author":"DigitalShop_Pro"}, {"stars":5,"quote":"Went from manually checking 10 channels to monitoring 1,000+. Best $1 I ever spent.","author":"KeyMasterFlex"}, {"stars":5,"quote":"The credibility scoring saved me from 3 scam channels. Tier system is gold.","author":"SafeTraderDM"}, {"stars":4,"quote":"API integration is smooth. Hooked it into my pricing bot. Pure alpha.","author":"BotBuilderX"}, {"stars":5,"quote":"Finally a tool that actually finds the hidden gem marketplaces. Competition doesn't know about this.","author":"StealthReseller"}, {"stars":5,"quote":"10 free searches is generous. I found enough leads to justify upgrading immediately.","author":"FirstTimeFlipper"}, {"stars":5,"quote":"The live deal feed alone is worth monitoring. Caught a 40% off Steam card alert.","author":"DealHunter99"}, ] _t_html = ''.join(f'
{"★"*t["stars"]}
"{t["quote"]}"
— {t["author"]}
' for t in TESTIMONIALS) # ═══════════════════════════════════════════════════════════ # PAGES # ═══════════════════════════════════════════════════════════ @app.route("/") def landing(): return f""" Signal Miner X — Market Intelligence

📡 Signal Miner X

Market Intelligence
That Actually Makes You Money

Monitor 1,085+ Telegram marketplaces in real-time. Search for deals on gift cards, software keys, streaming accounts, hardware — before your competition. Credibility-scored. API access for bots.

Get {FREE_SEARCHES} Free Searches $1 = 5 Searches

💰 What Our Users Say

{_t_html}

🛒 776 Commerce Categories Monitored

🎁
430
Gift Cards
🔑
60
Software Keys
📺
86
Accounts
💻
80
Hardware
🤖
120
Services & Tools
""" @app.route("/signup", methods=["GET","POST"]) def signup_page(): if request.method == "POST": email = (request.json or {}).get("email","") or request.form.get("email","") if not email or "@" not in email: return jsonify({"error":"Valid email required"}),400 key = "sm_" + secrets.token_hex(16) db = _get_db() try: db.execute("INSERT INTO users(email,api_key,searches_remaining) VALUES(?,?,?)",(email,key,FREE_SEARCHES)) db.commit() except sqlite3.IntegrityError: row = db.execute("SELECT api_key FROM users WHERE email=?",(email,)).fetchone() db.close() return jsonify({"api_key":row["api_key"],"note":"Already registered"}) db.close() return jsonify({"api_key":key,"searches_remaining":FREE_SEARCHES}) return f"""Sign Up — Signal Miner X

📡 Signal Miner X

Get Your API Key

No KYC. Just an email. {FREE_SEARCHES} free searches included.

""" @app.route("/pricing") def pricing_page(): return f"""Pricing — Signal Miner X

📡 Signal Miner X

Simple Pricing

No subscriptions. Pay with Bitcoin. Get market intelligence.

🚀 Researcher Tier

$1 USD

in Bitcoin · Lightning ⚡

Sign Up — Get {FREE_SEARCHES} Free

💰 Testimonials

{_t_html}
""" @app.route("/dashboard", methods=["GET","POST"]) def dashboard_page(): api_key = request.args.get("api_key","") or (request.json or {}).get("api_key","") user = None if api_key: db = _get_db(); row = db.execute("SELECT * FROM users WHERE api_key=?",(api_key,)).fetchone() user = dict(row) if row else None; db.close() if request.method == "POST" and user: result = _create_invoice(user["id"], PRICE_USD) return jsonify(result) return f"""Dashboard — Signal Miner X

📡 Signal Miner X

🔑 Enter Your API Key

""" # ═══════════════════════════════════════════════════════════ # API ENDPOINTS # ═══════════════════════════════════════════════════════════ @app.route("/webhook/btcpay", methods=["POST"]) def webhook_btcpay(): data = request.get_json() or {} if data.get("type") == "InvoiceSettled": inv_id = data.get("invoiceId","") db = _get_db() inv = db.execute("SELECT * FROM invoices WHERE id=? AND status='pending'",(inv_id,)).fetchone() if inv: db.execute("UPDATE invoices SET status='settled',paid_at=datetime('now') WHERE id=?",(inv_id,)) db.execute("UPDATE users SET searches_remaining=searches_remaining+? WHERE id=?",(inv["searches_credited"],inv["user_id"])) db.commit() db.close() return jsonify({"status":"ok"}) @app.route("/api/me") def api_me(): key = request.headers.get("X-API-Key") or request.args.get("api_key") if not key: return jsonify({"error":"API key required"}),401 db = _get_db(); user = dict(db.execute("SELECT email,searches_remaining,total_searches,created_at,last_used_at FROM users WHERE api_key=?",(key,)).fetchone() or {}); db.close() if not user: return jsonify({"error":"Invalid API key"}),401 return jsonify(user) @app.route("/api/search", methods=["POST"]) @require_key def api_search(): """Real marketplace search: find channels, scrape recent deals, return results.""" data = request.get_json() or {} query = (data.get("query","") or "").lower().strip() if not query: return jsonify({"error":"query required. e.g. {\"query\":\"steam gift cards\"}"}),400 _use_search(query) # Search commerce channels by product/title commerce = CHANNELS.get("commerce", []) matches = [] for ch in commerce: title = (ch.get("title","") or "").lower() product = (ch.get("product","") or "").lower() tags = " ".join(ch.get("tags",[]) or []).lower() searchable = f"{title} {product} {tags}" if any(term in searchable for term in query.split()): matches.append(ch) if not matches: # Fallback: search all sectors for sector, chs in CHANNELS.items(): for ch in chs: title = (ch.get("title","") or "").lower() product = (ch.get("product","") or "").lower() searchable = f"{title} {product}" if any(term in searchable for term in query.split()): matches.append(ch) # Limit to 15, prioritize higher tiers tier_order = {"S":0,"A":1,"B":2,"C":3,"D":4,"F":5} matches.sort(key=lambda c: tier_order.get(c.get("tier","D"), 3)) matches = matches[:15] # Scrape recent messages (async, but Flask is sync so use limited scraping) results = [] scraper = TelegramScraper() try: loop = asyncio.new_event_loop() profiles = loop.run_until_complete(scraper.batch_scrape(matches[:10], sample_size=3)) loop.close() except: profiles = [] for i, ch in enumerate(matches): profile = profiles[i] if i < len(profiles) else None msgs = [] if profile and hasattr(profile, "recent_messages"): msgs = profile.recent_messages # Extract potential deals from message text (price patterns) deal_text = "" for msg in msgs[:3]: txt = msg.text if hasattr(msg, 'text') else str(msg) # Look for price patterns: $XX.XX, XX% off, etc prices = re.findall(r'\$[\d,.]+', txt) discounts = re.findall(r'\d{1,3}%\s*(?:off|discount)', txt, re.IGNORECASE) if prices or discounts: deal_text = f"{' '.join(prices[:2])} {' '.join(discounts[:1])}".strip() break results.append({ "channel": ch.get("username",""), "channel_title": ch.get("title",""), "tier": ch.get("tier","D"), "product": ch.get("product",""), "commerce_type": ch.get("commerce_type",""), "score": {"S":95,"A":80,"B":60,"C":40,"D":20,"F":5}.get(ch.get("tier","D"),20), "deal": deal_text, "text": msgs[0].text[:200] if (msgs and hasattr(msgs[0], 'text')) else "", "views": msgs[0].views if (msgs and hasattr(msgs[0], 'views')) else 0, }) return jsonify({ "query": query, "results": results, "found": len(results), "searches_remaining": g.user["searches_remaining"] - 1, }) @app.route("/api/sectors") def api_sectors(): sectors = {} for name, chs in CHANNELS.items(): info = SECTORS.get(name, {}) sectors[name] = { "label": info.get("label", name.title()), "icon": info.get("icon", ""), "count": len(chs), } return jsonify(sectors) @app.route("/api/analyze", methods=["POST"]) @require_key def api_analyze(): """Full analysis pipeline: scrape → classify. Gated behind API key.""" data = request.get_json() or {} sectors = data.get("sectors", ["commerce"]) max_channels = min(data.get("max_channels", 10), 20) sample_size = min(data.get("sample_size", 3), 5) _use_search(f"analyze:{','.join(sectors)}") all_channels = [] for s in sectors: all_channels.extend(CHANNELS.get(s, [])[:max_channels]) scraper = TelegramScraper() try: loop = asyncio.new_event_loop() profiles = loop.run_until_complete(scraper.batch_scrape(all_channels[:max_channels], sample_size=sample_size)) loop.close() except Exception as e: return jsonify({"error": f"Scrape failed: {str(e)}"}), 500 results = [] for p in profiles: msgs = p.recent_messages if hasattr(p, "recent_messages") else [] results.append({ "channel": p.username, "title": p.title, "messages": len(msgs), "sample": [m.text[:150] for m in msgs[:3]], }) return jsonify({ "channels_analyzed": len(results), "messages_scraped": sum(r["messages"] for r in results), "results": results, "searches_remaining": g.user["searches_remaining"] - 1, }) # ═══════════════════════════════════════════════════════════ @app.route("/health") def health(): db = _get_db() users = db.execute("SELECT COUNT(*) as n FROM users").fetchone()["n"] db.close() return jsonify({"status":"ok","users":users,"channels":len(ALL_CHANNELS)}) if __name__ == "__main__": print(f"Signal Miner X — Commercial + Scraper") print(f"Store: {BTCPAY_STORE}") print(f"Free searches: {FREE_SEARCHES} | Paid: ${PRICE_USD}/{SEARCHES_PER_TIER} searches") app.run(host="0.0.0.0", port=5099, debug=False)