[object Object]

← back to Domain Sniper

watch-drops: scaffold Step 6 — env-gated, czds-only adapter, refuses by default

ded6c2216634986baf0487c8e022c5c6a2aafe78 · 2026-05-12 16:52:53 -0700 · steve

Adapter interface + JSONL output + day-indexed metadata in place. CZDS
adapter wired but stub (activates on CZDS_API_KEY via secrets skill).
Pool/DropCatch/SnapNames/ExpiredDomains.net intentionally NOT registered
— anti-bot walled AND visiting them is a leak signal per DoH-only rule.

Also recorded 3.24h honeypot DoH spot-check (slice 4-5, final never-checked
baits): 0/10. 30 of 50 baits total spot-checked, curve flat throughout.

Files touched

Diff

commit ded6c2216634986baf0487c8e022c5c6a2aafe78
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 12 16:52:53 2026 -0700

    watch-drops: scaffold Step 6 — env-gated, czds-only adapter, refuses by default
    
    Adapter interface + JSONL output + day-indexed metadata in place. CZDS
    adapter wired but stub (activates on CZDS_API_KEY via secrets skill).
    Pool/DropCatch/SnapNames/ExpiredDomains.net intentionally NOT registered
    — anti-bot walled AND visiting them is a leak signal per DoH-only rule.
    
    Also recorded 3.24h honeypot DoH spot-check (slice 4-5, final never-checked
    baits): 0/10. 30 of 50 baits total spot-checked, curve flat throughout.
---
 README.md      |   3 +-
 watch-drops.js | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 142 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 38f13f8..da74e47 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ We can't get the WHOIS query stream (it's gated to aggregator partners), but we
 - [x] **Step 3** — `dashboard.js` — live web viewer at :9895 of the CT firehose. Bloomberg-terminal UX with brand/hot filters.
 - [x] **Step 4** — `honeypot.js` — controlled leak-attribution hunt. Fires 50 DoH-confirmed-available baits across 5 channels (Verisign WHOIS, Google WHOIS .app, nic.co WHOIS, GoDaddy API, control). 24-48h later, WHOIS-fingerprints every snipe and clusters by registrar+IANA+NS to identify the attacker.
 - [x] **Step 5** — `register.js` — atomic registrar API call. Backends: NameSilo / Namecheap / GoDaddy. DRY-RUN default; pass `--confirm` to actually buy.
-- [ ] **Step 6** — `watch-drops.js` — daily pull of pending-delete CSVs from public auction houses. Surfaces names dropping in the next 5 days. Free sources: pool.com, dropcatch.com, snapnames.com, ICANN CZDS zone-diff.
+- [x] **Step 6** — `watch-drops.js` — daily pull of pending-delete names. Scaffold-only: adapter interface + index/JSONL output wired, but **refuses to fetch unless `WATCH_DROPS_SOURCES` is set**. Pool/DropCatch/SnapNames/ExpiredDomains.net intentionally NOT registered (anti-bot walls + leak-signal per DoH-only rule). Only sanctioned adapter is `czds` (ICANN zone-diff), which activates once `CZDS_API_KEY` is routed via secrets skill.
 - [x] **Step 7** — `brand-typo-watcher.js` — dedicated CertStream listener with Levenshtein distance ≤ 2 + Punycode/IDN homoglyph detection. macOS desktop notification on hit (NOTIFY=0 to disable). Run with `npm run watch:brand`.
 - [ ] **Step 8** — `registrar-fingerprints.json` — known aggregator signatures (OVH ns112.ovh.net, etc.) so the honeypot check phase can name-and-shame quickly.
 - [x] **Step 9** — added 16 deny rules to `~/.claude/settings.json` covering:
@@ -53,6 +53,7 @@ Spot-checks via DoH at increasing intervals to find where the snipe window close
 | ~2h    | A/B/C/D/E (2 ea, slice 0-1) | 0 / 10 |
 | ~3.1h  | A/B/C/D/E (2 ea, slice 0-1) | 0 / 10 |
 | ~3.2h  | A/B/C/D/E (2 ea, slice 2-3) | 0 / 10 |
+| ~3.24h | A/B/C/D/E (2 ea, slice 4-5) | 0 / 10 — **30 of 50 baits checked, all 0** |
 | ~6h    | _pending_        | — |
 | ~24h   | full batch       | — |
 | ~48h   | full batch (final) | — |
diff --git a/watch-drops.js b/watch-drops.js
new file mode 100755
index 0000000..39d07ac
--- /dev/null
+++ b/watch-drops.js
@@ -0,0 +1,140 @@
+#!/usr/bin/env node
+// watch-drops.js — daily pull of pending-delete CSVs from public drop sources.
+//
+// STATUS: SCAFFOLD. Refuses to fetch unless WATCH_DROPS_SOURCES is set.
+//
+// Why scaffolded-not-shipped:
+//   1. Anti-bot walls on pool.com / dropcatch.com / snapnames.com / expireddomains.net.
+//      Cloudflare / Akamai challenge pages bounce anonymous GETs.
+//   2. Same hygiene rule that bans WHOIS/RDAP probing applies here: any GET against
+//      a drop-house auction page is a "we're interested" signal. If we ever hit a
+//      drop list and then go register one, the request stream is correlatable.
+//   3. The only TOS-clean source is ICANN CZDS (Centralized Zone Data Service),
+//      which requires per-TLD application + API key. Once Steve has CZDS creds
+//      routed via the secrets skill, the czds adapter below activates.
+//
+// Adapter interface:
+//   { name, source, fetch(opts) -> { ok, names: [...domain], fetchedAt, raw } }
+//
+// Usage:
+//   node watch-drops.js list-sources         # show what's configured
+//   WATCH_DROPS_SOURCES=czds node watch-drops.js pull   # pull enabled adapters
+//
+// Output (per enabled adapter):
+//   data/drops/<source>-<YYYY-MM-DD>.jsonl   # one {ts, domain, source} per line
+//   data/drops/index.json                    # tally per day / source
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+
+const DATA_DIR = path.join(__dirname, 'data', 'drops');
+const INDEX_FILE = path.join(DATA_DIR, 'index.json');
+
+// Adapters keyed by short name. fetch() returns names ready to dedupe.
+const ADAPTERS = {
+  // ICANN CZDS — daily zone-file diff against yesterday's snapshot.
+  // Requires CZDS_API_KEY (routed via secrets skill once Steve approves a TLD).
+  // Each .com/.net/.app/.dev zone is a tab-separated NSDNAME record file ~GB-sized;
+  // diff yields adds (registrations) and deletes (drops).
+  czds: {
+    name: 'czds',
+    requiresEnv: 'CZDS_API_KEY',
+    description: 'ICANN Centralized Zone Data Service zone-diff (drops = yesterday \\ today)',
+    fetch: async () => {
+      const apiKey = process.env.CZDS_API_KEY;
+      if (!apiKey) return { ok: false, error: 'CZDS_API_KEY not set — route via secrets skill' };
+      // TODO: Implement per-TLD zone fetch + diff once a key is provisioned.
+      // CZDS flow: POST /api/authenticate -> JWT -> GET /czds/downloads/links -> per-zone GET
+      return { ok: false, error: 'czds adapter is a stub — implement after CZDS-TLD approval' };
+    },
+  },
+
+  // Pool.com / DropCatch / SnapNames / ExpiredDomains.net are intentionally NOT
+  // listed as adapters here. They have anti-bot walls AND visiting them is a
+  // leak signal per the same rule that bans WHOIS probing. Don't add them
+  // without explicit Steve sign-off.
+};
+
+function ensureDir(p) {
+  fs.mkdirSync(p, { recursive: true });
+}
+
+function loadIndex() {
+  try { return JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8')); }
+  catch { return { byDay: {} }; }
+}
+
+function saveIndex(ix) {
+  ensureDir(DATA_DIR);
+  fs.writeFileSync(INDEX_FILE, JSON.stringify(ix, null, 2));
+}
+
+function listSources() {
+  console.log('\n  watch-drops :: configured adapters');
+  console.log('  ' + '-'.repeat(78));
+  for (const [k, a] of Object.entries(ADAPTERS)) {
+    const envOk = a.requiresEnv ? (process.env[a.requiresEnv] ? '\x1b[32mYES\x1b[0m' : '\x1b[31mno\x1b[0m') : '\x1b[32mYES\x1b[0m';
+    console.log(`  ${k.padEnd(12)} env-ready=${envOk}  ${a.description}`);
+    if (a.requiresEnv && !process.env[a.requiresEnv]) {
+      console.log(`               requires ${a.requiresEnv} (route via secrets skill: ~/Projects/secrets-manager/cli.js)`);
+    }
+  }
+  console.log('\n  policy: drop-house URLs (pool.com / dropcatch.com / snapnames.com / expireddomains.net)');
+  console.log('          are intentionally NOT registered as adapters — they are leak channels per the');
+  console.log('          DoH-only rule and are anti-bot-walled. Add only with explicit Steve sign-off.\n');
+}
+
+async function pull() {
+  const enabled = (process.env.WATCH_DROPS_SOURCES || '').split(',').map((s) => s.trim()).filter(Boolean);
+  if (!enabled.length) {
+    console.error('\n  WATCH_DROPS_SOURCES is empty — refusing to fetch.');
+    console.error('  Run:  node watch-drops.js list-sources    to see what\'s available.\n');
+    process.exit(2);
+  }
+  ensureDir(DATA_DIR);
+  const ix = loadIndex();
+  const day = new Date().toISOString().slice(0, 10);
+  ix.byDay[day] = ix.byDay[day] || {};
+  let totalNames = 0;
+  for (const name of enabled) {
+    const a = ADAPTERS[name];
+    if (!a) {
+      console.error(`  unknown adapter: ${name}`);
+      continue;
+    }
+    console.log(`  [${name}] fetching…`);
+    try {
+      const r = await a.fetch({});
+      if (!r.ok) {
+        console.error(`  [${name}] skipped: ${r.error}`);
+        ix.byDay[day][name] = { count: 0, error: r.error };
+        continue;
+      }
+      const out = path.join(DATA_DIR, `${name}-${day}.jsonl`);
+      const ts = new Date().toISOString();
+      const lines = (r.names || []).map((d) => JSON.stringify({ ts, domain: d, source: name }) + '\n');
+      fs.writeFileSync(out, lines.join(''));
+      ix.byDay[day][name] = { count: lines.length, file: out };
+      totalNames += lines.length;
+      console.log(`  [${name}] wrote ${lines.length} names → ${out}`);
+    } catch (e) {
+      console.error(`  [${name}] error: ${e.message}`);
+      ix.byDay[day][name] = { count: 0, error: e.message };
+    }
+  }
+  saveIndex(ix);
+  console.log(`\n  total names pulled today: ${totalNames}`);
+}
+
+const cmd = process.argv[2] || 'help';
+if (cmd === 'list-sources') {
+  listSources();
+} else if (cmd === 'pull') {
+  pull().catch((e) => { console.error(e); process.exit(1); });
+} else {
+  console.error('usage:');
+  console.error('  node watch-drops.js list-sources');
+  console.error('  WATCH_DROPS_SOURCES=czds[,dropcatch...] node watch-drops.js pull');
+  process.exit(2);
+}

← 6852b69 ct-source-ctlog: emit 'resumed' status when cursor pre-exist  ·  back to Domain Sniper  ·  fingerprints: +5 aggressive operators (Key-Systems, Hexonet, 9109f57 →