HOW CAPTURE WORKS
+• With headless Chromium installed, /shot returns a real browser render as base64 PNG.
+• No browser on the host? You get text_fallback: an honest fetch of the page with rendered-text preview + page intel. A status field always tells you which.
+• SSRF guard: private/reserved network targets are refused before any fetch.
+• 25¢ per shot, free with PASS. Rate limit 6/min.
"""
+
+@app.route("/shot")
+def shot_page():
+ body = f"""
+
SCREEN SHOT
Point at a URL, get a render. Real headless-Chromium PNG when the host has one — an honest rendered-text + intel fallback when it doesn't. Never a fake image.
+
New capture
+
+
{'Free with your PASS — or 25¢ from balance.' if current_user_id() else 'Login + balance (or PASS): 25¢ per shot.'}
+
Result
+{SHOT_EXPLAINER}
+""" + SHOT_API + how(["Paste a URL — the job queues with a 25¢ charge (free with PASS).",
+ "With a headless browser on the host you get a real PNG back as base64.",
+ "No browser installed? You get text_fallback: title, description, headings, first 400 words — honestly labeled.",
+ "Agents: POST /api/shot/create then poll /api/shot/status/ until status != queued.",
+ "SSRF guard: localhost and private ranges are refused — this is a capture service, not a port scanner."])
+ return page("shot", body)
+
+@app.route("/api/shot/create", methods=["POST"])
+def api_shot_create():
+ r = rate_limit("shot", 6, 60)
+ if r: return r
+ uid = key_user() or current_user_id()
+ if not uid: return jsonify({"ok": False, "error": "auth required: account session (sign up at /inbox, no KYC) or Authorization: Bearer dk_ key"}), 401
+ url = str(jp("url") or "").strip()
+ if not url: return jsonify({"ok": False, "error": "url required"}), 400
+ url, err = shot_url_ok(url)
+ if err: return jsonify({"ok": False, "error": err}), 400
+ if not has_pass(uid) and not charge(uid, 25, "shot create"):
+ return jsonify({"ok": False, "error": "insufficient balance", "balance_cents": get_balance(uid), "topup": SITE + "/keys"}), 402
+ con = db()
+ cur = con.execute("INSERT INTO shots(user_id,url,status,created) VALUES(?,?,?,?)", (uid, url, "queued", int(time.time())))
+ con.commit()
+ shot_run(cur.lastrowid)
+ st = con.execute("SELECT status FROM shots WHERE id=?", (cur.lastrowid,)).fetchone()
+ return jsonify({"ok": True, "id": cur.lastrowid, "status": st["status"], "poll": f"{SITE}/api/shot/status/{cur.lastrowid}"}), 200, {"Cache-Control": "no-store"}
+
+def shot_dict(row):
+ d = {"ok": True, "id": row["id"], "status": row["status"]}
+ try:
+ r = json.loads(row["result"]) if row["result"] else None
+ except Exception:
+ r = row["result"]
+ if row["status"] == "done" and r:
+ d["png_b64"] = r
+ try:
+ d["png_bytes"] = len(base64.b64decode(r))
+ except Exception:
+ pass
+ elif r:
+ d.update(r if isinstance(r, dict) else {"detail": str(r)[:400]})
+ return d
+
+@app.route("/api/shot/status/")
+def api_shot_status(sid):
+ con = db()
+ s = con.execute("SELECT * FROM shots WHERE id=?", (sid,)).fetchone()
+ if not s: return jsonify({"ok": False, "error": "unknown shot id"}), 404
+ if s["status"] == "queued": shot_run(sid) # lazy exec (reload-safe)
+ s = con.execute("SELECT * FROM shots WHERE id=?", (sid,)).fetchone()
+ return jsonify(shot_dict(s))
+
# ---------- 3. SMS RENTALS ----------
SMSP = "https://api.smspool.net"
SERVICES = [("google","Google"),("discord","Discord"),("telegram","Telegram"),("whatsapp","WhatsApp"),("other","Other/Any")]
@@ -564,6 +838,7 @@ def sms_guard():
@app.route("/sms", methods=["GET", "POST"])
def sms():
+ uid = current_user_id()
msg = ""
if request.method == "POST":
act = request.form.get("act")
@@ -576,8 +851,8 @@ def sms():
d = jf(b) or {}
if d.get("success") == 1:
con = db(); now = int(time.time())
- con.execute("INSERT INTO sms_rentals(phone,service,country,purchase_id,cost,status,created,expires) VALUES(?,?,?,?,?,?,?,?)",
- (d.get("number"), request.form["service"], request.form["country"], str(d.get("purchase_id")), d.get("cost",0), "active", now, now+1800))
+ con.execute("INSERT INTO sms_rentals(user_id,phone,service,country,purchase_id,cost,status,created,expires) VALUES(?,?,?,?,?,?,?,?,?)",
+ (uid, d.get("number"), request.form["service"], request.form["country"], str(d.get("purchase_id")), d.get("cost",0), "active", now, now+1800))
con.commit()
msg = f'
RENTED Your number: +{d.get("number")} · 30 min · order #{d.get("purchase_id")}
'
else:
@@ -595,8 +870,8 @@ def sms():
con = db(); con.execute("UPDATE sms_rentals SET status=? WHERE purchase_id=?", ("refunded" if ok else "cancel-failed", request.form["pid"])); con.commit()
msg = f'
{"CANCELLED + REFUNDED" if ok else "CANCEL FAILED"}
'
con = db()
- hist = con.execute("SELECT * FROM sms_rentals ORDER BY id DESC LIMIT 8").fetchall()
- hist_rows = "".join(f"
" for h in hist)
+ hist = con.execute("SELECT * FROM sms_rentals WHERE user_id=? ORDER BY id DESC LIMIT 8", (uid,)).fetchall()
+ hist_rows = "".join(f"
API: POST /api/sms/rent (service,country) · GET /api/sms/check?pid= · GET /api/sms/cancel?pid= · GET /api/sms/history
""" + how(["Pick a service and country, rent — the number is live for 30 minutes exactly.","Use it for any signup/verification. The code arrives as a text.","Poll the order (auto or manual) until the code shows.","Cancel before a code arrives and you get every satoshi back.","Each rental is logged in the recent-rentals table with a live countdown."])
+ body += gloss([("OTC","one-time code — the PIN a service texts you"),("burn","cancel an unused rental inside the refund window"),("SMSPool","our upstream number provider")])
return page("sms", body)
@app.route("/api/sms/rent", methods=["POST"])
@@ -641,8 +920,8 @@ def api_sms_rent():
d = jf(b) or {}
if d.get("success") == 1:
con = db(); now = int(time.time())
- con.execute("INSERT INTO sms_rentals(phone,service,country,purchase_id,cost,status,created,expires) VALUES(?,?,?,?,?,?,?,?)",
- (d.get("number"), param("service"), param("country"), str(d.get("purchase_id")), d.get("cost",0), "active", now, now+1800))
+ con.execute("INSERT INTO sms_rentals(user_id,phone,service,country,purchase_id,cost,status,created,expires) VALUES(?,?,?,?,?,?,?,?,?)",
+ (uid, d.get("number"), param("service"), param("country"), str(d.get("purchase_id")), d.get("cost",0), "active", now, now+1800))
con.commit()
cost = int(d.get("cost_in_cents") or 5)
if uid and not has_pass(uid):
@@ -667,7 +946,10 @@ def api_sms_history():
con = db(); now = int(time.time())
con.execute("UPDATE sms_rentals SET status='expired' WHERE status='active' AND expires < ?", (now,))
con.commit()
- return jsonify([dict(r) for r in con.execute("SELECT * FROM sms_rentals ORDER BY id DESC LIMIT 50")])
+ uid = key_user() or current_user_id()
+ if not uid:
+ return jsonify({"ok": False, "error": "login required (POST /inbox act=login)"}), 401
+ return jsonify([dict(r) for r in con.execute("SELECT * FROM sms_rentals WHERE user_id=? ORDER BY id DESC LIMIT 50", (uid,))])
# ---------- 4. PROXY LAB ----------
@app.route("/proxy", methods=["GET", "POST"])
@@ -714,6 +996,7 @@ def proxy():
API: POST /api/proxy/test (user, pass) → egress IP + geo JSON.
""" + how(["Enter your Pleiades gateway user:pass — the same credentials work across the fleet.","The lab tunnels a CONNECT request through the gateway and reports the true egress IP, geo and ISP.","Use the geo builder to steer the exit: region, country, city, sticky 30-min sessions.","Need bandwidth? Buy GB plans at the Pleiades storefront."])
+ body += gloss([("sticky session","same exit IP kept across requests"),("egress","the exit IP the rest of the internet sees"),("Pleiades","our proxy gateway network")])
return page("proxy", body)
@app.route("/api/proxy/test", methods=["POST"])
@@ -893,7 +1176,8 @@ d.addEventListener('change',function(){{}});
document.getElementById('ih').addEventListener('change',function(){{document.querySelector('.fnh').textContent=this.files[0].name}});
document.getElementById('ie').addEventListener('change',function(){{document.querySelector('.fne').textContent=this.files[0].name}});
-
API: POST /api/steg/hide (image, text, password?, bits 1-3, spread) → PNG · POST /api/steg/extract (image, password?, bits?, spread?) → JSON
""" + how(["Drop a PNG — your words are written into the least-significant bits of its pixels.","Depth 1 = invisible and robust; depth 2-3 fits more text but is easier to detect.","Spread=randomized scatters bits across the image instead of top-down.","A password encrypts the payload AND derives the scatter pattern — wrong password = noise.","Extract reads the embedded metadata automatically — just drop the file and the words come back."])
+
API: POST /api/steg/hide (image, text, password?, bits 1-3, spread) → PNG · POST /api/steg/extract (image, password?, bits?, spread?) → JSON
""" + how(["Drop a PNG — your words are written into the least-significant bits of its pixels.","Depth 1 = invisible and robust; depth 2-3 fits more text but is easier to detect.","Spread=randomized scatters bits across the image instead of top-down.","A password encrypts the payload AND derives the scatter pattern — wrong password = noise.","Extract reads the embedded metadata automatically — just drop the file and the words come back."])
+ body += gloss([("LSB","least significant bit — pixel bits that carry hidden data"),("depth","how many bit planes carry the payload"),("spread","payload dispersed across the image to survive edits")])
return page("steg", body)
@app.route("/api/steg/hide", methods=["POST"])
@@ -959,6 +1243,8 @@ def btc_invoice(amount="1.00"):
def api_track_create():
fn = param("filename") or "file"
uid = key_user() or current_user_id()
+ if not uid:
+ return jsonify({"ok": False, "error": "login required — create a no-KYC account at /inbox (POST /inbox act=register), then retry"}), 401
token = secrets.token_urlsafe(16)
con = db()
if has_pass(uid):
@@ -976,7 +1262,7 @@ def api_track_create():
con.execute("INSERT INTO trackables(user_id,token,filename,kind,invoice_id,paid,created) VALUES(?,?,?,?,?,0,?)",
(uid or 0, token, esc(fn[:100]), "file", inv["id"], int(time.time())))
con.commit()
- return jsonify({"ok": True, "invoice_id": inv["id"], "checkoutLink": inv.get("checkoutLink"), "token": token,
+ return _checkout_or_json({"ok": True, "invoice_id": inv["id"], "checkoutLink": inv.get("checkoutLink"), "token": token,
"after_payment_upload_url": f"{SITE}/track/pay?token={token}"})
@app.route("/track/pay", methods=["GET"])
@@ -1121,7 +1407,7 @@ def mail():
Type your desired mailbox name, pick a length, pay the invoice — the mailbox activates the moment the payment settles.
{mine}
API: POST /api/mail/create (local, days) → invoice · GET /api/mail/inbox?addr= (needs login) — inbound via Cloudflare Email Routing → worker relay.
""" + how(["Pick a name and a package — 7, 30 or 90 days, BTC priced.","Pay the invoice; the mailbox activates the second it settles.","The address is receive-only: verification codes, confirmations, one-off handouts.","The countdown runs in real time; when it hits zero the mailbox retires itself.","All mail shows on the site inbox — nothing touches any other identity."])
- return page("steg", body)
+ return page("mail", body)
@app.route("/api/mail/create", methods=["POST"])
def api_mail_create():
@@ -1153,7 +1439,7 @@ def api_mail_create():
con.execute("INSERT INTO mailboxes(user_id,address,invoice_id,paid,expires,created,plan_days) VALUES(?,?,?,?,?,?,?)",
(uid or 0, addr, inv["id"], 0, 0, int(time.time()), pack[3]))
con.commit()
- return jsonify({"ok": True, "address": addr, "checkoutLink": inv.get("checkoutLink"), "invoice_id": inv["id"]})
+ return _checkout_or_json({"ok": True, "address": addr, "checkoutLink": inv.get("checkoutLink"), "invoice_id": inv["id"]})
@app.route("/api/mail/inbound", methods=["POST"])
def api_mail_inbound():
@@ -1172,15 +1458,15 @@ def api_mail_inbound():
@app.route("/mail/view")
def mail_view():
uid = current_user_id()
- if not uid: return page("inbox", "
')
addr = param("addr") or ""
con = db()
m = con.execute("SELECT * FROM mailboxes WHERE address=? AND user_id=?", (addr.lower(), uid)).fetchone()
- if not m: return page("inbox", "
not your mailbox
")
+ if not m: return page("mail", "
not your mailbox
")
mails = con.execute("SELECT * FROM mails WHERE mailbox_id=? ORDER BY id DESC LIMIT 100", (m["id"],)).fetchall()
rows = "".join(f'
API: POST /api/forensics (image) → JSON: exif, gps, flags, ELA score.
""" + how(["Drop any image — EXIF and GPS get dumped instantly.","Error-level analysis (ELA) re-compresses and diffs: edited regions glow in the amplified view.","Edit-tool tags (Photoshop/GIMP) are flagged automatically.","EXIF-stripped images get flagged too — usually means scrubbed or generated.","If the image carries a DARK0RBITS stego payload, this tool sees it."])
- return page("steg", body)
+ body += gloss([("ELA","error level analysis — regions re-saved after editing light up"),("EXIF","camera/software metadata embedded in the file"),("quantization","JPEG compression-table fingerprints")])
+ return page("forensics", body)
def _ela_score(img_bytes):
from PIL import Image, ImageChops, ImageEnhance
@@ -1505,7 +1793,7 @@ def api_forensics():
except Exception: pass
_, maxdiff = _ela_score(data)
return jsonify({"ok": True, "exif": ex, "gps_present": bool(exif.get_ifd(0x8825)) if hasattr(exif, "get_ifd") else False,
- "stego_auriga": ("auriga_meta" in im.info or "dark0rbits_meta" in im.info), "ela_max_diff": maxdiff,
+ "stego_payload": ("auriga_meta" in im.info or "dark0rbits_meta" in im.info), "ela_max_diff": maxdiff,
"flags": (["exif-stripped"] if not ex else [])})
# ---------- 6f. CANARY TRAPS ----------
@@ -1520,7 +1808,7 @@ def canary():
You get: a link (paste anywhere), a pixel URL (embed in docs/pages), and a fake credential line to drop in files.
{canary_list()}
API: POST /canary (tag) · GET /api/canary/list (login) · hits log like trackables.
""" + how(["Create a trap and tag it with who/where it belongs.","Plant the link anywhere — or embed the pixel URL, or drop the fake credential line.","The moment ANYONE touches it: IP, geo, ISP, device fire into your inbox.","Each trap shows its hit count and armed/triggered status.","One trap per place — re-plant after it fires."])
- return page("track", body)
+ return page("canary", body)
def canary_list():
uid = current_user_id()
@@ -1536,7 +1824,7 @@ def canary_list():
@app.route("/canary", methods=["POST"])
def canary_create():
uid = current_user_id()
- if not uid: return page("track", "
login required
")
+ if not uid: return page("canary", "
login required
")
tag = (param("tag") or "untagged")[:80]
con = db()
token = secrets.token_urlsafe(12)
@@ -1571,7 +1859,7 @@ def canary_pixel(token):
@app.route("/api/canary/list")
def api_canary_list():
uid = current_user_id()
- if not uid: return jsonify({"ok": False, "error": "login required"})
+ if not uid: return jsonify({"ok": False, "error": "login required — free no-KYC account at /inbox"})
con = db()
rows = [dict(r) | {"hits": con.execute("SELECT COUNT(*) c FROM canary_hits WHERE canary_id=?", (r["id"],)).fetchone()["c"]} for r in con.execute("SELECT * FROM canaries WHERE user_id=? ORDER BY id DESC LIMIT 50", (uid,))]
return jsonify(rows)
@@ -1610,7 +1898,7 @@ def admin_reply():
@app.route("/api/passport")
def api_passport():
uid = current_user_id()
- if not uid: return jsonify({"ok": False, "error": "login required"})
+ if not uid: return jsonify({"ok": False, "error": "login required — free no-KYC account at /inbox"})
con = db()
u = con.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
return jsonify({"holder": u["username"], "issued": u["created"], "pass_active": has_pass(uid), "site": "dark0rbits.thetempleofdoom.com"})
@@ -1695,6 +1983,266 @@ def api_inbox_msgs():
con = db()
return jsonify([dict(r) for r in con.execute("SELECT * FROM messages WHERE user_id=? ORDER BY id DESC LIMIT 100", (uid,))])
+# ---------- 7h. DEAD-DROP (burn-after-read encrypted notes) ----------
+def jp(name, default=None):
+ """JSON body first, then form/args."""
+ if request.is_json:
+ j = request.get_json(silent=True)
+ if isinstance(j, dict) and name in j: return j[name]
+ v = param(name)
+ return v if v is not None else default
+
+DD_API = ("
AGENT API
POST " + SITE + """/api/deaddrop/create
+ Content-Type: application/json (or form fields)
+ {"body":"meet at 03:00","burn_after_reads":3,"ttl_hours":24,"password":"hunter2"}
+ -> {"ok":true,"url":"BASE/drop/TOKEN","reads":3,"expires_epoch":...}
+ auth: session cookie or Authorization: Bearer dk_...
+GET /drop/TOKEN burns one read; append ?p=password when locked
+free with PASS - otherwise 5c/note from balance (top up at /keys)
+rate limit: 10 creates/min
""").replace("BASE", SITE)
+
+DD_EXPLAINER = """
OPSEC NOTES
+• Payload is sealed with AES-256-GCM before it touches disk. The server holds ciphertext only — no plaintext column, no log.
+• TTL (1-72h) and burn-after-read (1-10) are both armed at creation.
+• The link token is ~96 bits of randomness. No listing, no search, no directory. Lose it and it is gone.
+• Optional password gate — wrong attempts cost nothing.
+• Billing: free with PASS, otherwise 5¢ per note from your metered balance.
"""
+
+@app.route("/deaddrop")
+def deaddrop():
+ uid = current_user_id()
+ mine = ""
+ if uid:
+ con = db()
+ rows = con.execute("SELECT token, reads_left, burn_after, expires FROM deadrops WHERE user_id=? ORDER BY id DESC LIMIT 10", (uid,)).fetchall()
+ if rows:
+ trs = "".join(f'
Burn-after-read encrypted notes. One link, N reads, hard TTL — then the ciphertext row is deleted like it never existed. No sender, no receiver, no trace.
+
New drop
+
+
{'Free with your PASS — or 5¢ from balance.' if uid else 'Sign in first (no KYC, no email) — free with PASS, else 5¢ from balance.'}
+{mine}
+{DD_EXPLAINER}
+""" + DD_API + how(["Write the payload, set reads + TTL, add a password if the channel is noisy.",
+ "Nothing with PASS — or 5 cents from your metered balance. No KYC either way.",
+ "Share only the /drop/ link — once, over a channel you trust.",
+ "Every open burns a read; the remaining count shows live on the page.",
+ "The final read deletes the row server-side. A tombstone is all that remains."])
+ return page("deaddrop", body)
+
+@app.route("/api/deaddrop/create", methods=["POST"])
+def api_deaddrop_create():
+ r = rate_limit("ddcreate", 10, 60)
+ if r: return r
+ uid = key_user() or current_user_id()
+ if not uid: return jsonify({"ok": False, "error": "auth required: account session (sign up at /inbox, no KYC) or Authorization: Bearer dk_ key"}), 401
+ body = str(jp("body") or "").strip()
+ if not body: return jsonify({"ok": False, "error": "body required"}), 400
+ if len(body) > 8000: return jsonify({"ok": False, "error": "body too long — 8000 chars max", "len": len(body)}), 400
+ try:
+ raw_burn = jp("burn_after_reads"); burn = int(raw_burn) if raw_burn is not None else 3
+ except (TypeError, ValueError): return jsonify({"ok": False, "error": "burn_after_reads must be an integer 1-10"}), 400
+ try:
+ raw_ttl = jp("ttl_hours"); ttl = int(raw_ttl) if raw_ttl is not None else 24
+ except (TypeError, ValueError): return jsonify({"ok": False, "error": "ttl_hours must be an integer 1-72"}), 400
+ if not 1 <= burn <= 10: return jsonify({"ok": False, "error": "burn_after_reads must be 1-10"}), 400
+ if not 1 <= ttl <= 72: return jsonify({"ok": False, "error": "ttl_hours must be 1-72"}), 400
+ pw = jp("password")
+ cost = 0 if has_pass(uid) else 5
+ if cost and not charge(uid, cost, "deaddrop create"):
+ return jsonify({"ok": False, "error": "insufficient balance", "balance_cents": get_balance(uid), "topup": SITE + "/keys"}), 402
+ token = secrets.token_urlsafe(12)
+ con = db()
+ exp = int(time.time()) + ttl * 3600
+ con.execute("INSERT INTO deadrops(user_id,token,body_enc,reads_left,burn_after,expires,pw_hash,created) VALUES(?,?,?,?,?,?,?,?)",
+ (uid, token, dd_encrypt(body), burn, burn, exp, hash_pw(pw) if pw else "", int(time.time())))
+ con.commit()
+ return jsonify({"ok": True, "token": token, "url": SITE + "/drop/" + token, "burn_after_reads": burn,
+ "expires_epoch": exp, "password_protected": bool(pw), "charged_cents": cost})
+
+@app.route("/drop/", methods=["GET", "POST"])
+def drop_view(token):
+ pw = param("p") or ""
+ con = db()
+ d = con.execute("SELECT * FROM deadrops WHERE token=?", (token,)).fetchone()
+ head = '
DEAD DROP
burn-after-read viewer
'
+ if not d:
+ return page("deaddrop", head + '
GONEburned, expired, or never existed. there is no listing to check — that is the point.
')
+ if d["expires"] < int(time.time()):
+ con.execute("DELETE FROM deadrops WHERE id=?", (d["id"],)); con.commit()
+ return page("deaddrop", head + '
TTL EXPIREDthe note aged out and was destroyed server-side.
')
+ if d["pw_hash"] and hash_pw(pw) != d["pw_hash"]:
+ return page("deaddrop", head + """
LOCKED
+
Wrong attempts burn nothing — a read counts only when the note actually opens.
""")
+ left = d["reads_left"] - 1
+ content = dd_decrypt(d["body_enc"]) or "(payload unreadable)"
+ prot = " · password-protected" if d["pw_hash"] else ""
+ if left <= 0:
+ con.execute("DELETE FROM deadrops WHERE id=?", (d["id"],)); con.commit()
+ note = 'FINAL READ — NOTE DESTROYEDthe ciphertext row is gone. this is the last copy anyone will ever see.'
+ else:
+ con.execute("UPDATE deadrops SET reads_left=? WHERE id=?", (left, d["id"])); con.commit()
+ note = f'READ OK{left} of {d["burn_after"]} reads left{prot} — the link dies at zero.'
+ return page("deaddrop", head + f"""
+
{note}
+
PAYLOAD
{esc(content)}
""")
+
+
+# ---------- 8b. FRAUD-SCORE (composite heuristic 0-100) ----------
+DISPOSABLE_DOMAINS = {"mailinator.com","guerrillamail.com","guerrillamail.net","guerrillamail.org","10minutemail.com","10minutemail.net",
+"temp-mail.org","tempmail.com","tempmailo.com","yopmail.com","yopmail.net","throwawaymail.com","getnada.com","nada.email",
+"dispostable.com","maildrop.cc","mailnesia.com","trashmail.com","trashmail.de","mytrashmail.com","sharklasers.com","grr.la",
+"bugmenot.com","mailcatch.com","tempinbox.com","tmpmail.org","tmpmail.net","fakeinbox.com","spamgourmet.com","mailexpire.com",
+"moakt.com","mohmal.com","emailondeck.com","burnermail.io","33mail.com","mailsac.com","inboxkitten.com","linshiyouxiang.net",
+"tempmail.plus","minuteinbox.com","instantemailaddress.com","discard.email","spam4.me","1secmail.com","1secmail.net","1secmail.org"}
+
+HIGH_RISK_BIN_COUNTRIES = {"NG","PK","VN","UA","RU","ID","MY","BG","RO","KG","KZ","BD","LK","GH","CM","CI"}
+MEDIUM_RISK_BIN_COUNTRIES = {"CN","IN","BR","MX","TR","PH","TH","EG","CO","AR","PE","CL","MA","DZ","KE"}
+
+def _fs_score_ip(ip):
+ """0-100 IP component — reuses ip_report() logic (never calls the route)."""
+ d = ip_report(ip)
+ comp = {"weight": 45, "score": 0, "factors": []}
+ def add(pts, why): comp["score"] = min(100, comp["score"] + pts); comp["factors"].append(f"+{pts} {why}")
+ if d.get("proxy"): add(40, "proxy/VPN flag on IP")
+ if d.get("hosting"): add(25, "hosting/datacenter ASN (not residential)")
+ if d.get("mobile"): add(-10, "mobile carrier (typ. consumer device)")
+ cc = str(d.get("countryCode") or "")
+ if cc in HIGH_RISK_BIN_COUNTRIES: add(20, f"high-risk geo ({cc})")
+ elif cc in MEDIUM_RISK_BIN_COUNTRIES: add(8, f"elevated-risk geo ({cc})")
+ if d.get("status") == "fail" or not d.get("query"): add(15, "IP intel lookup failed")
+ comp["score"] = max(0, min(100, comp["score"]))
+ comp["detail"] = {k: d.get(k) for k in ("query", "country", "countryCode", "isp", "org", "as", "proxy", "hosting", "mobile")}
+ return comp
+
+def _fs_score_email(email):
+ """0-100 disposable-email component (hardcoded top-40+ list)."""
+ comp = {"weight": 25, "score": 0, "factors": []}
+ if not email:
+ comp["factors"].append("not provided — component skipped")
+ return comp
+ e = email.strip().lower()
+ if "@" not in e or e.startswith("@") or e.endswith("@"):
+ comp["score"] = 50; comp["factors"].append("+50 malformed address")
+ return comp
+ dom = e.rsplit("@", 1)[1]
+ if dom in DISPOSABLE_DOMAINS:
+ comp["score"] = 100; comp["factors"].append(f"+100 disposable domain ({dom})")
+ else:
+ comp["score"] = 5; comp["factors"].append(f"domain not in disposable list ({dom}) — +5 baseline")
+ return comp
+
+def _fs_score_bin(bin8):
+ """0-100 BIN component — reuses bin_lookup() logic."""
+ comp = {"weight": 30, "score": 0, "factors": []}
+ if not bin8:
+ comp["factors"].append("not provided — component skipped")
+ return comp
+ bin8 = re.sub(r"\D", "", str(bin8))[:8]
+ if len(bin8) < 6:
+ comp["score"] = 50; comp["factors"].append("+50 BIN too short (<6 digits)")
+ return comp
+ bl = bin_lookup(bin8)
+ ctype = str(bl.get("type") or "").lower()
+ prepaid = bl.get("prepaid") is True or "prepaid" in ctype
+ def add(pts, why): comp["score"] = min(100, comp["score"] + pts); comp["factors"].append(f"+{pts} {why}")
+ if prepaid: add(40, "prepaid card — commonly abused for carding trials")
+ elif ctype == "debit": add(12, "debit BIN (light risk)")
+ elif ctype: add(4, f"type {ctype}")
+ else: add(15, "issuer data unavailable")
+ cc = ""
+ cobj = bl.get("country") or {}
+ cc = (cobj.get("alpha2") or cobj.get("countryCode") or cobj.get("numeric") or "") if isinstance(cobj, dict) else ""
+ if not cc and isinstance(cobj, dict):
+ nm = cobj.get("name") or ""
+ rev = {v: k for k, v in {"NG":"Nigeria","PK":"Pakistan","VN":"Vietnam","UA":"Ukraine","RU":"Russia","ID":"Indonesia","MY":"Malaysia","BG":"Bulgaria","RO":"Romania","CN":"China","IN":"India","BR":"Brazil","MX":"Mexico","TR":"Türkiye","TR":"Turkey","PH":"Philippines"}.items()}
+ cc = rev.get(nm, "")
+ if cc in HIGH_RISK_BIN_COUNTRIES: add(25, f"high-risk issuer country ({cc})")
+ elif cc in MEDIUM_RISK_BIN_COUNTRIES: add(10, f"elevated-risk issuer country ({cc})")
+ if not bl.get("bank") or not (bl.get("bank") or {}).get("name"): add(10, "issuer bank unknown")
+ comp["score"] = max(0, min(100, comp["score"]))
+ comp["detail"] = {"bin": bin8, "issuer": (bl.get("bank") or {}).get("name"), "country": (cobj.get("name") if isinstance(cobj, dict) else None) or cc or None, "type": bl.get("type"), "prepaid": bl.get("prepaid"), "scheme": bl.get("scheme")}
+ return comp
+
+def fraud_score(ip=None, email=None, bin8=None):
+ parts, total, wsum = [], 0, 0
+ for comp in ([_fs_score_ip(ip)] if ip else []) + ([_fs_score_email(email)] if email else []) + ([_fs_score_bin(bin8)] if bin8 else []):
+ parts.append(comp); total += comp["score"] * comp["weight"]; wsum += comp["weight"]
+ if not wsum: return None
+ composite = round(total / wsum)
+ if composite >= 70: band = "HIGH"
+ elif composite >= 40: band = "MEDIUM"
+ else: band = "LOW"
+ conf = min(100, 30 + int(20 * (len(parts) - 1) + wsum / 3))
+ return {"score": composite, "band": band, "confidence": conf, "components": parts}
+
+SCORE_EXPLAINER = """
RISK MODEL
+• IP component (weight 45): datacenter or relay origins, high-risk geos.
+• Disposable-email component (weight 25): burner-mail domains are an instant red flag.
+• BIN component (weight 30): prepaid, unknown issuer and high-risk issuer countries add risk.
+• Composite = weighted average, banded LOW <40 ≤ MEDIUM <70 ≤ HIGH.
+• Confidence rises with the number of inputs scored. 2¢/call, free with PASS. Rate limit 20/min.
"""
+
+SCORE_API = ("
AGENT API
GET " + SITE + """/api/score?ip=1.2.3.4&email=victim@mailinator.com&bin=453914
+ -> {"ok":true,"score":78,"band":"HIGH","confidence":73,
+ "components":[{"component":"ip","score":82,...},"email":...,"bin":...]}
+ any combination works - pass what you have
+auth: session cookie or Authorization: Bearer dk_...
+2c/call, free with PASS - rate limit 20/min
""").replace("BASE", SITE)
+
+@app.route("/score")
+def score_page():
+ q_ip = (param("ip") or "").strip()
+ q_email = (param("email") or "").strip()
+ q_bin = (param("bin") or "").strip()
+ res = ""
+ if q_ip or q_email or q_bin:
+ r = fraud_score(q_ip or None, q_email or None, q_bin or None)
+ if r:
+ res = f"""
Composite 0-100 risk for an identity shard: IP + email + card BIN. Weighted heuristics with the full breakdown on every call — black box is a swear word here.
+
+{res}
+{SCORE_EXPLAINER}""" + SCORE_API + how(["Feed any combination of IP, email and BIN — components re-weight around what you provide.",
+ "IP: proxy/hosting flags + geo risk, via the same intel engine as /ip.",
+ "Email: matched against a hardcoded list of burner-mail domains.",
+ "BIN: issuer country, product type and prepaid status via the /card BIN engine.",
+ "Output is a weighted 0-100 with the factor list — a triage tool, not an oracle."])
+ return page("score", body)
+
+@app.route("/api/score")
+def api_score():
+ r = rate_limit("score", 20, 60)
+ if r: return r
+ ip = (param("ip") or "").strip() or None
+ email = (param("email") or "").strip() or None
+ bin8 = (param("bin") or "").strip() or None
+ if not (ip or email or bin8): return jsonify({"ok": False, "error": "at least one of ip, email, bin required"}), 400
+ uid = key_user() or current_user_id()
+ if not uid: return jsonify({"ok": False, "error": "auth required: account session (sign up at /inbox, no KYC) or Authorization: Bearer dk_ key"}), 401
+ if not has_pass(uid) and not charge(uid, 2, "fraud score"):
+ return jsonify({"ok": False, "error": "insufficient balance", "balance_cents": get_balance(uid), "topup": SITE + "/keys"}), 402
+ fr = fraud_score(ip, email, bin8)
+ if not fr: return jsonify({"ok": False, "error": "scoring failed"}), 500
+ return jsonify({"ok": True, "ip": ip, "email": email, "bin": bin8, "score": fr["score"], "band": fr["band"], "confidence": fr["confidence"], "components": fr["components"]})
+
# ---------- 8. FREE TOOLS ----------
TOOLS_JS = """
function tab(n){document.querySelectorAll('.pane').forEach(p=>p.style.display='none');document.getElementById(n).style.display='block'}
@@ -1751,7 +2299,11 @@ def tools():
Generators
-
"""
+
+
Heavy tools(full pages, each with a JSON API)
+◈ DEAD-DROP — burn-after-read encrypted notes ·
+◈ SCREENSHOT — page capture or rendered-text preview ·
+◈ FRAUD-SCORE — composite IP + email + BIN risk 0-100
')
con = db()
msgs = con.execute("SELECT m.*, u.username FROM messages m JOIN users u ON u.id=m.user_id ORDER BY m.id DESC LIMIT 100").fetchall()
msgs_html = "".join(f'