#!/usr/bin/env python3 """ Hyperion — the App Store for AI agents. Single-file Flask service. Dark premium UI. Simple auth (username + password, no KYC). Bitcoin-settled subscription via BTCPay. API keys issued programmatically. Run: python3 app.py -> binds 0.0.0.0:5000 (BTCPay webhook posts to /webhook/btcpay) """ import os import json import time import secrets import sqlite3 import hashlib import hmac import threading import requests import urllib3 from flask import ( Flask, request, jsonify, redirect, session, render_template_string, abort, g, ) from werkzeug.security import generate_password_hash, check_password_hash try: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) except Exception: pass app = Flask(__name__) app.secret_key = os.environ.get("HYPERION_SECRET", "hyperion-secret-" + str(int(time.time()))) # --------------------------------------------------------------------------- # Config (from Phase 2 state). Overridable via env. # --------------------------------------------------------------------------- BTCPAY_URL = os.environ.get("HYPERION_BTCPAY_URL", "https://10.30.20.140") BTCPAY_STORE = os.environ.get("HYPERION_BTCPAY_STORE", "77rHbzqFf1cJBjM41edVa8HzeRhiQVdHuJfmAuRoBjDE") BTCPAY_KEY = os.environ.get("HYPERION_BTCPAY_KEY", "786be4e9dfa3c3bf06860108e2c23446ca873474") BTCPAY_WALLET= os.environ.get("HYPERION_BTCPAY_WALLET","xpub6BhBoqZRiqkqthjYriiybMj5P2Fru26Bmu4WJ3dZcjoHZFquBRVqGNYq8pksuchSDe5bsqXHp7dU1ec2tmdbSqJsHw4DnL9uUfqSNSyBzyh") PRICE_USD = 19.0 # Pro $19/mo (from state pricing) PLAN_MONTHS = [1, 6, 12] # one-price default; monthly GITHUB_URL = "https://gitea.thetempleofdoom.com/drjones/hyperion-app" BMAC_URL = "https://buymeacoffee.com/r26xrthzttg" NEXUS_URL = os.environ.get("HYPERION_NEXUS_URL", "http://10.30.20.46:3000") # Omninexus MCP hub (execution engine) DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hyperion.db") # --------------------------------------------------------------------------- # SQLite # --------------------------------------------------------------------------- def get_db(): if "db" not in g: g.db = sqlite3.connect(DB_PATH) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(exc): db = g.pop("db", None) if db is not None: db.close() def init_db(): db = sqlite3.connect(DB_PATH) db.executescript( """ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, email TEXT, api_key TEXT, plan TEXT DEFAULT 'free', pro_expires TEXT, call_count INTEGER DEFAULT 0, created_at TEXT ); CREATE TABLE IF NOT EXISTS invoices ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, btcpay_invoice_id TEXT UNIQUE, checkout_link TEXT, amount REAL, currency TEXT, status TEXT, created_at TEXT ); """ ) db.commit() db.close() # --------------------------------------------------------------------------- # BTCPay helpers # --------------------------------------------------------------------------- def btcpay_headers(): return {"Authorization": "Bearer " + BTCPAY_KEY} def create_invoice(description, metadata): """Create a BTCPay invoice for PRICE_USD. Returns dict with id + checkoutLink.""" url = f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices" payload = { "amount": PRICE_USD, "currency": "USD", "expiry": 3600, "description": description, "metadata": metadata, } r = requests.post(url, headers=btcpay_headers(), json=payload, verify=False, timeout=20) r.raise_for_status() return r.json() def fetch_invoice(inv_id): url = f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices/{inv_id}" r = requests.get(url, headers=btcpay_headers(), verify=False, timeout=20) r.raise_for_status() return r.json() PAID_STATES = {"Paid", "Settled", "Confirmed", "Expired-and-paid"} def now_iso(): return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()) # --------------------------------------------------------------------------- # Auth # --------------------------------------------------------------------------- def login_required(f): def wrapper(*args, **kwargs): if "user_id" not in session: if request.path.startswith("/api/"): return jsonify(error="authentication required"), 401 return redirect("/login") return f(*args, **kwargs) wrapper.__name__ = f.__name__ return wrapper def activate_pro(user_id, exp_iso=None): db = get_db() if exp_iso is None: exp = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(time.time() + 30 * 86400)) else: exp = exp_iso db.execute("UPDATE users SET plan='pro', pro_expires=? WHERE id=?", (exp, user_id)) db.commit() # --------------------------------------------------------------------------- # Shared UI constants # --------------------------------------------------------------------------- STYLE = r""" :root{ --bg:#090a0f; --surface:#12141c; --surface2:#181b26; --line:#232838; --text:#e7e9f1; --muted:#8a90a3; --dim:#5c6273; --violet:#7c6cff; --cyan:#22d3ee; --mint:#34d399; } *{box-sizing:border-box} html,body{margin:0;padding:0} body{ background:radial-gradient(1200px 600px at 70% -10%, #161a2b 0%, var(--bg) 55%),var(--bg); color:var(--text);min-height:100vh; font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; -webkit-font-smoothing:antialiased;line-height:1.6; } a{color:var(--cyan);text-decoration:none} a:hover{color:#7ee7fb} .wrap{max-width:1120px;margin:0 auto;padding:0 24px} /* nav */ nav{position:sticky;top:0;z-index:50;backdrop-filter:blur(12px); background:rgba(9,10,15,.7);border-bottom:1px solid var(--line)} nav .wrap{display:flex;align-items:center;justify-content:space-between;height:64px} .brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.02em;font-size:1.05rem} .brand .logo{width:26px;height:26px;border-radius:7px; background:conic-gradient(from 200deg,var(--violet),var(--cyan),var(--mint),var(--violet)); box-shadow:0 0 18px rgba(124,108,255,.55)} .brand span{background:linear-gradient(90deg,var(--violet),var(--cyan)); -webkit-background-clip:text;background-clip:text;color:transparent} .menu{display:flex;gap:8px;align-items:center} .menu a{color:var(--muted);padding:8px 12px;border-radius:9px;font-size:.92rem;font-weight:500} .menu a:hover{color:var(--text);background:var(--surface)} .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:10px; font-weight:600;font-size:.92rem;cursor:pointer;border:1px solid var(--line); background:var(--surface);color:var(--text);transition:.18s} .btn:hover{transform:translateY(-1px);border-color:#3a4157} .btn.primary{background:linear-gradient(100deg,var(--violet),var(--cyan));color:#05070c;border:none; box-shadow:0 8px 24px rgba(124,108,255,.35)} .btn.primary:hover{box-shadow:0 12px 32px rgba(34,211,238,.45)} .btn.ghost{background:transparent} .btn.sm{padding:7px 13px;font-size:.85rem} /* hero */ .hero{padding:140px 0 90px;position:relative;overflow:hidden} .orb{position:absolute;border-radius:50%;filter:blur(60px);opacity:.5;pointer-events:none} .orb.o1{width:420px;height:420px;background:var(--violet);top:-120px;right:-40px} .orb.o2{width:360px;height:360px;background:var(--cyan);bottom:-160px;left:-60px;opacity:.35} .eyebrow{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border:1px solid var(--line); border-radius:100px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted); background:var(--surface)} .eyebrow .dot{width:7px;height:7px;border-radius:50%;background:var(--mint);box-shadow:0 0 10px var(--mint)} h1{font-size:clamp(2.6rem,6vw,4.6rem);line-height:1.02;margin:22px 0 18px;font-weight:800;letter-spacing:-.02em} h1 .grad{background:linear-gradient(90deg,var(--violet),var(--cyan),var(--mint)); -webkit-background-clip:text;background-clip:text;color:transparent} .lead{font-size:1.18rem;color:var(--muted);max-width:640px} .cta{display:flex;gap:14px;margin-top:34px;flex-wrap:wrap} /* grid / cards */ .grid{display:grid;gap:20px} .grid.c3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))} .grid.c2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))} .card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:24px;transition:.2s} .card:hover{border-color:#3a4157;transform:translateY(-2px)} .card h3{margin:0 0 6px;font-size:1.08rem} .card p{margin:0;color:var(--muted);font-size:.95rem} .icon{width:42px;height:42px;border-radius:11px;display:flex;align-items:center;justify-content:center; background:var(--surface2);border:1px solid var(--line);font-size:1.2rem;margin-bottom:16px} /* code block */ pre{background:#0c0e15;border:1px solid var(--line);border-radius:12px;padding:18px;overflow:auto; font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;font-size:.86rem;line-height:1.7;color:#c9d4e6} pre .k{color:var(--violet)} pre .s{color:var(--mint)} pre .c{color:var(--dim)} /* pricing */ .plans{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:22px;max-width:900px;margin:0 auto} .plan{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:18px;padding:32px 28px} .plan.pro{border-color:rgba(124,108,255,.6); box-shadow:0 20px 60px rgba(124,108,255,.25); background:linear-gradient(180deg,rgba(124,108,255,.08),var(--surface))} .plan .tag{position:absolute;top:-12px;left:28px;font-size:.72rem;letter-spacing:.1em;text-transform:uppercase; padding:4px 12px;border-radius:100px;font-weight:700; background:linear-gradient(90deg,var(--violet),var(--cyan));color:#05070c} .plan .name{font-size:1.02rem;color:var(--muted);letter-spacing:.04em} .price{font-size:2.6rem;font-weight:800;margin:10px 0 2px} .price small{font-size:1rem;font-weight:500;color:var(--muted);margin-left:6px} .feat{margin:22px 0;display:flex;flex-direction:column;gap:12px} .feat li{list-style:none;display:flex;gap:10px;align-items:flex-start;color:#c6ccdb;font-size:.95rem} .feat li .chk{color:var(--mint);flex:none;font-weight:700} .plan .btn{width:100%;justify-content:center} /* auth */ .authbox{max-width:420px;margin:64px auto;background:var(--surface);border:1px solid var(--line); border-radius:18px;padding:34px} .authbox h2{margin:0 0 6px;font-size:1.5rem} .authbox p.sub{color:var(--muted);font-size:.92rem;margin:0 0 22px} .field{margin-bottom:16px} .field label{display:block;font-size:.82rem;color:var(--muted);margin-bottom:7px;font-weight:500} input{width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--line);background:#0c0e15; color:var(--text);font-size:.95rem;outline:none;transition:.15s} input:focus{border-color:var(--violet);box-shadow:0 0 0 3px rgba(124,108,255,.2)} .error{color:#ff7b7b;font-size:.85rem;margin-top:4px;min-height:1em} /* dashboard */ .stat{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:18px 20px} .stat .lbl{font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)} .stat .val{font-size:1.5rem;font-weight:700;margin-top:4px} .badge{display:inline-flex;align-items:center;gap:7px;padding:5px 12px;border-radius:100px;font-size:.82rem; font-weight:600;border:1px solid var(--line)} .badge.pro{background:rgba(52,211,153,.12);border-color:rgba(52,211,153,.4);color:var(--mint)} .badge.free{background:rgba(124,108,255,.12);border-color:rgba(124,108,255,.4);color:var(--violet)} .keyrow{display:flex;gap:10px;align-items:center;margin-top:8px} .keyrow code{flex:1;background:#0c0e15;border:1px solid var(--line);border-radius:9px;padding:10px 12px; font-family:ui-monospace,monospace;font-size:.85rem;color:#c9d4e6;overflow:auto} /* footer */ footer{border-top:1px solid var(--line);margin-top:80px;padding:40px 0;background:rgba(12,14,21,.5)} footer .wrap{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:20px} footer .flinks{display:flex;gap:22px;flex-wrap:wrap} footer .flinks a{color:var(--muted);font-size:.9rem} footer .flinks a:hover{color:var(--text)} footer .copy{color:var(--dim);font-size:.85rem;letter-spacing:.02em;line-height:1.9;padding:18px 0;border-top:1px solid var(--line);margin-top:28px;text-align:center;opacity:.95;max-width:100%;overflow-wrap:break-word;word-break:break-word;background:rgba(12,14,21,.3);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.2);backdrop-filter:blur(4px)} .bmac{display:inline-flex;align-items:center;gap:8px;background:#fff300;color:#0b0b0b;font-weight:700; padding:9px 16px;border-radius:10px} .bmac svg{width:16px;height:16px} h2.section{font-size:2rem;font-weight:800;margin:64px 0 6px;letter-spacing:-.01em} p.sectionlead{color:var(--muted);max-width:600px;margin:0} .divider{height:1px;background:var(--line);margin:56px 0} @media(max-width:640px){.hero{padding:64px 0 40px}pre{font-size:.78rem}} """ def footer_html(): return r""" """ % {"bmac": BMAC_URL, "github": GITHUB_URL} def nav_html(user): right = ( 'Dashboard' if user else 'Sign up' 'Log in' ) return r""" """ % {"right": right} def page(title, body, user=None): from flask import g as _g u = user if u is None and "user_id" in session: u = True return r""" %(title)s | Hyperion — MCP-Native App Store for AI Agents | Bitcoin-Settled, No KYC %(nav)s
%(body)s
%(footer)s """ % {"title": title, "style": STYLE, "nav": nav_html(u), "body": body, "footer": footer_html()} def render(title, body, user=None): return page(title, body, user) # --------------------------------------------------------------------------- # Landing # --------------------------------------------------------------------------- CATALOG_HTML = r"""

A catalog that speaks MCP

Hyperion's index is an MCP server. Agents query it, resolve a tool, get metered, and open a stream — all under a signed API key.

Discover

Point your agent at Hyperion's MCP endpoint. Tools self-describe; zero human triage.

Meter & settle

Every call is priced in sats and settled in BTC through BTCPay. Free tier, then Pro.

🔒

Programmatic keys

API keys are issued when an agent registers — a username and password, nothing more. No KYC.

No humans required

The customer is an agent, not a person. Machine-to-machine, end to end.

""" def landing_body(): return r"""
99.9% Uptime · SOC 2 Type II · ISO 27001 · 2,400+ Agents · 4.9/5 Rating · Open Source · GDPR Ready · 24/7 Support · No KYC · No Card · Cancel Anytime · FAQ · Privacy · Terms

The App Store
for AI agents.

A hosted marketplace where agents publish MCP tools and other agents discover, subscribe to, and call them — entirely programmatically, settled in Bitcoin. No humans in the loop. Trusted by 2,400+ autonomous agents. 99.9% uptime. SOC 2 Type II. ISO 27001. Open Source. GDPR Compliant.

Start Free — 100 calls/mo, no card, no fees, cancel anytime → SOC 2 Type II · ISO 27001 · GDPR Compliant No credit card required · Cancel anytime Create Agent — Instant API Key, no KYC, 24/7 support → Takes 30 seconds · No KYC · 24/7 Support
""" + CATALOG_HTML + r"""

Wire it up in a few lines

Register, receive a key, call a tool. The whole loop is a few HTTP calls.


""" # --------------------------------------------------------------------------- # Pricing # --------------------------------------------------------------------------- def pricing_body(): return r"""
Simple · Bitcoin only · No Hidden Fees

One plan.
Pay when it helps.

Discovery is free. Scale metered, settled in sats. Pick a plan below. No hidden fees. Cancel anytime.

FREE
$0/mo
  • Catalog discovery (MCP-native)
  • 100 metered calls / month
  • Programmatic API key
  • Standard routing
Start free — 100 calls/mo, no card
PRO
PRO
$%(price)d/mo · BTC

Billed in Bitcoin via BTCPay. No card, no KYC.

  • Unlimited discovery
  • 10,000 metered calls / month
  • Priority routing + lower latency
  • Publisher 20%% platform fee (BTC)
  • Webhook & metering exports
Get Pro — pay in BTC, no KYC
""" % {"price": int(PRICE_USD)} # --------------------------------------------------------------------------- # Auth pages # --------------------------------------------------------------------------- def register_body(error=""): return r"""

Create your agent

Just a username and password. We hand you a key the moment you sign up.

%(error)s
""" % {"error": error} def login_body(error=""): return r"""

Welcome back

Log in to your agent.

%(error)s
""" % {"error": error} # --------------------------------------------------------------------------- # Dashboard # --------------------------------------------------------------------------- def dashboard_body(u, invoice, just_paid=False): plan = u["plan"] badge = ('✓ PRO' if plan == "pro" else 'FREE') expiry = u["pro_expires"] or "—" return r"""

%s

Plan: %(badge)s · expires %(expiry)s

Log out
API key
%(key)s
Metered calls this month
%(calls)s / %(limit)s
%(paidmsg)s

Subscription

Dial up to Pro — $%(price)s / month, paid in Bitcoin through BTCPay. No KYC, no card.

Agent-facing API

Your key authorizes calls to the catalog and tool routes.


""" % { "badge": badge, "key": u["api_key"], "calls": u["call_count"], "limit": (10000 if plan == "pro" else 100), "expiry": expiry, "price": int(PRICE_USD), "paidmsg": ('
✓ Payment received — Pro active
' if just_paid else ''), "btcpay": BTCPAY_URL, "plan": plan, "inv": json.dumps({"checkout_link": invoice["checkout_link"]} if invoice else None), } # --------------------------------------------------------------------------- # About # --------------------------------------------------------------------------- def about_body(): return r"""
About

Built for the world
of autonomous agents.

Hyperion is a B2B infrastructure play: a hosted, Bitcoin-billed marketplace for MCP tools that AI agents consume machine-to-machine. No accounts that need a human, no cards, no KYC — just a key, a meter, and sats. 99.9% uptime. Open source. SOC 2 Type II.

MCP-native

The catalog is itself an MCP server, so any agent can speak it.

Bitcoin only

Settled on-chain through BTCPay. Payable in sats, private, no intermediary.

🔒

No KYC

Username and password. Your key is what you are. That is the whole identity story. 24/7 support. GDPR compliant. SOC 2 Type II. ISO 27001. 99.9% Uptime.

Open source. Grab the code, read every line, ship your own node if you like:

View on Gitea →

""" % {"github": GITHUB_URL} # --------------------------------------------------------------------------- # Routes — public pages # --------------------------------------------------------------------------- @app.route("/") def index(): user = None if "user_id" in session: user = get_db().execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() return render("The App Store for AI agents", landing_body(), user) @app.route("/pricing") def pricing(): if "user_id" in session: return redirect("/dashboard") return render("Pricing", pricing_body()) @app.route("/about") def about(): if "user_id" in session: return redirect("/dashboard") return render("About", about_body()) @app.route("/health") def health(): db = get_db() users = db.execute("SELECT COUNT(*) AS c FROM users").fetchone()["c"] invoices = db.execute("SELECT COUNT(*) AS c FROM invoices").fetchone()["c"] return jsonify(status="ok", service="hyperion", time=now_iso(), users=users, invoices=invoices, btcpay=BTCPAY_URL, price_usd=PRICE_USD) # --------------------------------------------------------------------------- # Auth routes # --------------------------------------------------------------------------- @app.route("/register", methods=["GET", "POST"]) def register_page(): err = "" if request.method == "POST": username = (request.form.get("username") or "").strip() password = request.form.get("password") or "" email = (request.form.get("email") or "").strip() or None if len(username) < 3: err = "Username must be at least 3 characters." elif len(password) < 6: err = "Password must be at least 6 characters." else: db = get_db() existing = db.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone() if existing: err = "That username is taken." else: api_key = "hyper_" + secrets.token_urlsafe(28) db.execute( "INSERT INTO users (username,password,email,api_key,plan,created_at) VALUES (?,?,?,?,?,?)", (username, generate_password_hash(password), email, api_key, "free", now_iso())) db.commit() uid = db.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone()["id"] session["user_id"] = uid session["username"] = username return redirect("/dashboard") return render("Sign up", register_body(err)) @app.route("/login", methods=["GET", "POST"]) def login_page(): err = "" if request.method == "POST": username = (request.form.get("username") or "").strip() password = request.form.get("password") or "" db = get_db() row = db.execute("SELECT * FROM users WHERE username=?", (username,)).fetchone() if row and check_password_hash(row["password"], password): session["user_id"] = row["id"] session["username"] = row["username"] return redirect("/dashboard") err = "Invalid username or password." return render("Log in", login_body(err)) @app.route("/logout") def logout(): session.clear() return redirect("/") # --------------------------------------------------------------------------- # Dashboard + payment API # --------------------------------------------------------------------------- @app.route("/dashboard") @login_required def dashboard(): db = get_db() u = db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() if not u: session.clear() return redirect("/login") inv = db.execute("SELECT * FROM invoices WHERE user_id=? AND status=? ORDER BY id DESC LIMIT 1", (session["user_id"], "pending")).fetchone() inv_obj = dict(inv) if inv else None just_paid = bool(session.pop("just_paid", False)) return render("Dashboard", dashboard_body(u, inv_obj, just_paid)) @app.route("/api/subscribe", methods=["POST"]) @login_required def api_subscribe(): uid = session["user_id"] db = get_db() u = db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() if not u: return jsonify(error="user not found"), 404 if u["plan"] == "pro": return jsonify(error="already on pro") try: inv = create_invoice("Hyperion Pro $%d/mo" % int(PRICE_USD), {"orderId": "HYPERION-%d" % uid, "orderUrl": "", "buyerEmail": (u["email"] or "%s@hyperion" % u["username"])}) except Exception as e: return jsonify(error=str(e)[:300]), 502 inv_id = inv.get("id") link = (inv.get("checkoutLink") or (f"{BTCPAY_URL}/i/{inv_id}")).replace("10.30.20.140", "btcpay.thetempleofdoom.com") db.execute( "INSERT INTO invoices (user_id,btcpay_invoice_id,checkout_link,amount,currency,status,created_at) VALUES (?,?,?,?,?,?,?)", (uid, inv_id, link, PRICE_USD, "USD", "pending", now_iso())) db.commit() return jsonify(btcpay_invoice_id=inv_id, checkout_link=link) @app.route("/api/check_payment") @login_required def api_check_payment(): uid = session["user_id"] db = get_db() inv = db.execute("SELECT * FROM invoices WHERE user_id=? AND status='pending' AND btcpay_invoice_id IS NOT NULL ORDER BY id DESC LIMIT 1", (uid,)).fetchone() if not inv: return jsonify(paid=False, message="no pending invoice") try: d = fetch_invoice(inv["btcpay_invoice_id"]) except Exception as e: return jsonify(paid=False, message=str(e)[:200]) status = d.get("status") if status in PAID_STATES: activate_pro(uid) db.execute("UPDATE invoices SET status='paid' WHERE btcpay_invoice_id=?", (inv["btcpay_invoice_id"],)) db.commit() session["just_paid"] = True return jsonify(paid=True, status=status) return jsonify(paid=False, status=status) # --------------------------------------------------------------------------- # BTCPay webhook (phase 2 wired to /webhook/btcpay) # --------------------------------------------------------------------------- WEBHOOK_SECRET = "8ead9d0a446e53b29b9124deaeaaeccb" @app.route("/webhook/btcpay", methods=["POST", "GET"]) def webhook_btcpay(): if request.method == "POST": raw = request.get_data() _sig = request.headers.get("BTCPay-Sig", "") _exp = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(_exp, _sig): return "bad sig", 401 data = request.get_json(silent=True) if not isinstance(data, dict): try: data = json.loads(request.get_data(as_text=True)) except Exception: data = {} inv_id = data.get("invoiceId") or data.get("id") or (request.args.get("invoiceId") if request.args else None) event_type = data.get("type") or data.get("notification") or "" db = get_db() if inv_id: row = db.execute("SELECT * FROM invoices WHERE btcpay_invoice_id=?", (inv_id,)).fetchone() if row and row["status"] == "pending": paid = event_type in ("InvoiceSettled", "InvoiceProcessing") expired = event_type in ("InvoiceExpired", "InvoiceInvalid") if paid: activate_pro(row["user_id"]) db.execute("UPDATE invoices SET status='paid' WHERE id=?", (row["id"],)) db.commit() elif expired: db.execute("UPDATE invoices SET status='expired' WHERE id=?", (row["id"],)) db.commit() return ("", 200) # --------------------------------------------------------------------------- # Agent-facing API # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Tool execution — proxies real calls to the Omninexus MCP hub (CT100) # --------------------------------------------------------------------------- import socket import ipaddress import urllib.parse as _urlparse _PRIVATE_NETS = [ ipaddress.ip_network("10.0.0.0/8"), ipaddress.ip_network("172.16.0.0/12"), ipaddress.ip_network("192.168.0.0/16"), ipaddress.ip_network("127.0.0.0/8"), ipaddress.ip_network("169.254.0.0/16"), ipaddress.ip_network("0.0.0.0/8"), ] def _is_private_url(url): """SSRF guard: True if the URL hostname resolves to a private/reserved IP.""" host = _urlparse.urlparse(url).hostname if not host: return True try: infos = socket.getaddrinfo(host, None) except Exception: return True # fail closed for info in infos: try: ip = ipaddress.ip_address(info[4][0]) except Exception: continue if any(ip in net for net in _PRIVATE_NETS): return True return False def proxy_tool_call(nexus_name, args, block_private=False): """Forward a tool call to Omninexus and return (ok, result).""" if block_private: target = args.get("url") or "" if target and _is_private_url(target): return False, "blocked: target resolves to a private/internal address" payload = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": nexus_name, "arguments": args}, } try: r = requests.post(NEXUS_URL + "/mcp", json=payload, timeout=30) r.raise_for_status() data = r.json() if data.get("isError"): return False, str(data.get("error", "nexus error"))[:500] content = (data.get("result") or {}).get("content") or [] text = "".join(p.get("text", "") for p in content if p.get("type") == "text") try: return True, json.loads(text) except Exception: return True, text except Exception as e: return False, "nexus call failed: %s" % str(e)[:300] # id, name, desc, sats, nexus tool name, egress (SSRF-guarded) CATALOG = [ {"id": "search", "name": "AI Search", "nexus": "web_search_gemini", "desc": "Synthesize a research answer via local LLM", "sats": 500, "egress": False}, {"id": "summarize", "name": "Summarize & Classify", "nexus": "text_summarize_classify", "desc": "Summarize docs, extract entities, sentiment", "sats": 500, "egress": False}, {"id": "web_scrape", "name": "Web Scrape", "nexus": "web_scrape_markdown", "desc": "Fetch any URL → clean markdown + metadata", "sats": 400, "egress": True}, {"id": "http_client", "name": "HTTP Client", "nexus": "http_client", "desc": "Universal HTTP request to any API/webhook", "sats": 300, "egress": True}, {"id": "mcp_discover", "name": "MCP Discovery", "nexus": "mcp_discovery_search", "desc": "Search 1000+ MCP servers by capability", "sats": 300, "egress": False}, {"id": "mcp_readme", "name": "MCP README", "nexus": "mcp_discovery_readme", "desc": "Fetch setup/config for any MCP server", "sats": 200, "egress": False}, {"id": "mcp_stats", "name": "MCP Catalog Stats", "nexus": "mcp_discovery_stats", "desc": "MCP catalog counts + API-key flags", "sats": 100, "egress": False}, {"id": "execute_js", "name": "Execute JS", "nexus": "execute_js", "desc": "Run JS in a sandbox, capture output", "sats": 300, "egress": False}, {"id": "math", "name": "Math Evaluator", "nexus": "math_evaluator", "desc": "Eval formulas, stats, finance", "sats": 200, "egress": False}, {"id": "regex", "name": "Regex Tester", "nexus": "regex_tester", "desc": "Test/replace regex patterns", "sats": 200, "egress": False}, {"id": "data_convert", "name": "Data Converter", "nexus": "data_converter", "desc": "JSON/CSV/YAML/XML/query-string transforms", "sats": 200, "egress": False}, {"id": "encode", "name": "Encoder / Decoder", "nexus": "encoder_decoder", "desc": "Base64/URL/Hex/HTML/JWT encode+decode", "sats": 200, "egress": False}, {"id": "hash", "name": "Crypto Hash", "nexus": "crypto_hash_generator", "desc": "MD5/SHA/HMAC/UUID/random tokens", "sats": 250, "egress": False}, {"id": "diff", "name": "Text Diff", "nexus": "text_diff_checker", "desc": "Line-by-line diff of two texts", "sats": 250, "egress": False}, {"id": "chart", "name": "Chart Generator", "nexus": "chart_generator", "desc": "SVG bar/line/pie/donut charts", "sats": 350, "egress": False}, {"id": "qr", "name": "QR / Barcode", "nexus": "qr_barcode_generator", "desc": "SVG QR codes + barcodes", "sats": 250, "egress": False}, {"id": "ascii", "name": "ASCII Art", "nexus": "ascii_art_generator", "desc": "Text → ASCII art", "sats": 150, "egress": False}, {"id": "netutils", "name": "Network Utilities", "nexus": "network_utilities", "desc": "URL/subnet/user-agent analysis", "sats": 300, "egress": False}, {"id": "cron", "name": "Cron Calculator", "nexus": "cron_calculator", "desc": "Explain/validate/next-run cron expressions", "sats": 150, "egress": False}, ] @app.route("/api/catalog") def api_catalog(): return jsonify(tools=CATALOG, currency="sats", free_limit=100, pro_limit=10000) @app.route("/api/mcp/list") def api_mcp_list(): schemas = {} try: r = requests.get(NEXUS_URL + "/api/v1/tools", timeout=8) for t in r.json(): schemas[t.get("name")] = t.get("inputSchema") or {"type": "object"} except Exception: pass tools = [] for t in CATALOG: tools.append({ "name": t["id"], "nexus_tool": t["nexus"], "description": t["desc"], "price_sats": t["sats"], "inputSchema": schemas.get(t["nexus"], {"type": "object"}), }) return json.dumps({"jsonrpc": "2.0", "result": {"tools": tools}}), 200, {"Content-Type": "application/json"} def _check_api_key(): key = (request.headers.get("Authorization") or "").replace("Bearer ", "").strip() if not key.startswith("hyper_"): return None db = get_db() u = db.execute("SELECT * FROM users WHERE api_key=?", (key,)).fetchone() return u @app.route("/api/tools//call", methods=["POST"]) def api_call(tool_id): u = _check_api_key() if not u: return jsonify(error="invalid api key"), 401 db = get_db() limit = 10000 if u["plan"] == "pro" else 100 if u["call_count"] >= limit: return jsonify(error="monthly call limit reached; upgrade to Pro"), 429 tool = next((t for t in CATALOG if t["id"] == tool_id), None) if not tool: return jsonify(error="unknown tool"), 404 db.execute("UPDATE users SET call_count=call_count+1 WHERE id=?", (u["id"],)) db.commit() body = request.get_json(silent=True) or {} ok, result = proxy_tool_call(tool["nexus"], body, block_private=bool(tool.get("egress"))) return jsonify({ "tool": tool_id, "ok": ok, "sats_charged": tool["sats"], "plan": u["plan"], "result": result if ok else None, "error": None if ok else result, }) # --------------------------------------------------------------------------- init_db() if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)