commit d0c7e0b8810580d2a73801452cd6033cbbf544c8 Author: drjones Date: Wed Aug 19 19:29:15 2026 -0700 Initial commit: Hyperion — App Store for AI agents - app.py: single-file Flask service (MCP-native catalog, BTCPay billing) - README.md: overview + how to run - LICENSE: MIT - requirements.txt: flask, requests, werkzeug diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faf7761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +hyperion.db +__pycache__/ +*.pyc +.env diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..92a9d72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Hyperion + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c934f86 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Hyperion + +**Hyperion is the App Store for AI agents.** A hosted marketplace where developers publish MCP +servers/tools and agents discover, subscribe to, and call them entirely programmatically — no human +in the loop. Discovery is MCP-native, access is via programmatically-issued API keys, and billing is +Bitcoin pay-per-call / subscription settled through BTCPay. The primary customer is an agent, not a +person: an agent finds a tool, pays for a meter in sats, gets a key, and calls it. + +## What it is + +A single-file Flask service. Dark premium UI. Simple auth (username + password, no KYC). +Bitcoin-settled subscription via BTCPay, with API keys issued programmatically at signup. + +### Feature overview + +- **MCP-native catalog** — the index itself is an MCP server + JSON API; agents query it, resolve a + tool, get metered, and open a stream under a signed API key. +- **Programmatic API keys** — keys are handed to an agent the moment it registers (username + password + only, no KYC). +- **Bitcoin settlement** — Pro subscriptions ($19/mo) and metered calls are paid in BTC through + BTCPay. No card, no KYC. +- **Free tier** — catalog discovery + 100 metered calls/month + a programmatic API key. +- **Pro tier** — unlimited discovery, 10,000 metered calls/month, priority routing, and webhook / + metering exports. Publisher side carries a 20% platform fee, settled in BTC. + +## How to run it + +```bash +# 1. Install dependencies (Python 3) +pip3 install flask requests werkzeug + +# 2. Run the service +python3 app.py +``` + +The service binds to `0.0.0.0:5000`. A BTCPay webhook posts to `/webhook/btcpay` to mark invoices paid. + +## Configuration (environment variables) + +All BTCPay values are overridable via env vars; sensible defaults are baked in. + +| Variable | Purpose | +| ----------------------- | ------------------------------------------ | +| `HYPERION_BTCPAY_URL` | Base URL of the BTCPay instance | +| `HYPERION_BTCPAY_STORE` | BTCPay store ID | +| `HYPERION_BTCPAY_KEY` | BTCPay API key | +| `HYPERION_BTCPAY_WALLET`| xpub (extended public key) of the wallet | +| `HYPERION_SECRET` | Flask session secret (random if unset) | + +Example: + +```bash +HYPERION_BTCPAY_URL=https://btcpay.example \ +HYPERION_BTCPAY_STORE= \ +HYPERION_BTCPAY_KEY= \ +HYPERION_BTCPAY_WALLET= \ +python3 app.py +``` + +## Files + +- `app.py` — the entire application (Flask app, UI, auth, BTCPay integration). +- `LICENSE` — MIT License. + +## API surface + +- `/` — landing page (MCP-native catalog pitch). +- `/pricing` — Free vs. Pro pricing. +- `/register`, `/login`, `/logout` — auth. +- `/dashboard` — agent dashboard: API key, metered-call counter, Pro subscription via BTCPay. +- `/about`, `/health` — meta endpoints. +- `/api/...` — JSON routes authorized by the agent's API key. +- `/webhook/btcpay` — BTCPay payment webhook (marks invoices paid, activates Pro). + +## License + +MIT. See `LICENSE`. diff --git a/app.py b/app.py new file mode 100644 index 0000000..24482d0 --- /dev/null +++ b/app.py @@ -0,0 +1,812 @@ +#!/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.

+ +
+""" + 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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a9dc73a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask>=3.0 +requests>=2.31 +werkzeug>=3.0