← 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
M brand-typo-watcher.jsA honeypot-v2.jsM package.json
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 →