Add HydraProxy provider (aggregator): wrapper + /hydraproxy storefront + admin balance + API + MCP tool (management-only, broker-fulfilled)

This commit is contained in:
drjones
2026-09-20 18:40:25 -07:00
parent 2e238a626e
commit 16dde93ce2
6 changed files with 161 additions and 1 deletions

36
app.py
View File

@@ -13,6 +13,7 @@ from flask import (Flask, abort, flash, jsonify, redirect, render_template,
import db
import iproyal
import hydraproxy
CFG = db.CFG
app = Flask(__name__)
@@ -476,6 +477,33 @@ def poll_provisioning():
return ok, failed
# ---------- HydraProxy provider (management-only, manual broker fulfillment) ----------
HYDRA_PRODUCTS = CFG.get("hydraproxy", {}).get("products", {})
@app.route("/hydraproxy")
def hydraproxy_page():
account = None
err = None
try:
account = hydraproxy.get_account_info()
except Exception as e:
err = str(e)
return render_template("hydraproxy.html", products=HYDRA_PRODUCTS,
account=account, err=err, user=current_user())
@app.route("/api/v1/hydraproxy/info")
def api_hydraproxy_info():
try:
account = hydraproxy.get_account_info()
locations = hydraproxy.get_locations()
return jsonify({"account": account, "locations": locations,
"products": HYDRA_PRODUCTS})
except Exception as e:
return jsonify({"error": str(e)}), 502
# ---------- admin ----------
ADMIN_PASSWORD = CFG.get("admin_password", "")
@@ -507,10 +535,16 @@ def admin():
ipr_balance = iproyal.get_balance()
except Exception:
ipr_balance = "unavailable"
try:
hydra_acct = hydraproxy.get_account_info()
hydra_balance = hydra_acct.get("balance_usd", "unavailable")
except Exception:
hydra_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, low_balance=low_balance)
dedicated=deds, ipr_balance=ipr_balance, low_balance=low_balance,
hydra_balance=hydra_balance)
@app.route("/admin/credit", methods=["POST"])

88
hydraproxy.py Normal file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""HydraProxy management API wrapper.
Base: https://app.hydraproxy.com/api/ (auth: Authorization: Token <key>)
NOTE: HydraProxy's API is MANAGEMENT-ONLY — it CANNOT place/buy proxy orders.
Orders are purchased manually from app.hydraproxy.com; this wrapper lists and
manages them (details, rotation, location, whitelist, usage history).
Gotchas (from their docs):
- All digit POST params must be sent as STRINGS (not ints).
- POST URLs must end with a trailing slash.
- API only accepts calls from whitelisted IPs (up to 3, manual, 30-min lockdown).
- First item of every response is `status` ("OK" or "ERROR").
"""
import json
import os
import requests
CFG = json.load(open(os.environ.get("PLEIADES_CONFIG", "/opt/pleiades/config.json")))
HP = CFG["hydraproxy"]
TOKEN = HP["token"]
API = HP["api"].rstrip("/")
def _hdr():
return {"Authorization": f"Token {TOKEN}", "Accept": "application/json"}
def get_account_info():
"""{status, username, email, balance_usd}"""
r = requests.get(f"{API}/get-account-info/", headers=_hdr(), timeout=20)
r.raise_for_status()
return r.json()
def get_locations():
"""List of available geo locations."""
r = requests.get(f"{API}/get-locations-list/", headers=_hdr(), timeout=20)
r.raise_for_status()
return r.json()
def proxy_details(order_id):
"""Details of one order (network, proxy_info, proxy host/port)."""
r = requests.post(f"{API}/proxy-details/", headers=_hdr(),
data={"order_id": str(order_id)}, timeout=20)
r.raise_for_status()
return r.json()
def force_rotation(order_id):
r = requests.post(f"{API}/force-rotation/", headers=_hdr(),
data={"order_id": str(order_id)}, timeout=20)
r.raise_for_status()
return r.json()
def update_location(order_id, location):
r = requests.post(f"{API}/update-location/", headers=_hdr(),
data={"order_id": str(order_id), "location": str(location)}, timeout=20)
r.raise_for_status()
return r.json()
def update_rotation(order_id, auto=True):
r = requests.post(f"{API}/update-rotation/", headers=_hdr(),
data={"order_id": str(order_id), "auto": "1" if auto else "0"}, timeout=20)
r.raise_for_status()
return r.json()
def update_whitelist_ip(order_id, ip):
r = requests.post(f"{API}/update-whitelist-ip/", headers=_hdr(),
data={"order_id": str(order_id), "whitelist_ip": str(ip)}, timeout=20)
r.raise_for_status()
return r.json()
def view_usage_history(order_id, date_from="", date_to=""):
data = {"order_id": str(order_id)}
if date_from:
data["date_from"] = date_from
if date_to:
data["date_to"] = date_to
r = requests.post(f"{API}/view-usage-history/", headers=_hdr(), data=data, timeout=20)
r.raise_for_status()
return r.json()

View File

@@ -91,6 +91,12 @@ def list_dedicated_locations(product: int = 9) -> str:
return json.dumps(_get("/dedicated/locations", params={"product": product}), indent=2)
@mcp.tool()
def hydraproxy_account() -> str:
"""HydraProxy account info, balance, locations, and broker catalog (broker-fulfilled provider)."""
return json.dumps(_get("/api/v1/hydraproxy/info"), indent=2)
if __name__ == "__main__":
mcp.settings.host = "0.0.0.0"
mcp.settings.port = int(os.environ.get("MCP_PORT", "8090"))

View File

@@ -9,6 +9,7 @@
<div class="stats-row">
<div class="card stat"><span class="lbl">Revenue (paid)</span><span class="val">{{ stats.revenue_sats }} sats</span></div>
<div class="card stat"><span class="lbl">IPRoyal balance</span><span class="val">${{ ipr_balance }}</span></div>
<div class="card stat"><span class="lbl">HydraProxy balance</span><span class="val">${{ hydra_balance }}</span></div>
<div class="card stat"><span class="lbl">Users</span><span class="val">{{ stats.users }} <span style="font-size:14px;color:var(--muted)">({{ stats.active }} active)</span></span></div>
</div>
<div class="stats-row">

View File

@@ -39,6 +39,7 @@
<a href="/">Home</a>
<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>
{% else %}<a href="/login">Login</a><a class="cta" href="/signup">Get proxies</a>{% endif %}
</div>

30
templates/hydraproxy.html Normal file
View File

@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}HydraProxy — Clean Proxys{% endblock %}
{% block body %}
<div class="hero" style="padding:64px 0 28px">
<h1 style="font-size:36px">Hydra<span class="grad">Proxy</span> tier.</h1>
<p class="lede">Residential, 4G mobile, and static-mobile IPs — brokered and delivered by Clean Proxys. You pay us, we place the order with HydraProxy, you connect.</p>
</div>
{% if err %}
<div class="flash warn">HydraProxy link is offline right now: {{ err }}<br><span class="muted">(Usually means the broker IP isn't whitelisted at HydraProxy yet — the catalog below still shows what's offered.)</span></div>
{% else %}
<div class="statsbar">
<div class="s"><div class="n grad">${{ account.balance_usd if account else '—' }}</div><div class="l">HydraProxy balance</div></div>
<div class="s"><div class="n grad">{{ account.username if account else '—' }}</div><div class="l">Account</div></div>
</div>
{% endif %}
<div class="grid">
{% for key, p in products.items() %}
<div class="card plan">
<h3>{{ p.name }}</h3>
<p>{{ p.desc }}</p>
<div class="price">{{ p.price_sats }} <span style="font-size:16px;color:var(--muted)">sats / {{ p.unit }}</span></div>
<p class="muted" style="font-size:13px;margin-top:8px">Brokered — we place the order with HydraProxy after payment and hand you the live proxy.</p>
</div>
{% endfor %}
</div>
<p class="muted" style="margin-top:26px">These are broker-fulfilled (order placed manually with HydraProxy). For instant self-serve, see <a href="/plans">residential plans</a> and <a href="/dedicated">dedicated IPs</a>.</p>
{% endblock %}