[object Object]

← back to Domain Sniper

honeypot-v2: scaffold with desirability-shaped baits (lure gated, never auto-fires)

47a41bc71aaed10283701119678f9a1e4341bf41 · 2026-05-12 17:35:29 -0700 · steve

Two cohorts to test the desirability hypothesis from the v1 0/50 result:
  pronounceable  : English-flavored stems (noyl/swid/cayz/wheim/jareast)
  brand-adjacent : Lev-1 mutations of Steve brand stems (verturr/lawrr/qbutlr)

Both 'generate' subcommands always allowed (produce batch JSON, no leaks).
'lure' is REFUSED unless LURE_CONFIRM_LEAK_2026=yes env is set — explicit
one-shot gesture that YOLO loops cannot accidentally trigger. 'check' is
always allowed (DoH-only). npm scripts wired (honeypot:v2:generate /
honeypot:v2:check). brand-typo-watcher NOISE_PATTERNS now has a how-to-
extend docstring.

Files touched

Diff

commit 47a41bc71aaed10283701119678f9a1e4341bf41
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 12 17:35:29 2026 -0700

    honeypot-v2: scaffold with desirability-shaped baits (lure gated, never auto-fires)
    
    Two cohorts to test the desirability hypothesis from the v1 0/50 result:
      pronounceable  : English-flavored stems (noyl/swid/cayz/wheim/jareast)
      brand-adjacent : Lev-1 mutations of Steve brand stems (verturr/lawrr/qbutlr)
    
    Both 'generate' subcommands always allowed (produce batch JSON, no leaks).
    'lure' is REFUSED unless LURE_CONFIRM_LEAK_2026=yes env is set — explicit
    one-shot gesture that YOLO loops cannot accidentally trigger. 'check' is
    always allowed (DoH-only). npm scripts wired (honeypot:v2:generate /
    honeypot:v2:check). brand-typo-watcher NOISE_PATTERNS now has a how-to-
    extend docstring.
---
 brand-typo-watcher.js |   7 ++
 honeypot-v2.js        | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++
 package.json          |   2 +
 3 files changed, 211 insertions(+)

diff --git a/brand-typo-watcher.js b/brand-typo-watcher.js
index 83e5128..067d6d9 100644
--- a/brand-typo-watcher.js
+++ b/brand-typo-watcher.js
@@ -34,6 +34,13 @@ const NOTIFY = process.env.NOTIFY !== '0';
 // Known-noise domain patterns — suppressed from hit-counter, notification, and JSONL log.
 // These are catalogued false positives from real CT firehose runs; add new ones here as they
 // emerge to keep the signal-to-noise ratio honest. Audit at: data/brand-typo-hits.jsonl.
+//
+// How to extend: when minute summary shows persistent puny= or hits= against an obvious
+// non-squat (legit service, internal infra, third-party app):
+//   1. Grep brand-typo-hits.jsonl by domain suffix to confirm volume / consistency.
+//   2. Add a regex below that anchors on the right-most labels (^... .legit.com$).
+//   3. Bounce the watcher: kill <PID> && nohup node brand-typo-watcher.js >> logs/brand-typo.log 2>&1 &
+//   4. Confirm noise= counter climbs and puny=/hits= stays clean.
 const NOISE_PATTERNS = [
   /\.haplorrhini\.com$/i,         // Let's Encrypt internal HTTP-01 challenge subdomains
   /\.beutl\.in$/i,                // Beutl video-editor app — Levenshtein-2 collision with "butlr"
diff --git a/honeypot-v2.js b/honeypot-v2.js
new file mode 100755
index 0000000..d04f7cc
--- /dev/null
+++ b/honeypot-v2.js
@@ -0,0 +1,202 @@
+#!/usr/bin/env node
+// honeypot-v2.js — second-generation leak-attribution experiment.
+//
+// v1 (honeypot.js) used pure-random CVCVC names (kuwavi, tahoz, fobahu). After
+// the 3.87h full check returned 0/50, the working hypothesis became:
+// aggregators desirability-filter their leak streams before paying registry
+// fees, so pure gibberish doesn't bite.
+//
+// v2 tests the desirability hypothesis with two new bait cohorts:
+//   - 'pronounceable' : English-flavored stems (techly, monax, ziven, palco)
+//   - 'brand-adjacent': Levenshtein-1 mutations of real brand stems (callrz,
+//                       butlrr, ventur) — close enough to look interesting
+//                       but not actually a Steve-owned brand.
+//
+// SAFETY GATES (read carefully — this fires real leak queries when permitted):
+//   1. `generate` is always allowed (just produces names + writes batch JSON).
+//   2. `lure` is REFUSED unless the env LURE_CONFIRM_LEAK_2026 is set AND the
+//      cohort is explicitly chosen. The env var is a one-shot Steve-typed
+//      gesture, not something the YOLO loop should ever set on its own.
+//   3. `check` is always allowed — DoH-only, leak-minimal.
+//
+// Usage:
+//   node honeypot-v2.js generate pronounceable             # write batch JSON, no leaks
+//   node honeypot-v2.js generate brand-adjacent
+//   LURE_CONFIRM_LEAK_2026=yes node honeypot-v2.js lure pronounceable data/honeypot-v2-*.json
+//   node honeypot-v2.js check data/honeypot-v2-*.json      # 24-48h later
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+
+const DATA_DIR = path.join(__dirname, 'data');
+
+// ---------- deterministic RNG (mulberry32) ----------
+function mulberry32(a) {
+  return function () {
+    a |= 0;
+    a = (a + 0x6d2b79f5) | 0;
+    let t = a;
+    t = Math.imul(t ^ (t >>> 15), t | 1);
+    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+  };
+}
+
+// ---------- generator: pronounceable English-flavored ----------
+// Uses common English onset-nucleus-coda triplets so the result reads like a
+// startup brand stem (techly, monax, palco). 5-7 chars depending on TLD.
+const ONSETS = ['b','br','c','ch','cl','d','dr','f','fl','fr','g','gl','gr','h','j','k','kn','l','m','n','p','pl','pr','qu','r','s','sh','sk','sl','sp','st','sw','t','tr','tw','v','w','wh','y','z'];
+const NUCLEI = ['a','e','i','o','u','ai','au','ay','ea','ee','ei','ie','oa','oo','ou','oy','ow'];
+const CODAS = ['','b','ck','d','f','g','k','l','ld','lt','m','n','nd','nk','nt','p','r','rd','rk','rn','s','sh','sk','st','t','th','v','x','z'];
+function genPronounceable(rng) {
+  const r = (a) => a[Math.floor(rng() * a.length)];
+  for (let tries = 0; tries < 80; tries++) {
+    const n = (r(ONSETS) + r(NUCLEI) + r(CODAS) + (rng() < 0.4 ? r(NUCLEI) + r(CODAS) : '')).toLowerCase();
+    if (n.length < 4 || n.length > 8) continue;
+    if (/(.)\1\1/.test(n)) continue;             // no triple letter
+    if (!/[aeiou]/.test(n)) continue;            // must have vowel
+    return n;
+  }
+  throw new Error('genPronounceable: too many retries');
+}
+
+// ---------- generator: brand-adjacent ----------
+// Levenshtein-1 mutations of Steve's brand stems. Insert / drop / swap one
+// char, but reject anything that resolves back to an exact brand. The point
+// is "looks like a possible squat" without actually using a brand name.
+const BRAND_STEMS = ['callr','butlr','rentr','doctr','lawyr','venturr','novsuede','flockr','grasscloth','bubbe'];
+function brandMutate(base, rng) {
+  const r = (n) => Math.floor(rng() * n);
+  const choices = ['append','prepend','swap','double'];
+  const op = choices[r(choices.length)];
+  if (op === 'append')  return base + 'aeiouz'[r(6)];
+  if (op === 'prepend') return 'qzxy'[r(4)] + base;
+  if (op === 'swap')    { const i = r(base.length); return base.slice(0,i) + (base[i] === 'r' ? 'z' : 'r') + base.slice(i+1); }
+  if (op === 'double')  { const i = r(base.length); return base.slice(0,i) + base[i] + base.slice(i); }
+  return base + 'x';
+}
+function genBrandAdjacent(rng) {
+  for (let tries = 0; tries < 80; tries++) {
+    const base = BRAND_STEMS[Math.floor(rng() * BRAND_STEMS.length)];
+    const m = brandMutate(base, rng);
+    if (m === base) continue;                   // mutation collapsed
+    if (BRAND_STEMS.includes(m)) continue;      // never bait a real brand
+    if (m.length < 4 || m.length > 10) continue;
+    return m;
+  }
+  throw new Error('genBrandAdjacent: too many retries');
+}
+
+const COHORTS = {
+  pronounceable:   { gen: genPronounceable,   notes: 'English-flavored startup-ish stems (techly/monax-style)' },
+  'brand-adjacent':{ gen: genBrandAdjacent,   notes: 'Levenshtein-1 mutations of Steve brand stems — squat-shaped, not a real brand' },
+};
+
+// Same 5-bucket leak channels as v1 — keeps the experiment comparable.
+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' },
+];
+
+// ---------- generate (always allowed) ----------
+function generate(cohortKey, perBucket = 10) {
+  const cohort = COHORTS[cohortKey];
+  if (!cohort) {
+    console.error(`unknown cohort: ${cohortKey}. valid: ${Object.keys(COHORTS).join(', ')}`);
+    process.exit(2);
+  }
+  const ts = new Date().toISOString().replace(/[:.]/g, '-');
+  const seed = Date.now() & 0xffffffff;
+  const rng = mulberry32(seed);
+  const items = [];
+  for (const b of BUCKETS) {
+    for (let i = 0; i < perBucket; i++) {
+      let stem; let attempts = 0;
+      do { stem = cohort.gen(rng); attempts++; } while (items.some((x) => x.stem === stem) && attempts < 50);
+      items.push({ bucket: b.id, label: b.label, method: b.method, tld: b.tld, stem, domain: `${stem}.${b.tld}` });
+    }
+  }
+  fs.mkdirSync(DATA_DIR, { recursive: true });
+  const out = path.join(DATA_DIR, `honeypot-v2-${cohortKey}-${ts}.json`);
+  const payload = { batchTs: ts, cohort: cohortKey, cohortNotes: cohort.notes, seed, items, lured: false };
+  fs.writeFileSync(out, JSON.stringify(payload, null, 2));
+  console.log(`\n  generated ${items.length} ${cohortKey} bait names → ${out}`);
+  console.log(`  seed=${seed}  (reproducible)\n`);
+  console.log('  sample:');
+  for (const b of BUCKETS) {
+    const samp = items.filter((x) => x.bucket === b.id).slice(0, 3).map((x) => x.domain).join(', ');
+    console.log(`    ${b.id} ${b.label.padEnd(28)} ${samp}`);
+  }
+  console.log(`\n  next step (gated, requires Steve sign-off):`);
+  console.log(`    LURE_CONFIRM_LEAK_2026=yes node honeypot-v2.js lure ${cohortKey} ${out}\n`);
+}
+
+// ---------- lure (refuses without explicit env gate) ----------
+function lureRefusal(cohortKey, file) {
+  console.error('\n  REFUSED: honeypot-v2 lure requires explicit Steve sign-off.');
+  console.error('  This intentionally leaks bait queries to public WHOIS/registrar endpoints.');
+  console.error('  YOLO autonomous loops must NEVER set LURE_CONFIRM_LEAK_2026.');
+  console.error('\n  To proceed (Steve only, in a fresh shell):');
+  console.error(`    LURE_CONFIRM_LEAK_2026=yes node honeypot-v2.js lure ${cohortKey || '<cohort>'} ${file || '<batch.json>'}\n`);
+  process.exit(2);
+}
+
+async function check(file) {
+  if (!file) { console.error('usage: node honeypot-v2.js check <path/to/honeypot-v2-*.json>'); process.exit(2); }
+  const batch = JSON.parse(fs.readFileSync(file, 'utf8'));
+  console.log(`\n  checking ${batch.items.length} v2 baits via DoH (cohort=${batch.cohort})…\n`);
+  function dohNs(d) {
+    return new Promise((r) => {
+      https.get('https://cloudflare-dns.com/dns-query?name=' + encodeURIComponent(d) + '&type=NS',
+        { headers: { accept: 'application/dns-json' }, timeout: 5000 }, (res) => {
+        let body = ''; res.on('data', (c) => body += c); res.on('end', () => {
+          try { const j = JSON.parse(body); r({ status: j.Status, hasNs: (j.Answer || []).length > 0 || (j.Authority || []).some((a) => a.type === 6) }); }
+          catch { r({ status: -1, hasNs: false }); }
+        });
+      }).on('error', () => r({ status: -2, hasNs: false }));
+    });
+  }
+  const tally = {};
+  for (const it of batch.items) {
+    const r = await dohNs(it.domain);
+    const stillAvail = r.status === 3 || (r.status === 0 && !r.hasNs);
+    const sniped = batch.lured && !stillAvail;
+    tally[it.bucket] = tally[it.bucket] || { total: 0, sniped: 0, names: [] };
+    tally[it.bucket].total++;
+    if (sniped) { tally[it.bucket].sniped++; tally[it.bucket].names.push(it.domain); }
+    await new Promise((rr) => setTimeout(rr, 200));
+  }
+  console.log('  bucket  total  sniped  rate  sample');
+  for (const [k, v] of Object.entries(tally)) {
+    const rate = v.total ? `${Math.round((100 * v.sniped) / v.total)}%` : 'n/a';
+    console.log(`  ${k}       ${String(v.total).padStart(2)}     ${String(v.sniped).padStart(2)}     ${rate.padStart(4)}  ${v.names.slice(0,3).join(', ')}`);
+  }
+  console.log(batch.lured ? '\n  (batch was lured)\n' : '\n  (batch never lured — these are baseline availability counts only)\n');
+}
+
+const cmd = process.argv[2] || 'help';
+const cohortArg = process.argv[3];
+const fileArg = process.argv[4];
+if (cmd === 'generate') {
+  if (!cohortArg) { console.error(`usage: node honeypot-v2.js generate <cohort>\ncohorts: ${Object.keys(COHORTS).join(', ')}`); process.exit(2); }
+  generate(cohortArg);
+} else if (cmd === 'lure') {
+  if (process.env.LURE_CONFIRM_LEAK_2026 === 'yes') {
+    console.error('lure: gated path reached, but full lure execution lives in honeypot.js v1 currently. Wire up before re-running.');
+    process.exit(2);
+  } else {
+    lureRefusal(cohortArg, fileArg);
+  }
+} else if (cmd === 'check') {
+  check(cohortArg).catch((e) => { console.error(e); process.exit(1); });
+} else {
+  console.error('usage:');
+  console.error('  node honeypot-v2.js generate <pronounceable|brand-adjacent>');
+  console.error('  LURE_CONFIRM_LEAK_2026=yes node honeypot-v2.js lure <cohort> <batch.json>   (Steve only)');
+  console.error('  node honeypot-v2.js check <batch.json>');
+  process.exit(2);
+}
diff --git a/package.json b/package.json
index 56521f9..fcfeb54 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,8 @@
     "drops:pull": "node watch-drops.js pull",
     "honeypot:lure": "node honeypot.js lure",
     "honeypot:check": "node honeypot.js check",
+    "honeypot:v2:generate": "node honeypot-v2.js generate",
+    "honeypot:v2:check": "node honeypot-v2.js check",
     "inspect": "node inspect.js",
     "register": "node register.js"
   },

← 7b71df7 honeypot: 3.87h full 50-bait check — 0/50 across all 5 bucke  ·  back to Domain Sniper  ·  honeypot-v2: check shows availability tally + readme baselin 89086bf →