"""registry.py - publish the offer manifest and payment-verification records to the UnyKorn chain (chain.unykorn.ai) as the registry / settlement layer. STATUS: the chain write path is PENDING A CONFIRMED API. As of Oct 13, 2026 chain.unykorn.ai serves a website plus read-only JSON (GET /api/live, GET /api/site); JSON-RPC POSTs to it return {"error":"read only"}. No public write endpoint is documented. So this module: 1. always appends every record to a local append-only file (REGISTRY_LOG), and 2. if REGISTRY_URL is set, POSTs an HMAC-SHA256-signed JSON envelope there, queueing failures in REGISTRY_QUEUE and retrying every REGISTRY_RETRY_SECONDS. Every paid response carries X-UnyKorn-Record: offer=; record=; status=... Records contain hashes only: never keys, preimages, macaroons or payloads. """ import hashlib, hmac, json, os, threading, time, urllib.error, urllib.request REGISTRY_URL = os.environ.get("REGISTRY_URL", "") # unset until a write API is confirmed REGISTRY_SECRET = os.environ.get("REGISTRY_HMAC_SECRET", "") REGISTRY_LOG = os.environ.get("REGISTRY_LOG", "registry.jsonl") LAYER = "the UnyKorn chain (chain.unykorn.ai)" def sha256(data): if not isinstance(data, bytes): data = json.dumps(data, sort_keys=True).encode() return hashlib.sha256(data).hexdigest() def envelope(kind, body): rec = {"v": 1, "kind": kind, "ts": int(time.time()), "layer": LAYER, "body": body} canon = json.dumps(rec, sort_keys=True, separators=(",", ":")).encode() rec["digest"] = hashlib.sha256(canon).hexdigest() if REGISTRY_SECRET: rec["hmac"] = hmac.new(REGISTRY_SECRET.encode(), canon, hashlib.sha256).hexdigest() return rec REGISTRY_QUEUE = os.environ.get("REGISTRY_QUEUE", "registry.queue.jsonl") OFFER_ID = None # digest of the offer manifest registered at startup _qlock = threading.Lock() def _post(rec): req = urllib.request.Request(REGISTRY_URL, data=json.dumps(rec).encode(), headers={"Content-Type": "application/json", "User-Agent": "btc402-registry/1"}) with urllib.request.urlopen(req, timeout=15) as r: return r.status def publish(kind, body): """Append locally (always), then try the chain write; queue for retry on failure.""" if kind != "offer" and OFFER_ID: body = {**body, "offer_id": OFFER_ID} rec = envelope(kind, body) with _qlock, open(REGISTRY_LOG, "a") as f: f.write(json.dumps(rec, sort_keys=True) + "\n") status = "pending-confirmed-api (local record only)" if REGISTRY_URL: try: status = f"posted {_post(rec)}" except Exception as e: with _qlock, open(REGISTRY_QUEUE, "a") as f: f.write(json.dumps(rec, sort_keys=True) + "\n") status = f"queued for retry ({e.__class__.__name__})" return {"record_id": rec["digest"], "status": status} def flush_queue(): """Retry queued chain writes; keep the ones that still fail.""" if not REGISTRY_URL or not os.path.exists(REGISTRY_QUEUE): return 0 with _qlock: recs = [json.loads(l) for l in open(REGISTRY_QUEUE) if l.strip()] left = [] for r in recs: try: _post(r) except Exception: left.append(r) with open(REGISTRY_QUEUE, "w") as f: f.writelines(json.dumps(r, sort_keys=True) + "\n" for r in left) return len(recs) - len(left) def start_retry(interval=int(os.environ.get("REGISTRY_RETRY_SECONDS", "300"))): def loop(): while True: time.sleep(interval) try: flush_queue() except Exception: pass threading.Thread(target=loop, daemon=True).start() def register_offer(manifest): global OFFER_ID r = publish("offer", manifest) OFFER_ID = r["record_id"] return r def header(rec): """Value for the X-UnyKorn-Record response header.""" return f"offer={OFFER_ID}; record={rec['record_id']}; status={rec['status'].split(' ')[0]}" def offer_manifest(routes, cfg): return { "service": "btc402 Bitcoin node data", "network": "bitcoin mainnet", "node": "Bitcoin Core 31.1, pruned", "endpoints": [{"path": p, "rpc": " ".join(a), "description": d, "price": {"usdc_atomic": cfg["PRICE_ATOMIC"], "sats": cfg["PRICE_SATS"]}} for p, (a, d) in routes.items()], "rails": {"x402": {"network": cfg["X402_NETWORK"], "asset": cfg["X402_ASSET"], "payTo": cfg["PAY_TO"]}, "lightning": {"onchain_btc_payto": cfg.get("BTC_PAY_TO", ""), "topup": "POST /topup", "subscribe": "POST /subscribe", "plans": cfg["PLANS"]}}, } def verification_record(rail, ref, amount, unit, path): """ref = settlement receipt (x402) or invoice payment_hash (Lightning); stored hashed.""" return publish("payment", {"rail": rail, "ref_hash": sha256(str(ref).encode()), "amount": str(amount), "unit": unit, "resource": path}) if __name__ == "__main__": import btc_x402 # loads btc402.env print(register_offer(offer_manifest(btc_x402.ROUTES, btc_x402.cfg_summary()))) def replay_local(): """Sign and send records that were logged locally before REGISTRY_URL was configured. Same kind/ts/body, so record ids (digests) are unchanged.""" if not (REGISTRY_URL and REGISTRY_SECRET and os.path.exists(REGISTRY_LOG)): return {"sent": 0, "failed": 0} sent = failed = 0 for line in open(REGISTRY_LOG): if not line.strip(): continue rec = json.loads(line) base = {k: rec[k] for k in ("v", "kind", "ts", "layer", "body")} canon = json.dumps(base, sort_keys=True, separators=(",", ":")).encode() rec = {**base, "digest": hashlib.sha256(canon).hexdigest(), "hmac": hmac.new(REGISTRY_SECRET.encode(), canon, hashlib.sha256).hexdigest()} try: _post(rec); sent += 1 except urllib.error.HTTPError as e: if e.code == 200: sent += 1 else: failed += 1 except Exception: failed += 1 return {"sent": sent, "failed": failed}