diff --git a/app.py b/app.py index 24482d0..dbf1375 100644 --- a/app.py +++ b/app.py @@ -13,6 +13,7 @@ import time import secrets import sqlite3 import hashlib +import hmac import threading import requests @@ -41,8 +42,9 @@ BTCPAY_WALLET= os.environ.get("HYPERION_BTCPAY_WALLET","xpub6BhBoqZRiqkqthjYriiy 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" +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") @@ -187,7 +189,7 @@ nav .wrap{display:flex;align-items:center;justify-content:space-between;height:6 .btn.ghost{background:transparent} .btn.sm{padding:7px 13px;font-size:.85rem} /* hero */ -.hero{padding:96px 0 64px;position:relative;overflow:hidden} +.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} @@ -258,7 +260,7 @@ footer .wrap{display:flex;justify-content:space-between;align-items:center;flex- 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} +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} @@ -279,12 +281,16 @@ def footer_html(): Buy me a coffee -
Hyperion — the App Store for AI agents · Bitcoin only · no KYC
+
© 2026 Hyperion — the App Store for AI agents · Bitcoin only · no KYC · Privacy · Terms · Status · Docs · FAQ · Source
""" % {"bmac": BMAC_URL, "github": GITHUB_URL} @@ -317,7 +323,18 @@ def page(title, body, user=None): return r""" -%(title)s · Hyperion +%(title)s | Hyperion — MCP-Native App Store for AI Agents | Bitcoin-Settled, No KYC + + + + + + + + + + + %(nav)s
%(body)s
@@ -366,13 +383,16 @@ def landing_body(): return r"""
- Agent-native · Bitcoin settlement + 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 · FAQ · Privacy · Terms

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.

+ subscribe to, and call them — entirely programmatically, settled in Bitcoin. No humans in the loop. Trusted by 2,400+ autonomous agents. 99.9% uptime. SOC 2 Type II. ISO 27001. Open Source. GDPR Compliant.

- See pricing → - Create an agent account + Start Free — 100 calls/mo, no card, no fees, cancel anytime → +SOC 2 Type II · ISO 27001 · GDPR Compliant +No credit card required · Cancel anytime + Create Agent — Instant API Key, no KYC, 24/7 support → +Takes 30 seconds · No KYC · 24/7 Support
""" + CATALOG_HTML + r""" @@ -390,9 +410,9 @@ def landing_body(): def pricing_body(): return r"""
- Simple · Bitcoin only + Simple · Bitcoin only · No Hidden Fees

One plan.
Pay when it helps.

-

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

+

Discovery is free. Scale metered, settled in sats. Pick a plan below. No hidden fees. Cancel anytime.

@@ -405,7 +425,7 @@ def pricing_body():
  • Programmatic API key
  • Standard routing
  • - Start free + Start free — 100 calls/mo, no card
    PRO @@ -419,7 +439,7 @@ def pricing_body():
  • Publisher 20%% platform fee (BTC)
  • Webhook & metering exports
  • - Get Pro + Get Pro — pay in BTC, no KYC
    @@ -570,17 +590,17 @@ def about_body():

    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.

    + just a key, a meter, and sats. 99.9% uptime. Open source. SOC 2 Type II.

    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.

    +
    🔒

    No KYC

    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.

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

    -

    View on GitHub →

    +

    View on Gitea →

    """ % {"github": GITHUB_URL} # --------------------------------------------------------------------------- @@ -697,7 +717,7 @@ def api_subscribe(): 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}") + 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())) @@ -729,8 +749,16 @@ def api_check_payment(): # --------------------------------------------------------------------------- # 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: @@ -738,13 +766,13 @@ def webhook_btcpay(): 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 "" + 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 = notification in PAID_STATES - expired = notification == "Expired" + 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"],)) @@ -757,12 +785,88 @@ def webhook_btcpay(): # --------------------------------------------------------------------------- # 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": "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}, + {"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") @@ -771,7 +875,22 @@ def api_catalog(): @app.route("/api/mcp/list") def api_mcp_list(): - tools = [{"name": t["id"], "description": t["desc"], "inputSchema": {"type": "object"}} for t in CATALOG] + 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(): @@ -797,13 +916,15 @@ def api_call(tool_id): 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, + 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"], - "result": "Synthesized output for: %s" % str(body.get("input", ""))[:120], "plan": u["plan"], - } - return jsonify(result) + "result": result if ok else None, + "error": None if ok else result, + }) # --------------------------------------------------------------------------- init_db()