[object Object]

← back to Domain Sniper

step 5: register.js — atomic registrar API with dry-run safety

4aecb56c5d72f593ff011f39b792c4dd882f1f83 · 2026-05-12 14:03:42 -0700 · Steve Abrams

Backend auto-detect picks the cheapest available (NameSilo > Namecheap > GoDaddy).
DRY-RUN by default; --confirm required to spend money. YOLO autonomous mode never
passes --confirm — registrations are not reversible. Skip-check pattern: no
availability call before the register, the registrar tells us success-or-taken
in one round-trip (the check IS the leak).

Pivoted away from Porkbun (no public direct-register endpoint for non-resellers).
Roadmap updated; YOLO loop notes section added to README.

Files touched

Diff

commit 4aecb56c5d72f593ff011f39b792c4dd882f1f83
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 12 14:03:42 2026 -0700

    step 5: register.js — atomic registrar API with dry-run safety
    
    Backend auto-detect picks the cheapest available (NameSilo > Namecheap > GoDaddy).
    DRY-RUN by default; --confirm required to spend money. YOLO autonomous mode never
    passes --confirm — registrations are not reversible. Skip-check pattern: no
    availability call before the register, the registrar tells us success-or-taken
    in one round-trip (the check IS the leak).
    
    Pivoted away from Porkbun (no public direct-register endpoint for non-resellers).
    Roadmap updated; YOLO loop notes section added to README.
---
 README.md    |  26 +++++++---
 package.json |   5 +-
 register.js  | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 185 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index e0d1968..b6b616e 100644
--- a/README.md
+++ b/README.md
@@ -27,12 +27,26 @@ We can't get the WHOIS query stream (it's gated to aggregator partners), but we
 
 ## 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.
+- [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.
+- [ ] **Step 6** — `watch-drops.js` — daily pull of pending-delete CSVs from public auction houses. Surfaces names dropping in the next 5 days. Free sources: pool.com, dropcatch.com, snapnames.com, ICANN CZDS zone-diff.
+- [ ] **Step 7** — `brand-typo-watcher.js` — promote the brand-keyword logic out of `watch-ct.js` into its own job. Alerts immediately on hit.
+- [ ] **Step 8** — `registrar-fingerprints.json` — known aggregator signatures (OVH ns112.ovh.net, etc.) so the honeypot check phase can name-and-shame quickly.
+- [ ] **Step 9** — kill `mcp__domain-suite__check_availability` from `~/.claude.json` so it can never be invoked accidentally.
+- [ ] **Step 10** — extend dashboard with sort, click-to-register, sound alerts on brand hits.
+
+## 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")
 
diff --git a/package.json b/package.json
index 30a1712..bba0304 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,10 @@
     "watch:ct": "node watch-ct.js",
     "watch:outbound": "node watch-outbound.js",
     "dashboard": "node dashboard.js",
-    "watch:drops": "node watch-drops.js"
+    "watch:drops": "node watch-drops.js",
+    "honeypot:lure": "node honeypot.js lure",
+    "honeypot:check": "node honeypot.js check",
+    "register": "node register.js"
   },
   "dependencies": {
     "express": "^5.2.1",
diff --git a/register.js b/register.js
new file mode 100644
index 0000000..181542a
--- /dev/null
+++ b/register.js
@@ -0,0 +1,161 @@
+#!/usr/bin/env node
+// register.js — atomic domain registration via direct registrar API.
+//
+// SAFETY: defaults to DRY-RUN. Pass --confirm to actually spend money.
+// In YOLO autonomous mode, NEVER pass --confirm — registrations are not reversible.
+//
+// Backend auto-detection (cheapest/simplest first):
+//   - NAMESILO_API_KEY                          → NameSilo  (~$9 .com, no IP whitelist)
+//   - NAMECHEAP_API_USER + NAMECHEAP_API_KEY    → Namecheap (needs IP whitelist + $50 prefund)
+//   - GODADDY_KEY + GODADDY_SECRET              → GoDaddy   (needs payment on file)
+//
+// Skip-check pattern: this script DOES NOT call any availability API before
+// registering. The registrar tells us success-or-taken in one round-trip. The
+// "check" step IS the leak we're trying to avoid.
+//
+// Usage:
+//   node register.js <domain>              # dry-run
+//   node register.js <domain> --confirm    # actually buy
+
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const DATA_DIR = path.join(__dirname, 'data');
+const LOG_FILE = path.join(DATA_DIR, 'registrations.jsonl');
+
+function detectBackend() {
+  if (process.env.NAMESILO_API_KEY) return 'namesilo';
+  if (process.env.NAMECHEAP_API_USER && process.env.NAMECHEAP_API_KEY) return 'namecheap';
+  if (process.env.GODADDY_KEY && process.env.GODADDY_SECRET) return 'godaddy';
+  return null;
+}
+
+function httpsRequest(opts, body) {
+  return new Promise((resolve, reject) => {
+    const req = https.request(opts, (res) => {
+      let d = '';
+      res.on('data', (c) => (d += c));
+      res.on('end', () => resolve({ status: res.statusCode, body: d }));
+    });
+    req.on('error', reject);
+    if (body) req.write(body);
+    req.end();
+  });
+}
+
+async function registerNamesilo(domain) {
+  const key = process.env.NAMESILO_API_KEY;
+  const url = `/api/registerDomain?version=1&type=xml&key=${encodeURIComponent(key)}&domain=${encodeURIComponent(domain)}&years=1&private=1&auto_renew=0`;
+  const r = await httpsRequest({ hostname: 'www.namesilo.com', path: url, method: 'GET' });
+  const code = (r.body.match(/<code>(\d+)<\/code>/) || [])[1];
+  const detail = (r.body.match(/<detail>([^<]+)<\/detail>/) || [])[1];
+  // NameSilo 300 = success
+  return { success: code === '300', code, detail, http: r.status };
+}
+
+async function registerNamecheap(domain) {
+  const user = process.env.NAMECHEAP_API_USER;
+  const key = process.env.NAMECHEAP_API_KEY;
+  const clientIp = process.env.NAMECHEAP_CLIENT_IP || '127.0.0.1';
+  const username = process.env.NAMECHEAP_USERNAME || user;
+  const contact = process.env.NAMECHEAP_CONTACT_JSON
+    ? JSON.parse(process.env.NAMECHEAP_CONTACT_JSON)
+    : null;
+  if (!contact) {
+    return {
+      success: false,
+      code: 'NO_CONTACT',
+      detail: 'NAMECHEAP_CONTACT_JSON env required (firstName/lastName/address1/city/stateProvince/postalCode/country/phone/emailAddress). Namecheap registration needs full registrant contact.',
+    };
+  }
+  const params = new URLSearchParams({
+    ApiUser: user, ApiKey: key, UserName: username, ClientIp: clientIp,
+    Command: 'namecheap.domains.create', DomainName: domain, Years: '1',
+  });
+  for (const role of ['Registrant', 'Tech', 'Admin', 'AuxBilling']) {
+    for (const [k, v] of Object.entries(contact)) {
+      params.set(role + k.charAt(0).toUpperCase() + k.slice(1), v);
+    }
+  }
+  const r = await httpsRequest(
+    { hostname: 'api.namecheap.com', path: '/xml.response?' + params.toString(), method: 'GET' },
+    null,
+  );
+  const success = /Status="OK"/.test(r.body) && /<DomainCreateResult/.test(r.body);
+  const errMatch = r.body.match(/<Error[^>]*>([^<]+)<\/Error>/);
+  return { success, code: success ? 'OK' : 'ERROR', detail: errMatch ? errMatch[1] : (success ? 'registered' : 'unknown'), http: r.status };
+}
+
+async function registerGodaddy(domain) {
+  // GoDaddy /v1/domains/purchase needs full contact JSON + signed consent.
+  // Implementing this safely requires Steve's stored contact + payment method.
+  // For now, refuse to call so we don't silently fail-open and bill him.
+  return {
+    success: false,
+    code: 'NOT_WIRED',
+    detail: 'GoDaddy direct-purchase needs full contact + consent payload + saved payment method. Wire via secrets-manager + GODADDY_PURCHASE_PAYLOAD env before use.',
+  };
+}
+
+async function main() {
+  const args = process.argv.slice(2);
+  const domain = args.find((a) => !a.startsWith('--'));
+  const confirm = args.includes('--confirm');
+
+  if (!domain) {
+    console.error('usage: node register.js <domain> [--confirm]');
+    console.error('  default = DRY-RUN. Pass --confirm to actually buy.');
+    process.exit(2);
+  }
+  if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z]{2,})+$/i.test(domain)) {
+    console.error(`invalid domain syntax: ${domain}`);
+    process.exit(2);
+  }
+
+  const backend = detectBackend();
+
+  console.log(`\n  domain:   ${domain}`);
+  console.log(`  backend:  ${backend || '(none — credentials missing)'}`);
+  console.log(`  mode:     ${confirm ? '\x1b[31mREAL PURCHASE\x1b[0m' : 'dry-run'}`);
+
+  if (!backend) {
+    console.error(`\n  no registrar credentials found in env. add via the \`secrets\` skill:`);
+    console.error(`    NAMESILO_API_KEY                        # cheapest, simplest`);
+    console.error(`    NAMECHEAP_API_USER + NAMECHEAP_API_KEY  # needs IP whitelist + prefund`);
+    console.error(`    GODADDY_KEY + GODADDY_SECRET            # needs payment-on-file`);
+    process.exit(3);
+  }
+
+  if (!confirm) {
+    console.log(`\n  [DRY-RUN] would register ${domain} via ${backend}.`);
+    console.log(`  pass --confirm to spend money. registration is NOT reversible.\n`);
+    return;
+  }
+
+  // Real call
+  let result;
+  const start = Date.now();
+  try {
+    if (backend === 'namesilo') result = await registerNamesilo(domain);
+    else if (backend === 'namecheap') result = await registerNamecheap(domain);
+    else if (backend === 'godaddy') result = await registerGodaddy(domain);
+  } catch (e) {
+    result = { success: false, code: 'EXCEPTION', detail: e.message };
+  }
+  const elapsed = Date.now() - start;
+
+  const rec = { ts: new Date().toISOString(), domain, backend, result, elapsedMs: elapsed };
+  fs.mkdirSync(DATA_DIR, { recursive: true });
+  fs.appendFileSync(LOG_FILE, JSON.stringify(rec) + '\n');
+
+  if (result.success) {
+    console.log(`\n  \x1b[32m✓ REGISTERED ${domain}\x1b[0m  (${elapsed}ms via ${backend})`);
+    console.log(`    log: ${LOG_FILE}\n`);
+  } else {
+    console.log(`\n  \x1b[31m✗ FAILED:\x1b[0m ${result.code || ''} — ${result.detail || ''}\n`);
+    process.exit(4);
+  }
+}
+
+main().catch((e) => { console.error('error:', e.message); process.exit(1); });

← c8f80e8 honeypot v2: DoH pre-check is ground-truth; 6-letter .com to  ·  back to Domain Sniper  ·  step 8: registrar-fingerprints.json + cross-ref in honeypot 7b38b4a →