#!/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""" %(title)s · Hyperion %(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"""
Agent-native · Bitcoin settlement

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.

See pricing → Create an agent account
""" + 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

One plan.
Pay when it helps.

Discovery is free. Scale metered, settled in sats. Pick a plan below.

FREE
$0/mo
  • Catalog discovery (MCP-native)
  • 100 metered calls / month
  • Programmatic API key
  • Standard routing
Start free
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
""" % {"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.

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.

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

View on GitHub →

""" % {"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//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 {} result = { "tool": tool_id, "ok": True, "sats_charged": tool["sats"], "result": "Synthesized output for: %s" % str(body.get("input", ""))[:120], "plan": u["plan"], } return jsonify(result) # --------------------------------------------------------------------------- init_db() if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)