Merged commercial + scraper: single 5099 app, real /api/search, BTCPay payments, 1085 channels

This commit is contained in:
drjones
2026-08-05 16:38:54 -07:00
commit c8d0f32194
2 changed files with 1907 additions and 0 deletions

469
app.py Normal file
View File

@@ -0,0 +1,469 @@
#!/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'<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)

1438
channel_db.py Normal file

File diff suppressed because it is too large Load Diff