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
-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.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.
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.The catalog is itself an MCP server, so any agent can speak it.
Settled on-chain through BTCPay. Payable in sats, private, no intermediary.
Username and password. Your key is what you are. That is the whole identity story.
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:
- + """ % {"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()