Domain Scout
- Agents and operations
- Agent Tooling
- 2026
- Private
Go, Cobra, NestJS, Next.js and 4 more
Domain Scout checks domain names against their registries and compares what registrars charge for them. Everything runs on my own machine. A NestJS backend owns the jobs, the SQLite store and every outbound call. A Go CLI and an MCP server give coding agents structured, non-interactive access, and a Next.js UI gives me the same behavior on a screen.
The rule underneath is that a registry answer is evidence, not a promise. A missing record means the registry had none when asked. It never means a name can be bought, or at what price.
# discovery and quotes are separate commandsdomain-scout scan --help | head -1Submit a registry-only scan (never quotes or provider checks)domain-scout quote --help | grep -o -- '--providers.*'--providers strings provider ids to query (required)domain-scout mcp --help | head -1Serve Model Context Protocol tools over stdio for coding agents.domain-scout version | jq -r .data.contractVersion1.0.0
4 commands4 lines of output
- Chapter
- Agents and operations
- Category
- Agent Tooling
- Year
- Availability
- Private
- Go
- Cobra
- NestJS
- Next.js
- node:sqlite
- Zod
- OpenAPI
- Turborepo
Trying an existing tool
Before building, I tried Domain Hunter, an MIT-licensed checker with a UI, a JSON CLI and an MCP server. It worked, and it covered much of the workflow. It missed the goal I cared about most. After an authoritative registry 404, it still queried Cloudflare RDAP and DNS with the name, then labeled the result probably available. An optional premium check sent the name to another third-party API. Its prices were extension-level catalog values, not quotes for a name. I wanted discovery to send names to registries only, every other destination to be opt-in and recorded, and uncertainty to stay its own outcome. So I built my own, with broader registrar coverage.
One local API
Every route is declared once in packages/contracts/src/routes.ts, beside Zod schemas for each public object. That table binds the Nest handlers, validates requests, drives the TypeScript client and emits openapi.json. oapi-codegen turns that document into the Go client. Next.js owns no state and makes no external calls. It forwards /api/v1/* to the backend on 127.0.0.1:4317, streaming server-sent events without buffering. The CLI reads <dataDir>/run/backend.json and checks instanceId and protocolVersion on GET /api/v1/health before reusing a backend, or starts one. The browser trades a single-use bootstrap code for an HttpOnly session cookie and a CSRF token. The API token never reaches browser JavaScript, and requests carrying any other Host are rejected to block DNS rebinding.
Pacing the registries
Routing starts from the IANA RDAP bootstrap, adds curated RDAP routes, and falls back to WHOIS over TCP port 43. Registry-specific WHOIS classifiers cover 45 suffixes. An extension without a verified route is unsupported, because DNS is never a substitute. Of the seven outcomes, only registered and no_record count as answers. The scheduler budgets by registry:<registryId>, so RDAP and WHOIS for one registry share it. Defaults allow 16 requests in flight, one per registry, and 1,000 ms between starts: one request per second per registry. Registries that publish stricter per-IP limits are held to those. A rate-limited name is retried automatically, up to three attempts with exponential backoff, after any Retry-After. Cooldowns persist in a source_cooldowns table, so a restarted backend still honors them.
Running a scan
Commands print the JSON envelope with its contractVersion on stdout, or one row per line with --output ndjson. Progress and errors go to stderr, and nothing prompts.
domain-scout input preview --file names.txt --tlds com,io # no network
domain-scout scan --file names.txt --tlds com,io --wait
domain-scout jobs results <job-id> --outcomes no_record --all
domain-scout prices published com io --years 3 # no names sent
domain-scout quote <domain> --providers porkbun --wait
Exit code 3 means the job finished with unresolved items, and 4 means provider access is missing, so a script can branch without parsing text. domain-scout ui --job <job-id> opens a run in the web UI, where History labels each run by origin. domain-scout mcp serves eleven tools over stdio on the same API. docker compose up --build publishes the UI on host loopback only.
Decisions and costs
- Built-in
node:sqliteover better-sqlite3. With no native module, one backend payload serves every OS. Node.js 24.11 or later must already be installed, since the Go binary cannot carry the backend. - Money as decimal strings through decimal.js, with unknown as
null, never zero. An unknown part makes a sum unknown, so some offers cannot be ranked at all. - Discovery and quotes as separate jobs. A scan never reaches a registrar, and a quote sends exact names only to the providers in
--providers. Quotes then need keys and a second step. - Published limits counted in memory, per registry and per backend process. A restart begins a fresh window, so the limits hold within one run but not across restarts. This is the one I would revisit.
- A task-based UI. I rejected a first build whose nine pages mirrored backend modules, and paid for a second pass: Search, Saved, Watching, History and Settings.
Limits and status
Nine registrar adapters exist: Hostinger, Dynadot, Porkbun, Namecheap, GoDaddy, Cloudflare Registrar, Spaceship, NameSilo and Name.com. Only Porkbun’s price catalog works without API keys. Every authenticated adapter is tested against documentation-derived fixtures and has never run against a real account. Renewal and transfer purchase links do not exist yet. Two WHOIS registries have no passing live check, and internationalized names are only unit-tested. The Windows archive builds but has never run on Windows. A 1,200-name acceptance run exercises the engine against a local stub registry, not a public one. Saved keys live in an owner-only credentials.json outside SQLite and never come back through the API. The repository is private with no licence file.