← back to Domain Sniper
README.md
186 lines
# 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** — `check.js` — leak-minimal availability checker via Cloudflare DoH. No WHOIS/RDAP/registrar APIs.
- [x] **Step 1** — `watch-ct.js` — live CertStream WebSocket listener with hot-pattern + brand-typo filters. JSONL logs.
- [x] **Step 2** — `watch-outbound.js` — local HTTPS forward proxy that surfaces every domain-intel request leaving the machine. Used to ID which tool is leaking.
- [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.
- [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:
- MCP tools: `mcp__domain-suite__check_availability`, `mcp__domain-suite__get_whois_contact`, `mcp__godaddy__check_availability`, `mcp__namecheap__check_availability`
- Raw `whois -h <registry>` against verisign-grs.com / nic.google / nic.co / iana.org
- Direct `curl` against rdap.verisign.com / rdap.nic.google / api.godaddy.com `/v1/domains/available` / Namecheap `domains.check`
- Backup: `~/.claude/settings.json.bak-2026-05-12-yolo-tick2`
- Does NOT affect domain-sniper scripts (honeypot.js, check.js) — those use Node's child_process / https directly and aren't routed through Claude's Bash permission layer.
- [x] **Step 10** — dashboard UX extension: sort tabs (time / brand-first / hot-first), click-any-row to copy `node register.js <domain>` to clipboard, audible BRAND ping via WebAudio, toast confirmation. All settings persist in localStorage (`sniper.sort`, `sniper.audio`, etc.).
## Snipe-timing curve (honeypot 2026-05-12T20:36 UTC)
Spot-checks via DoH at increasing intervals to find where the snipe window closes. Datapoints logged to `data/honeypot-spotcheck.jsonl`.
| Elapsed | Buckets checked | Snipes |
| --- | --- | --- |
| ~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** |
| ~3.49h | A/B/C/D/E (2 ea, slice 0-1 re-check) | 0 / 10 — no late snipes between 3.08h and 3.49h |
| ~3.87h | full 50-bait sweep | **0 / 50** (A 0/10, B 0/10, C 0/10, D 0/10, E 0/10) |
| ~4.20h | A/B/C/D/E (2 ea, slice 0-1 re-check #2) | 0 / 10 — **120 cumulative DoH checks, 0 snipes** |
| ~5.46h | full 50-bait sweep (second) | **0 / 50** — flat result reinforced 1.5h after 4h checkpoint |
| ~5.82h | A/B/C/D/E (2 ea, slice 6-7 re-check #3) | 0 / 10 — **330 cumulative DoH checks, 0 snipes** across 29 datapoints |
| ~6.27h | full 50-bait sweep (third) | **0 / 50** — hypothesis triple-confirmed; **380 cumulative DoH checks across 30 datapoints, all 0** |
| ~11.4h | slice 4-5 (6h gap since 5.37h) | 0 / 10 — **400 cumulative DoH checks across 32 datapoints, still 0** |
| ~24h | full batch | — |
| ~48h | full batch (final) | — |
**Interpretation of the 3.87h flat result** — pure-random CVCVC bait names (`kuwavi`, `tahoz`, `fobahu`) across Verisign/Google-.app/nic.co/GoDaddy/control channels produced ZERO snipes despite deliberate leaks. The original callr.app / callr.co / butlr.app snipes hit pronounceable English-flavored names. Hypothesis: aggregators filter leak streams for desirability (English-likeness, brand-adjacency, prior-search signal) before paying registry fees. Next experimental iteration should use English-pronounceable / brand-adjacent baits to confirm.
### Honeypot v1 vs v2 — head-to-head
| Dimension | v1 (`honeypot.js`) | v2 (`honeypot-v2.js`) |
| --- | --- | --- |
| Bait shape | random CVCVC (`kuwavi`, `tahoz`) | `pronounceable` or `brand-adjacent` cohorts |
| Generates + lures | one shot (`lure` does both) | two-step (`generate` → `lure`, batch persisted between) |
| Pre-check availability | at generation time only | at lure time (so freshly-taken names get auto-skipped) |
| Lure safety gate | command-line only | **`LURE_CONFIRM_LEAK_2026=yes` env required** (YOLO cannot trigger) |
| Resumable lure | partial — writes after each phase | yes — writes after every item |
| Result (so far) | **0/50 at 4.2h; 0/120 DoH cumulative** | not yet lured |
The v1 0-result over 120 DoH checks across 13 datapoints is the headline finding driving v2: pure-gibberish baits don't bite, so v2 tests whether English-pronounceable or brand-adjacent shapes do.
### Honeypot v2 — desirability-shaped baits (scaffold; lure NOT auto-fired)
`honeypot-v2.js` ships two new bait cohorts to test the desirability hypothesis:
| Cohort | Example baits | Baseline availability (DoH) | Notes |
| --- | --- | --- | --- |
| `pronounceable` | `noyl.com`, `swid.com`, `cayz.app`, `twelt.co`, `wheim.com` | 29/50 (58%) | .app 100% / .co 90% / .com saturated 30-40% |
| `brand-adjacent` | `verturr.app`, `lawrr.app`, `qbutlr.co`, `florkr.co`, `flockro.com` | 38/50 (76%) | .co 100% / .app 90% / .com 50-70% |
**Safety gates** — `generate` and `check` always allowed (no leaks). `lure` is **refused** unless `LURE_CONFIRM_LEAK_2026=yes` env is set as a one-shot Steve gesture; YOLO autonomous loops cannot trigger it. The lure body itself is currently a stub — wire-up before re-firing.
**Ambient-drift baseline (2026-05-13)** — both v2 cohorts re-checked ~1.4h after generation, *no lure ever fired*: pronounceable 29/50 → 29/50 (zero change per-bucket too), brand-adjacent 38/50 → 38/50 (zero change). So **the natural registration rate on these specific candidate names is zero over a ~1h window**. Any future v2 lure result with snipes > 0 is directly attributable to the lure rather than ambient demand.
## Known issues — public CT-log infrastructure is fragile
State of the world for **free** real-time CT-log access, as of 2026-05-12 ~23:05 UTC:
| Source | Status | Why it doesn't (currently) work |
| --- | --- | --- |
| `wss://certstream.calidog.io/` | Connects, closes after ~60s with code 1000, **0 messages**. Known flaky for 18+ months. | Public Calidog server depopulated; the official replacement is "host your own". |
| Cert Spotter free `/v1/issuances` (anonymous firehose) | 400 — requires `domain=` per request | No anonymous firehose tier. |
| Cert Spotter free `/v1/issuances?domain=<keyword>` (per-brand) | 403 `not_allowed_by_plan` — domain must be FQDN | Bare keywords (`callr`) rejected; FQDNs (`callr.com`) only match issuances *for* that exact domain, useless for typo-squat defense. |
| crt.sh `?q=<kw>&output=json` | 502 nginx (transient) | crt.sh frequently overloaded; not reliable as a primary source. |
| Cert Spotter paid (`Authorization: Bearer`) | Untested — would need API key via secrets-manager | Cheapest plan ~$/mo. |
| Run own CertStream server | Heavy (Go + indexed Postgres of CT logs) | Last resort. |
**Code shipped covering this landscape:**
- `ct-source-certspotter.js` — anonymous-firehose attempt (blocked by free-tier policy; ready for API key)
- `ct-source-certspotter-brands.js` — per-brand polling attempt (blocked by FQDN-only restriction)
- Both modules emit the CertStream payload shape, so once a working source comes online they drop in to `watch-ct.js` / `brand-typo-watcher.js` / `dashboard.js` via a `CT_SOURCE` env switch.
**RESOLVED 2026-05-12 23:13 UTC** — bypassed all middlemen by talking directly to CT logs per RFC 6962. `ct-source-ctlog.js` polls `get-sth` against currently-active 2026 logs (Argon2026h1 / Xenon2026h1 / Wyvern2026h1 — Nimbus2026 seems CDN-cached, skip it), fetches `get-entries`, parses MerkleTreeLeaf, extracts SAN DNS names from each X.509 cert via Node 22 `crypto.X509Certificate`. Smoke run: **8 emitted cert events in 25s** from Argon (14 X509 parsed, 43 precerts skipped — precert ASN.1 parse is TODO).
**EXPANDED 2026-05-13 00:00 UTC** — fan-out grew from 3 → 6 logs (added Sphinx/DigiCert + Elephant/Tiger/Sectigo). Sustained throughput on the brand-typo-watcher: **~25 cert events/sec** (was ~14/sec on 3 logs). Per-log breakdown in minute summary confirms all 6 logs producing. Throughput skew is real: Google logs (Argon, Xenon) and DigiCert logs (Wyvern, Sphinx) deliver ~100 emit/min steady; Sectigo logs (Elephant, Tiger) burst-batch with quiet windows between (~5-50/min, irregular).
Meanwhile the **honeypot + DoH spot-check** path (the actual aggregator-attribution experiment) is unaffected — that runs on Cloudflare 1.1.1.1 DoH and Node `child_process whois`, neither of which depends on CT logs.
## `data/owned.json` — single source of truth
Used by `register.js` (refuse re-registration, exit 5) and
`honeypot-v2.js generate` (refuse to write a batch that baits an owned
domain, exit 6). File is gitignored along with the rest of `data/`. Both
shapes accepted:
```json
["foo.com", "bar.app", "baz.io"]
```
or
```json
{ "domains": ["foo.com", "bar.app", "baz.io"], "_updated": "2026-05-13" }
```
When the file doesn't exist, both checks no-op cleanly — there's nothing
to compare against, so neither tool refuses. Add domains as you register
them (or import from `~/cncp-starter/cncp-config.json domains[]`).
## Liveness watchdog (opt-in)
`launchd/sniper-watchdog.sh` polls `/healthz` once a minute, restarts the
dashboard on HTTP 503 (or unreachable). Defaults: present in repo, not loaded.
PM2-first (looks for `sniper-dashboard`), falls back to pgrep + nohup respawn.
To enable on Mac2:
```
cp launchd/com.steve.sniper-watchdog.plist ~/Library/LaunchAgents/
launchctl load -w ~/Library/LaunchAgents/com.steve.sniper-watchdog.plist
```
Logs at `logs/watchdog.log` (silent on healthy; one line per UNHEALTHY+ACTION).
## YOLO loop notes
When running in autonomous /loop mode, each tick must:
- Be reversible (no `register.js --confirm` ever; no DNS changes; no email sends)
- Produce one commit max
- Use only local resources (no paid external LLM/API calls)
- Check `data/honeypot-2026-05-12T20-36-16-324Z.json` status if 4h+ has elapsed since lure (early-tick snipes get logged but full attribution waits for 48h mark)
- Pick the next unchecked Step from the list above
## 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.