Wallet system (sats ledger + deposit + spend), HydraProxy fulfillment queue, paid IP-quality scorer (85/100 verified)

This commit is contained in:
drjones
2026-09-20 18:51:47 -07:00
parent 16dde93ce2
commit a7d6b98183
8 changed files with 481 additions and 12 deletions

173
app.py
View File

@@ -14,12 +14,14 @@ from flask import (Flask, abort, flash, jsonify, redirect, render_template,
import db
import iproyal
import hydraproxy
import quality
CFG = db.CFG
app = Flask(__name__)
app.secret_key = CFG["secret_key"]
app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # mitigate CSRF on modern browsers
app.config["SESSION_COOKIE_HTTPONLY"] = True # block JS session theft (XSS)
db.init_db() # ensure schema + migrations on gunicorn start
# simple SQLite-backed rate limiter (shared across gunicorn workers)
def _client_ip():
@@ -196,20 +198,24 @@ def plans():
@app.route("/buy/<int:plan_id>", methods=["POST"])
@login_required
def buy(plan_id):
u = current_user()
d = db.get_db()
plan = d.execute("SELECT * FROM plans WHERE id=?", (plan_id,)).fetchone()
d.close()
if not plan:
abort(404)
order_id = db.random_token(12)
price = plan["price_sats"]
if db.get_balance_sats(u["id"]) < price:
flash("Insufficient wallet balance. Deposit first.", "warn")
return redirect(url_for("wallet"))
db.add_transaction(u["id"], -price, "gb", f"plan_{plan_id}")
d = db.get_db()
d.execute("INSERT INTO orders (user_id,plan_id,invoice_id,amount_sats,status,created_at) "
"VALUES (?,?,?,?,'pending',?)",
(current_user()["id"], plan_id, order_id, plan["price_sats"], int(time.time())))
d.execute("UPDATE users SET balance_gb = balance_gb + ?, active=1 WHERE id=?",
(plan["gb"], u["id"]))
d.commit()
d.close()
inv = create_invoice(plan["price_sats"], order_id)
return redirect(inv["checkoutLink"])
flash(f"Added {plan['gb']} GB. Happy scraping.", "ok")
return redirect(url_for("dashboard"))
@app.route("/order/<order_id>")
@@ -235,6 +241,20 @@ def webhook_btcpay():
kind = (payload.get("metadata") or {}).get("kind")
if not order_id:
return "ok"
if kind == "deposit":
uid = (payload.get("metadata") or {}).get("user_id")
amt = (payload.get("metadata") or {}).get("amount_sats")
d = db.get_db()
already = d.execute("SELECT 1 FROM transactions WHERE ref=? AND type='deposit'",
(order_id,)).fetchone()
if not already and uid and amt:
d.execute("UPDATE users SET balance_sats = balance_sats + ? WHERE id=?",
(int(amt), uid))
d.execute("INSERT INTO transactions (user_id, amount_sats, type, ref, ts) "
"VALUES (?,?,?,?,?)", (uid, int(amt), "deposit", order_id, int(time.time())))
d.commit()
d.close()
return "ok"
d = db.get_db()
if kind == "dedicated":
row = d.execute("SELECT * FROM dedicated_ips WHERE invoice_id=? AND status='pending_payment'",
@@ -265,6 +285,31 @@ def webhook_btcpay():
return "ok"
@app.route("/wallet")
@login_required
def wallet():
u = current_user()
bal = db.get_balance_sats(u["id"])
txs = db.get_transactions(u["id"])
return render_template("wallet.html", user=u, balance=bal, txs=txs)
@app.route("/wallet/deposit", methods=["POST"])
@login_required
def wallet_deposit():
u = current_user()
try:
sats = int(request.form.get("sats", 0))
except ValueError:
sats = 0
if sats < 500:
flash("Minimum deposit is 500 sats.", "warn")
return redirect(url_for("wallet"))
ref = db.random_token(12)
inv = create_invoice(sats, ref, meta={"kind": "deposit", "user_id": u["id"], "amount_sats": sats})
return redirect(inv["checkoutLink"])
@app.route("/dashboard")
@login_required
def dashboard():
@@ -277,11 +322,15 @@ def dashboard():
"WHERE o.user_id=? ORDER BY o.id DESC LIMIT 20", (u["id"],)).fetchall()]
sessions = [dict(r) for r in d.execute(
"SELECT * FROM usage_log WHERE user_id=? ORDER BY id DESC LIMIT 10", (u["id"],)).fetchall()]
hydra = [dict(r) for r in d.execute(
"SELECT * FROM hydraproxy_orders WHERE user_id=? ORDER BY id DESC", (u["id"],)).fetchall()]
d.close()
bal = db.get_balance_sats(u["id"])
socks = f"socks5://{u['username']}:{u['proxy_password_plain']}@{PROXY['host']}:{PROXY['socks_port']}"
http = f"http://{u['username']}:{u['proxy_password_plain']}@{PROXY['host']}:{PROXY['http_port']}"
return render_template("dashboard.html", user=u, used_gb=used / 1e9,
socks=socks, http=http, dedicated=deds, orders=orders, sessions=sessions)
socks=socks, http=http, dedicated=deds, orders=orders, sessions=sessions,
hydra=hydra, balance=bal)
@app.route("/dashboard/test")
@@ -410,16 +459,39 @@ def dedicated_buy():
flash("Pick a location.", "warn")
return redirect(url_for("dedicated"))
_, price = DEDICATED_PRODUCTS[pid]["plans"][days]
u = current_user()
if db.get_balance_sats(u["id"]) < price:
flash("Insufficient wallet balance. Deposit first.", "warn")
return redirect(url_for("wallet"))
oid = db.random_token(12)
d = db.get_db()
d.execute("INSERT INTO dedicated_ips (user_id,product_id,product_name,plan_days,location_id,"
"location_name,invoice_id,price_sats,status,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
(current_user()["id"], pid, DEDICATED_PRODUCTS[pid]["name"], days, loc_id, loc_name,
(u["id"], pid, DEDICATED_PRODUCTS[pid]["name"], days, loc_id, loc_name,
oid, price, "pending_payment", int(time.time())))
d.commit()
d.close()
inv = create_invoice(price, oid, meta={"kind": "dedicated"})
return redirect(inv["checkoutLink"])
db.add_transaction(u["id"], -price, "dedicated", oid)
# place the IPRoyal order directly (no webhook needed in wallet model)
try:
plan_id = DEDICATED_PRODUCTS[pid]["plans"][days][0]
o = iproyal.create_order(pid, plan_id, loc_id, 1)
oid2 = o.get("id") or o.get("order_id") or (o.get("data") or {}).get("id")
d = db.get_db()
d.execute("UPDATE dedicated_ips SET status='provisioning', iproyal_order_id=? WHERE invoice_id=?",
(oid2, oid))
d.commit()
d.close()
flash("Dedicated IP ordered — provisioning now.", "ok")
except Exception as e:
app.logger.error("dedicated order failed: %s", e)
d = db.get_db()
d.execute("UPDATE dedicated_ips SET status='failed' WHERE invoice_id=?", (oid,))
d.commit()
d.close()
db.add_transaction(u["id"], price, "dedicated_refund", oid)
flash("Dedicated order failed — wallet refunded.", "warn")
return redirect(url_for("dashboard"))
@app.route("/dedicated/poll", methods=["POST"])
@@ -504,6 +576,53 @@ def api_hydraproxy_info():
return jsonify({"error": str(e)}), 502
@app.route("/hydraproxy/order", methods=["POST"])
@login_required
def hydraproxy_order():
u = current_user()
product = request.form.get("product", "").strip()
location = request.form.get("location", "").strip()
if product not in HYDRA_PRODUCTS:
flash("Bad product.", "warn")
return redirect(url_for("hydraproxy_page"))
price = HYDRA_PRODUCTS[product]["price_sats"]
if db.get_balance_sats(u["id"]) < price:
flash("Insufficient wallet balance. Deposit first.", "warn")
return redirect(url_for("wallet"))
d = db.get_db()
d.execute("INSERT INTO hydraproxy_orders (user_id, product, location, price_sats, status, created_at) "
"VALUES (?,?,?,?,'pending_fulfillment',?)",
(u["id"], product, location, price, int(time.time())))
d.commit()
d.close()
db.add_transaction(u["id"], -price, "hydraproxy", product)
flash("Order placed — our broker fulfills it shortly and delivers your proxy to the dashboard.", "ok")
return redirect(url_for("dashboard"))
@app.route("/quality")
@login_required
def quality_page():
return render_template("quality.html", user=current_user(),
price=CFG.get("quality_price_sats", 220))
@app.route("/quality/run", methods=["POST"])
@login_required
def quality_run():
u = current_user()
price = CFG.get("quality_price_sats", 220)
if db.get_balance_sats(u["id"]) < price:
return jsonify({"error": "Insufficient balance. Deposit first."}), 402
proxy = request.form.get("proxy", "").strip()
if not proxy:
proxy = f"socks5h://{u['username']}:{u['proxy_password_plain']}@127.0.0.1:{PROXY['socks_port']}"
db.add_transaction(u["id"], -price, "quality", "ip_check")
result = quality.score_proxy(proxy)
result["charged_sats"] = price
return jsonify(result)
# ---------- admin ----------
ADMIN_PASSWORD = CFG.get("admin_password", "")
@@ -530,6 +649,7 @@ def admin():
"FROM users ORDER BY id DESC LIMIT 50")]
orders = [dict(r) for r in d.execute("SELECT * FROM orders ORDER BY id DESC LIMIT 20")]
deds = [dict(r) for r in d.execute("SELECT * FROM dedicated_ips ORDER BY id DESC LIMIT 30")]
hydra_orders = [dict(r) for r in d.execute("SELECT * FROM hydraproxy_orders ORDER BY id DESC LIMIT 30")]
d.close()
try:
ipr_balance = iproyal.get_balance()
@@ -544,7 +664,7 @@ def admin():
ipr_balance < CFG.get("alerts", {}).get("low_balance_threshold", 10.0)
return render_template("admin.html", stats=stats, users=users, orders=orders,
dedicated=deds, ipr_balance=ipr_balance, low_balance=low_balance,
hydra_balance=hydra_balance)
hydra_balance=hydra_balance, hydra_orders=hydra_orders)
@app.route("/admin/credit", methods=["POST"])
@@ -564,6 +684,37 @@ def admin_credit():
return redirect(url_for("admin"))
@app.route("/admin/hydraproxy/fulfill", methods=["POST"])
def admin_hydraproxy_fulfill():
if not session.get("admin"):
abort(403)
oid = request.form.get("order_id", "").strip()
hydra_order_id = request.form.get("hydra_order_id", "").strip()
if not oid or not hydra_order_id:
flash("Need our order id and the HydraProxy order id.", "warn")
return redirect(url_for("admin"))
try:
details = hydraproxy.proxy_details(hydra_order_id)
pinfo = details.get("proxy_info", {})
proxy = details.get("proxy", {})
host = proxy.get("hostname") or proxy.get("server_ip")
port = proxy.get("port")
if isinstance(port, list):
port = port[0]
user = pinfo.get("username", "")
pw = pinfo.get("password", "")
d = db.get_db()
d.execute("UPDATE hydraproxy_orders SET status='active', hydra_order_id=?, proxy_host=?, "
"proxy_port=?, proxy_user=?, proxy_pass=?, fulfilled_at=? WHERE id=?",
(hydra_order_id, host, str(port), user, pw, int(time.time()), oid))
d.commit()
d.close()
flash(f"Fulfilled order {oid} with HydraProxy order {hydra_order_id}.", "ok")
except Exception as e:
flash(f"Fulfill failed: {e}", "warn")
return redirect(url_for("admin"))
# ---------- agent discovery ----------
@app.route("/llms.txt")
def llms_txt():

55
db.py
View File

@@ -73,6 +73,29 @@ CREATE TABLE IF NOT EXISTS rate_limits (
ip TEXT,
ts REAL
);
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
amount_sats INTEGER, -- positive = deposit/credit, negative = spend
type TEXT, -- deposit / gb / dedicated / hydraproxy / quality / admin_credit
ref TEXT,
ts INTEGER
);
CREATE TABLE IF NOT EXISTS hydraproxy_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
product TEXT, -- residential / mobile / static
location TEXT,
price_sats INTEGER,
status TEXT DEFAULT 'pending_fulfillment', -- pending_fulfillment/active/failed/refunded
hydra_order_id TEXT,
proxy_host TEXT,
proxy_port TEXT,
proxy_user TEXT,
proxy_pass TEXT,
created_at INTEGER,
fulfilled_at INTEGER
);
"""
@@ -85,6 +108,10 @@ def get_db():
def init_db():
db = get_db()
db.executescript(SCHEMA)
# migrate: add wallet column if missing (older DBs)
cols = [r[1] for r in db.execute("PRAGMA table_info(users)")]
if "balance_sats" not in cols:
db.execute("ALTER TABLE users ADD COLUMN balance_sats INTEGER DEFAULT 0")
# seed default plans if empty
cur = db.execute("SELECT COUNT(*) c FROM plans")
if cur.fetchone()["c"] == 0:
@@ -142,3 +169,31 @@ def add_usage(user_id, nbytes):
def random_token(n=24):
return base64.urlsafe_b64encode(os.urandom(n)).decode().rstrip("=")
# ---------- wallet ----------
def add_transaction(user_id, amount_sats, type_, ref=""):
"""Record a ledger entry AND atomically adjust the user's sats balance."""
db = get_db()
db.execute("INSERT INTO transactions (user_id, amount_sats, type, ref, ts) "
"VALUES (?,?,?,?,?)", (user_id, amount_sats, type_, ref, int(time.time())))
db.execute("UPDATE users SET balance_sats = balance_sats + ? WHERE id=?",
(amount_sats, user_id))
db.commit()
db.close()
def get_balance_sats(user_id):
db = get_db()
r = db.execute("SELECT balance_sats FROM users WHERE id=?", (user_id,)).fetchone()
db.close()
return r["balance_sats"] if r else 0
def get_transactions(user_id, limit=50):
db = get_db()
rows = [dict(r) for r in db.execute(
"SELECT * FROM transactions WHERE user_id=? ORDER BY id DESC LIMIT ?",
(user_id, limit))]
db.close()
return rows

116
quality.py Normal file
View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Proxy/IP quality scorer — battery of checks via free APIs + DNSBL.
Scores 0-100: connectivity, residential/ISP type, proxy-flag, blacklists,
latency, and header-leak anonymity. No API keys required.
"""
import json
import socket
import subprocess
import time
def _curl(proxy, url, timeout=20):
cmd = ["curl", "-s", "--max-time", str(timeout)]
if proxy:
cmd += ["-x", proxy]
cmd.append(url)
t0 = time.time()
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5)
out = (r.stdout or "").strip()
except subprocess.TimeoutExpired:
out = ""
return out, (time.time() - t0) * 1000
def _dnsbl(ip):
rev = ".".join(reversed(ip.split(".")))
hits = []
for bl in ("zen.spamhaus.org", "bl.spamcop.net", "b.barracudacentral.org"):
try:
socket.gethostbyname(f"{rev}.{bl}")
hits.append(bl)
except socket.gaierror:
pass
return hits
def score_proxy(proxy=None):
checks = []
score = 0
out, latency = _curl(proxy, "http://ip-api.com/json"
"?fields=status,query,country,city,regionName,isp,org,as,hosting,proxy,mobile")
try:
d = json.loads(out)
except Exception:
d = {}
if d.get("status") != "success":
checks.append(("Connectivity", False, "proxy failed to connect"))
return {"score": 0, "checks": checks, "breakdown": {}, "charged": True}
ip = d.get("query")
score += 30
checks.append(("Connectivity", True, f"egress {ip}"))
hosting = bool(d.get("hosting"))
if not hosting:
score += 25
checks.append(("Residential / ISP", True, f"{d.get('isp')} ({d.get('org')})"))
else:
checks.append(("Residential / ISP", False, "datacenter / hosting IP"))
flagged = bool(d.get("proxy"))
if not flagged:
score += 20
checks.append(("Not proxy-flagged", True, "clean"))
else:
checks.append(("Not proxy-flagged", False, "IP flagged as proxy/VPN"))
bl = _dnsbl(ip)
if not bl:
score += 15
checks.append(("Blacklists", True, "no hits"))
else:
checks.append(("Blacklists", False, ", ".join(bl)))
if latency < 2000:
score += 10
checks.append(("Latency", True, f"{latency:.0f}ms"))
elif latency < 5000:
score += 5
checks.append(("Latency", True, f"{latency:.0f}ms (slow)"))
else:
checks.append(("Latency", False, f"{latency:.0f}ms"))
leak = []
try:
hout, _ = _curl(proxy, "https://httpbin.org/headers", timeout=15)
hd = json.loads(hout).get("headers", {})
for k in ("X-Forwarded-For", "Via", "X-Real-Ip"):
if k in hd:
leak.append(k)
except Exception:
pass
if leak:
checks.append(("Anonymity", False, f"leaks: {', '.join(leak)}"))
else:
checks.append(("Anonymity", True, "no header leaks"))
breakdown = {
"egress_ip": ip,
"geo": f"{d.get('city')}, {d.get('regionName')}, {d.get('country')}",
"isp": d.get("isp"),
"asn": d.get("as"),
"hosting": hosting,
"proxy_flagged": flagged,
"mobile": bool(d.get("mobile")),
"blacklists": bl,
"latency_ms": round(latency),
"leak_headers": leak,
}
return {"score": score, "checks": checks, "breakdown": breakdown, "charged": True}

View File

@@ -27,6 +27,31 @@
</form>
</div>
<div class="card" style="margin-top:18px">
<h3>HydraProxy fulfillment queue</h3>
{% if hydra_orders %}
<table style="width:100%;font-size:13px;border-collapse:collapse">
<tr style="color:var(--muted);text-align:left"><th>ID</th><th>User</th><th>Product</th><th>Location</th><th>Status</th></tr>
{% for h in hydra_orders %}
<tr style="border-top:1px solid var(--border)">
<td style="padding:4px">{{ h.id }}</td><td>{{ h.user_id }}</td><td>{{ h.product }}</td>
<td>{{ h.location or '—' }}</td>
<td style="color:{{ 'var(--ok)' if h.status=='active' else 'var(--warn)' }}">{{ h.status }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="muted">No HydraProxy orders yet.</p>
{% endif %}
<h3 style="margin-top:16px">Fulfill an order</h3>
<form method="post" action="/admin/hydraproxy/fulfill" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap">
<div style="flex:1;min-width:140px"><label>Our order ID</label><input type="number" name="order_id" required></div>
<div style="flex:1;min-width:140px"><label>HydraProxy order ID</label><input type="text" name="hydra_order_id" required></div>
<input type="submit" value="Fulfill" style="width:auto;margin:0;padding:11px 20px">
</form>
<p class="muted" style="font-size:12px;margin-bottom:0">Buy the proxy at HydraProxy's dashboard, paste its order ID here — creds auto-pull via proxy-details and land on the customer's dashboard.</p>
</div>
<div class="dash-grid" style="margin-top:18px">
<div class="card">
<h3>Recent orders</h3>

View File

@@ -40,7 +40,7 @@
<a href="/plans">Residential</a>
<a href="/dedicated">Dedicated IPs</a>
<a href="/hydraproxy">HydraProxy</a>
{% if user %}<a href="/dashboard">Dashboard</a><a href="/logout">Logout</a>
{% if user %}<a href="/dashboard">Dashboard</a><a href="/wallet">Wallet</a><a href="/quality">Quality</a><a href="/logout">Logout</a>
{% else %}<a href="/login">Login</a><a class="cta" href="/signup">Get proxies</a>{% endif %}
</div>
</div>

View File

@@ -3,6 +3,7 @@
<h2>Dashboard</h2>
<div class="stats-row">
<div class="card stat"><span class="lbl">Balance</span><span class="val">{{ '%.2f'|format(user.balance_gb) }} GB</span></div>
<div class="card stat"><span class="lbl">Wallet</span><span class="val">{{ balance }} <span style="font-size:14px;color:var(--muted)">sats</span></span></div>
<div class="card stat"><span class="lbl">Used</span><span class="val">{{ '%.3f'|format(used_gb) }} GB</span></div>
<div class="card stat"><span class="lbl">Status</span>
<span class="val" style="color:{{ 'var(--ok)' if user.active else 'var(--danger)' }}">{{ 'ACTIVE' if user.active else 'INACTIVE' }}</span>
@@ -93,6 +94,29 @@
<p class="muted" style="margin-top:18px">No dedicated IPs yet — <a href="/dedicated">order one</a>.</p>
{% endif %}
{% if hydra %}
<div class="card" style="margin-top:18px">
<h3>HydraProxy orders <span class="muted" style="font-size:12px">(broker-fulfilled)</span></h3>
{% for h in hydra %}
<div style="border:1px solid var(--border);border-radius:8px;padding:12px;margin:10px 0">
<div style="display:flex;justify-content:space-between">
<strong style="color:var(--text)">{{ h.product }}{% if h.location %} · {{ h.location }}{% endif %}</strong>
<span style="color:{{ 'var(--ok)' if h.status=='active' else 'var(--warn)' }}">{{ h.status }}</span>
</div>
{% if h.status == 'active' %}
<label style="font-size:11px;color:var(--muted)">Proxy</label>
<div class="copy">
<input type="text" readonly value="{{ h.proxy_user }}:{{ h.proxy_pass }}@{{ h.proxy_host }}:{{ h.proxy_port }}" onclick="this.select()">
<button onclick="copy(this.previousElementSibling)">Copy</button>
</div>
{% elif h.status == 'pending_fulfillment' %}
<p class="muted" style="font-size:12px;margin:6px 0 0">Broker is fulfilling your order — your proxy appears here when ready.</p>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if orders %}
<div class="card" style="margin-top:18px">
<h3>Order history</h3>

53
templates/quality.html Normal file
View File

@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}IP Quality Tester — Clean Proxys{% endblock %}
{% block body %}
<h2>IP Quality <span class="accent">Tester</span></h2>
<p class="sub">Check how clean a proxy is before you trust it. {{ price }} sats (~$0.15) per check, billed to your wallet.</p>
<div class="card">
<h3>Run a check</h3>
<p class="muted" style="font-size:14px">Leave blank to test your own Clean Proxys proxy, or paste any <span class="mono">socks5://</span> or <span class="mono">http://</span> proxy.</p>
<form id="qf" onsubmit="return runQuality(event)">
<label>Proxy (optional)</label>
<input type="text" id="proxy" placeholder="socks5://user:pass@host:1080" style="font-family:var(--mono)">
<input type="submit" value="Run quality check ({{ price }} sats)">
</form>
</div>
<div id="result" style="margin-top:18px"></div>
<script>
async function runQuality(e){
e.preventDefault();
const r = document.getElementById('result');
r.innerHTML = '<div class="card"><p class="muted">Running checks…</p></div>';
const fd = new FormData();
fd.append('proxy', document.getElementById('proxy').value);
let data;
try { data = await (await fetch('/quality/run', {method:'POST', body: fd})).json(); }
catch(err){ r.innerHTML = '<div class="flash warn">Check failed.</div>'; return false; }
if (data.error){
r.innerHTML = '<div class="flash warn">'+data.error+' — <a href="/wallet">deposit</a> first.</div>';
return false;
}
const b = data.breakdown || {};
const checksHtml = (data.checks||[]).map(c =>
'<tr><td>'+(c[1]?'✅':'❌')+'</td><td>'+c[0]+'</td><td class="muted" style="font-size:13px">'+c[2]+'</td></tr>'
).join('');
const scoreColor = data.score >= 70 ? 'var(--neon)' : (data.score >= 40 ? 'var(--warn)' : 'var(--danger)');
const bl = (b.blacklists && b.blacklists.length) ? b.blacklists.join(', ') : 'none';
r.innerHTML =
'<div class="card">'
+ '<div style="display:flex;align-items:center;gap:22px;flex-wrap:wrap">'
+ '<div style="font-size:54px;font-weight:800;font-family:var(--mono);color:'+scoreColor+'">'+data.score+'<span style="font-size:18px;color:var(--muted)">/100</span></div>'
+ '<div><div class="lbl" style="color:var(--muted)">'+ (b.geo||'') + ' · ' + (b.isp||'') + '</div>'
+ '<div class="muted" style="font-size:14px">egress <span class="mono">'+(b.egress_ip||'-')+'</span> · '+ (b.latency_ms||'') +'ms</div>'
+ '<div class="muted" style="font-size:13px">hosting: '+(b.hosting?'yes':'no')+' · proxy-flagged: '+(b.proxy_flagged?'yes':'no')+' · blacklists: '+bl+'</div></div>'
+ '</div>'
+ '<table style="width:100%;margin-top:18px"><tr><th></th><th>Check</th><th>Detail</th></tr>'+checksHtml+'</table>'
+ '<p class="muted" style="font-size:12px;margin-top:12px">charged '+data.charged_sats+' sats</p>'
+ '</div>';
return false;
}
</script>
{% endblock %}

45
templates/wallet.html Normal file
View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}Wallet — Clean Proxys{% endblock %}
{% block body %}
<h2>Wallet</h2>
<p class="sub">Deposit Bitcoin, spend sats on proxies and tools. No KYC, no email.</p>
<div class="statsbar">
<div class="s"><div class="n grad">{{ balance }}</div><div class="l">Balance (sats)</div></div>
<div class="s"><div class="n grad">{{ (balance / 1e8) | round(8, 'floor') }}</div><div class="l">BTC</div></div>
</div>
<div class="card" style="margin-top:18px">
<h3>Deposit</h3>
<p class="muted" style="font-size:14px">Pick an amount — funds credit instantly on payment confirmation.</p>
<form method="POST" action="/wallet/deposit">
<div class="cta-row" style="justify-content:flex-start;flex-wrap:wrap;gap:10px;margin:14px 0">
{% for amt in [10000, 50000, 100000, 250000] %}
<button type="button" class="btn btn-ghost" onclick="document.getElementById('sats').value={{ amt }}">{{ amt }} sats</button>
{% endfor %}
</div>
<label>Custom amount (sats)</label>
<input type="number" id="sats" name="sats" min="500" placeholder="e.g. 50000" required>
<input type="submit" value="Deposit via Bitcoin">
</form>
</div>
<div class="card" style="margin-top:18px">
<h3>Transactions</h3>
{% if txs %}
<table style="width:100%">
<tr><th>When</th><th>Type</th><th>Amount</th><th>Ref</th></tr>
{% for t in txs %}
<tr>
<td class="mono">{{ t.ts }}</td>
<td>{{ t.type }}</td>
<td class="mono" style="color:{% if t.amount_sats >= 0 %}var(--neon){% else %}var(--danger){% endif %}">{{ '+' if t.amount_sats >= 0 else '' }}{{ t.amount_sats }} sats</td>
<td class="mono" style="font-size:12px;color:var(--muted)">{{ t.ref }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="muted">No transactions yet — make your first deposit.</p>
{% endif %}
</div>
{% endblock %}