[object Object]

← back to Domain Sniper

honeypot v2: DoH pre-check is ground-truth; 6-letter .com to escape saturation

c8f80e8cfd643989c672407db45b32570e8bfecd · 2026-05-12 13:39:36 -0700 · Steve Abrams

Phase 1 finds 10 DoH-confirmed-available names per bucket (regenerates until
each slot passes a Cloudflare NXDOMAIN+NXDOMAIN check). Phase 2 fires the
bait query through the suspect leak channel — its result is recorded but
not used for snipe attribution. preCheckAvailable becomes the only baseline
the check phase needs.

.com bumped from 5 to 6 letters (5-letter .com pronounceable namespace is
80%+ saturated, killing test power).

Files touched

Diff

commit c8f80e8cfd643989c672407db45b32570e8bfecd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 12 13:39:36 2026 -0700

    honeypot v2: DoH pre-check is ground-truth; 6-letter .com to escape saturation
    
    Phase 1 finds 10 DoH-confirmed-available names per bucket (regenerates until
    each slot passes a Cloudflare NXDOMAIN+NXDOMAIN check). Phase 2 fires the
    bait query through the suspect leak channel — its result is recorded but
    not used for snipe attribution. preCheckAvailable becomes the only baseline
    the check phase needs.
    
    .com bumped from 5 to 6 letters (5-letter .com pronounceable namespace is
    80%+ saturated, killing test power).
---
 honeypot.js | 72 ++++++++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 47 insertions(+), 25 deletions(-)

diff --git a/honeypot.js b/honeypot.js
index d47cb3e..ad2ffcc 100644
--- a/honeypot.js
+++ b/honeypot.js
@@ -39,12 +39,14 @@ const BANNED_STEMS = new Set([
   // any Steve-brand-adjacent stems live here so we never bait something he'd want
   'callr', 'butlr', 'callr', 'doctr', 'lawyr', 'rentr',
 ]);
-function genName(rng) {
+function genName(rng, len = 6) {
   const C = 'bcdfghjklmnprstvwz';
   const V = 'aeiou';
   const r = (max) => Math.floor(rng() * max);
   for (let tries = 0; tries < 50; tries++) {
-    const n = C[r(C.length)] + V[r(V.length)] + C[r(C.length)] + V[r(V.length)] + C[r(C.length)];
+    // CVCVCV(C) alternating
+    let n = '';
+    for (let i = 0; i < len; i++) n += (i % 2 === 0 ? C : V)[r((i % 2 === 0 ? C : V).length)];
     if (/(.)\1/.test(n)) continue;
     if (BANNED_STEMS.has(n)) continue;
     return n;
@@ -53,11 +55,12 @@ function genName(rng) {
 }
 
 const BUCKETS = [
-  { id: 'A', label: 'Verisign WHOIS (.com)', tld: 'com', method: 'whois-verisign-com' },
-  { id: 'B', label: 'Google WHOIS (.app)', tld: 'app', method: 'whois-google-app' },
-  { id: 'C', label: 'nic.co WHOIS (.co)', tld: 'co', method: 'whois-nic-co' },
-  { id: 'D', label: 'GoDaddy API (.com)', tld: 'com', method: 'godaddy-api-com' },
-  { id: 'E', label: 'CONTROL no-query (.com)', tld: 'com', method: 'none' },
+  // nameLen 6 for .com to escape the saturated 5-letter namespace
+  { id: 'A', label: 'Verisign WHOIS (.com)', tld: 'com', nameLen: 6, method: 'whois-verisign-com' },
+  { id: 'B', label: 'Google WHOIS (.app)', tld: 'app', nameLen: 5, method: 'whois-google-app' },
+  { id: 'C', label: 'nic.co WHOIS (.co)', tld: 'co', nameLen: 5, method: 'whois-nic-co' },
+  { id: 'D', label: 'GoDaddy API (.com)', tld: 'com', nameLen: 6, method: 'godaddy-api-com' },
+  { id: 'E', label: 'CONTROL no-query (.com)', tld: 'com', nameLen: 6, method: 'none' },
 ];
 
 const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -152,6 +155,16 @@ function whoisFingerprint(domain) {
 }
 
 // ---------- LURE: generate names + fire bait queries ----------
+async function findAvailable(bucket, rng, maxTries = 30) {
+  for (let i = 0; i < maxTries; i++) {
+    const stem = genName(rng, bucket.nameLen);
+    const domain = `${stem}.${bucket.tld}`;
+    if (await dohAvailable(domain)) return { stem, domain };
+    await sleep(180);
+  }
+  return null;
+}
+
 async function lure() {
   const batchTs = new Date().toISOString().replace(/[:.]/g, '-');
   const seed = Date.now() & 0x7fffffff;
@@ -159,35 +172,44 @@ async function lure() {
   const batch = {
     batchTs,
     seed,
-    queriedFromIp: 'local (check `curl ifconfig.me` if you need to record)',
+    note: 'DoH pre-check is ground-truth baseline. WHOIS/RDAP/API queries are pure bait — their results are recorded but not used for snipe detection.',
     items: [],
   };
+  fs.mkdirSync(DATA_DIR, { recursive: true });
+  const batchPath = path.join(DATA_DIR, `honeypot-${batchTs}.json`);
+
+  console.log(`\n  honeypot batch ${batchTs}`);
+  console.log(`  seed=${seed}\n`);
+  console.log(`  phase 1: finding 10 verifiably-available names per bucket via Cloudflare DoH...\n`);
 
-  // Generate 50 names, 10 per bucket
   for (const bucket of BUCKETS) {
-    for (let i = 0; i < 10; i++) {
-      const stem = genName(rng);
+    let found = 0;
+    while (found < 10) {
+      const cand = await findAvailable(bucket, rng);
+      if (!cand) {
+        console.log(`  [${bucket.id}] giving up at ${found}/10 — namespace too saturated`);
+        break;
+      }
       batch.items.push({
         bucket: bucket.id,
         label: bucket.label,
         method: bucket.method,
         tld: bucket.tld,
-        stem,
-        domain: `${stem}.${bucket.tld}`,
+        stem: cand.stem,
+        domain: cand.domain,
+        preCheckAvailable: true,
+        preCheckedAt: new Date().toISOString(),
         queriedAt: null,
         queryResult: null,
       });
+      found++;
+      process.stdout.write(`  [${bucket.id}] ${cand.domain.padEnd(14)} confirmed AVAILABLE via DoH\n`);
     }
+    fs.writeFileSync(batchPath, JSON.stringify(batch, null, 2));
   }
 
-  fs.mkdirSync(DATA_DIR, { recursive: true });
-  const batchPath = path.join(DATA_DIR, `honeypot-${batchTs}.json`);
-  fs.writeFileSync(batchPath, JSON.stringify(batch, null, 2));
-
-  console.log(`\n  honeypot batch ${batchTs}`);
-  console.log(`  seed=${seed}  buckets=${BUCKETS.length}  names=${batch.items.length}`);
-  console.log(`  → ${batchPath}\n`);
-  console.log(`  firing bait queries (this DELIBERATELY leaks — that's the point):\n`);
+  console.log(`\n  ${batch.items.length} fresh-available bait names secured.\n`);
+  console.log(`  phase 2: firing bait queries through suspect leak channels...\n`);
 
   for (let i = 0; i < batch.items.length; i++) {
     const item = batch.items[i];
@@ -217,11 +239,11 @@ async function lure() {
       `  [${item.bucket}] ${item.domain.padEnd(14)} ${item.method.padEnd(22)} ${item.queryResult}`,
     );
     fs.writeFileSync(batchPath, JSON.stringify(batch, null, 2));
-    // pace it so we look like a curious human, not a scanner
-    await sleep(1800 + Math.random() * 1400);
+    await sleep(1500 + Math.random() * 1200);
   }
 
   console.log(`\n  bait deployed. ${batch.items.length} honeypots in the wild.`);
+  console.log(`  → ${batchPath}\n`);
   console.log(`  wait 24-48h, then:`);
   console.log(`    node honeypot.js check ${path.relative(__dirname, batchPath)}\n`);
 }
@@ -244,7 +266,7 @@ async function check(batchPath) {
 
   for (const item of batch.items) {
     const stillAvailable = await dohAvailable(item.domain);
-    const sniped = item.queryResult === 'AVAILABLE' && !stillAvailable;
+    const sniped = item.preCheckAvailable && !stillAvailable;
     results.items.push({ ...item, stillAvailable, sniped });
     const flag = sniped ? '\x1b[31m🎯 SNIPED\x1b[0m' : stillAvailable ? '   open  ' : '   taken ';
     console.log(`  [${item.bucket}] ${item.domain.padEnd(14)} ${flag}  was=${item.queryResult.padEnd(10)} now=${stillAvailable ? 'avail' : 'taken'}`);
@@ -264,7 +286,7 @@ async function check(batchPath) {
     };
     const t = tally[item.bucket];
     t.total++;
-    if (item.queryResult === 'AVAILABLE') t.wasAvailable++;
+    if (item.preCheckAvailable) t.wasAvailable++;
     if (item.sniped) {
       t.sniped++;
       t.snipedNames.push(item.domain);

← 28b7d63 step 4: honeypot.js — controlled leak-attribution hunt  ·  back to Domain Sniper  ·  step 5: register.js — atomic registrar API with dry-run safe 4aecb56 →