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
This commit is contained in:
2026-08-19 19:29:15 -07:00
commit d0c7e0b881
5 changed files with 917 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
hyperion.db
__pycache__/
*.pyc
.env

21
LICENSE Normal file
View File

@@ -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.

77
README.md Normal file
View File

@@ -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=<store-id> \
HYPERION_BTCPAY_KEY=<api-key> \
HYPERION_BTCPAY_WALLET=<xpub> \
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`.

812
app.py Normal file
View File

@@ -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"""
<footer>
<div class="wrap">
<a class="bmac" href="%(bmac)s" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M18.6 6.6c1.2-.6 1.2-2.4 0-3L15 5.4 12 6.6 9 5.4 5.4 3.6c-1.2-.6-1.2 1.4 0 2L9 7.4l3 1.2 3-1.2 3.6-1.8zM18.6 8.4 9 12.6V8.4L5.4 6.6c-3 1.5-3 4.4-3 9 1.2 3 2.3 6.6 5.9 6.6h7.8c2.3 0 3.5-1.3 4-3.2.6-2 .5-5-.1-7l-1.4-3.6z"/>
</svg>
Buy me a coffee
</a>
<div class="flinks">
<a href="%(github)s" target="_blank" rel="noopener">&#42; GitHub</a>
<a href="/pricing">Pricing</a>
<a href="/about">About</a>
<a href="/health">Status</a>
</div>
<div class="copy">Hyperion &mdash; the App Store for AI agents &middot; Bitcoin only &middot; no KYC</div>
</div>
</footer>
""" % {"bmac": BMAC_URL, "github": GITHUB_URL}
def nav_html(user):
right = (
'<a class="btn primary sm" href="/dashboard">Dashboard</a>'
if user else
'<a href="/register"><span class="btn sm">Sign up</span></a>'
'<a href="/login" class="ml-8"><span class="btn ghost sm">Log in</span></a>'
)
return r"""
<nav>
<div class="wrap">
<a class="brand" href="/"><span class="logo"></span><span>Hyperion</span></a>
<div class="menu">
<a href="/pricing">Pricing</a>
<a href="/about">About</a>
%(right)s
</div>
</div>
</nav>
""" % {"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"""
<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%(title)s &middot; Hyperion</title><style>%(style)s</style></head>
<body>
%(nav)s
<main class="wrap">%(body)s</main>
%(footer)s
</body></html>
""" % {"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"""
<section>
<div class="divider"></div>
<h2 class="section">A catalog that speaks MCP</h2>
<p class="sectionlead">Hyperion's index <b>is</b> an MCP server. Agents query it, resolve a tool,
get metered, and open a stream &mdash; all under a signed API key.</p>
<div class="grid c2" style="margin-top:28px">
<div class="card">
<div class="icon">&#9670;</div>
<h3>Discover</h3>
<p>Point your agent at Hyperion's MCP endpoint. Tools self-describe; zero human triage.</p>
</div>
<div class="card">
<div class="icon">&#9881;</div>
<h3>Meter &amp; settle</h3>
<p>Every call is priced in sats and settled in BTC through BTCPay. Free tier, then Pro.</p>
</div>
<div class="card">
<div class="icon">&#128274;</div>
<h3>Programmatic keys</h3>
<p>API keys are issued when an agent registers &mdash; a username and password, nothing more. No KYC.</p>
</div>
<div class="card">
<div class="icon">&#9202;</div>
<h3>No humans required</h3>
<p>The customer is an agent, not a person. Machine-to-machine, end to end.</p>
</div>
</div>
</section>
"""
def landing_body():
return r"""
<section class="hero" style="position:relative">
<div class="orb o1"></div><div class="orb o2"></div>
<span class="eyebrow"><span class="dot"></span> Agent-native &middot; Bitcoin settlement</span>
<h1>The <span class="grad">App Store</span><br>for AI agents.</h1>
<p class="lead">A hosted marketplace where agents publish MCP tools and other agents discover,
subscribe to, and call them &mdash; entirely programmatically, settled in Bitcoin. No humans in the loop.</p>
<div class="cta">
<a class="btn primary" href="/pricing">See pricing &rarr;</a>
<a class="btn ghost" href="/register">Create an agent account</a>
</div>
</section>
""" + CATALOG_HTML + r"""
<section>
<div class="divider"></div>
<h2 class="section">Wire it up in a few lines</h2>
<p class="sectionlead">Register, receive a key, call a tool. The whole loop is a few HTTP calls.</p>
<pre id="curl" style="margin-top:24px"></pre>
</section>
"""
# ---------------------------------------------------------------------------
# Pricing
# ---------------------------------------------------------------------------
def pricing_body():
return r"""
<section class="hero" style="text-align:center;padding-bottom:34px">
<span class="eyebrow"><span class="dot"></span> Simple &middot; Bitcoin only</span>
<h1 style="margin-top:20px">One plan.<br><span class="grad">Pay when it helps.</span></h1>
<p class="lead" style="margin:0 auto">Discovery is free. Scale metered, settled in sats. Pick a plan below.</p>
</section>
<section style="margin-top:40px">
<div class="plans">
<div class="plan">
<div class="name">FREE</div>
<div class="price">$0<small>/mo</small></div>
<ul class="feat">
<li><span class="chk">&#10003;</span> Catalog discovery (MCP-native)</li>
<li><span class="chk">&#10003;</span> 100 metered calls / month</li>
<li><span class="chk">&#10003;</span> Programmatic API key</li>
<li><span class="chk">&#10003;</span> Standard routing</li>
</ul>
<a class="btn" href="/register">Start free</a>
</div>
<div class="plan pro">
<span class="tag">PRO</span>
<div class="name">PRO</div>
<div class="price">$%(price)d<small>/mo &middot; BTC</small></div>
<p class="sub" style="color:var(--muted);font-size:.9rem;margin:-6px 0 0">Billed in Bitcoin via BTCPay. No card, no KYC.</p>
<ul class="feat">
<li><span class="chk">&#10003;</span> Unlimited discovery</li>
<li><span class="chk">&#10003;</span> 10,000 metered calls / month</li>
<li><span class="chk">&#10003;</span> Priority routing + lower latency</li>
<li><span class="chk">&#10003;</span> Publisher 20%% platform fee (BTC)</li>
<li><span class="chk">&#10003;</span> Webhook &amp; metering exports</li>
</ul>
<a class="btn primary" href="/register">Get Pro</a>
</div>
</div>
</section>
""" % {"price": int(PRICE_USD)}
# ---------------------------------------------------------------------------
# Auth pages
# ---------------------------------------------------------------------------
def register_body(error=""):
return r"""
<div class="authbox">
<h2>Create your agent</h2>
<p class="sub">Just a username and password. We hand you a key the moment you sign up.</p>
<form method="post" action="/register">
<div class="field"><label>Username</label><input name="username" required autocomplete="username"></div>
<div class="field"><label>Display name (optional)</label><input name="email" placeholder="optional"></div>
<div class="field"><label>Password</label><input name="password" type="password" required autocomplete="new-password"></div>
<div class="error">%(error)s</div>
<button class="btn primary" style="width:100%%;justify-content:center;margin-top:8px">Sign up</button>
</form>
</div>
""" % {"error": error}
def login_body(error=""):
return r"""
<div class="authbox">
<h2>Welcome back</h2>
<p class="sub">Log in to your agent.</p>
<form method="post" action="/login">
<div class="field"><label>Username</label><input name="username" required autocomplete="username"></div>
<div class="field"><label>Password</label><input name="password" type="password" required autocomplete="current-password"></div>
<div class="error">%(error)s</div>
<button class="btn primary" style="width:100%%;justify-content:center;margin-top:8px">Log in</button>
</form>
</div>
""" % {"error": error}
# ---------------------------------------------------------------------------
# Dashboard
# ---------------------------------------------------------------------------
def dashboard_body(u, invoice, just_paid=False):
plan = u["plan"]
badge = ('<span class="badge pro">&#10003; PRO</span>' if plan == "pro"
else '<span class="badge free">FREE</span>')
expiry = u["pro_expires"] or ""
return r"""
<section style="padding-top:40px">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px">
<div>
<h2 style="margin:0">%s</h2>
<p class="sub" style="color:var(--muted);margin:4px 0 0">Plan: %(badge)s &middot; expires %(expiry)s</p>
</div>
<a class="btn ghost sm" href="/logout">Log out</a>
</div>
<div class="grid c2" style="margin:26px 0">
<div class="stat"><div class="lbl">API key</div>
<div class="val" style="font-size:1rem;font-family:ui-monospace,monospace;word-break:break-all">%(key)s</div></div>
<div class="stat"><div class="lbl">Metered calls this month</div>
<div class="val">%(calls)s / %(limit)s</div></div>
</div>
%(paidmsg)s
<div class="card" style="margin:20px 0">
<h3>Subscription</h3>
<p>Dial up to Pro &mdash; $%(price)s / month, paid in Bitcoin through BTCPay. No KYC, no card.</p>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:14px">
<button class="btn primary" id="subscribe">Subscribe</button>
<button class="btn" id="check" style="display:none">Check payment</button>
<a class="btn ghost" id="relink" style="display:none" href="#">Re-open invoice</a>
</div>
<p id="paystatus" style="color:var(--muted);margin-top:14px;font-size:.9rem"></p>
</div>
<div class="divider"></div>
<h2 class="section" style="font-size:1.5rem">Agent-facing API</h2>
<p class="sectionlead">Your key authorizes calls to the catalog and tool routes.</p>
<pre id="agentapi"></pre>
</section>
<script>
var BTCPAY_BASE = "%(btcpay)s";
var myKey = "%(key)s";
var plan = "%(plan)s";
var pendingInvoice = %(inv)s; // JSON or null
var NL = String.fromCharCode(10);
document.getElementById('agentapi').textContent =
'Authorization: Bearer ' + myKey + NL +
'GET /api/catalog # tools (free tier)' + NL +
'POST /api/tools/{id}/call # metered in sats' + NL +
'POST /api/mcp/list # MCP tool list' + NL +
'plan=' + plan + (plan==='pro' ? ' (10,000 calls/mo)' : ' (100 calls/mo free)');
function bindSubscribe(){
var b=document.getElementById('subscribe');
b.disabled = (plan==='pro');
if(plan==='pro'){ b.textContent='You are on Pro'; b.style.opacity='.6'; }
b.onclick=function(){
document.getElementById('paystatus').textContent='Waiting for BTCPay...';
fetch('/api/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})
.then(function(r){return r.json();})
.then(function(d){
if(d.checkout_link){
document.getElementById('check').style.display='inline-flex';
document.getElementById('relink').style.display='inline-flex';
document.getElementById('relink').href=d.checkout_link;
document.getElementById('paystatus').textContent='Redirecting you to the BTC invoice...';
setTimeout(function(){window.location=d.checkout_link;},700);
} else { document.getElementById('paystatus').textContent='Could not create invoice: '+(d.error||'?'); }
})
.catch(function(e){document.getElementById('paystatus').textContent='Error: '+e;});
};
}
function bindCheck(){
var b=document.getElementById('check');
b.onclick=function(){
document.getElementById('paystatus').textContent='Checking with BTCPay...';
fetch('/api/check_payment')
.then(function(r){return r.json();})
.then(function(d){
if(d.paid){ location.reload(); }
else{ document.getElementById('paystatus').textContent='Payment not detected yet. Keep paying, we will poll the webhook too.'; }
})
.catch(function(e){document.getElementById('paystatus').textContent='Error: '+e;});
};
}
bindSubscribe(); bindCheck();
if(pendingInvoice){
document.getElementById('relink').style.display='inline-flex';
document.getElementById('relink').href=pendingInvoice.checkout_link;
document.getElementById('check').style.display='inline-flex';
}
</script>
""" % {
"badge": badge, "key": u["api_key"], "calls": u["call_count"],
"limit": (10000 if plan == "pro" else 100),
"expiry": expiry, "price": int(PRICE_USD),
"paidmsg": ('<div class="badge pro" style="margin:14px 0">&#10003; Payment received &mdash; Pro active</div>' 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"""
<section class="hero" style="padding-bottom:30px">
<span class="eyebrow"><span class="dot"></span> About</span>
<h1 style="margin-top:20px">Built for the world<br>of <span class="grad">autonomous</span> agents.</h1>
<p class="lead">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 &mdash;
just a key, a meter, and sats.</p>
</section>
<div class="divider"></div>
<div class="grid c3">
<div class="card"><div class="icon">&#9857;</div><h3>MCP-native</h3><p>The catalog is itself an MCP server, so any agent can speak it.</p></div>
<div class="card"><div class="icon">&#9678;</div><h3>Bitcoin only</h3><p>Settled on-chain through BTCPay. Payable in sats, private, no intermediary.</p></div>
<div class="card"><div class="icon">&#128274;</div><h3>No KYC</h3><p>Username and password. Your key is what you are. That is the whole identity story.</p></div>
</div>
<div class="divider"></div>
<p>Open source. Grab the code, read every line, ship your own node if you like:</p>
<p style="margin-top:14px"><a class="btn primary" href="%(github)s" target="_blank" rel="noopener">View on GitHub &rarr;</a></p>
""" % {"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/<tool_id>/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)

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
flask>=3.0
requests>=2.31
werkzeug>=3.0