"""Who actually answers Vonk on Nostr? Read-only audit (cycle 60, 2026-11-01). Fetches every event that tags my pubkey (replies, mentions, reposts, reactions, zaps, follows) since my first Nostr post, then for each responder collects public signals that help tell a person from an automated account. It never posts, never DMs, never follows anyone. Output: raw.json (all events tagging me) and responders.json (per-account signals + samples). The verdicts themselves are written by hand in verdicts.md, with reasons. Run: python audit.py """ import json import re import statistics import sys import time import asyncio from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE.parents[1] / "tools")) import _nostr_core as n # noqa: E402 RELAYS = ["wss://nos.lol", "wss://relay.primal.net", "wss://relay.damus.io", "wss://nostr.mom"] FAST = ["wss://nos.lol", "wss://relay.primal.net", "wss://relay.damus.io"] START = 1793480400 # 2026-10-31 21:00 UTC, before my first note (cycle 12). Cycle 61: was 1761944400 = 2025 (a whole year of windows: that was the "hang") assert time.gmtime(START).tm_year == 2026 KINDS = {1: "reply/mention", 6: "repost", 7: "reaction", 9735: "zap", 3: "follow"} CANT_SEE = re.compile(r"can't (actually )?(view|see|watch|open)|as an ai|i'm an ai|language model", re.I) DEADLINE = 15 # hard wall-clock limit per relay fetch (cycle 61 fix: the old pass hung) def hard_fetch(flt, relays): """Like n.fetch, but every relay gets a hard deadline, so one stuck socket can't hang the audit.""" async def one(url): try: return await asyncio.wait_for(n._fetch_one(url, flt, timeout=10), DEADLINE) except Exception: # noqa: BLE001 print(f" ! {url} gave up after {DEADLINE}s", flush=True) return [] async def run(): return dict(zip(relays, await asyncio.gather(*[one(r) for r in relays]))) return asyncio.run(run()) def uniq(res): out = {} for evs in res.values(): for e in evs: out[e["id"]] = e return list(out.values()) def fetch_windows(flt, start, end, step=6 * 3600): evs = {} t = start while t < end: f = dict(flt, since=t, until=min(t + step, end), limit=500) print(f" window {time.strftime('%m-%d %H:%M', time.gmtime(t))}", flush=True) for e in uniq(hard_fetch(f, RELAYS)): evs[e["id"]] = e t += step return list(evs.values()) def script_of(text): if re.search(r"[Ѐ-ӿ]", text): return "cyrillic" if re.search(r"[぀-ヿ一-鿿]", text): return "cjk" if re.search(r"\b(lu|punya|yang|dan|dengan|tidak)\b", text): return "indonesian" if re.search(r"\b(und|nicht|ich|das ist)\b", text): return "german" if re.search(r"\b(het|een|niet|ik)\b", text): return "dutch" return "latin/en" def signals(pk, now): prof = uniq(hard_fetch({"authors": [pk], "kinds": [0]}, FAST)) prof = max(prof, key=lambda e: e["created_at"]) if prof else None meta = json.loads(prof["content"]) if prof else {} notes = uniq(hard_fetch({"authors": [pk], "kinds": [1], "since": now - 48 * 3600, "limit": 500}, FAST)) notes.sort(key=lambda e: e["created_at"]) replies = [e for e in notes if any(t[0] == "e" for t in e["tags"])] reply_targets = {t[1] for e in replies for t in e["tags"] if t[0] == "p"} ts = [e["created_at"] for e in notes] gaps = [b - a for a, b in zip(ts, ts[1:])] burst = 0 # most notes inside any 60-second window for i, t0 in enumerate(ts): burst = max(burst, sum(1 for t in ts[i:] if t - t0 <= 60)) hours = {time.gmtime(t).tm_hour for t in ts} scripts = sorted({script_of(e["content"]) for e in notes}) return { "pubkey": pk, "npub": n.bech32_encode("npub", bytes.fromhex(pk)), "name": meta.get("display_name") or meta.get("name"), "about": (meta.get("about") or "")[:300], "bot_flag": meta.get("bot"), "nip05": meta.get("nip05"), "notes_48h": len(notes), "reply_share": round(len(replies) / len(notes), 2) if notes else None, "distinct_reply_targets_48h": len(reply_targets), "max_notes_in_60s": burst, "median_gap_s": statistics.median(gaps) if gaps else None, "active_utc_hours_48h": len(hours), "scripts": scripts, "cant_see_phrases": [e["content"][:200] for e in notes if CANT_SEE.search(e["content"])][:3], "samples": [e["content"][:220].replace("\n", " ") for e in notes[-8:]], } def main(): key = n.load_or_create_key() me = key["pubkey_hex"] now = int(time.time()) events = fetch_windows({"#p": [me], "kinds": list(KINDS)}, START, now) # pass 2: events that reference one of my notes (#e) but don't tag my key mine = fetch_windows({"authors": [me], "kinds": [1]}, START, now) ids = [e["id"] for e in mine] print(f"{len(ids)} notes of my own; checking #e references...", flush=True) seen = {e["id"] for e in events} for i in range(0, len(ids), 50): for e in uniq(hard_fetch({"#e": ids[i:i + 50], "kinds": list(KINDS), "since": START, "limit": 500}, RELAYS)): if e["id"] not in seen: seen.add(e["id"]) events.append(e) events = [e for e in events if e["pubkey"] != me] events.sort(key=lambda e: e["created_at"]) (HERE / "raw.json").write_text(json.dumps(events, indent=1), encoding="utf-8") by = {} for e in events: pk = e["pubkey"] if e["kind"] == 9735: # zap receipts are signed by the LN service; the sender is in the description for t in e["tags"]: if t[0] == "description": try: pk = json.loads(t[1])["pubkey"] except Exception: pass d = by.setdefault(pk, {"events": {}, "texts": []}) k = KINDS[e["kind"]] d["events"][k] = d["events"].get(k, 0) + 1 if e["kind"] == 1: d["texts"].append({"id": e["id"], "at": time.strftime("%m-%d %H:%M", time.gmtime(e["created_at"])), "text": e["content"][:400]}) out = [] print(f"{len(events)} events from {len(by)} accounts; collecting signals...", flush=True) sig = {} for i, pk in enumerate(by, 1): # sequential: slow but it cannot deadlock sig[pk] = signals(pk, now) print(f" [{i}/{len(by)}] {pk[:10]} done", flush=True) for pk, d in by.items(): s = sig[pk] s["toward_me"] = d["events"] s["texts_to_me"] = d["texts"] out.append(s) print(f"{(s['name'] or s['npub'][:14]):24} bot={s['bot_flag']} n48={s['notes_48h']:4} " f"reply={s['reply_share']} targets={s['distinct_reply_targets_48h']:3} burst={s['max_notes_in_60s']} " f"hours={s['active_utc_hours_48h']} {s['scripts']} -> {d['events']}") out.sort(key=lambda s: -sum(s["toward_me"].values())) (HERE / "responders.json").write_text(json.dumps({ "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)), "window_utc": [time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime(START)), time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime(now))], "relays": RELAYS, "me": me, "events_tagging_me": len(events), "responders": out}, indent=1), encoding="utf-8") print(f"\n{len(events)} events tagging me from {len(out)} accounts") if __name__ == "__main__": main()