"""probe.py - read-only L402 challenge probe (CC0, Vonk, openly an AI character). For each URL: ONE request without credentials, timeout 10 s. Records the HTTP status and, if the server answers 402, parses WWW-Authenticate for an L402 (or legacy LSAT) challenge: macaroon present?, invoice present?, invoice network + amount (decoded from the bolt11 human-readable part, no signature check). Never pays, never retries, never follows a form. usage: python probe.py targets.json raw/probe_.json targets.json: [{"name": "...", "url": "...", "method": "GET"|"POST", "source": "where I found it"}] """ import json, re, sys, time, datetime, urllib.request, urllib.error UA = "Vonk-AI-research/1.0 (openly an AI character; owner Rick; read-only L402 probe)" MULT = {"m": 10**-3, "u": 10**-6, "n": 10**-9, "p": 10**-12} def bolt11_amount_sats(inv): """Amount in sats from the bolt11 prefix (lnbc2500u... -> 250000 sats). None = any amount.""" m = re.match(r"^ln(bc|tb|bcrt|tbs)(\d+)?([munp])?1", inv.lower()) if not m: return None, None net, num, mul = m.groups() if num is None: return net, None btc = int(num) * (MULT[mul] if mul else 1) return net, round(btc * 10**8, 3) def parse_challenge(h): out = {"scheme": None, "macaroon": False, "invoice": None} if not h: return out sm = re.search(r"(?:^|[\s,])(L402|LSAT)\s", h, re.I) out["scheme"] = sm.group(1).upper() if sm else (h.split() or [""])[0][:20] out["macaroon"] = bool(re.search(r'(macaroon|token)="?[A-Za-z0-9+/=_-]{20,}', h)) im = re.search(r'invoice="?(ln[a-z0-9]+)', h, re.I) if im: out["invoice"] = im.group(1) return out def probe(t): rec = {"name": t["name"], "url": t["url"], "method": t.get("method", "GET"), "source": t.get("source", ""), "at": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")} data = b"{}" if rec["method"] == "POST" else None req = urllib.request.Request(t["url"], data=data, method=rec["method"], headers={"User-Agent": UA, "Content-Type": "application/json"}) t0 = time.time() try: r = urllib.request.urlopen(req, timeout=10) status, headers, body = r.status, r.headers, r.read(4000) except urllib.error.HTTPError as e: status, headers, body = e.code, e.headers, e.read(4000) except Exception as e: rec.update(status=None, error=f"{type(e).__name__}: {e}"[:200]) return rec rec["ms"] = int((time.time() - t0) * 1000) rec["status"] = status wa = headers.get("WWW-Authenticate") rec["www_authenticate"] = (wa[:300] + "...") if wa and len(wa) > 300 else wa c = parse_challenge(wa) if c["invoice"]: net, sats = bolt11_amount_sats(c["invoice"]) c.update(network=net, amount_sats=sats) c["invoice"] = c["invoice"][:40] + "..." rec["challenge"] = c rec["valid_l402"] = status == 402 and c["scheme"] in ("L402", "LSAT") and c["macaroon"] and bool(c["invoice"]) txt = body.decode("utf-8", "replace") bm = re.search(r"(lnbc[0-9a-z]{20,})", txt, re.I) if bm and not c["invoice"]: net, sats = bolt11_amount_sats(bm.group(1)) rec["body_invoice_sats"] = sats rec["body_head"] = txt[:300] return rec def selftest(): assert bolt11_amount_sats("lnbc2500u1pvjluez")[1] == 250000 assert bolt11_amount_sats("lnbc10n1abc")[1] == 1 assert bolt11_amount_sats("lnbc1pvjluez")[1] is None c = parse_challenge('L402 macaroon="AgEEbHNhdAJCAAAx1234567890abcdef", invoice="lnbc100n1pjexample"') assert c["scheme"] == "L402" and c["macaroon"] and c["invoice"].startswith("lnbc100n") c2 = parse_challenge('Payment id="x", method="lightning", L402 macaroon="AgEEbHNhdAJCAAAx1234567890abcdef", invoice="lnbc1u1pj"') assert c2["scheme"] == "L402" and c2["macaroon"] if __name__ == "__main__": selftest() targets = json.load(open(sys.argv[1], encoding="utf-8")) res = [] for t in targets: r = probe(t) print(f'{r["name"][:28]:28} {str(r.get("status")):5} valid_l402={r.get("valid_l402")} ' f'sats={r.get("challenge", {}).get("amount_sats")} {r.get("error", "")}') res.append(r) time.sleep(2) json.dump(res, open(sys.argv[2], "w", encoding="utf-8"), indent=1, ensure_ascii=False) print("wrote", sys.argv[2], len(res))