"""Market data in your app: a small backend that fetches Blockmetric data on the server, caches it and hands your frontend exactly what it shows, with the credit line the licence asks for. Python 3.9+, standard library only. Your key never reaches the browser, and 1,000 visitors cost the same few API requests as one. export BLOCKMETRIC_API_KEY=bm_live_... # API Pro: display licence for data shown to your users export BLOCKMETRIC_BASE_URL=https:///api/v1 python server.py # http://localhost:8080/api/market Options: PORT (8080), CACHE_SECONDS (30), TOP (10), ONCE=1 (print the JSON once, then exit). """ import json import os import threading import time import urllib.error import urllib.request from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer BASE = os.environ.get("BLOCKMETRIC_BASE_URL", "").rstrip("/") KEY = os.environ.get("BLOCKMETRIC_API_KEY") PORT = int(os.environ.get("PORT", "8080")) CACHE_SECONDS = float(os.environ.get("CACHE_SECONDS", "30")) TOP = int(os.environ.get("TOP", "10")) etags = {} # path -> (etag, body): an unchanged answer comes back as 304 without a body def api(path): headers = {"Authorization": f"Bearer {KEY}"} if KEY else {"X-Blockmetric-Access": "public"} if path in etags: headers["If-None-Match"] = etags[path][0] try: with urllib.request.urlopen(urllib.request.Request(BASE + path, headers=headers), timeout=20) as res: body = json.load(res) if res.headers.get("ETag"): etags[path] = (res.headers["ETag"], body) return body except urllib.error.HTTPError as e: if e.code == 304 and path in etags: return etags[path][1] err = json.load(e).get("error", {}) raise RuntimeError(f"{e.code} {err.get('code')}: {err.get('message')}") def credit(lines): """"Data by Blockmetric · Source: A · B" from several credit lines (the sources of every answer, each named once).""" sources = [] for line in lines: for s in (line.split(" · Source: ") + [""])[1].split(" · "): if s and s not in sources: sources.append(s) head = lines[0].split(" · Source: ")[0] return f"{head} · Source: {' · '.join(sources)}" if sources else head def build(): """What the frontend needs, nothing more: top markets by open interest, 24h liquidations and the credit line.""" markets = api("/markets?venue=hyperliquid") liq = api("/liquidations/summary?hours=24") keep = ("coin", "mark", "fundingApr", "openInterestUsd", "volume24hUsd") return { "updatedAt": markets["meta"]["updatedAt"], "markets": [{k: m.get(k) for k in keep} for m in markets["data"][:TOP]], "liquidations24h": {k: liq["data"][k] for k in ("totalUsd", "longUsd", "shortUsd")}, # Show this credit next to the data; it names the sources of both answers. "attribution": {"text": credit([markets["meta"]["attribution"]["text"], liq["meta"]["attribution"]["text"]]), "url": markets["meta"]["attribution"]["url"]}, "license": markets["meta"]["license"]["id"], } cache = {"at": 0.0, "value": None} lock = threading.Lock() def cached(): with lock: # one refresh at a time, every visitor gets the same answer if cache["value"] is None or time.time() - cache["at"] >= CACHE_SECONDS: try: cache.update(at=time.time(), value=build()) except (RuntimeError, urllib.error.URLError): if cache["value"] is None: raise # serve the last good answer while the API is unreachable return cache["value"] class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path != "/api/market": self.send_response(404) self.end_headers() return try: body, status = json.dumps(cached()).encode(), 200 except Exception as e: # noqa: BLE001 - report any upstream problem as 502 body, status = json.dumps({"error": str(e)}).encode(), 502 self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Cache-Control", f"public, max-age={int(CACHE_SECONDS)}") self.end_headers() self.wfile.write(body) if __name__ == "__main__": if os.environ.get("ONCE") == "1": print(json.dumps(cached(), indent=2, ensure_ascii=False)) else: print(f"http://localhost:{PORT}/api/market", flush=True) ThreadingHTTPServer(("", PORT), Handler).serve_forever()