← back to Domain Sniper
initial scaffold — leak-minimal DoH checker + CertStream watcher
b738669095a5fae2a74ae31af884147cd9e66243 · 2026-05-12 13:07:05 -0700 · Steve Abrams
Step 0 (check.js): single-domain availability check via Cloudflare 1.1.1.1 DoH.
Queries NS + SOA only. Never touches WHOIS, RDAP, or registrar APIs — all
confirmed leak vectors after callr.app/callr.co/butlr.app got sniped.
Step 1 (watch-ct.js): live CertStream WebSocket listener with hot-pattern
filter and brand-typo defense (designerwallcoverings, philipperomano,
venturacorridor, etc.). Logs hits to data/ct-hits.jsonl.
Files touched
A .gitignoreA README.mdA check.jsA package-lock.jsonA package.jsonA watch-ct.js
Diff
commit b738669095a5fae2a74ae31af884147cd9e66243
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 12 13:07:05 2026 -0700
initial scaffold — leak-minimal DoH checker + CertStream watcher
Step 0 (check.js): single-domain availability check via Cloudflare 1.1.1.1 DoH.
Queries NS + SOA only. Never touches WHOIS, RDAP, or registrar APIs — all
confirmed leak vectors after callr.app/callr.co/butlr.app got sniped.
Step 1 (watch-ct.js): live CertStream WebSocket listener with hot-pattern
filter and brand-typo defense (designerwallcoverings, philipperomano,
venturacorridor, etc.). Logs hits to data/ct-hits.jsonl.
---
.gitignore | 11 +++++
README.md | 62 ++++++++++++++++++++++++
check.js | 75 +++++++++++++++++++++++++++++
package-lock.json | 44 +++++++++++++++++
package.json | 24 ++++++++++
watch-ct.js | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 356 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f75534c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+node_modules/
+.env
+.env.*
+!.env.example
+data/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+tmp/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e0d1968
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+# domain-sniper
+
+Steve's offensive counterpart to the aggregators that sniped callr.app, callr.co, and butlr.app. Watches public domain-intel feeds, surfaces candidates worth grabbing, registers them atomically before anyone else moves.
+
+> "I want to scrape just like they did to me." — Steve, 2026-05-12
+
+## What the aggregators are doing to us
+
+They monitor public registry data streams and front-run human searches:
+
+1. **Verisign / Google Registry WHOIS query logs** — every `whois -h whois.verisign-grs.com <domain>` query is logged and (per industry reports) resold to drop-catch partners. Same for Google's `whois.nic.google` for `.app`/`.dev`. When Steve queried `callr.app`, a partner saw the query stream within seconds and OVH/French aggregator beat him to registration.
+2. **Registrar availability-API partners** — GoDaddy, Namecheap, etc. expose `/v1/domains/available` endpoints whose query streams are visible to API partners.
+3. **Browser search-box telemetry** — typing into a registrar's homepage search box is the loudest possible signal.
+
+We can't get the WHOIS query stream (it's gated to aggregator partners), but we don't need it. There's enough public intel to compete.
+
+## Sniper data sources (all free, all public)
+
+| Source | What it tells us | Auth |
+| --- | --- | --- |
+| **Certificate Transparency logs** (`certstream.calidog.io`) | Every TLS cert issued in real-time. Tells us a domain was JUST registered + set up. | None — public WebSocket |
+| **Pool.com / DropCatch / SnapNames daily auction lists** | Domains pending deletion (5-day grace window before drop) | None — CSV download |
+| **ICANN CZDS zone files** (`czds.icann.org`) | Daily snapshot of every registered .com/.net/.app/.dev. Diff = drops + new. | Free application |
+| **crt.sh** | Historical CT search by keyword | None |
+| **DNS Census / passive DNS** | Historical NS/A records | Free tier |
+| **WhoisXMLAPI free tier** | 500 RDAP queries/mo for verification | API key |
+
+## Architecture (one step at a time)
+
+- [x] **Step 0** — leak-minimal availability checker (`check.js`, Cloudflare DoH, no WHOIS/RDAP) — built first so we can SAFELY verify a candidate before racing to register. Used as a utility inside the sniper.
+- [ ] **Step 1** — `watch-ct.js` — live CertStream WebSocket listener. Pipes every new cert through a filter and surfaces hits. Tells us what's getting registered RIGHT NOW.
+- [ ] **Step 2** — `watch-drops.js` — daily pull of pending-delete CSVs from public auction houses. Surfaces candidates dropping in the next 5 days.
+- [ ] **Step 3** — `brand-typo-watcher.js` — watches for typos of Steve's brands (designerwallcoverings, philipperomano, ventura-corridor, etc.) hitting the market, alerts immediately.
+- [ ] **Step 4** — `register.js` — atomic register via Porkbun direct API (skip availability check entirely; Porkbun returns success-or-taken). Bound to a hotkey for hand-typed candidates + auto-fired by filters above.
+- [ ] **Step 5** — `viewer/` — web dashboard listing live hits, with one-click register.
+
+## Filters (what makes a candidate "hot")
+
+The CT stream is a firehose (~50/sec). Filter aggressively. A domain is a candidate if:
+
+- Length ≤ 8 characters (excluding TLD)
+- TLD in {com, app, io, co, ai, dev}
+- Doesn't contain digits (usually)
+- Doesn't contain hyphens
+- Pronounceable (consonant-vowel pattern)
+- Optional: matches a Steve-supplied keyword list
+
+## Operational discipline
+
+1. **Never check via WHOIS/RDAP from Steve's IP.** Use `check.js` only — and only at the moment of decision, not exploratory.
+2. **Don't type candidates into registrar search boxes.** Direct API only.
+3. **Single source of truth** for "what we already grabbed" — `data/owned.json`. Always check this before pinging anything external.
+4. **Audit-log every external query** to `data/queries.log` so we can trace any future leak back to its source.
+
+## Usage (so far)
+
+```bash
+# Check a single candidate, leak-minimally
+node check.js callr.app
+```
+
+More commands land as each step ships.
diff --git a/check.js b/check.js
new file mode 100644
index 0000000..2cda34c
--- /dev/null
+++ b/check.js
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+// Leak-minimal single-domain availability check.
+// Method: Cloudflare 1.1.1.1 DoH (DNS-over-HTTPS), query type=NS.
+// Avoids: WHOIS, RDAP at registry, GoDaddy availability API, registrar search boxes —
+// all known leak vectors after 3 confirmed snipes (callr.app, callr.co, butlr.app).
+
+const https = require('https');
+
+const target = (process.argv[2] || '').trim().toLowerCase();
+if (!target || !target.includes('.')) {
+ console.error('usage: node check.js <domain>');
+ process.exit(2);
+}
+
+function doh(name, type = 'NS') {
+ return new Promise((resolve, reject) => {
+ const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=${type}`;
+ https
+ .get(url, { headers: { Accept: 'application/dns-json' } }, (res) => {
+ let data = '';
+ res.on('data', (c) => (data += c));
+ res.on('end', () => {
+ try {
+ resolve(JSON.parse(data));
+ } catch (e) {
+ reject(new Error(`bad DoH response: ${data.slice(0, 200)}`));
+ }
+ });
+ })
+ .on('error', reject);
+ });
+}
+
+(async () => {
+ // Two parallel queries — NS (does the TLD know about it) + SOA (does it have a zone of its own).
+ // Both via Cloudflare. We do NOT call WHOIS/RDAP under any circumstance.
+ const [ns, soa] = await Promise.all([doh(target, 'NS'), doh(target, 'SOA')]);
+
+ // DNS Status codes:
+ // 0 = NOERROR — TLD knows the name (registered, possibly parked)
+ // 2 = SERVFAIL — temporary, retry
+ // 3 = NXDOMAIN — TLD doesn't know the name (unregistered)
+ const nxNs = ns.Status === 3;
+ const nxSoa = soa.Status === 3;
+ const hasNs = !!(ns.Answer && ns.Answer.length);
+ const hasSoa = !!(soa.Answer && soa.Answer.length);
+
+ // Available if BOTH NS + SOA come back NXDOMAIN. Either NOERROR signals the TLD recognizes it.
+ const available = nxNs && nxSoa;
+
+ const result = {
+ domain: target,
+ available,
+ confidence: available ? 'high' : hasNs || hasSoa ? 'high' : 'medium',
+ signals: {
+ ns_status: ns.Status,
+ soa_status: soa.Status,
+ has_ns: hasNs,
+ has_soa: hasSoa,
+ ns_records: hasNs ? ns.Answer.map((a) => a.data) : [],
+ },
+ method: 'cloudflare-doh-ns-soa',
+ leak_minimal: true,
+ timestamp: new Date().toISOString(),
+ advice: available
+ ? 'AVAILABLE — register within minutes via API. Do not type into registrar search boxes.'
+ : 'TAKEN — TLD recognizes the name. Move on to the next candidate.',
+ };
+
+ console.log(JSON.stringify(result, null, 2));
+ process.exit(available ? 0 : 1);
+})().catch((err) => {
+ console.error(JSON.stringify({ error: err.message, domain: target }, null, 2));
+ process.exit(3);
+});
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..487f0b2
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,44 @@
+{
+ "name": "domain-sniper",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "domain-sniper",
+ "version": "0.1.0",
+ "license": "UNLICENSED",
+ "dependencies": {
+ "ws": "^8.20.1"
+ },
+ "bin": {
+ "check": "check.js",
+ "watch-ct": "watch-ct.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..02e4253
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "domain-sniper",
+ "version": "0.1.0",
+ "description": "Offensive domain-intel sniper. Watches public registry feeds, surfaces hot candidates, registers atomically.",
+ "private": true,
+ "type": "commonjs",
+ "bin": {
+ "check": "./check.js",
+ "watch-ct": "./watch-ct.js"
+ },
+ "scripts": {
+ "check": "node check.js",
+ "watch:ct": "node watch-ct.js",
+ "watch:drops": "node watch-drops.js"
+ },
+ "dependencies": {
+ "ws": "^8.20.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "author": "Steve Abrams",
+ "license": "UNLICENSED"
+}
diff --git a/watch-ct.js b/watch-ct.js
new file mode 100644
index 0000000..2b63615
--- /dev/null
+++ b/watch-ct.js
@@ -0,0 +1,140 @@
+#!/usr/bin/env node
+// Live Certificate Transparency watcher.
+// Source: wss://certstream.calidog.io/ — public WebSocket of every TLS cert issued.
+// Use cases:
+// 1. INTEL — see what kinds of names other people are registering right now (patterns to grab next)
+// 2. BRAND DEFENSE — alert if someone registers a typo of one of Steve's brands
+// 3. TIMING — measure the latency between WHOIS-check and cert-issue to calibrate aggregator behavior
+//
+// Reminder: certs = ALREADY REGISTERED domains. This is not a "drops" feed.
+// For actual snipe targets (pending-delete), see watch-drops.js (Step 2).
+
+const WebSocket = require('ws');
+const fs = require('fs');
+const path = require('path');
+
+const CERTSTREAM_URL = process.env.CERTSTREAM_URL || 'wss://certstream.calidog.io/';
+const DATA_DIR = path.join(__dirname, 'data');
+const HITS_LOG = path.join(DATA_DIR, 'ct-hits.jsonl');
+const STATS_INTERVAL_MS = 30_000;
+
+// Brand-defense keywords. Any new cert whose domain CONTAINS one of these substrings is a hit.
+// Edit freely — keep them lowercase.
+const BRAND_KEYWORDS = (process.env.BRAND_KEYWORDS || [
+ 'designerwallcoverings',
+ 'philipperomano',
+ 'venturacorridor',
+ 'venturaclaw',
+ 'starsofdesign',
+ 'bubbesblock',
+ 'wholivedthere',
+ 'agentabrams',
+ 'nationalpaperhangers',
+ 'lawyer-directory',
+ 'novasuede',
+ 'callr',
+ 'butlr',
+ 'flockedwallpaper',
+ 'grasscloth',
+ 'glassbeaded',
+].join(',')).split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
+
+// "Hot pattern" filter — short, no digits, no hyphens, mainstream TLD.
+const HOT_TLDS = new Set(['com', 'app', 'io', 'co', 'ai', 'dev']);
+const MAX_STEM_LEN = 8;
+
+function looksHot(domain) {
+ if (!domain || domain.startsWith('*.')) return false;
+ const parts = domain.split('.');
+ if (parts.length < 2) return false;
+ const tld = parts[parts.length - 1].toLowerCase();
+ if (!HOT_TLDS.has(tld)) return false;
+ // For multi-label hosts (foo.bar.app), only consider the registrable label (= parts[-2]).
+ const stem = parts[parts.length - 2].toLowerCase();
+ if (stem.length > MAX_STEM_LEN || stem.length < 3) return false;
+ if (/[-0-9]/.test(stem)) return false;
+ // Pronounceability heuristic: must have both a vowel and a consonant.
+ if (!/[aeiou]/.test(stem)) return false;
+ if (!/[bcdfghjklmnpqrstvwxyz]/.test(stem)) return false;
+ return true;
+}
+
+function brandMatch(domain) {
+ const d = domain.toLowerCase().replace(/^\*\./, '');
+ return BRAND_KEYWORDS.find((kw) => d.includes(kw));
+}
+
+let count = { total: 0, hot: 0, brand: 0 };
+let buffer = [];
+
+function appendHit(rec) {
+ buffer.push(JSON.stringify(rec));
+ if (buffer.length >= 25) flushHits();
+}
+
+function flushHits() {
+ if (!buffer.length) return;
+ fs.mkdirSync(DATA_DIR, { recursive: true });
+ fs.appendFileSync(HITS_LOG, buffer.join('\n') + '\n');
+ buffer = [];
+}
+
+function ts() {
+ return new Date().toISOString();
+}
+
+let reconnectDelay = 1000;
+
+function connect() {
+ console.error(`[${ts()}] connecting to ${CERTSTREAM_URL}`);
+ const ws = new WebSocket(CERTSTREAM_URL, { handshakeTimeout: 15_000 });
+
+ ws.on('open', () => {
+ console.error(`[${ts()}] connected — watching for hot + brand hits`);
+ reconnectDelay = 1000;
+ });
+
+ ws.on('message', (raw) => {
+ let msg;
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
+ if (msg.message_type !== 'certificate_update') return;
+ const domains = (msg.data && msg.data.leaf_cert && msg.data.leaf_cert.all_domains) || [];
+ const seenAt = ts();
+ for (const d of domains) {
+ count.total++;
+ const brand = brandMatch(d);
+ const hot = looksHot(d);
+ if (brand) {
+ count.brand++;
+ const line = { ts: seenAt, type: 'BRAND', domain: d, keyword: brand, cert_index: msg.data.cert_index };
+ console.log(`[31m[BRAND][0m ${d} (matches: ${brand})`);
+ appendHit(line);
+ } else if (hot) {
+ count.hot++;
+ const line = { ts: seenAt, type: 'HOT', domain: d, cert_index: msg.data.cert_index };
+ console.log(`[33m[HOT][0m ${d}`);
+ appendHit(line);
+ }
+ }
+ });
+
+ ws.on('close', (code) => {
+ console.error(`[${ts()}] connection closed (code ${code}). reconnecting in ${reconnectDelay}ms`);
+ setTimeout(connect, reconnectDelay);
+ reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
+ });
+
+ ws.on('error', (err) => {
+ console.error(`[${ts()}] ws error: ${err.message}`);
+ });
+}
+
+setInterval(() => {
+ console.error(`[${ts()}] seen=${count.total} hot=${count.hot} brand=${count.brand}`);
+ flushHits();
+}, STATS_INTERVAL_MS);
+
+process.on('SIGINT', () => { flushHits(); process.exit(0); });
+process.on('SIGTERM', () => { flushHits(); process.exit(0); });
+
+connect();
(oldest)
·
back to Domain Sniper
·
step 2: watch-outbound.js — HTTPS forward proxy to identify f13a4f9 →