From 24a36eec205a46409648d4838e04f79bb89c5f06 Mon Sep 17 00:00:00 2001 From: drjones Date: Tue, 6 Oct 2026 23:43:40 -0700 Subject: [PATCH] Snapshot: full project state --- .gitignore | 17 +++ README.md | 7 ++ app.py | 81 +++++++++++++ processors.json | 21 ++++ search_engine.py | 199 +++++++++++++++++++++++++++++++ static/index.html | 289 ++++++++++++++++++++++++++++++++++++++++++++++ vision.py | 87 ++++++++++++++ 7 files changed, 701 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 processors.json create mode 100644 search_engine.py create mode 100644 static/index.html create mode 100644 vision.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24b2937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +__pycache__/ +*.pyc +node_modules/ +.venv/ +venv/ +.env +*.db +*.sqlite* +*.log +.DS_Store +out/ +work/ +.pio/ +briefs/ +dns-backup/ +archive/ +*.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b5ec50 --- /dev/null +++ b/README.md @@ -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. + diff --git a/app.py b/app.py new file mode 100644 index 0000000..22d642d --- /dev/null +++ b/app.py @@ -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/') +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/') +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) diff --git a/processors.json b/processors.json new file mode 100644 index 0000000..81221e2 --- /dev/null +++ b/processors.json @@ -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"] +} diff --git a/search_engine.py b/search_engine.py new file mode 100644 index 0000000..b9de55a --- /dev/null +++ b/search_engine.py @@ -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']*class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)', html) + snippets = re.findall(r']*class="result__snippet"[^>]*>([^<]+(?:<[^>]+>[^<]*)*)', 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 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..3994b51 --- /dev/null +++ b/static/index.html @@ -0,0 +1,289 @@ + + + + + +Payment Recon β€” Search Engine + + + +
+
+

πŸ” Payment Recon

+

Search the web for payment processors. Screenshots. AI descriptions. Organized by industry.

+
+ + + +
+
+ + + +
+ + + + + diff --git a/vision.py b/vision.py new file mode 100644 index 0000000..42830ee --- /dev/null +++ b/vision.py @@ -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