#!/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 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://github.com/drjones/hyperion" BMAC_URL = "https://buymeacoffee.com/r26xrthzttg" 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:96px 0 64px;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} .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"""
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.
Point your agent at Hyperion's MCP endpoint. Tools self-describe; zero human triage.
Every call is priced in sats and settled in BTC through BTCPay. Free tier, then Pro.
API keys are issued when an agent registers — a username and password, nothing more. No KYC.
The customer is an agent, not a person. Machine-to-machine, end to end.
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.
Register, receive a key, call a tool. The whole loop is a few HTTP calls.
Discovery is free. Scale metered, settled in sats. Pick a plan below.
Billed in Bitcoin via BTCPay. No card, no KYC.
Just a username and password. We hand you a key the moment you sign up.
Log in to your agent.
Plan: %(badge)s · expires %(expiry)s
Dial up to Pro — $%(price)s / month, paid in Bitcoin through BTCPay. No KYC, no card.
Your key authorizes calls to the catalog and tool routes.
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.
The catalog is itself an MCP server, so any agent can speak it.
Settled on-chain through BTCPay. Payable in sats, private, no intermediary.
Username and password. Your key is what you are. That is the whole identity story.
Open source. Grab the code, read every line, ship your own node if you like:
""" % {"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}") 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) # --------------------------------------------------------------------------- @app.route("/webhook/btcpay", methods=["POST", "GET"]) def webhook_btcpay(): 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) notification = data.get("notification") or data.get("status") 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 = notification in PAID_STATES expired = notification == "Expired" 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 # --------------------------------------------------------------------------- CATALOG = [ {"id": "search", "name": "Search", "desc": "Semantic search over any corpus", "sats": 500}, {"id": "summarize", "name": "Summarize", "desc": "Condense docs to an abstract", "sats": 700}, {"id": "translate", "name": "Translate", "desc": "40 languages, fast", "sats": 300}, {"id": "code_review", "name": "Code Review", "desc": "Security + style lint", "sats": 1200}, {"id": "extract", "name": "Extract", "desc": "Structured fields from text", "sats": 400}, ] @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(): tools = [{"name": t["id"], "description": t["desc"], "inputSchema": {"type": "object"}} for t in CATALOG] 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/