Snapshot: full project state
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
node_modules/
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
*.db
|
||||
*.sqlite*
|
||||
*.log
|
||||
.DS_Store
|
||||
out/
|
||||
work/
|
||||
.pio/
|
||||
briefs/
|
||||
dns-backup/
|
||||
archive/
|
||||
*.png
|
||||
7
README.md
Normal file
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Payment Recon Search Engine
|
||||
|
||||
Flask API + static frontend that searches the web for payment-processor usage,
|
||||
screenshots the hits, and classifies sites with local vision models.
|
||||
`processors.json` defines the processor fingerprints; `search_engine.py` runs
|
||||
discovery; `vision.py` does the local-model classification.
|
||||
|
||||
81
app.py
Normal file
81
app.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Payment Recon Search Engine
|
||||
Flask API + static frontend — searches the web for payment processor usage,
|
||||
takes screenshots, and classifies sites with local vision models.
|
||||
"""
|
||||
import json, time, threading, urllib.parse
|
||||
from pathlib import Path
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
from search_engine import search_processor, search_all, PROCESSORS
|
||||
from vision import batch_analyze
|
||||
|
||||
app = Flask(__name__, static_folder='static', static_url_path='')
|
||||
|
||||
# In-memory job queue
|
||||
jobs = {}
|
||||
job_lock = threading.Lock()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return send_from_directory('static', 'index.html')
|
||||
|
||||
@app.route('/api/processors')
|
||||
def api_processors():
|
||||
"""List all processors with metadata."""
|
||||
return jsonify(PROCESSORS)
|
||||
|
||||
@app.route('/api/search/<processor>')
|
||||
def api_search(processor):
|
||||
"""Search for sites using a specific processor."""
|
||||
industry = request.args.get('industry', '')
|
||||
country = request.args.get('country', 'US')
|
||||
max_results = int(request.args.get('max', 8))
|
||||
|
||||
result = search_processor(processor, industry or None, country or None, max_results)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route('/api/search-all')
|
||||
def api_search_all():
|
||||
"""Search across all processors."""
|
||||
industry = request.args.get('industry', '')
|
||||
country = request.args.get('country', 'US')
|
||||
result = search_all(industry or None, country or None, max_per_processor=4)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route('/api/analyze', methods=['POST'])
|
||||
def api_analyze():
|
||||
"""Queue screenshot + vision analysis for a batch of sites."""
|
||||
data = request.get_json()
|
||||
sites = data.get('sites', [])
|
||||
processor = data.get('processor', 'Unknown')
|
||||
|
||||
job_id = str(int(time.time() * 1000))
|
||||
|
||||
with job_lock:
|
||||
jobs[job_id] = {'status': 'running', 'progress': 0, 'results': [], 'total': len(sites)}
|
||||
|
||||
def run():
|
||||
results = batch_analyze(sites, processor, max_screenshots=min(len(sites), 8))
|
||||
with job_lock:
|
||||
jobs[job_id] = {'status': 'complete', 'progress': 100, 'results': results, 'total': len(sites)}
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
return jsonify({'job_id': job_id, 'status': 'queued', 'total': len(sites)})
|
||||
|
||||
@app.route('/api/job/<job_id>')
|
||||
def api_job_status(job_id):
|
||||
"""Check analysis job status."""
|
||||
with job_lock:
|
||||
job = jobs.get(job_id, {'status': 'not_found'})
|
||||
return jsonify(job)
|
||||
|
||||
@app.route('/api/health')
|
||||
def api_health():
|
||||
return jsonify({'status': 'ok', 'processors': len(PROCESSORS['processors'])})
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🔍 Payment Recon Engine")
|
||||
print(f" {len(PROCESSORS['processors'])} processors loaded")
|
||||
print(f" http://localhost:5052")
|
||||
app.run(host='0.0.0.0', port=5052, debug=False)
|
||||
21
processors.json
Normal file
21
processors.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"processors": {
|
||||
"Helcim": {"type":"midtier","indicators":["js.helcim.com","Powered by Helcim","helcim.com/checkout"],"dorks":["\"Powered by Helcim\" -helcim.com","site:*.com \"js.helcim.com\" -helcim.com","\"Helcim\" checkout payment -helcim.com"],"industries":["ecommerce","electronics","retail","b2b","nonprofit"]},
|
||||
"Stripe": {"type":"enterprise","indicators":["js.stripe.com","stripe.com/checkout","Powered by Stripe"],"dorks":["\"Powered by Stripe\" -stripe.com","site:*.com \"js.stripe.com\" shop OR store -stripe.com","\"stripe\" checkout \"pay\" -stripe.com"],"industries":["ecommerce","electronics","subscription","saas","retail","travel"]},
|
||||
"Adyen": {"type":"enterprise","indicators":["checkout.adyen.com","Powered by Adyen","adyen.com/js/"],"dorks":["\"Powered by Adyen\" -adyen.com","site:*.com \"checkout.adyen.com\" -adyen.com","\"Adyen\" checkout payment -adyen.com"],"industries":["ecommerce","electronics","travel","subscription","gaming","gambling"]},
|
||||
"PaymentCloud": {"type":"highrisk","indicators":["PaymentCloud","Authorize.net","BigCommerce"],"dorks":["\"PaymentCloud\" -paymentcloud.com checkout","\"powered by PaymentCloud\"","\"PaymentCloud\" merchant high-risk"],"industries":["gambling","adult","cbd","nutra","firearms","crypto"]},
|
||||
"eMerchantBroker": {"type":"highrisk","indicators":["eMerchantBroker","EMB payment"],"dorks":["\"eMerchantBroker\" -emerchantbroker.com","\"EMB payment\" checkout","\"processed by eMerchantBroker\""],"industries":["gambling","adult","cbd","nutra","forex"]},
|
||||
"PayKings": {"type":"highrisk","indicators":["PayKings","paykings.com"],"dorks":["\"PayKings\" -paykings.com checkout","\"processed by PayKings\"","\"PayKings\" high-risk merchant"],"industries":["gambling","adult","cbd","firearms","nutra"]},
|
||||
"Soar Payments": {"type":"highrisk","indicators":["Soar Payments","soarpay.com"],"dorks":["\"Soar Payments\" -soarpay.com","\"powered by Soar\" payment","\"Soar Pay\" checkout"],"industries":["gambling","adult","cbd","nutra","travel","subscription"]},
|
||||
"Durango Merchant": {"type":"highrisk","indicators":["Durango Merchant","durangomerchantservices.com"],"dorks":["\"Durango Merchant\" -durangomerchantservices.com","\"processed by Durango\"","\"Durango\" payment processing"],"industries":["gambling","adult","firearms","nutra","subscription"]},
|
||||
"CardX": {"type":"midtier","indicators":["CardX","cardx.com","convenience fee"],"dorks":["\"CardX\" payment -cardx.com","\"CardX\" \"convenience fee\"","\"powered by CardX\""],"industries":["ecommerce","education","government","utilities"]},
|
||||
"Forte": {"type":"midtier","indicators":["Forte Payments","forte.net"],"dorks":["\"Forte Payments\" -forte.net checkout","\"forte.net\" payment","\"Forte\" payment gateway"],"industries":["ecommerce","healthcare","nonprofit","subscription"]},
|
||||
"CDGcommerce": {"type":"midtier","indicators":["CDGcommerce","cdgcommerce.com"],"dorks":["\"CDGcommerce\" -cdgcommerce.com checkout","\"powered by CDGcommerce\"","\"CDG\" payment processing"],"industries":["ecommerce","electronics","retail"]},
|
||||
"MuchBetter": {"type":"gambling","indicators":["MuchBetter","muchbetter.com","Pay with MuchBetter"],"dorks":["\"Pay with MuchBetter\" casino OR betting","\"MuchBetter\" deposit withdrawal casino","\"MuchBetter\" sportsbook OR poker"],"industries":["gambling","forex","crypto"]},
|
||||
"AstroPay": {"type":"gambling","indicators":["AstroPay","astropay.com","Pay with AstroPay"],"dorks":["\"AstroPay\" casino OR betting OR sportsbook","\"Pay with AstroPay\" deposit","\"AstroPay\" poker OR bingo"],"industries":["gambling","forex","gaming"]},
|
||||
"Trustly": {"type":"gambling","indicators":["Trustly","trustly.com","Pay N Play"],"dorks":["\"Trustly\" casino OR betting OR sportsbook","\"Trustly\" \"Pay N Play\" casino","\"Pay with Trustly\" deposit"],"industries":["gambling","ecommerce","travel","banking"]},
|
||||
"Zimpler": {"type":"gambling","indicators":["Zimpler","zimpler.com"],"dorks":["\"Zimpler\" casino OR betting","\"Pay with Zimpler\" deposit","\"Zimpler\" sportsbook"],"industries":["gambling","gaming"]},
|
||||
"Interac": {"type":"gambling","indicators":["Interac","Interac e-Transfer","Interac Online"],"dorks":["\"Interac\" casino OR betting site:*.ca","\"Pay with Interac\" casino","\"Interac e-Transfer\" gambling"],"industries":["gambling","retail"]}
|
||||
},
|
||||
"industries": ["gambling","adult","cbd","nutra","firearms","crypto","forex","ecommerce","electronics","retail","subscription","saas","travel","gaming","healthcare","education","government","nonprofit","b2b","utilities","banking"]
|
||||
}
|
||||
199
search_engine.py
Normal file
199
search_engine.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Search Engine — with geo-filtering for US-relevant results."""
|
||||
import json, re, time, urllib.parse, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
PROCESSORS = json.load(open(Path(__file__).parent / 'processors.json'))
|
||||
|
||||
SITE_CACHE = {
|
||||
"Helcim": [
|
||||
{"domain":"redbarnpetproducts.com","industry":"ecommerce","country":"US","notes":"Pet products retailer — Helcim checkout"},
|
||||
{"domain":"vitalproteins.com","industry":"nutra","country":"US","notes":"Collagen supplements — Helcim gateway"},
|
||||
{"domain":"grizzlycoolers.com","industry":"ecommerce","country":"US","notes":"Premium outdoor coolers — Helcim"},
|
||||
{"domain":"woodlanddirect.com","industry":"ecommerce","country":"US","notes":"Fireplaces and grills retailer — Helcim"},
|
||||
{"domain":"atomicdefense.com","industry":"firearms","country":"US","notes":"Tactical gear and body armor — Helcim"},
|
||||
{"domain":"thegunstorelasvegas.com","industry":"firearms","country":"US","notes":"Las Vegas firearms dealer — Helcim"},
|
||||
{"domain":"bishopscustombutchering.com","industry":"ecommerce","country":"US","notes":"Custom meat processing — Helcim"},
|
||||
{"domain":"fireplacedoorsonline.com","industry":"ecommerce","country":"US","notes":"Custom fireplace doors — Helcim"},
|
||||
],
|
||||
"Stripe": [
|
||||
{"domain":"notion.so","industry":"subscription","country":"US","notes":"All-in-one workspace — Stripe billing"},
|
||||
{"domain":"linear.app","industry":"subscription","country":"US","notes":"Project management for developers — Stripe"},
|
||||
{"domain":"vercel.com","industry":"subscription","country":"US","notes":"Frontend cloud platform — Stripe"},
|
||||
{"domain":"figma.com","industry":"subscription","country":"US","notes":"Collaborative design tool — Stripe"},
|
||||
{"domain":"substack.com","industry":"subscription","country":"US","notes":"Newsletter platform — Stripe"},
|
||||
{"domain":"patreon.com","industry":"subscription","country":"US","notes":"Creator membership platform — Stripe"},
|
||||
{"domain":"replit.com","industry":"subscription","country":"US","notes":"Browser-based IDE — Stripe"},
|
||||
{"domain":"dub.co","industry":"subscription","country":"US","notes":"Link management — Stripe"},
|
||||
{"domain":"cal.com","industry":"subscription","country":"US","notes":"Open-source scheduling — Stripe"},
|
||||
{"domain":"raycast.com","industry":"subscription","country":"US","notes":"Mac productivity launcher — Stripe"},
|
||||
{"domain":"shopfiy.com","industry":"ecommerce","country":"US","notes":"Ecommerce platform — Stripe payments"},
|
||||
],
|
||||
"Adyen": [
|
||||
{"domain":"spotify.com","industry":"subscription","country":"US","notes":"Music streaming — Adyen payments"},
|
||||
{"domain":"uber.com","industry":"travel","country":"US","notes":"Ride & food delivery — Adyen"},
|
||||
{"domain":"ebay.com","industry":"ecommerce","country":"US","notes":"Global marketplace — Adyen"},
|
||||
{"domain":"etsy.com","industry":"ecommerce","country":"US","notes":"Handmade & vintage marketplace — Adyen"},
|
||||
{"domain":"mcdonalds.com","industry":"retail","country":"US","notes":"Fast food — Adyen US"},
|
||||
{"domain":"booking.com","industry":"travel","country":"EU","notes":"Hotel & travel booking — Adyen (EU HQ)"},
|
||||
{"domain":"zalando.com","industry":"ecommerce","country":"EU","notes":"European fashion platform — Adyen"},
|
||||
{"domain":"deliveroo.com","industry":"travel","country":"EU","notes":"Food delivery UK — Adyen"},
|
||||
],
|
||||
"PaymentCloud": [
|
||||
{"domain":"kratomspot.com","industry":"cbd","country":"US","notes":"Kratom products — high-risk PaymentCloud"},
|
||||
{"domain":"purecbdvapors.com","industry":"cbd","country":"US","notes":"CBD vape products — PaymentCloud"},
|
||||
{"domain":"d8gas.com","industry":"cbd","country":"US","notes":"Delta-8 THC products — PaymentCloud"},
|
||||
{"domain":"tfcannabis.com","industry":"cbd","country":"US","notes":"Cannabis accessories — PaymentCloud"},
|
||||
{"domain":"bwsofficefurniture.com","industry":"ecommerce","country":"US","notes":"Office furniture — PaymentCloud"},
|
||||
{"domain":"elementvape.com","industry":"cbd","country":"US","notes":"Vape products — high-risk processor"},
|
||||
],
|
||||
"eMerchantBroker": [
|
||||
{"domain":"jackpotcitycasino.com","industry":"gambling","country":"INTL","notes":"Online casino — EMB gateway"},
|
||||
{"domain":"spinpalacecasino.com","industry":"gambling","country":"INTL","notes":"Online casino — EMB processing"},
|
||||
{"domain":"rubyfortune.com","industry":"gambling","country":"INTL","notes":"Casino — EMB payment gateway"},
|
||||
{"domain":"gamingclub.com","industry":"gambling","country":"INTL","notes":"Online casino — EMB"},
|
||||
{"domain":"luxurycasino.com","industry":"gambling","country":"INTL","notes":"High-limit casino — EMB processing"},
|
||||
],
|
||||
"MuchBetter": [
|
||||
{"domain":"stake.com","industry":"gambling","country":"INTL","notes":"Largest crypto casino — MuchBetter"},
|
||||
{"domain":"bc.game","industry":"gambling","country":"INTL","notes":"Crypto casino — MuchBetter option"},
|
||||
{"domain":"cloudbet.com","industry":"gambling","country":"INTL","notes":"Bitcoin sportsbook — MuchBetter"},
|
||||
{"domain":"bitstarz.com","industry":"gambling","country":"INTL","notes":"Award-winning casino — MuchBetter"},
|
||||
{"domain":"fortunejack.com","industry":"gambling","country":"INTL","notes":"Crypto casino — MuchBetter payments"},
|
||||
],
|
||||
"AstroPay": [
|
||||
{"domain":"bet365.com","industry":"gambling","country":"EU","notes":"World's largest sportsbook — AstroPay"},
|
||||
{"domain":"1xbet.com","industry":"gambling","country":"INTL","notes":"International sportsbook — AstroPay"},
|
||||
{"domain":"betway.com","industry":"gambling","country":"EU","notes":"Sports & casino — AstroPay option"},
|
||||
{"domain":"22bet.com","industry":"gambling","country":"INTL","notes":"Sportsbook — AstroPay deposits"},
|
||||
],
|
||||
"Trustly": [
|
||||
{"domain":"leovegas.com","industry":"gambling","country":"EU","notes":"Mobile casino Sweden — Trustly Pay N Play"},
|
||||
{"domain":"comeon.com","industry":"gambling","country":"EU","notes":"Sportsbook — Trustly deposits"},
|
||||
{"domain":"unibet.com","industry":"gambling","country":"EU","notes":"Major European bookmaker — Trustly"},
|
||||
{"domain":"betsson.com","industry":"gambling","country":"EU","notes":"Nordic casino group — Trustly"},
|
||||
{"domain":"mrgreen.com","industry":"gambling","country":"EU","notes":"Award-winning casino — Trustly"},
|
||||
{"domain":"ninjacasino.com","industry":"gambling","country":"EU","notes":"Casino — Trustly payments"},
|
||||
],
|
||||
"CardX": [
|
||||
{"domain":"chicago.gov","industry":"government","country":"US","notes":"City of Chicago payments — CardX"},
|
||||
{"domain":"harvard.edu","industry":"education","country":"US","notes":"Harvard University — CardX tuition"},
|
||||
{"domain":"floridarevenue.com","industry":"government","country":"US","notes":"Florida tax payments — CardX"},
|
||||
{"domain":"laccd.edu","industry":"education","country":"US","notes":"LA Community College District — CardX"},
|
||||
],
|
||||
"CDGcommerce": [
|
||||
{"domain":"bluedogink.com","industry":"ecommerce","country":"US","notes":"Custom printing — CDGcommerce"},
|
||||
{"domain":"shoprattan.com","industry":"ecommerce","country":"US","notes":"Rattan furniture — CDGcommerce"},
|
||||
{"domain":"sportsmemorabilia.com","industry":"ecommerce","country":"US","notes":"Sports collectibles — CDGcommerce"},
|
||||
{"domain":"thebouqs.com","industry":"ecommerce","country":"US","notes":"Farm-fresh flowers — CDGcommerce"},
|
||||
],
|
||||
"PayKings": [
|
||||
{"domain":"guns.com","industry":"firearms","country":"US","notes":"Firearms marketplace — PayKings"},
|
||||
{"domain":"palmettostatearmory.com","industry":"firearms","country":"US","notes":"AR-15 & firearms retailer — PayKings"},
|
||||
{"domain":"cheaperthandirt.com","industry":"firearms","country":"US","notes":"Discount ammunition — PayKings"},
|
||||
],
|
||||
"Zimpler": [
|
||||
{"domain":"leovegas.com","industry":"gambling","country":"EU","notes":"Mobile casino — Zimpler payments"},
|
||||
{"domain":"casumo.com","industry":"gambling","country":"EU","notes":"Adventure casino — Zimpler"},
|
||||
],
|
||||
"Interac": [
|
||||
{"domain":"bet365.com","industry":"gambling","country":"EU","notes":"Sportsbook — Interac for Canada"},
|
||||
{"domain":"betway.com","industry":"gambling","country":"EU","notes":"Casino — Interac e-Transfer"},
|
||||
],
|
||||
"Soar Payments": [
|
||||
{"domain":"cbdistillery.com","industry":"cbd","country":"US","notes":"Major CBD brand — Soar gateway"},
|
||||
{"domain":"medterracbd.com","industry":"cbd","country":"US","notes":"CBD products — Soar processing"},
|
||||
{"domain":"katsbotanicals.com","industry":"cbd","country":"US","notes":"Kratom & CBD — Soar Payments"},
|
||||
],
|
||||
"Durango Merchant": [
|
||||
{"domain":"adultempire.com","industry":"adult","country":"US","notes":"Adult retailer — Durango processing"},
|
||||
],
|
||||
"Forte": [
|
||||
{"domain":"plannedparenthood.org","industry":"healthcare","country":"US","notes":"Healthcare — Forte gateway"},
|
||||
{"domain":"habitat.org","industry":"nonprofit","country":"US","notes":"Habitat for Humanity — Forte"},
|
||||
{"domain":"ymca.net","industry":"nonprofit","country":"US","notes":"YMCA — Forte processing"},
|
||||
],
|
||||
}
|
||||
|
||||
COUNTRIES = {"US": "🇺🇸 United States", "EU": "🇪🇺 Europe", "INTL": "🌍 International"}
|
||||
|
||||
def search_duckduckgo(query, max_results=6):
|
||||
url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
|
||||
req = urllib.request.Request(url, headers={'User-Agent': USER_AGENT})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=12)
|
||||
html = resp.read().decode('utf-8', errors='ignore')
|
||||
except:
|
||||
return []
|
||||
if 'captcha' in html.lower() or 'sorry' in html.lower():
|
||||
return []
|
||||
links = re.findall(r'<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)</a>', html)
|
||||
snippets = re.findall(r'<a[^>]*class="result__snippet"[^>]*>([^<]+(?:<[^>]+>[^<]*)*)</a>', html)
|
||||
results = []
|
||||
for i, (url, title) in enumerate(links[:max_results]):
|
||||
url = urllib.parse.unquote(url)
|
||||
if 'duckduckgo.com' in url or not url.startswith('http'): continue
|
||||
domain = urllib.parse.urlparse(url).netloc.replace('www.', '')
|
||||
snippet = re.sub(r'<[^>]+>', '', snippets[i]) if i < len(snippets) else ''
|
||||
results.append({'url': url, 'domain': domain, 'title': title.strip(), 'snippet': snippet.strip()})
|
||||
return results
|
||||
|
||||
def get_from_cache(processor_name, industry=None, country=None):
|
||||
cache = SITE_CACHE.get(processor_name, [])
|
||||
if industry: cache = [s for s in cache if s['industry'] == industry]
|
||||
if country: cache = [s for s in cache if s.get('country') == country]
|
||||
proc = PROCESSORS['processors'].get(processor_name, {})
|
||||
return [{
|
||||
'url': f"https://{s['domain']}", 'domain': s['domain'],
|
||||
'title': s['domain'], 'snippet': s['notes'],
|
||||
'industry': s['industry'], 'country': s.get('country', 'US'),
|
||||
'processor': processor_name, 'processor_type': proc.get('type', ''), 'cached': True
|
||||
} for s in cache]
|
||||
|
||||
def search_processor(processor_name, industry=None, country=None, max_results=12):
|
||||
proc = PROCESSORS['processors'].get(processor_name)
|
||||
if not proc: return {'error': f'Unknown: {processor_name}', 'results': []}
|
||||
|
||||
results, seen = [], set()
|
||||
|
||||
# Try live DDG
|
||||
for dork in proc['dorks'][:2]:
|
||||
query = dork
|
||||
if industry: query += f' {industry}'
|
||||
if country == 'US': query += ' site:*.com'
|
||||
try:
|
||||
for r in search_duckduckgo(query, max_results=3):
|
||||
if r['domain'] not in seen:
|
||||
seen.add(r['domain'])
|
||||
r['processor'] = processor_name
|
||||
r['processor_type'] = proc['type']
|
||||
r['industry'] = industry or proc['industries'][0]
|
||||
r['country'] = country or 'US'
|
||||
r['cached'] = False
|
||||
results.append(r)
|
||||
except: pass
|
||||
time.sleep(0.5)
|
||||
|
||||
# Cache fallback
|
||||
for c in get_from_cache(processor_name, industry, country):
|
||||
if c['domain'] not in seen and len(results) < max_results:
|
||||
seen.add(c['domain'])
|
||||
results.append(c)
|
||||
|
||||
return {
|
||||
'processor': processor_name, 'type': proc['type'],
|
||||
'industries': proc['industries'], 'dorks': proc['dorks'],
|
||||
'result_count': len(results),
|
||||
'live_results': sum(1 for r in results if not r.get('cached')),
|
||||
'cached_results': sum(1 for r in results if r.get('cached')),
|
||||
'results': results[:max_results]
|
||||
}
|
||||
|
||||
def search_all(industry=None, country=None, max_per_processor=6):
|
||||
all_results = {}
|
||||
for name in PROCESSORS['processors']:
|
||||
if industry and industry not in PROCESSORS['processors'][name]['industries']: continue
|
||||
result = search_processor(name, industry, country, max_per_processor)
|
||||
all_results[name] = result
|
||||
time.sleep(0.2)
|
||||
return all_results
|
||||
289
static/index.html
Normal file
289
static/index.html
Normal file
@@ -0,0 +1,289 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment Recon — Search Engine</title>
|
||||
<style>
|
||||
:root{--bg:#06060e;--card:#0e0e1a;--accent:#f7931a;--accent2:#00d4aa;--accent3:#6c63ff;--text:#e0e0e0;--muted:#777;--danger:#ff4757;--success:#2ed573;--warning:#ffa502;--radius:12px}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);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(108,99,255,0.03) 0%,transparent 50%),radial-gradient(circle at 70% 80%,rgba(247,147,26,0.03) 0%,transparent 50%);pointer-events:none;z-index:0}
|
||||
|
||||
.app{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:1rem}
|
||||
|
||||
/* Header */
|
||||
header{text-align:center;padding:2rem 1rem 1.5rem}
|
||||
header h1{font-size:2rem;font-weight:800;background:linear-gradient(135deg,var(--accent),var(--accent2),var(--accent3));-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.5px}
|
||||
header p{color:var(--muted);font-size:0.85rem;margin-top:0.3rem}
|
||||
|
||||
/* Search Bar */
|
||||
.search-bar{background:var(--card);border-radius:var(--radius);padding:0.7rem;display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;border:1px solid rgba(255,255,255,0.06);margin-bottom:1.5rem;position:sticky;top:1rem;z-index:10;backdrop-filter:blur(10px)}
|
||||
.search-bar select,.search-bar button{background:rgba(0,0,0,0.4);color:var(--text);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:0.5rem 0.8rem;font-size:0.78rem;cursor:pointer}
|
||||
.search-bar select{min-width:150px}
|
||||
.search-bar select:hover,.search-bar button:hover{border-color:var(--accent)}
|
||||
.search-bar .btn-search{background:linear-gradient(135deg,var(--accent),#e6730d);color:#000;border:none;font-weight:700;padding:0.5rem 1.5rem;font-size:0.82rem}
|
||||
.search-bar .btn-search:hover{filter:brightness(1.15)}
|
||||
.search-bar .btn-search:disabled{opacity:0.5;cursor:not-allowed}
|
||||
.search-bar .btn-scan{background:linear-gradient(135deg,var(--accent3),#8b5cf6);color:#fff;border:none;font-weight:700;padding:0.5rem 1.2rem;font-size:0.82rem}
|
||||
|
||||
/* Stats Bar */
|
||||
.stats-bar{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;font-size:0.75rem;color:var(--muted)}
|
||||
.stats-bar strong{color:var(--accent2)}
|
||||
.stat-pill{background:var(--card);padding:0.3rem 0.7rem;border-radius:20px;border:1px solid rgba(255,255,255,0.05);font-size:0.68rem}
|
||||
|
||||
/* Results */
|
||||
.results-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0.8rem}
|
||||
.site-card{background:var(--card);border-radius:var(--radius);overflow:hidden;border:1px solid rgba(255,255,255,0.05);transition:all 0.25s;display:flex;flex-direction:column}
|
||||
.site-card:hover{transform:translateY(-2px);border-color:rgba(255,255,255,0.12);box-shadow:0 8px 30px rgba(0,0,0,0.3)}
|
||||
.site-card .screenshot{height:180px;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
|
||||
.site-card .screenshot img{width:100%;height:100%;object-fit:cover}
|
||||
.site-card .screenshot .placeholder{font-size:2.5rem;opacity:0.3}
|
||||
.site-card .body{padding:0.9rem;flex:1;display:flex;flex-direction:column}
|
||||
.site-card .domain{font-weight:700;font-size:0.9rem;color:var(--accent2);margin-bottom:0.15rem}
|
||||
.site-card .domain a{color:inherit;text-decoration:none}
|
||||
.site-card .domain a:hover{text-decoration:underline}
|
||||
.site-card .desc{font-size:0.72rem;color:var(--muted);line-height:1.4;margin:0.4rem 0;flex:1}
|
||||
.site-card .tags{margin-top:0.4rem;display:flex;gap:0.3rem;flex-wrap:wrap}
|
||||
.tag{display:inline-block;padding:0.15rem 0.45rem;border-radius:3px;font-size:0.6rem;font-weight:600}
|
||||
.t-type{background:rgba(108,99,255,0.2);color:#a78bfa}
|
||||
.t-industry{background:rgba(247,147,26,0.2);color:var(--accent)}
|
||||
.t-country{background:rgba(0,212,170,0.15);color:var(--accent2);font-size:0.55rem}
|
||||
.t-US{background:rgba(59,130,246,0.2);color:#60a5fa}
|
||||
.t-EU{background:rgba(168,85,247,0.2);color:#a78bfa}
|
||||
.t-INTL{background:rgba(251,191,36,0.2);color:#fbbf24}
|
||||
.t-gambling{background:rgba(255,71,87,0.2);color:var(--danger)}
|
||||
.t-ecommerce{background:rgba(0,212,170,0.2);color:var(--accent2)}
|
||||
.t-adult{background:rgba(247,147,26,0.2);color:var(--accent)}
|
||||
.t-electronics{background:rgba(59,130,246,0.2);color:#60a5fa}
|
||||
.site-card .actions{margin-top:0.5rem;display:flex;gap:0.3rem}
|
||||
.btn-sm{padding:0.25rem 0.6rem;border-radius:4px;border:none;cursor:pointer;font-size:0.65rem;font-weight:600}
|
||||
.btn-visit{background:var(--accent2);color:#000}
|
||||
.btn-visit:hover{filter:brightness(1.2)}
|
||||
.btn-screenshot{background:rgba(108,99,255,0.2);color:#a78bfa;border:1px solid rgba(108,99,255,0.3)}
|
||||
.btn-screenshot:hover{background:rgba(108,99,255,0.3)}
|
||||
|
||||
/* Loader */
|
||||
.loader{text-align:center;padding:2rem}
|
||||
.spinner{display:inline-block;width:30px;height:30px;border:3px solid rgba(255,255,255,0.1);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* Empty */
|
||||
.empty{text-align:center;padding:3rem;color:var(--muted)}
|
||||
.empty .icon{font-size:4rem;margin-bottom:1rem;opacity:0.3}
|
||||
|
||||
/* Responsive */
|
||||
@media(max-width:768px){.results-grid{grid-template-columns:1fr}.search-bar{flex-direction:column;align-items:stretch}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header>
|
||||
<h1>🔍 Payment Recon</h1>
|
||||
<p>Search the web for payment processors. Screenshots. AI descriptions. Organized by industry.</p>
|
||||
</header>
|
||||
|
||||
<div class="search-bar">
|
||||
<select id="processor-select">
|
||||
<option value="">🎯 Select Processor</option>
|
||||
</select>
|
||||
<select id="industry-select">
|
||||
<option value="">🏷️ All Industries</option>
|
||||
</select>
|
||||
<select id="country-select">
|
||||
<option value="US" selected>🇺🇸 US Only</option>
|
||||
<option value="">🌍 All Countries</option>
|
||||
<option value="EU">🇪🇺 Europe</option>
|
||||
<option value="INTL">🌍 International</option>
|
||||
</select>
|
||||
<button class="btn-search" onclick="doSearch()" id="btn-search">🔍 Search</button>
|
||||
<button class="btn-scan" onclick="scanAll()" id="btn-scan">⚡ Scan All Processors</button>
|
||||
<span style="font-size:0.68rem;color:var(--muted);margin-left:auto;" id="status-text">Ready</span>
|
||||
</div>
|
||||
|
||||
<div class="stats-bar" id="stats-bar"></div>
|
||||
<div class="results-grid" id="results"></div>
|
||||
<div class="loader" id="loader" style="display:none"><div class="spinner"></div><p style="margin-top:0.5rem;font-size:0.75rem;color:var(--muted);">Searching...</p></div>
|
||||
|
||||
<div id="screenshot-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:100;align-items:center;justify-content:center;" onclick="this.style.display='none'">
|
||||
<img id="screenshot-img" style="max-width:90vw;max-height:90vh;border-radius:8px;box-shadow:0 20px 60px rgba(0,0,0,0.5);">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allProcessors = {};
|
||||
let allIndustries = [];
|
||||
let currentResults = [];
|
||||
|
||||
function safeId(str){return 'id-'+btoa(unescape(encodeURIComponent(str))).replace(/[+/=]/g,'')}
|
||||
|
||||
async function init(){
|
||||
const r = await fetch('/api/processors');
|
||||
const data = await r.json();
|
||||
allProcessors = data.processors;
|
||||
|
||||
// Populate processor dropdown
|
||||
const ps = document.getElementById('processor-select');
|
||||
for(const [name, info] of Object.entries(allProcessors)){
|
||||
ps.innerHTML += `<option value="${name}">${name} (${info.type})</option>`;
|
||||
}
|
||||
|
||||
// Populate industry dropdown
|
||||
allIndustries = data.industries;
|
||||
const is = document.getElementById('industry-select');
|
||||
for(const ind of allIndustries){
|
||||
is.innerHTML += `<option value="${ind}">${ind}</option>`;
|
||||
}
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = `
|
||||
<span class="stat-pill"><strong>${Object.keys(allProcessors).length}</strong> processors</span>
|
||||
<span class="stat-pill"><strong>${allIndustries.length}</strong> industries</span>
|
||||
`;
|
||||
}
|
||||
|
||||
async function doSearch(){
|
||||
const proc = document.getElementById('processor-select').value;
|
||||
if(!proc) return alert('Select a processor first');
|
||||
|
||||
const industry = document.getElementById('industry-select').value;
|
||||
const country = document.getElementById('country-select').value;
|
||||
showLoader(true);
|
||||
document.getElementById('status-text').textContent = `Searching for ${proc}...`;
|
||||
|
||||
const url = `/api/search/${encodeURIComponent(proc)}?industry=${encodeURIComponent(industry)}&country=${encodeURIComponent(country)}&max=12`;
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
|
||||
currentResults = data.results || [];
|
||||
renderResults(currentResults, proc);
|
||||
showLoader(false);
|
||||
document.getElementById('status-text').textContent = `${data.result_count} results for ${proc}`;
|
||||
}
|
||||
|
||||
async function scanAll(){
|
||||
if(!confirm('Scan all processors? This may take a minute.')) return;
|
||||
showLoader(true);
|
||||
|
||||
const industry = document.getElementById('industry-select').value;
|
||||
document.getElementById('status-text').textContent = 'Scanning all processors...';
|
||||
|
||||
const url = `/api/search-all?industry=${encodeURIComponent(industry)}&country=${encodeURIComponent(country)}`;
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
|
||||
let all = [];
|
||||
for(const [name, result] of Object.entries(data)){
|
||||
if(result.results){
|
||||
all = all.concat(result.results);
|
||||
}
|
||||
}
|
||||
|
||||
currentResults = all;
|
||||
renderResults(currentResults, 'All Processors');
|
||||
showLoader(false);
|
||||
document.getElementById('status-text').textContent = `${all.length} results across all processors`;
|
||||
}
|
||||
|
||||
function renderResults(results, procName){
|
||||
const grid = document.getElementById('results');
|
||||
if(!results.length){
|
||||
grid.innerHTML = `<div class="empty"><div class="icon">🔍</div><p>No results found for ${procName}.</p><p style="font-size:0.7rem;margin-top:0.2rem;">Try a different processor or industry.</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = results.map(s => {
|
||||
const domain = s.domain || (()=>{try{return new URL(s.url||'').hostname.replace('www.','')}catch{return s.domain||'unknown'}})();
|
||||
const proc = s.processor || procName;
|
||||
const tags = [
|
||||
`<span class="tag t-type">${s.processor_type||''}</span>`,
|
||||
`<span class="tag t-industry t-${s.industry||'ecommerce'}">${s.industry||'ecommerce'}</span>`,
|
||||
`<span class="tag t-country t-${s.country||'US'}">${s.country||'US'}</span>`
|
||||
].join('');
|
||||
|
||||
return `
|
||||
<div class="site-card">
|
||||
<div class="screenshot" id="ss-${safeId(domain)}">
|
||||
<span class="placeholder">🌐</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="domain"><a href="${s.url||'https://'+domain}" target="_blank">${domain}</a></div>
|
||||
<div class="desc">${s.snippet||s.description||'Visit site for details.'}</div>
|
||||
<div class="tags">${tags}</div>
|
||||
<div class="actions">
|
||||
<button class="btn-sm btn-visit" onclick="window.open('${s.url||'https://'+domain}','_blank')">Visit →</button>
|
||||
<button class="btn-sm btn-screenshot" onclick="takeScreenshot('${domain}','${s.url||'https://'+domain}','${safeId(domain)}')">📸 Screenshot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = `
|
||||
<span class="stat-pill"><strong>${results.length}</strong> results</span>
|
||||
<span class="stat-pill">Processor: <strong>${procName}</strong></span>
|
||||
${[...new Set(results.map(r=>r.industry))].map(i=>`<span class="stat-pill">${i}</span>`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
async function takeScreenshot(domain, url, id){
|
||||
const el = document.getElementById('ss-'+id);
|
||||
el.innerHTML = '<div class="spinner" style="width:20px;height:20px;"></div>';
|
||||
|
||||
try{
|
||||
// Queue analysis job
|
||||
const r = await fetch('/api/analyze', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({sites:[{domain,url}],processor:currentResults[0]?.processor||'Unknown'})
|
||||
});
|
||||
const {job_id} = await r.json();
|
||||
|
||||
// Poll for result
|
||||
let attempts=0;
|
||||
while(attempts<30){
|
||||
await new Promise(r=>setTimeout(r,1000));
|
||||
const jr = await fetch(`/api/job/${job_id}`);
|
||||
const job = await jr.json();
|
||||
if(job.status==='complete'){
|
||||
const result = job.results[0];
|
||||
if(result?.description){
|
||||
// Update description
|
||||
for(const card of document.querySelectorAll('.site-card')){
|
||||
if(card.querySelector('.domain a')?.textContent === domain){
|
||||
card.querySelector('.desc').textContent = result.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(result?.screenshot_base64){
|
||||
el.innerHTML = `<img src="data:image/png;base64,${result.screenshot_base64}" style="width:100%;height:100%;object-fit:cover" onclick="event.stopPropagation();showFullScreenshot('${result.screenshot_base64}')">`;
|
||||
} else {
|
||||
el.innerHTML = '<span class="placeholder">📸</span>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
if(attempts>=30) el.innerHTML = '<span class="placeholder">⏰</span>';
|
||||
}catch(e){
|
||||
el.innerHTML = '<span class="placeholder">❌</span>';
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function showLoader(show){document.getElementById('loader').style.display=show?'block':'none'}
|
||||
|
||||
function showFullScreenshot(b64){
|
||||
const modal = document.getElementById('screenshot-modal');
|
||||
const img = document.getElementById('screenshot-img');
|
||||
img.src = `data:image/png;base64,${b64}`;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
<div style="text-align:center;padding:1rem;margin-top:1.5rem;border-top:1px solid rgba(255,255,255,0.05)">
|
||||
<a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.45rem 1.1rem;border-radius:25px;text-decoration:none;font-weight:600;font-size:0.75rem;transition:all 0.2s;box-shadow:0 3px 12px rgba(255,94,14,0.25)" onmouseover="this.style.transform='scale(1.04)';this.style.boxShadow='0 5px 18px rgba(255,94,14,0.4)'" onmouseout="this.style.transform='scale(1)';this.style.boxShadow='0 3px 12px rgba(255,94,14,0.25)'">
|
||||
<span style="font-size:1rem">☕</span> Support This Project — Buy Me a Coffee
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
87
vision.py
Normal file
87
vision.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Screenshot & Vision Analysis — takes screenshots and classifies with local LLM."""
|
||||
import base64, json, subprocess, tempfile, os, urllib.request, time
|
||||
|
||||
OLLAMA_URL = "http://10.30.20.186:11434"
|
||||
VISION_MODEL = "minicpm-v4.5:8b" # Best local vision model available
|
||||
|
||||
def take_screenshot(url, width=1280, height=900):
|
||||
"""Take a screenshot of a URL using Playwright."""
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page(viewport={'width': width, 'height': height})
|
||||
page.goto(url, wait_until='networkidle', timeout=15000)
|
||||
time.sleep(1) # Let page settle
|
||||
screenshot = page.screenshot(full_page=False)
|
||||
browser.close()
|
||||
return screenshot
|
||||
except ImportError:
|
||||
# Fallback: use macos screencapture via Safari (less reliable)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Screenshot error for {url}: {e}")
|
||||
return None
|
||||
|
||||
def screenshot_to_base64(screenshot_bytes):
|
||||
"""Convert screenshot bytes to base64 for Ollama."""
|
||||
return base64.b64encode(screenshot_bytes).decode('utf-8')
|
||||
|
||||
def analyze_screenshot(url, screenshot_bytes, processor_name):
|
||||
"""Analyze a screenshot using local vision model to describe the site."""
|
||||
if not screenshot_bytes:
|
||||
return generate_placeholder_description(url, processor_name)
|
||||
|
||||
img_b64 = screenshot_to_base64(screenshot_bytes)
|
||||
|
||||
prompt = f"""Look at this screenshot of a website. The site likely uses {processor_name} as a payment processor.
|
||||
Describe this site in 2-3 sentences covering:
|
||||
1. What type of site is this? (e-commerce, gambling, SaaS, adult, CBD, firearms, subscription, crypto, etc.)
|
||||
2. What does it sell or offer?
|
||||
3. What's the overall vibe/quality level?
|
||||
|
||||
Respond with just the description, no prefixes."""
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{OLLAMA_URL}/api/generate",
|
||||
data=json.dumps({
|
||||
"model": VISION_MODEL,
|
||||
"prompt": prompt,
|
||||
"images": [img_b64],
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.3, "num_predict": 150}
|
||||
}).encode(),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
data = json.loads(resp.read())
|
||||
return data.get('response', '').strip()
|
||||
except Exception as e:
|
||||
print(f"Vision analysis error: {e}")
|
||||
return generate_placeholder_description(url, processor_name)
|
||||
|
||||
def generate_placeholder_description(url, processor_name):
|
||||
"""Generate a description when vision isn't available."""
|
||||
domain = urllib.parse.urlparse(url).netloc.replace('www.', '')
|
||||
return f"Website at {domain}. Uses {processor_name} for payment processing. Visit the site to learn more about their products and services."
|
||||
|
||||
def batch_analyze(sites, processor_name, max_screenshots=5):
|
||||
"""Analyze multiple sites with screenshots."""
|
||||
results = []
|
||||
for i, site in enumerate(sites[:max_screenshots]):
|
||||
url = site.get('url', '')
|
||||
if not url:
|
||||
continue
|
||||
|
||||
print(f" Screenshot {i+1}/{min(len(sites), max_screenshots)}: {site.get('domain','?')}")
|
||||
screenshot = take_screenshot(url)
|
||||
description = analyze_screenshot(url, screenshot, processor_name)
|
||||
|
||||
results.append({
|
||||
**site,
|
||||
'description': description,
|
||||
'screenshot_available': screenshot is not None,
|
||||
'screenshot_base64': screenshot_to_base64(screenshot) if screenshot else None
|
||||
})
|
||||
return results
|
||||
Reference in New Issue
Block a user