470 lines
31 KiB
Python
470 lines
31 KiB
Python
#!/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, 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'<div class="testimonial"><div class="stars">{"★"*t["stars"]}</div><div class="quote">"{t["quote"]}"</div><div class="author">— {t["author"]}</div></div>' for t in TESTIMONIALS)
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# PAGES
|
|
# ═══════════════════════════════════════════════════════════
|
|
@app.route("/")
|
|
def landing():
|
|
return f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>Signal Miner X — Market Intelligence</title>
|
|
<style>{CSS}</style></head><body>
|
|
<div class="container">
|
|
<div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
|
<h2>Market Intelligence<br><span style="background:linear-gradient(135deg,var(--purple),var(--pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent">That Actually Makes You Money</span></h2>
|
|
<p>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.</p>
|
|
<div style="display:flex;gap:12px;margin:24px 0;flex-wrap:wrap">
|
|
<a href="/signup" class="btn btn-primary">Get {FREE_SEARCHES} Free Searches</a>
|
|
<a href="/pricing" class="btn btn-secondary">$1 = 5 Searches</a>
|
|
</div>
|
|
<div class="card"><h3>💰 What Our Users Say</h3><div class="testimonial-scroll">{_t_html}</div></div>
|
|
<div class="card"><h3>🛒 776 Commerce Categories Monitored</h3>
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:16px;margin-top:16px">
|
|
<div><span style="font-size:28px">🎁</span><br><strong style="color:var(--bright);font-size:18px">430</strong><br><span style="font-size:11px;color:var(--muted)">Gift Cards</span></div>
|
|
<div><span style="font-size:28px">🔑</span><br><strong style="color:var(--bright);font-size:18px">60</strong><br><span style="font-size:11px;color:var(--muted)">Software Keys</span></div>
|
|
<div><span style="font-size:28px">📺</span><br><strong style="color:var(--bright);font-size:18px">86</strong><br><span style="font-size:11px;color:var(--muted)">Accounts</span></div>
|
|
<div><span style="font-size:28px">💻</span><br><strong style="color:var(--bright);font-size:18px">80</strong><br><span style="font-size:11px;color:var(--muted)">Hardware</span></div>
|
|
<div><span style="font-size:28px">🤖</span><br><strong style="color:var(--bright);font-size:18px">120</strong><br><span style="font-size:11px;color:var(--muted)">Services & Tools</span></div>
|
|
</div></div>
|
|
<div class="footer"><p>Signal Miner X · No KYC · Bitcoin Payments · <a href="https://buymeacoffee.com/r26xrthzttg">☕ Buy Me a Coffee</a></p></div>
|
|
</div></body></html>"""
|
|
|
|
@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"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Sign Up — Signal Miner X</title><style>{CSS}</style></head><body>
|
|
<div class="container"><div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
|
<div class="card" style="max-width:500px;margin:0 auto"><h2 style="font-size:24px">Get Your API Key</h2>
|
|
<p>No KYC. Just an email. <strong style="color:var(--green)">{FREE_SEARCHES} free searches</strong> included.</p>
|
|
<input type="email" id="email" placeholder="you@email.com" style="font-size:16px">
|
|
<button class="btn btn-primary" onclick="signup()" style="width:100%;font-size:16px">Generate My API Key</button>
|
|
<div id="result" style="margin-top:20px;display:none"><p style="color:var(--green)">✅ Your API key:</p>
|
|
<pre id="apikey" style="cursor:pointer" onclick="copyKey()"></pre>
|
|
<p style="color:var(--muted);font-size:12px;margin-top:8px">Click to copy. Store safely. <a href="/dashboard" style="color:var(--purple)">Go to Dashboard →</a></p></div></div></div>
|
|
<script>async function signup(){{const e=document.getElementById('email').value;const r=await fetch('/signup',{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify({{email:e}})}});const d=await r.json();document.getElementById('result').style.display='block';document.getElementById('apikey').textContent=d.api_key||d.error}}
|
|
function copyKey(){{navigator.clipboard.writeText(document.getElementById('apikey').textContent);alert('Copied!')}}</script></body></html>"""
|
|
|
|
@app.route("/pricing")
|
|
def pricing_page():
|
|
return f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Pricing — Signal Miner X</title><style>{CSS}</style></head><body>
|
|
<div class="container"><div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
|
<h2>Simple Pricing</h2><p>No subscriptions. Pay with Bitcoin. Get market intelligence.</p>
|
|
<div class="card" style="max-width:500px;margin:20px auto;text-align:center;border-color:var(--purple);border-width:2px">
|
|
<h3>🚀 Researcher Tier</h3><div class="price">$1<span> USD</span></div>
|
|
<p style="color:var(--muted);margin-bottom:16px">in Bitcoin · Lightning ⚡</p>
|
|
<ul class="feature-list" style="text-align:left;max-width:300px;margin:0 auto">
|
|
<li>5 marketplace searches</li><li>Full access to 1,085 channels</li><li>Credibility scoring</li><li>API access for bots</li><li>No expiration</li><li>No KYC required</li></ul>
|
|
<div style="margin-top:24px"><a href="/signup" class="btn btn-primary" style="width:100%">Sign Up — Get {FREE_SEARCHES} Free</a></div></div>
|
|
<div class="card"><h3>💰 Testimonials</h3><div class="testimonial-scroll">{_t_html}</div></div></div></body></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"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Dashboard — Signal Miner X</title><style>{CSS}</style></head><body>
|
|
<div class="container"><div class="header"><h1>📡 Signal Miner X</h1><nav><a href="/">Home</a><a href="/pricing">Pricing</a><a href="/signup">Sign Up</a><a href="/dashboard">Dashboard</a></nav></div>
|
|
<div id="login-section"><div class="card" style="max-width:500px;margin:0 auto"><h3>🔑 Enter Your API Key</h3>
|
|
<input type="text" id="apikey-input" placeholder="sm_..." style="font-family:monospace">
|
|
<button class="btn btn-primary" onclick="login()" style="width:100%">View Dashboard</button></div></div>
|
|
<div id="dash-section" style="display:none"><div class="card" style="text-align:center"><div class="price" id="dash-searches">0</div>
|
|
<p style="color:var(--muted)">searches remaining</p><p style="color:var(--muted);font-size:12px">Total used: <span id="dash-total">0</span></p></div>
|
|
<div class="card"><h3>🔋 Buy More Searches</h3><p style="color:var(--muted);margin-bottom:16px">$1 = 5 searches. Bitcoin/Lightning. Instant credit on payment.</p>
|
|
<button class="btn btn-primary" onclick="buy()" style="width:100%">Buy 5 Searches — $1.00 ⚡</button><div id="buy-result" style="margin-top:12px;display:none"></div></div>
|
|
<div class="card"><h3>📋 Search Marketplace</h3>
|
|
<p style="color:var(--muted);font-size:12px;margin-bottom:8px">Try searching for deals:</p>
|
|
<div style="display:flex;gap:8px"><input type="text" id="search-query" placeholder='e.g. \"steam gift cards cheap\" or \"netflix accounts\"' style="flex:1">
|
|
<button class="btn btn-primary btn-sm" onclick="doSearch()">Search</button></div>
|
|
<div id="search-results" style="margin-top:12px"></div></div>
|
|
<div class="card"><h3>📋 API Usage</h3>
|
|
<pre>curl -H "X-API-Key: <span id="dash-key">...</span>" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{{\"query\":\"steam gift cards\"}}' \\
|
|
http://10.30.20.23:5099/api/search</pre></div></div>
|
|
<div class="footer"><p><a href="https://buymeacoffee.com/r26xrthzttg">☕ Buy Me a Coffee</a></p></div></div>
|
|
<script>let key='';
|
|
async function login(){{key=document.getElementById('apikey-input').value.trim();if(!key)return;const r=await fetch('/api/me?api_key='+key);const d=await r.json();if(d.error){{alert(d.error);return}}document.getElementById('login-section').style.display='none';document.getElementById('dash-section').style.display='block';document.getElementById('dash-searches').textContent=d.searches_remaining;document.getElementById('dash-total').textContent=d.total_searches;document.getElementById('dash-key').textContent=key}}
|
|
async function buy(){{const r=await fetch('/dashboard',{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify({{api_key:key}})}});const d=await r.json();const div=document.getElementById('buy-result');div.style.display='block';if(d.checkout_url){{div.innerHTML='<a href=\"'+d.checkout_url+'\" target=\"_blank\" class=\"btn btn-primary\">Pay with Bitcoin ⚡</a><p style=\"color:var(--muted);font-size:12px;margin-top:8px\">Refresh page after payment</p>'}}else{{div.innerHTML='<p style=\"color:var(--red)\">Error: '+JSON.stringify(d)+'</p>'}}}}
|
|
async function doSearch(){{const q=document.getElementById('search-query').value.trim();if(!q||!key)return;const res=document.getElementById('search-results');res.innerHTML='<div class=\"spinner\"></div>';try{{const r=await fetch('/api/search',{{method:'POST',headers:{{'Content-Type':'application/json','X-API-Key':key}},body:JSON.stringify({{query:q}})}});const d=await r.json();if(d.error){{res.innerHTML='<div class=\"card\" style=\"border-color:var(--red)\"><p style=\"color:var(--red)\">'+d.error+'</p>'+(d.pricing?'<a href=\"'+d.pricing+'\" class=\"btn btn-primary btn-sm\">Buy More</a>':'')+'</div>';return}}let h='<p style=\"color:var(--green);margin-bottom:8px\">Found '+d.results.length+' deals. '+d.searches_remaining+' searches left.</p>';for(const r of d.results){{h+='<div class=\"result-card\"><div class=\"ch-name\">'+r.channel_title+' <span class=\"badge badge-'+(r.tier=='S'||r.tier=='A'?'green':'purple')+'\">'+r.tier+'</span></div>';if(r.deal)h+='<div class=\"ch-deal\">'+r.deal+'</div>';if(r.text)h+='<div class=\"ch-text\">'+r.text.substring(0,150)+'</div>';h+='<div class=\"ch-meta\">@'+r.channel+' · Score: '+r.score+'/100 · Views: '+(r.views||'?')+'</div></div>'}}if(d.results.length==0)h+='<p style=\"color:var(--muted)\">No deals found for that query. Try different keywords.</p>';res.innerHTML=h}}catch(e){{res.innerHTML='<p style=\"color:var(--red)\">'+e.message+'</p>'}}}}
|
|
const p=new URLSearchParams(window.location.search);if(p.get('api_key')){{document.getElementById('apikey-input').value=p.get('api_key');login()}}
|
|
</script></body></html>"""
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# 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)
|