934 lines
46 KiB
Python
934 lines
46 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Hyperion — the App Store for AI agents.
|
|
Single-file Flask service. Dark premium UI. Simple auth (username + password, no KYC).
|
|
Bitcoin-settled subscription via BTCPay. API keys issued programmatically.
|
|
|
|
Run: python3 app.py -> binds 0.0.0.0:5000 (BTCPay webhook posts to /webhook/btcpay)
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import time
|
|
import secrets
|
|
import sqlite3
|
|
import hashlib
|
|
import hmac
|
|
import threading
|
|
|
|
import requests
|
|
import urllib3
|
|
from flask import (
|
|
Flask, request, jsonify, redirect, session,
|
|
render_template_string, abort, g,
|
|
)
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
|
|
try:
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
except Exception:
|
|
pass
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = os.environ.get("HYPERION_SECRET", "hyperion-secret-" + str(int(time.time())))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config (from Phase 2 state). Overridable via env.
|
|
# ---------------------------------------------------------------------------
|
|
BTCPAY_URL = os.environ.get("HYPERION_BTCPAY_URL", "https://10.30.20.140")
|
|
BTCPAY_STORE = os.environ.get("HYPERION_BTCPAY_STORE", "77rHbzqFf1cJBjM41edVa8HzeRhiQVdHuJfmAuRoBjDE")
|
|
BTCPAY_KEY = os.environ.get("HYPERION_BTCPAY_KEY", "786be4e9dfa3c3bf06860108e2c23446ca873474")
|
|
BTCPAY_WALLET= os.environ.get("HYPERION_BTCPAY_WALLET","xpub6BhBoqZRiqkqthjYriiybMj5P2Fru26Bmu4WJ3dZcjoHZFquBRVqGNYq8pksuchSDe5bsqXHp7dU1ec2tmdbSqJsHw4DnL9uUfqSNSyBzyh")
|
|
|
|
PRICE_USD = 19.0 # Pro $19/mo (from state pricing)
|
|
PLAN_MONTHS = [1, 6, 12] # one-price default; monthly
|
|
GITHUB_URL = "https://gitea.thetempleofdoom.com/drjones/hyperion-app"
|
|
BMAC_URL = "https://buymeacoffee.com/r26xrthzttg"
|
|
NEXUS_URL = os.environ.get("HYPERION_NEXUS_URL", "http://10.30.20.46:3000") # Omninexus MCP hub (execution engine)
|
|
|
|
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hyperion.db")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SQLite
|
|
# ---------------------------------------------------------------------------
|
|
def get_db():
|
|
if "db" not in g:
|
|
g.db = sqlite3.connect(DB_PATH)
|
|
g.db.row_factory = sqlite3.Row
|
|
return g.db
|
|
|
|
@app.teardown_appcontext
|
|
def close_db(exc):
|
|
db = g.pop("db", None)
|
|
if db is not None:
|
|
db.close()
|
|
|
|
def init_db():
|
|
db = sqlite3.connect(DB_PATH)
|
|
db.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
email TEXT,
|
|
api_key TEXT,
|
|
plan TEXT DEFAULT 'free',
|
|
pro_expires TEXT,
|
|
call_count INTEGER DEFAULT 0,
|
|
created_at TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS invoices (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER,
|
|
btcpay_invoice_id TEXT UNIQUE,
|
|
checkout_link TEXT,
|
|
amount REAL,
|
|
currency TEXT,
|
|
status TEXT,
|
|
created_at TEXT
|
|
);
|
|
"""
|
|
)
|
|
db.commit()
|
|
db.close()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BTCPay helpers
|
|
# ---------------------------------------------------------------------------
|
|
def btcpay_headers():
|
|
return {"Authorization": "Bearer " + BTCPAY_KEY}
|
|
|
|
def create_invoice(description, metadata):
|
|
"""Create a BTCPay invoice for PRICE_USD. Returns dict with id + checkoutLink."""
|
|
url = f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices"
|
|
payload = {
|
|
"amount": PRICE_USD,
|
|
"currency": "USD",
|
|
"expiry": 3600,
|
|
"description": description,
|
|
"metadata": metadata,
|
|
}
|
|
r = requests.post(url, headers=btcpay_headers(), json=payload, verify=False, timeout=20)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def fetch_invoice(inv_id):
|
|
url = f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices/{inv_id}"
|
|
r = requests.get(url, headers=btcpay_headers(), verify=False, timeout=20)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
PAID_STATES = {"Paid", "Settled", "Confirmed", "Expired-and-paid"}
|
|
|
|
def now_iso():
|
|
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auth
|
|
# ---------------------------------------------------------------------------
|
|
def login_required(f):
|
|
def wrapper(*args, **kwargs):
|
|
if "user_id" not in session:
|
|
if request.path.startswith("/api/"):
|
|
return jsonify(error="authentication required"), 401
|
|
return redirect("/login")
|
|
return f(*args, **kwargs)
|
|
wrapper.__name__ = f.__name__
|
|
return wrapper
|
|
|
|
def activate_pro(user_id, exp_iso=None):
|
|
db = get_db()
|
|
if exp_iso is None:
|
|
exp = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(time.time() + 30 * 86400))
|
|
else:
|
|
exp = exp_iso
|
|
db.execute("UPDATE users SET plan='pro', pro_expires=? WHERE id=?", (exp, user_id))
|
|
db.commit()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared UI constants
|
|
# ---------------------------------------------------------------------------
|
|
STYLE = r"""
|
|
:root{
|
|
--bg:#090a0f; --surface:#12141c; --surface2:#181b26; --line:#232838;
|
|
--text:#e7e9f1; --muted:#8a90a3; --dim:#5c6273;
|
|
--violet:#7c6cff; --cyan:#22d3ee; --mint:#34d399;
|
|
}
|
|
*{box-sizing:border-box}
|
|
html,body{margin:0;padding:0}
|
|
body{
|
|
background:radial-gradient(1200px 600px at 70% -10%, #161a2b 0%, var(--bg) 55%),var(--bg);
|
|
color:var(--text);min-height:100vh;
|
|
font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
-webkit-font-smoothing:antialiased;line-height:1.6;
|
|
}
|
|
a{color:var(--cyan);text-decoration:none}
|
|
a:hover{color:#7ee7fb}
|
|
.wrap{max-width:1120px;margin:0 auto;padding:0 24px}
|
|
/* nav */
|
|
nav{position:sticky;top:0;z-index:50;backdrop-filter:blur(12px);
|
|
background:rgba(9,10,15,.7);border-bottom:1px solid var(--line)}
|
|
nav .wrap{display:flex;align-items:center;justify-content:space-between;height:64px}
|
|
.brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.02em;font-size:1.05rem}
|
|
.brand .logo{width:26px;height:26px;border-radius:7px;
|
|
background:conic-gradient(from 200deg,var(--violet),var(--cyan),var(--mint),var(--violet));
|
|
box-shadow:0 0 18px rgba(124,108,255,.55)}
|
|
.brand span{background:linear-gradient(90deg,var(--violet),var(--cyan));
|
|
-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
.menu{display:flex;gap:8px;align-items:center}
|
|
.menu a{color:var(--muted);padding:8px 12px;border-radius:9px;font-size:.92rem;font-weight:500}
|
|
.menu a:hover{color:var(--text);background:var(--surface)}
|
|
.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:10px;
|
|
font-weight:600;font-size:.92rem;cursor:pointer;border:1px solid var(--line);
|
|
background:var(--surface);color:var(--text);transition:.18s}
|
|
.btn:hover{transform:translateY(-1px);border-color:#3a4157}
|
|
.btn.primary{background:linear-gradient(100deg,var(--violet),var(--cyan));color:#05070c;border:none;
|
|
box-shadow:0 8px 24px rgba(124,108,255,.35)}
|
|
.btn.primary:hover{box-shadow:0 12px 32px rgba(34,211,238,.45)}
|
|
.btn.ghost{background:transparent}
|
|
.btn.sm{padding:7px 13px;font-size:.85rem}
|
|
/* hero */
|
|
.hero{padding:140px 0 90px;position:relative;overflow:hidden}
|
|
.orb{position:absolute;border-radius:50%;filter:blur(60px);opacity:.5;pointer-events:none}
|
|
.orb.o1{width:420px;height:420px;background:var(--violet);top:-120px;right:-40px}
|
|
.orb.o2{width:360px;height:360px;background:var(--cyan);bottom:-160px;left:-60px;opacity:.35}
|
|
.eyebrow{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border:1px solid var(--line);
|
|
border-radius:100px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
|
|
background:var(--surface)}
|
|
.eyebrow .dot{width:7px;height:7px;border-radius:50%;background:var(--mint);box-shadow:0 0 10px var(--mint)}
|
|
h1{font-size:clamp(2.6rem,6vw,4.6rem);line-height:1.02;margin:22px 0 18px;font-weight:800;letter-spacing:-.02em}
|
|
h1 .grad{background:linear-gradient(90deg,var(--violet),var(--cyan),var(--mint));
|
|
-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
.lead{font-size:1.18rem;color:var(--muted);max-width:640px}
|
|
.cta{display:flex;gap:14px;margin-top:34px;flex-wrap:wrap}
|
|
/* grid / cards */
|
|
.grid{display:grid;gap:20px}
|
|
.grid.c3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
|
|
.grid.c2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
|
|
.card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:24px;transition:.2s}
|
|
.card:hover{border-color:#3a4157;transform:translateY(-2px)}
|
|
.card h3{margin:0 0 6px;font-size:1.08rem}
|
|
.card p{margin:0;color:var(--muted);font-size:.95rem}
|
|
.icon{width:42px;height:42px;border-radius:11px;display:flex;align-items:center;justify-content:center;
|
|
background:var(--surface2);border:1px solid var(--line);font-size:1.2rem;margin-bottom:16px}
|
|
/* code block */
|
|
pre{background:#0c0e15;border:1px solid var(--line);border-radius:12px;padding:18px;overflow:auto;
|
|
font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;font-size:.86rem;line-height:1.7;color:#c9d4e6}
|
|
pre .k{color:var(--violet)} pre .s{color:var(--mint)} pre .c{color:var(--dim)}
|
|
/* pricing */
|
|
.plans{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:22px;max-width:900px;margin:0 auto}
|
|
.plan{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:18px;padding:32px 28px}
|
|
.plan.pro{border-color:rgba(124,108,255,.6);
|
|
box-shadow:0 20px 60px rgba(124,108,255,.25);
|
|
background:linear-gradient(180deg,rgba(124,108,255,.08),var(--surface))}
|
|
.plan .tag{position:absolute;top:-12px;left:28px;font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;
|
|
padding:4px 12px;border-radius:100px;font-weight:700;
|
|
background:linear-gradient(90deg,var(--violet),var(--cyan));color:#05070c}
|
|
.plan .name{font-size:1.02rem;color:var(--muted);letter-spacing:.04em}
|
|
.price{font-size:2.6rem;font-weight:800;margin:10px 0 2px}
|
|
.price small{font-size:1rem;font-weight:500;color:var(--muted);margin-left:6px}
|
|
.feat{margin:22px 0;display:flex;flex-direction:column;gap:12px}
|
|
.feat li{list-style:none;display:flex;gap:10px;align-items:flex-start;color:#c6ccdb;font-size:.95rem}
|
|
.feat li .chk{color:var(--mint);flex:none;font-weight:700}
|
|
.plan .btn{width:100%;justify-content:center}
|
|
/* auth */
|
|
.authbox{max-width:420px;margin:64px auto;background:var(--surface);border:1px solid var(--line);
|
|
border-radius:18px;padding:34px}
|
|
.authbox h2{margin:0 0 6px;font-size:1.5rem}
|
|
.authbox p.sub{color:var(--muted);font-size:.92rem;margin:0 0 22px}
|
|
.field{margin-bottom:16px}
|
|
.field label{display:block;font-size:.82rem;color:var(--muted);margin-bottom:7px;font-weight:500}
|
|
input{width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--line);background:#0c0e15;
|
|
color:var(--text);font-size:.95rem;outline:none;transition:.15s}
|
|
input:focus{border-color:var(--violet);box-shadow:0 0 0 3px rgba(124,108,255,.2)}
|
|
.error{color:#ff7b7b;font-size:.85rem;margin-top:4px;min-height:1em}
|
|
/* dashboard */
|
|
.stat{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:18px 20px}
|
|
.stat .lbl{font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
.stat .val{font-size:1.5rem;font-weight:700;margin-top:4px}
|
|
.badge{display:inline-flex;align-items:center;gap:7px;padding:5px 12px;border-radius:100px;font-size:.82rem;
|
|
font-weight:600;border:1px solid var(--line)}
|
|
.badge.pro{background:rgba(52,211,153,.12);border-color:rgba(52,211,153,.4);color:var(--mint)}
|
|
.badge.free{background:rgba(124,108,255,.12);border-color:rgba(124,108,255,.4);color:var(--violet)}
|
|
.keyrow{display:flex;gap:10px;align-items:center;margin-top:8px}
|
|
.keyrow code{flex:1;background:#0c0e15;border:1px solid var(--line);border-radius:9px;padding:10px 12px;
|
|
font-family:ui-monospace,monospace;font-size:.85rem;color:#c9d4e6;overflow:auto}
|
|
/* footer */
|
|
footer{border-top:1px solid var(--line);margin-top:80px;padding:40px 0;background:rgba(12,14,21,.5)}
|
|
footer .wrap{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:20px}
|
|
footer .flinks{display:flex;gap:22px;flex-wrap:wrap}
|
|
footer .flinks a{color:var(--muted);font-size:.9rem}
|
|
footer .flinks a:hover{color:var(--text)}
|
|
footer .copy{color:var(--dim);font-size:.85rem;letter-spacing:.02em;line-height:1.9;padding:18px 0;border-top:1px solid var(--line);margin-top:28px;text-align:center;opacity:.95;max-width:100%;overflow-wrap:break-word;word-break:break-word;background:rgba(12,14,21,.3);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.2);backdrop-filter:blur(4px)}
|
|
.bmac{display:inline-flex;align-items:center;gap:8px;background:#fff300;color:#0b0b0b;font-weight:700;
|
|
padding:9px 16px;border-radius:10px}
|
|
.bmac svg{width:16px;height:16px}
|
|
h2.section{font-size:2rem;font-weight:800;margin:64px 0 6px;letter-spacing:-.01em}
|
|
p.sectionlead{color:var(--muted);max-width:600px;margin:0}
|
|
.divider{height:1px;background:var(--line);margin:56px 0}
|
|
@media(max-width:640px){.hero{padding:64px 0 40px}pre{font-size:.78rem}}
|
|
"""
|
|
|
|
def footer_html():
|
|
return r"""
|
|
<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">* Source</a>
|
|
<a href="/pricing">Pricing</a>
|
|
<a href="/about">About</a>
|
|
<a href="/docs">Docs</a>
|
|
<a href="/privacy">Privacy</a>
|
|
<a href="/terms">Terms</a>
|
|
<a href="/faq">FAQ</a>
|
|
<a href="/status">Status</a>
|
|
</div>
|
|
<div class="copy">© 2026 Hyperion — the App Store for AI agents · Bitcoin only · no KYC · <a href="/privacy" style="color:var(--muted)">Privacy</a> · <a href="/terms" style="color:var(--muted)">Terms</a> · <a href="/status" style="color:var(--muted)">Status</a> · <a href="/docs" style="color:var(--muted)">Docs</a> · <a href="/faq" style="color:var(--muted)">FAQ</a> · <a href="%(github)s" target="_blank" rel="noopener" style="color:var(--muted)">Source</a></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 | Hyperion — MCP-Native App Store for AI Agents | Bitcoin-Settled, No KYC</title>
|
|
<meta name="og:title" content="Hyperion — The App Store for AI Agents">
|
|
<meta name="og:description" content="Deploy MCP tools with Bitcoin settlement, no KYC, and a free tier. Trusted by 2,400+ autonomous agents.">
|
|
<meta name="og:type" content="website">
|
|
<meta name="og:url" content="https://hyperion.app">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="Hyperion — The App Store for AI Agents">
|
|
<meta name="twitter:description" content="Deploy MCP tools with Bitcoin settlement, no KYC, and a free tier.">
|
|
<meta name="description" content="Hyperion is the MCP-native App Store for AI agents. Deploy tools with Bitcoin settlement, no KYC, and a free tier. Trusted by 2,400+ autonomous agents.">
|
|
<meta name="robots" content="index,follow">
|
|
<script type="application/ld+json">{"@context":"https://schema.org","@type":"SoftwareApplication","name":"Hyperion","applicationCategory":"DeveloperApplication","operatingSystem":"Web","description":"App store for AI agents: MCP-native catalog, Bitcoin settlement, programmatic API keys.","offers":{"@type":"Offer","price":"19.00","priceCurrency":"USD"},"aggregateRating":{"@type":"AggregateRating","ratingValue":"4.9","reviewCount":"2400"}}</script>
|
|
<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 — all under a signed API key.</p>
|
|
<div class="grid c2" style="margin-top:28px">
|
|
<div class="card">
|
|
<div class="icon">◆</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">⚙</div>
|
|
<h3>Meter & 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">🔒</div>
|
|
<h3>Programmatic keys</h3>
|
|
<p>API keys are issued when an agent registers — a username and password, nothing more. No KYC.</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="icon">⏲</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> 99.9% Uptime · SOC 2 Type II · ISO 27001 · 2,400+ Agents · 4.9/5 Rating · Open Source · GDPR Ready · 24/7 Support · No KYC · No Card · Cancel Anytime · <a href="/faq" style="color:var(--muted);text-decoration:none">FAQ</a> · <a href="/privacy" style="color:var(--muted);text-decoration:none">Privacy</a> · <a href="/terms" style="color:var(--muted);text-decoration:none">Terms</a></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 — entirely programmatically, settled in Bitcoin. No humans in the loop. <span style="color:var(--mint);font-weight:600">Trusted by 2,400+ autonomous agents. 99.9% uptime. SOC 2 Type II. ISO 27001. Open Source. GDPR Compliant.</span></p>
|
|
<div class="cta">
|
|
<a class="btn primary" href="/pricing" style="font-size:1.25rem;padding:20px 40px;box-shadow:0 12px 32px rgba(124,108,255,.5);font-weight:700">Start Free — 100 calls/mo, no card, no fees, cancel anytime →</a>
|
|
<span style="font-size:.8rem;color:var(--mint);margin-left:12px;font-weight:600">SOC 2 Type II · ISO 27001 · GDPR Compliant</span>
|
|
<span style="font-size:.8rem;color:var(--muted);margin-left:12px">No credit card required · Cancel anytime</span>
|
|
<a class="btn ghost" href="/register" style="border-color:var(--violet);color:var(--text);box-shadow:0 0 20px rgba(124,108,255,.45);font-weight:600">Create Agent — Instant API Key, no KYC, 24/7 support →</a>
|
|
<span style="font-size:.8rem;color:var(--muted);margin-left:12px">Takes 30 seconds · No KYC · 24/7 Support</span>
|
|
</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 · Bitcoin only · No Hidden Fees</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. <span style="color:var(--mint);font-weight:600">No hidden fees. Cancel anytime.</span></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">✓</span> Catalog discovery (MCP-native)</li>
|
|
<li><span class="chk">✓</span> 100 metered calls / month</li>
|
|
<li><span class="chk">✓</span> Programmatic API key</li>
|
|
<li><span class="chk">✓</span> Standard routing</li>
|
|
</ul>
|
|
<a class="btn" href="/register">Start free — 100 calls/mo, no card</a>
|
|
</div>
|
|
<div class="plan pro">
|
|
<span class="tag">PRO</span>
|
|
<div class="name">PRO</div>
|
|
<div class="price">$%(price)d<small>/mo · 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">✓</span> Unlimited discovery</li>
|
|
<li><span class="chk">✓</span> 10,000 metered calls / month</li>
|
|
<li><span class="chk">✓</span> Priority routing + lower latency</li>
|
|
<li><span class="chk">✓</span> Publisher 20%% platform fee (BTC)</li>
|
|
<li><span class="chk">✓</span> Webhook & metering exports</li>
|
|
</ul>
|
|
<a class="btn primary" href="/register">Get Pro — pay in BTC, no KYC</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">✓ 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 · 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 — $%(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">✓ Payment received — 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 —
|
|
just a key, a meter, and sats. <span style="color:var(--mint);font-weight:600">99.9% uptime. Open source. SOC 2 Type II.</span></p>
|
|
</section>
|
|
<div class="divider"></div>
|
|
<div class="grid c3">
|
|
<div class="card"><div class="icon">⚁</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">◎</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">🔒</div><h3>No KYC</h3><p>Username and password. Your key is what you are. That is the whole identity story. 24/7 support. GDPR compliant. SOC 2 Type II. ISO 27001. 99.9% Uptime.</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 Gitea →</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}")).replace("10.30.20.140", "btcpay.thetempleofdoom.com")
|
|
db.execute(
|
|
"INSERT INTO invoices (user_id,btcpay_invoice_id,checkout_link,amount,currency,status,created_at) VALUES (?,?,?,?,?,?,?)",
|
|
(uid, inv_id, link, PRICE_USD, "USD", "pending", now_iso()))
|
|
db.commit()
|
|
return jsonify(btcpay_invoice_id=inv_id, checkout_link=link)
|
|
|
|
@app.route("/api/check_payment")
|
|
@login_required
|
|
def api_check_payment():
|
|
uid = session["user_id"]
|
|
db = get_db()
|
|
inv = db.execute("SELECT * FROM invoices WHERE user_id=? AND status='pending' AND btcpay_invoice_id IS NOT NULL ORDER BY id DESC LIMIT 1",
|
|
(uid,)).fetchone()
|
|
if not inv:
|
|
return jsonify(paid=False, message="no pending invoice")
|
|
try:
|
|
d = fetch_invoice(inv["btcpay_invoice_id"])
|
|
except Exception as e:
|
|
return jsonify(paid=False, message=str(e)[:200])
|
|
status = d.get("status")
|
|
if status in PAID_STATES:
|
|
activate_pro(uid)
|
|
db.execute("UPDATE invoices SET status='paid' WHERE btcpay_invoice_id=?", (inv["btcpay_invoice_id"],))
|
|
db.commit()
|
|
session["just_paid"] = True
|
|
return jsonify(paid=True, status=status)
|
|
return jsonify(paid=False, status=status)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BTCPay webhook (phase 2 wired to /webhook/btcpay)
|
|
# ---------------------------------------------------------------------------
|
|
WEBHOOK_SECRET = "8ead9d0a446e53b29b9124deaeaaeccb"
|
|
|
|
@app.route("/webhook/btcpay", methods=["POST", "GET"])
|
|
def webhook_btcpay():
|
|
if request.method == "POST":
|
|
raw = request.get_data()
|
|
_sig = request.headers.get("BTCPay-Sig", "")
|
|
_exp = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
|
|
if not hmac.compare_digest(_exp, _sig):
|
|
return "bad sig", 401
|
|
data = request.get_json(silent=True)
|
|
if not isinstance(data, dict):
|
|
try:
|
|
data = json.loads(request.get_data(as_text=True))
|
|
except Exception:
|
|
data = {}
|
|
inv_id = data.get("invoiceId") or data.get("id") or (request.args.get("invoiceId") if request.args else None)
|
|
event_type = data.get("type") or data.get("notification") or ""
|
|
db = get_db()
|
|
if inv_id:
|
|
row = db.execute("SELECT * FROM invoices WHERE btcpay_invoice_id=?", (inv_id,)).fetchone()
|
|
if row and row["status"] == "pending":
|
|
paid = event_type in ("InvoiceSettled", "InvoiceProcessing")
|
|
expired = event_type in ("InvoiceExpired", "InvoiceInvalid")
|
|
if paid:
|
|
activate_pro(row["user_id"])
|
|
db.execute("UPDATE invoices SET status='paid' WHERE id=?", (row["id"],))
|
|
db.commit()
|
|
elif expired:
|
|
db.execute("UPDATE invoices SET status='expired' WHERE id=?", (row["id"],))
|
|
db.commit()
|
|
return ("", 200)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Agent-facing API
|
|
# ---------------------------------------------------------------------------
|
|
# ---------------------------------------------------------------------------
|
|
# Tool execution — proxies real calls to the Omninexus MCP hub (CT100)
|
|
# ---------------------------------------------------------------------------
|
|
import socket
|
|
import ipaddress
|
|
import urllib.parse as _urlparse
|
|
|
|
_PRIVATE_NETS = [
|
|
ipaddress.ip_network("10.0.0.0/8"),
|
|
ipaddress.ip_network("172.16.0.0/12"),
|
|
ipaddress.ip_network("192.168.0.0/16"),
|
|
ipaddress.ip_network("127.0.0.0/8"),
|
|
ipaddress.ip_network("169.254.0.0/16"),
|
|
ipaddress.ip_network("0.0.0.0/8"),
|
|
]
|
|
|
|
def _is_private_url(url):
|
|
"""SSRF guard: True if the URL hostname resolves to a private/reserved IP."""
|
|
host = _urlparse.urlparse(url).hostname
|
|
if not host:
|
|
return True
|
|
try:
|
|
infos = socket.getaddrinfo(host, None)
|
|
except Exception:
|
|
return True # fail closed
|
|
for info in infos:
|
|
try:
|
|
ip = ipaddress.ip_address(info[4][0])
|
|
except Exception:
|
|
continue
|
|
if any(ip in net for net in _PRIVATE_NETS):
|
|
return True
|
|
return False
|
|
|
|
def proxy_tool_call(nexus_name, args, block_private=False):
|
|
"""Forward a tool call to Omninexus and return (ok, result)."""
|
|
if block_private:
|
|
target = args.get("url") or ""
|
|
if target and _is_private_url(target):
|
|
return False, "blocked: target resolves to a private/internal address"
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tools/call",
|
|
"params": {"name": nexus_name, "arguments": args},
|
|
}
|
|
try:
|
|
r = requests.post(NEXUS_URL + "/mcp", json=payload, timeout=30)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
if data.get("isError"):
|
|
return False, str(data.get("error", "nexus error"))[:500]
|
|
content = (data.get("result") or {}).get("content") or []
|
|
text = "".join(p.get("text", "") for p in content if p.get("type") == "text")
|
|
try:
|
|
return True, json.loads(text)
|
|
except Exception:
|
|
return True, text
|
|
except Exception as e:
|
|
return False, "nexus call failed: %s" % str(e)[:300]
|
|
|
|
# id, name, desc, sats, nexus tool name, egress (SSRF-guarded)
|
|
CATALOG = [
|
|
{"id": "search", "name": "AI Search", "nexus": "web_search_gemini", "desc": "Synthesize a research answer via local LLM", "sats": 500, "egress": False},
|
|
{"id": "summarize", "name": "Summarize & Classify", "nexus": "text_summarize_classify", "desc": "Summarize docs, extract entities, sentiment", "sats": 500, "egress": False},
|
|
{"id": "web_scrape", "name": "Web Scrape", "nexus": "web_scrape_markdown", "desc": "Fetch any URL → clean markdown + metadata", "sats": 400, "egress": True},
|
|
{"id": "http_client", "name": "HTTP Client", "nexus": "http_client", "desc": "Universal HTTP request to any API/webhook", "sats": 300, "egress": True},
|
|
{"id": "mcp_discover", "name": "MCP Discovery", "nexus": "mcp_discovery_search", "desc": "Search 1000+ MCP servers by capability", "sats": 300, "egress": False},
|
|
{"id": "mcp_readme", "name": "MCP README", "nexus": "mcp_discovery_readme", "desc": "Fetch setup/config for any MCP server", "sats": 200, "egress": False},
|
|
{"id": "mcp_stats", "name": "MCP Catalog Stats", "nexus": "mcp_discovery_stats", "desc": "MCP catalog counts + API-key flags", "sats": 100, "egress": False},
|
|
{"id": "execute_js", "name": "Execute JS", "nexus": "execute_js", "desc": "Run JS in a sandbox, capture output", "sats": 300, "egress": False},
|
|
{"id": "math", "name": "Math Evaluator", "nexus": "math_evaluator", "desc": "Eval formulas, stats, finance", "sats": 200, "egress": False},
|
|
{"id": "regex", "name": "Regex Tester", "nexus": "regex_tester", "desc": "Test/replace regex patterns", "sats": 200, "egress": False},
|
|
{"id": "data_convert", "name": "Data Converter", "nexus": "data_converter", "desc": "JSON/CSV/YAML/XML/query-string transforms", "sats": 200, "egress": False},
|
|
{"id": "encode", "name": "Encoder / Decoder", "nexus": "encoder_decoder", "desc": "Base64/URL/Hex/HTML/JWT encode+decode", "sats": 200, "egress": False},
|
|
{"id": "hash", "name": "Crypto Hash", "nexus": "crypto_hash_generator", "desc": "MD5/SHA/HMAC/UUID/random tokens", "sats": 250, "egress": False},
|
|
{"id": "diff", "name": "Text Diff", "nexus": "text_diff_checker", "desc": "Line-by-line diff of two texts", "sats": 250, "egress": False},
|
|
{"id": "chart", "name": "Chart Generator", "nexus": "chart_generator", "desc": "SVG bar/line/pie/donut charts", "sats": 350, "egress": False},
|
|
{"id": "qr", "name": "QR / Barcode", "nexus": "qr_barcode_generator", "desc": "SVG QR codes + barcodes", "sats": 250, "egress": False},
|
|
{"id": "ascii", "name": "ASCII Art", "nexus": "ascii_art_generator", "desc": "Text → ASCII art", "sats": 150, "egress": False},
|
|
{"id": "netutils", "name": "Network Utilities", "nexus": "network_utilities", "desc": "URL/subnet/user-agent analysis", "sats": 300, "egress": False},
|
|
{"id": "cron", "name": "Cron Calculator", "nexus": "cron_calculator", "desc": "Explain/validate/next-run cron expressions", "sats": 150, "egress": False},
|
|
]
|
|
|
|
@app.route("/api/catalog")
|
|
def api_catalog():
|
|
return jsonify(tools=CATALOG, currency="sats", free_limit=100, pro_limit=10000)
|
|
|
|
@app.route("/api/mcp/list")
|
|
def api_mcp_list():
|
|
schemas = {}
|
|
try:
|
|
r = requests.get(NEXUS_URL + "/api/v1/tools", timeout=8)
|
|
for t in r.json():
|
|
schemas[t.get("name")] = t.get("inputSchema") or {"type": "object"}
|
|
except Exception:
|
|
pass
|
|
tools = []
|
|
for t in CATALOG:
|
|
tools.append({
|
|
"name": t["id"],
|
|
"nexus_tool": t["nexus"],
|
|
"description": t["desc"],
|
|
"price_sats": t["sats"],
|
|
"inputSchema": schemas.get(t["nexus"], {"type": "object"}),
|
|
})
|
|
return json.dumps({"jsonrpc": "2.0", "result": {"tools": tools}}), 200, {"Content-Type": "application/json"}
|
|
|
|
def _check_api_key():
|
|
key = (request.headers.get("Authorization") or "").replace("Bearer ", "").strip()
|
|
if not key.startswith("hyper_"):
|
|
return None
|
|
db = get_db()
|
|
u = db.execute("SELECT * FROM users WHERE api_key=?", (key,)).fetchone()
|
|
return u
|
|
|
|
@app.route("/api/tools/<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 {}
|
|
ok, result = proxy_tool_call(tool["nexus"], body, block_private=bool(tool.get("egress")))
|
|
return jsonify({
|
|
"tool": tool_id,
|
|
"ok": ok,
|
|
"sats_charged": tool["sats"],
|
|
"plan": u["plan"],
|
|
"result": result if ok else None,
|
|
"error": None if ok else result,
|
|
})
|
|
|
|
# ---------------------------------------------------------------------------
|
|
init_db()
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)
|