#!/usr/bin/env python3
"""
AI Research Engine — Commercial Backend
Postgres auth, BTCPay payments, API key system, premium UI.
"""
import os, json, hashlib, re, html as html_mod, random, secrets, time
from datetime import datetime, timezone
from urllib.parse import urlparse
import httpx
import psycopg2
import psycopg2.extras
from fastapi import FastAPI, Query, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
import uvicorn
app = FastAPI(title="AI Research Engine")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# ── Config ──────────────────────────────────────────────────
YACY_URL = os.getenv("YACY_URL", "http://host.docker.internal:8090")
OPENSEARCH_URL = os.getenv("OPENSEARCH_URL", "http://host.docker.internal:9200")
QDRANT_URL = os.getenv("QDRANT_URL", "http://10.30.20.68:6333")
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://10.30.20.186:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "ornith:latest")
INDEX_NAME = os.getenv("INDEX_NAME", "research_docs")
BTCPAY_URL = "https://10.30.20.140"
BTCPAY_KEY = "6026288e2e315984661c748baafd509e81a75f22"
BTCPAY_STORE = "8ERS6v1UyQ46bQqaWr4LotvMCJLsmUhH2sT8zbzfjkT6"
WEBHOOK_SECRET = "XjfhDd9DzXsUkA91B4SwRz"
PRICE_USD = 5.00 # $5
CALLS_PER_TIER = 5 # 5 API calls
# Proxies
PROXIES = [
"http://10.30.20.154:3128", # Tokyo
"http://10.30.20.71:3128", # London
"http://10.30.20.189:3128", # Sydney
]
# DB
def get_db():
return psycopg2.connect(
host="host.docker.internal", port=5432,
user="research", password="ResearchDB2026!",
database="research_engine"
)
client = httpx.Client(timeout=30.0)
ollama = httpx.Client(timeout=120.0, base_url=OLLAMA_URL)
# ── Auth Helpers ─────────────────────────────────────────────
def _auth(request: Request) -> dict:
"""Authenticate by API key, return user row or raise 401."""
api_key = request.headers.get("X-API-Key") or request.query_params.get("api_key")
if not api_key:
raise HTTPException(401, "API key required. Get one at /signup or /pricing.")
db = get_db()
cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute("SELECT * FROM users WHERE api_key = %s", (api_key,))
user = cur.fetchone()
db.close()
if not user:
raise HTTPException(401, "Invalid API key.")
if not user["is_admin"] and user["calls_remaining"] <= 0:
raise HTTPException(402, "No calls remaining. Buy more at /pricing.")
return dict(user)
def _track_usage(user_id: int, tool: str, request: Request):
"""Deduct a call and log usage."""
db = get_db()
cur = db.cursor()
cur.execute("UPDATE users SET calls_remaining = calls_remaining - 1, total_calls = total_calls + 1, last_used_at = NOW() WHERE id = %s AND NOT is_admin", (user_id,))
cur.execute("INSERT INTO usage_log (user_id, tool_name, endpoint, ip_address) VALUES (%s,%s,%s,%s)",
(user_id, tool, str(request.url), request.client.host if request.client else ""))
db.commit()
db.close()
# ── Search Helpers (unchanged from v1) ────────────────────────
def _ensure_index():
try: client.get(f"{OPENSEARCH_URL}/{INDEX_NAME}")
except: client.put(f"{OPENSEARCH_URL}/{INDEX_NAME}", json={"settings":{"number_of_shards":1,"number_of_replicas":0},"mappings":{"properties":{"url":{"type":"keyword"},"title":{"type":"text"},"content":{"type":"text"},"excerpt":{"type":"text"},"category":{"type":"keyword"},"source_domain":{"type":"keyword"},"crawled_at":{"type":"date"},"indexed_at":{"type":"date"},"metadata":{"type":"object"}}}})
def _ai_chat(prompt: str, system: str = "You are a research assistant. Be concise and factual.") -> str:
r = ollama.post("/api/chat", json={"model":OLLAMA_MODEL,"messages":[{"role":"system","content":system},{"role":"user","content":prompt}],"stream":False,"options":{"temperature":0.3,"num_predict":2048}})
body = r.json()
return body.get("message",{}).get("content","") or body.get("thinking","") or ""
def _get_embedding(text: str) -> list:
try:
r = ollama.post("/api/embeddings", json={"model":"nomic-embed-text-v2-moe:latest","prompt":text[:2048]})
return r.json().get("embedding",[])
except: return []
def _fetch_and_index(url: str, category: str = ""):
proxy_url = random.choice(PROXIES)
try:
pc = httpx.Client(proxy=proxy_url, timeout=15.0)
r = pc.get(url, headers={"User-Agent":"Mozilla/5.0 (compatible; ResearchBot/1.0)"})
if r.status_code != 200: return None
html_text = r.text
text = re.sub(r'','',html_text,flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r'','',text,flags=re.DOTALL|re.IGNORECASE)
text = re.sub(r'<[^>]+>',' ',text)
text = re.sub(r'\s+',' ',text).strip()
text = html_mod.unescape(text)
title_m = re.search(r'
]*>(.*?)',html_text,re.IGNORECASE|re.DOTALL)
title = html_mod.unescape(title_m.group(1).strip()) if title_m else url
desc_m = re.search(r']+name=["\']description["\'][^>]+content=["\']([^"\']+)',html_text,re.IGNORECASE)
excerpt = desc_m.group(1)[:500] if desc_m else text[:500]
domain = urlparse(url).netloc
if not category:
for k,v in {"wikipedia":"reference","github":"software","arxiv":"science","docs.":"documentation","blog.":"blog","news.":"news"}.items():
if k in domain: category = v; break
if not category: category = "web"
doc = {"url":url,"title":title,"content":text[:50000],"excerpt":excerpt[:1000],"category":category,"source_domain":domain,"crawled_at":datetime.now(timezone.utc).isoformat(),"indexed_at":datetime.now(timezone.utc).isoformat()}
_ensure_index()
client.put(f"{OPENSEARCH_URL}/{INDEX_NAME}/_doc/{hashlib.md5(url.encode()).hexdigest()}",json=doc,params={"refresh":"true"})
emb = _get_embedding(excerpt[:1000])
if emb:
try:
client.put(f"{QDRANT_URL}/collections/{INDEX_NAME}/points",json={"points":[{"id":hashlib.md5(url.encode()).hexdigest(),"vector":emb,"payload":{"url":url,"title":title,"excerpt":excerpt[:500]}}]})
except: pass
return {"title":title,"domain":domain,"category":category,"size":len(text)}
except: return None
# ═══════════════════════════════════════════════════════════════
# PUBLIC PAGES
# ═══════════════════════════════════════════════════════════════
@app.get("/", response_class=HTMLResponse)
def landing():
return HTMLResponse(LANDING_HTML)
@app.get("/pricing", response_class=HTMLResponse)
def pricing_page():
return HTMLResponse(PRICING_HTML)
@app.get("/signup", response_class=HTMLResponse)
def signup_page():
return HTMLResponse(SIGNUP_HTML)
@app.get("/dashboard", response_class=HTMLResponse)
def dashboard_page():
return HTMLResponse(DASHBOARD_HTML)
# ═══════════════════════════════════════════════════════════════
# AUTH API
# ═══════════════════════════════════════════════════════════════
@app.post("/api/signup")
async def api_signup(request: Request):
data = await request.json()
email = data.get("email","").strip().lower()
if not email or "@" not in email:
return JSONResponse({"error":"Valid email required"}, 400)
api_key = "sk-" + secrets.token_hex(24)
db = get_db()
cur = db.cursor()
try:
cur.execute("INSERT INTO users (email, password_hash, api_key, calls_remaining) VALUES (%s,%s,%s,0)", (email, "bcrypt_placeholder", api_key))
db.commit()
except psycopg2.errors.UniqueViolation:
db.rollback()
cur.execute("SELECT api_key FROM users WHERE email = %s", (email,))
api_key = cur.fetchone()[0]
db.close()
return {"email": email, "api_key": api_key, "message": "Signup successful. Purchase API calls at /pricing to start using the engine."}
@app.get("/api/my-usage")
async def my_usage(request: Request):
# Allow even with 0 calls — users need to see their balance
api_key = request.headers.get("X-API-Key") or request.query_params.get("api_key")
if not api_key:
raise HTTPException(401, "API key required.")
db = get_db()
cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute("SELECT * FROM users WHERE api_key = %s", (api_key,))
user = cur.fetchone()
if not user:
db.close()
raise HTTPException(401, "Invalid API key.")
user = dict(user)
cur.execute("SELECT tool_name, COUNT(*) as cnt FROM usage_log WHERE user_id = %s GROUP BY tool_name ORDER BY cnt DESC", (user["id"],))
user["usage_by_tool"] = [dict(r) for r in cur.fetchall()]
db.close()
return user
# ═══════════════════════════════════════════════════════════════
# BTCPAY INTEGRATION
# ═══════════════════════════════════════════════════════════════
@app.post("/api/create-invoice")
async def create_invoice(request: Request):
"""Create BTCPay invoice for $5 = 5 calls. Works even with 0 calls."""
api_key = request.headers.get("X-API-Key") or request.query_params.get("api_key")
if not api_key:
raise HTTPException(401, "API key required.")
db = get_db()
cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute("SELECT * FROM users WHERE api_key = %s", (api_key,))
user = cur.fetchone()
if not user:
db.close()
raise HTTPException(401, "Invalid API key.")
user = dict(user)
try:
r = httpx.post(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices",
headers={"Authorization": f"token {BTCPAY_KEY}", "Content-Type": "application/json"},
json={
"amount": str(PRICE_USD),
"currency": "USD",
"metadata": {
"user_id": user["id"],
"user_email": user["email"],
"calls": CALLS_PER_TIER,
"orderId": f"research-{user['id']}-{int(time.time())}"
},
"checkout": {"redirectURL": f"http://10.30.20.249:8000/dashboard"}
},
verify=False, timeout=15.0
)
inv = r.json()
# Log pending payment
db = get_db()
cur = db.cursor()
cur.execute("INSERT INTO payments (user_id, btcpay_invoice_id, amount_usd, calls_purchased, status) VALUES (%s,%s,%s,%s,'pending')",
(user["id"], inv["id"], PRICE_USD, CALLS_PER_TIER))
db.commit()
db.close()
return {"invoice_id": inv["id"], "checkout_url": inv["checkoutLink"], "amount": f"${PRICE_USD}", "calls": CALLS_PER_TIER}
except Exception as e:
return JSONResponse({"error": str(e)}, 500)
@app.post("/webhook/btcpay")
async def btcpay_webhook(request: Request):
"""BTCPay calls this when invoice is settled."""
body = await request.json()
event_type = body.get("type", "")
invoice_id = body.get("invoiceId", "")
metadata = body.get("metadata", {})
# Handle nested metadata (BTCPay wraps it)
if isinstance(metadata, dict):
user_id = metadata.get("user_id")
calls = metadata.get("calls", CALLS_PER_TIER)
else:
user_id = None
calls = CALLS_PER_TIER
if event_type in ("InvoiceSettled", "InvoiceProcessing") and user_id:
db = get_db()
cur = db.cursor()
cur.execute("UPDATE payments SET status = 'settled', settled_at = NOW() WHERE btcpay_invoice_id = %s", (invoice_id,))
cur.execute("UPDATE users SET calls_remaining = calls_remaining + %s WHERE id = %s", (calls, user_id))
db.commit()
db.close()
return {"status": "ok"}
# ═══════════════════════════════════════════════════════════════
# API TOOLS (auth-gated)
# ═══════════════════════════════════════════════════════════════
@app.get("/api/status")
def status():
svc = {}
try:
r = client.get(f"{OPENSEARCH_URL}/_cluster/health")
cnt = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_count").json()
svc["opensearch"] = {"status": r.json().get("status"), "documents": cnt.get("count",0)}
except: svc["opensearch"] = {"status":"down"}
try:
client.get(f"{QDRANT_URL}/healthz")
col = client.get(f"{QDRANT_URL}/collections/{INDEX_NAME}").json()
svc["qdrant"] = {"status":"ok","points":col.get("result",{}).get("points_count",0)}
except: svc["qdrant"] = {"status":"down"}
try:
client.get(f"{YACY_URL}/yacysearch.json",params={"query":"test","maximumRecords":1})
svc["yacy"] = {"status":"ok"}
except: svc["yacy"] = {"status":"down"}
try:
r = ollama.get("/api/tags")
svc["ollama"] = {"status":"ok","models":len(r.json().get("models",[]))}
except: svc["ollama"] = {"status":"down"}
# DB stats
try:
db = get_db()
cur = db.cursor()
cur.execute("SELECT COUNT(*) FROM users")
users = cur.fetchone()[0]
cur.execute("SELECT COALESCE(SUM(total_calls),0) FROM users")
total = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM payments WHERE status = 'settled'")
payments = cur.fetchone()[0]
db.close()
svc["business"] = {"status":"ok","users":users,"total_api_calls":total,"payments_settled":payments}
except: svc["business"] = {"status":"down"}
return {"services":svc}
@app.get("/api/search")
def search_web(request: Request, q: str = Query(...), category: str = "", limit: int = 10):
user = _auth(request)
_track_usage(user["id"], "search_web", request)
_ensure_index()
body = {"size":limit,"query":{"bool":{"must":[{"multi_match":{"query":q,"fields":["title^3","content","excerpt"]}}]}},"highlight":{"fields":{"content":{"fragment_size":200,"number_of_fragments":2}}}}
if category: body["query"]["bool"]["filter"] = [{"term":{"category":category}}]
try:
r = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_search", json=body)
result = r.json()
hits = []
for h in result.get("hits",{}).get("hits",[]):
src = h["_source"]
hits.append({"url":src.get("url"),"title":src.get("title"),"excerpt":src.get("excerpt") or (h.get("highlight",{}).get("content",[""])[0]),"category":src.get("category"),"crawled_at":src.get("crawled_at"),"score":h["_score"]})
return {"query":q,"total":result.get("hits",{}).get("total",{}).get("value",0),"hits":hits}
except Exception as e:
return {"query":q,"total":0,"hits":[],"note":str(e)}
@app.get("/api/semantic-search")
def semantic_search(request: Request, q: str = Query(...), limit: int = 10):
user = _auth(request)
_track_usage(user["id"], "semantic_search", request)
emb = _get_embedding(q)
if not emb: return {"hits":[],"error":"Embedding model unavailable"}
try:
r = client.post(f"{QDRANT_URL}/collections/{INDEX_NAME}/points/search",json={"vector":emb,"limit":limit,"with_payload":True,"with_vector":False})
hits = [{"url":p.get("payload",{}).get("url"),"title":p.get("payload",{}).get("title"),"excerpt":str(p.get("payload",{}).get("excerpt",""))[:300],"score":p.get("score")} for p in r.json().get("result",[])]
return {"query":q,"total":len(hits),"hits":hits}
except Exception as e:
return {"hits":[],"error":str(e)}
@app.get("/api/crawl")
def crawl_url(request: Request, url: str = Query(...), depth: int = 1):
user = _auth(request)
_track_usage(user["id"], "crawl_url", request)
indexed = _fetch_and_index(url)
try:
client.get(f"{YACY_URL}/Crawler_p.json", params={"crawlingDomMaxPages":50,"crawlingDepth":depth,"crawlingStart":url,"crawlingQ":"on","bookmarkTitle":"research","bookmarkFolder":"/research","indexText":"on","indexMedia":"on","crawlingMode":"url","cachePolicy":"iffresh"})
return {"status":"crawl_started","url":url,"depth":depth,"indexed":indexed}
except:
return {"status":"indexed_only","url":url,"indexed":indexed}
@app.get("/api/research")
def research_topic(request: Request, topic: str = Query(...)):
user = _auth(request)
_track_usage(user["id"], "research_topic", request)
steps = []; kw_result = {"hits":[]}; sem_result = {"hits":[]}
try:
r = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_search", json={"size":5,"query":{"multi_match":{"query":topic,"fields":["title^3","content","excerpt"]}},"highlight":{"fields":{"content":{"fragment_size":200,"number_of_fragments":1}}}})
hits = [{"title":h["_source"].get("title"),"excerpt":h.get("highlight",{}).get("content",[h["_source"].get("excerpt","")])[0]} for h in r.json().get("hits",{}).get("hits",[])]
kw_result = {"total":len(hits),"hits":hits}; steps.append("keyword_search_done")
except: steps.append("keyword_search_skipped")
try:
emb = _get_embedding(topic)
if emb:
r = client.post(f"{QDRANT_URL}/collections/{INDEX_NAME}/points/search",json={"vector":emb,"limit":5,"with_payload":True})
hits2 = [{"title":p.get("payload",{}).get("title",""),"excerpt":str(p.get("payload",{}).get("excerpt",""))[:200],"score":p.get("score")} for p in r.json().get("result",[])]
sem_result = {"total":len(hits2),"hits":hits2}; steps.append("semantic_search_done")
except: steps.append("semantic_search_skipped")
try:
r = client.get(f"{YACY_URL}/yacysearch.json",params={"query":topic,"maximumRecords":10,"resource":"global"})
discovered = [item.get("link") for ch in r.json().get("channels",[]) for item in ch.get("items",[]) if item.get("link")]
for url in discovered[:10]:
try: client.get(f"{YACY_URL}/Crawler_p.json",params={"crawlingDomMaxPages":10,"crawlingDepth":0,"crawlingStart":url,"crawlingQ":"on","indexText":"on","indexMedia":"on","crawlingMode":"url","cachePolicy":"iffresh"},timeout=5.0)
except: pass
steps.append(f"crawl_dispatched_{len(discovered[:10])}")
except: steps.append("crawl_skipped")
all_src = ""
for h in kw_result.get("hits",[])[:3] + sem_result.get("hits",[])[:3]:
all_src += f"- {h.get('title','Unknown')}: {h.get('excerpt','')[:200]}\n"
summary = _ai_chat(f"Research topic: {topic}\n\nSources:\n{all_src}\n\nConcise research summary (3-5 paragraphs): key findings, important sources, knowledge gaps, next steps.") if all_src else ""
return {"topic":topic,"steps":steps,"keyword_results":kw_result,"semantic_results":sem_result,"ai_summary":summary}
@app.get("/api/report")
def create_report(request: Request, topic: str = Query(...), sources: str = ""):
user = _auth(request)
_track_usage(user["id"], "create_report", request)
if sources:
urls = [u.strip() for u in sources.split(",") if u.strip()]
else:
try:
r = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_search", json={"size":8,"query":{"multi_match":{"query":topic,"fields":["title^3","content"]}}})
urls = [h["_source"]["url"] for h in r.json().get("hits",{}).get("hits",[])]
except: urls = []
gathered = ""
for url in urls[:8]:
try:
r = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_search", json={"size":1,"query":{"term":{"url":url}}})
hits = r.json().get("hits",{}).get("hits",[])
if hits:
src = hits[0]["_source"]
gathered += f"\n\n### {src.get('title','Source')}\nURL: {url}\n{src.get('content',src.get('excerpt',''))[:1500]}"
except: pass
report = _ai_chat(f"Research topic: {topic}\nSources:{gathered if gathered else ' No sources found.'}\n\nGenerate a comprehensive report:\n1. Executive Summary\n2. Key Findings (numbered)\n3. Source Analysis\n4. Knowledge Gaps\n5. Recommendations\n\nBe thorough, use markdown, cite sources.", system="You are a senior research analyst. Produce thorough, structured reports.")
return {"topic":topic,"sources_used":len(urls),"report":report}
@app.get("/api/document")
def retrieve_document(request: Request, url: str = Query(...)):
user = _auth(request)
_track_usage(user["id"], "retrieve_document", request)
try:
r = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_search", json={"size":1,"query":{"term":{"url":url}}})
hits = r.json().get("hits",{}).get("hits",[])
if not hits: return {"error":"Not found","url":url}
src = hits[0]["_source"]
return {"url":src.get("url"),"title":src.get("title"),"content":src.get("content","")[:10000],"excerpt":src.get("excerpt"),"category":src.get("category"),"crawled_at":src.get("crawled_at")}
except Exception as e:
return {"error":str(e),"url":url}
@app.get("/api/summarize")
def summarize_sources(request: Request, urls: str = Query(...), instruction: str = "Summarize key points"):
user = _auth(request)
_track_usage(user["id"], "summarize_sources", request)
url_list = [u.strip() for u in urls.split(",") if u.strip()]
combined = ""
for url in url_list[:5]:
try:
r = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_search", json={"size":1,"query":{"term":{"url":url}}})
hits = r.json().get("hits",{}).get("hits",[])
if hits:
src = hits[0]["_source"]
combined += f"\n\n--- {url} ---\n{src.get('title','')}\n{src.get('content',src.get('excerpt',''))[:2000]}"
except: pass
if not combined.strip(): return {"error":"No content retrieved"}
summary = _ai_chat(f"Instruction: {instruction}\n\nSources:{combined}\n\nProvide a structured summary.")
return {"instruction":instruction,"sources":len(url_list),"summary":summary}
@app.get("/api/extract")
def extract_information(request: Request, url: str = Query(...), schema: str = Query("company names, products, prices")):
user = _auth(request)
_track_usage(user["id"], "extract_information", request)
try:
r = client.post(f"{OPENSEARCH_URL}/{INDEX_NAME}/_search", json={"size":1,"query":{"term":{"url":url}}})
hits = r.json().get("hits",{}).get("hits",[])
if not hits: return {"error":"Not found","url":url}
content = hits[0]["_source"].get("content","")[:8000]
result = _ai_chat(f"Extract: {schema}\n\nDocument:\n{content}\n\nReturn ONLY valid JSON.", system="Extract structured data. Return ONLY valid JSON.")
return {"url":url,"schema":schema,"extracted":result}
except Exception as e:
return {"error":str(e)}
# ═══════════════════════════════════════════════════════════════
# UI TEMPLATES
# ═══════════════════════════════════════════════════════════════
CSS = """
:root{--bg:#050508;--surface:#0c0c14;--border:#1e1e30;--accent:#7c3aed;--accent2:#a855f7;--gold:#f59e0b;--text:#f4f4f5;--muted:#a1a1aa;--green:#22c55e;--red:#ef4444}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
.gradient-bg{background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(120,60,255,0.15),transparent)}
.header{background:rgba(12,12,20,0.8);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:16px 32px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100}
.header h1{font-size:1.3rem;font-weight:800;background:linear-gradient(135deg,var(--accent),var(--accent2),#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.header nav{display:flex;gap:20px;align-items:center}
.header nav a{color:var(--muted);text-decoration:none;font-size:0.9rem;transition:color .2s}
.header nav a:hover{color:var(--text)}
.btn{padding:12px 24px;border-radius:12px;border:none;font-weight:600;font-size:0.95rem;cursor:pointer;transition:all .2s;text-decoration:none;display:inline-block}
.btn-primary{background:var(--accent);color:white}
.btn-primary:hover{background:var(--accent2);transform:translateY(-1px);box-shadow:0 8px 25px rgba(124,58,237,0.3)}
.btn-gold{background:linear-gradient(135deg,var(--gold),#fbbf24);color:#1a1a1a}
.btn-gold:hover{transform:translateY(-1px);box-shadow:0 8px 25px rgba(245,158,11,0.3)}
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
.container{max-width:1200px;margin:0 auto;padding:40px 24px}
.hero{text-align:center;padding:80px 0 60px}
.hero h2{font-size:3rem;font-weight:900;letter-spacing:-0.04em;line-height:1.1;margin-bottom:20px;background:linear-gradient(135deg,var(--text),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.hero p{color:var(--muted);font-size:1.2rem;max-width:650px;margin:0 auto 32px;line-height:1.6}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin:40px 0}
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:28px;transition:all .2s}
.card:hover{border-color:var(--accent);transform:translateY(-2px)}
.card h3{font-size:1.2rem;margin-bottom:10px}
.card p{color:var(--muted);font-size:0.9rem;line-height:1.5}
.price{font-size:3rem;font-weight:900;text-align:center;margin:20px 0}
.price span{font-size:1rem;color:var(--muted);font-weight:400}
.feature-list{list-style:none;margin:20px 0}
.feature-list li{padding:8px 0;color:var(--muted);font-size:0.9rem}
.feature-list li::before{content:'✓ ';color:var(--green);font-weight:700}
pre{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;overflow-x:auto;font-size:0.85rem;margin:10px 0}
.footer{text-align:center;padding:40px;color:var(--muted);font-size:0.85rem;border-top:1px solid var(--border);margin-top:60px}
.footer a{color:var(--accent2);text-decoration:none}
input,textarea{width:100%;padding:14px 18px;border-radius:12px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:1rem;outline:none;transition:border-color .2s;margin-bottom:12px}
input:focus,textarea:focus{border-color:var(--accent)}
"""
LANDING_HTML = f"""AI Research Engine — Private Knowledge Cloud
🧠 AI Research Engine
The Ultimate Source of Uncensored Internet Knowledge
A self-hosted AI workforce that continuously gathers, organizes, and acts on information. Your private research cloud — no filters, no tracking, no limits.
"""
DASHBOARD_HTML = f"""Dashboard — AI Research Engine
🧠 AI Research Engine
Dashboard
Your account, usage, and API access.
🔑 Enter Your API Key
⚡ Buy API Calls
$5 Bitcoin = 5 API calls. Pay with Lightning.
📋 Quick Reference
# Use your key in any HTTP request:
curl -H "X-API-Key: YOUR_KEY" \\
"http://10.30.20.249:8000/api/search?q=your+query"
# Or as a query parameter:
curl "http://10.30.20.249:8000/api/search?q=test&api_key=YOUR_KEY"