diff --git a/app.py b/app.py index c1d8042..6772a2c 100644 --- a/app.py +++ b/app.py @@ -487,8 +487,10 @@ def admin(): ipr_balance = iproyal.get_balance() except Exception: ipr_balance = "unavailable" + low_balance = isinstance(ipr_balance, (int, float)) and \ + 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) + dedicated=deds, ipr_balance=ipr_balance, low_balance=low_balance) @app.route("/admin/credit", methods=["POST"]) diff --git a/monitor.py b/monitor.py new file mode 100644 index 0000000..721be8d --- /dev/null +++ b/monitor.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Monitor IPRoyal reseller balance. Prints an ALERT line (for cron -> Telegram) +when below threshold. Exit 0 = OK, 1 = low/failed.""" +import sys + +import iproyal + +threshold = iproyal.CFG.get("alerts", {}).get("low_balance_threshold", 10.0) + +try: + bal = iproyal.get_balance() +except Exception as e: + print(f"ALERT: IPRoyal balance check FAILED: {e}") + sys.exit(1) + +if isinstance(bal, (int, float)) and bal < threshold: + print(f"ALERT: IPRoyal reseller balance ${bal:.2f} is below ${threshold:.2f}. Top up now.") + sys.exit(1) + +print(f"OK: IPRoyal balance ${bal}") +sys.exit(0) diff --git a/sweep.py b/sweep.py new file mode 100644 index 0000000..3b1efa1 --- /dev/null +++ b/sweep.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Auto-top-up (sweep): send BTC to IPRoyal when reseller balance is low. + +SAFETY: DRY-RUN by default — prints what it *would* do, never moves money. +To go live you must: + 1. set sweep.iproyal_btc_address in config.json (IPRoyal's BTC deposit address), + 2. wire the send() function to your source wallet (BTCPay hot wallet / Alby), + 3. pass --live, with a hard cap via --cap-sats. +""" +import argparse +import sys + +import iproyal + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--live", action="store_true", help="actually send (default: dry-run)") + ap.add_argument("--cap-sats", type=int, default=0, help="hard cap on sats to send") + args = ap.parse_args() + + cfg = iproyal.CFG.get("sweep", {}) + threshold = iproyal.CFG.get("alerts", {}).get("low_balance_threshold", 10.0) + max_sats = args.cap_sats or int(cfg.get("max_sats", 100000)) + dest = cfg.get("iproyal_btc_address", "") + + try: + bal = iproyal.get_balance() + except Exception as e: + print(f"balance check failed: {e}") + sys.exit(1) + + if bal >= threshold: + print(f"balance ${bal} >= threshold ${threshold} — no top-up needed") + return + + if not dest: + print("NO destination set (sweep.iproyal_btc_address). " + "Set it in config.json before going live.") + return + + if args.live: + print(f"LIVE: balance ${bal} < ${threshold} — would send {max_sats} sats to {dest}") + # TODO: wire send() to source wallet (BTCPay hot wallet / Alby on-chain). + # Hard cap enforced here: never exceed max_sats. + print("WARNING: send() not yet wired — this is a no-op placeholder.") + else: + print(f"DRY-RUN: balance ${bal} < ${threshold} — would top up {max_sats} sats to {dest}") + + +if __name__ == "__main__": + main() diff --git a/templates/admin.html b/templates/admin.html index fa9e21f..0fabce1 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1,6 +1,11 @@ {% extends "base.html" %} {% block body %}