{
  "markdown": "# Domain Finder\n\nSearch and recommend good domain names to buy. Describe a product or idea; the\napp brainstorms brandable names (AI + rule-based combos), checks real\navailability via RDAP, and returns a **ranked** list of buy candidates.\n\nSource-available under the **Elastic License 2.0** (`SPDX-License-Identifier: Elastic-2.0`) — see [License](#license).\n\n## Connect to the MCP server\n\nDomain Finder is also a **remote MCP server** — check a brand name across domains\nand the GitHub / npm / PyPI namespaces from any MCP-capable agent.\n\n- **Endpoint:** `https://domain-finder-theta.vercel.app/api/mcp` (Streamable HTTP)\n- **Tools:**\n  - `check_name` — check whether one or more candidate names are free across\n    domains and the GitHub / npm / PyPI namespaces, in a single call.\n  - `generate_names` — generate candidate names from a description, each\n    pre-checked for domain availability; feed the favorites into `check_name`.\n- **Read-only, and no credentials required** — nothing to sign up for, no keys\n  to paste. Safe to connect.\n- **Requests are logged.** Inputs and outputs are stored server-side (with a\n  salted hash of the caller IP, never the raw address) for analytics and abuse\n  review — see [SECURITY.md](SECURITY.md).\n\n**Cursor** (`~/.cursor/mcp.json`) and any client with native Streamable-HTTP\nsupport:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"domain-finder\": {\n      \"type\": \"streamable-http\",\n      \"url\": \"https://domain-finder-theta.vercel.app/api/mcp\"\n    }\n  }\n}\n```\n\n**Claude Desktop** (`claude_desktop_config.json`) — bridge a remote server with\n`mcp-remote`:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"domain-finder\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"mcp-remote\", \"https://domain-finder-theta.vercel.app/api/mcp\"]\n    }\n  }\n}\n```\n\n## How it works\n\n```\nquery ─▶ generate ─▶ check availability ─▶ rank ─▶ results\n         (AI + rules)   (RDAP)              (0–100 score)\n```\n\n- **Generation** (`lib/generate.ts`) — merges three sources of candidates:\n  - _Rule-based_ combos: keyword extraction (stopword-filtered) × prefixes/\n    suffixes (`get`, `try`, `-ly`, `-hq`, `-app`, …), shorter-first.\n  - _AI brainstorm_ (optional): Vercel AI SDK via AI Gateway (`generateObject`\n    + zod). Degrades gracefully to rule-based when no API key is present.\n  - _Domain hacks_ (`lib/hacks.ts`): read the word across the dot —\n    `delicio.us`, `bit.ly`, `recip.es`. Built from IANA's full TLD list; needs\n    no API. These ignore the TLD filter by design (the word picks the zone), so\n    they have their own toggle.\n  - Labels are deduped, capped, and expanded across the selected TLDs\n    (round-robin so every label gets its `.com` before any gets a second TLD).\n- **Availability** (`lib/availability.ts`) — RDAP behind a swappable\n  `AvailabilityProvider` interface. Resolves each TLD to its authoritative\n  registry RDAP server via the IANA bootstrap file (+ curated overrides), then\n  queries the registry directly. Retries on `429`.\n- **Status taxonomy** (`lib/rdap-status.ts`) — \"taken\" isn't one thing. Modelled\n  on [Domainr's taxonomy](https://domainr.com/docs/api/v2/status), each domain\n  gets a `status` and a coarse `bucket`:\n\n  | bucket | statuses | meaning |\n  | --- | --- | --- |\n  | `registrable` | `available` | buy it now, at retail |\n  | `dropping` | `deleting`, `expiring` | in pendingDelete/redemption — backorder territory |\n  | `aftermarket` | `parked` | owned, but nameservers say \"for sale\" |\n  | `unavailable` | `active`, `reserved` | registered and in use |\n  | `unknown` | `unknown` | no RDAP server for the TLD, or an error |\n\n  All derived free from RDAP's RFC 8056 status codes, `expiration` event, and\n  nameservers. `deleting` domains also get an `estimatedDropAt` (pendingDelete\n  is a fixed ~5-day window).\n- **Ranking** (`lib/rank.ts`) — a 0–100 score from availability (45), TLD\n  desirability (20), length (20), cleanliness/no-hyphens-or-digits (10), and a\n  brandability nudge (5, hack > AI > combo). Sorted by **bucket first**, then\n  score — so \"buy it now\" always outranks \"might be gettable\".\n\nSwapping RDAP for a paid registrar API later (pricing + real buy flow) only\ntouches `lib/availability.ts` — nothing else in the pipeline knows the source.\n\n## Watchlist\n\nTrack a domain, get emailed when it enters the drop path, hand off to a\nbackorder service. **This is a scheduler around `availabilityProvider.check()`,\nnot new availability logic.**\n\n```\ncron ─▶ due-queue ─▶ check ─▶ transition? ─▶ alert (deleting | available)\n        (next_check_at)       (watch_events)   deduped via `alerts`\n```\n\n- **Adaptive due-queue** (`lib/cadence.ts`) — every domain carries a\n  `next_check_at`; the poller only touches what's due. Cadence follows status:\n  far-off `active` weekly → expiry <30d or redemption daily → `pendingDelete`\n  every 6h → hourly inside 24h of the estimated drop. RDAP load stays\n  proportional to how *interesting* a domain is, not how many watches exist.\n- **State is keyed by domain, not by watch** (`lib/db/schema.ts`). A hundred\n  people watching the same short `.com` is one row, one RDAP call, one\n  transition, fanned out to a hundred inboxes. Load scales with *unique domains\n  observed*, not users × domains — which matters because registries don't sell\n  capacity.\n- **Transition log** (`watch_events`) is the source of truth. Alerts fire only\n  on a change, and `alerts` has `unique(watch_id, event_id)` so a cron retry\n  can't double-send.\n- **Only two statuses alert**: `deleting` (pendingDelete — the fixed ~5-day\n  window, i.e. the last moment a backorder can be placed) and `available`.\n  Everything else is logged and shown in the UI but stays silent.\n- **An `unknown` is not a transition.** A 429 or timeout means \"we couldn't\n  tell\", not \"the domain changed\" — the last known status stands, and the check\n  backs off exponentially (1h, 2h, 4h … capped at a day).\n- **TLDs we can't observe are refused at watch time.** `.co`/`.es`/`.at`/`.gg`\n  have no public RDAP server, so a watch on them could never fire. Better to say\n  so than to accept it and silently never alert.\n- Free watches are capped at 3 per email (`FREE_WATCH_LIMIT` in `lib/watch.ts`).\n\nIdentity is an email plus an unguessable manage token — no accounts, no\nsessions, no users table. The token rides in alert links and authenticates\n`/watch/<token>`.\n\n### Running the watchlist locally\n\n```bash\n# any Postgres will do\ndocker run -d --name df-pg -e POSTGRES_PASSWORD=domainfinder \\\n  -e POSTGRES_USER=domainfinder -e POSTGRES_DB=domainfinder \\\n  -p 5433:5432 postgres:16-alpine\n\ncp .env.example .env.local   # set DATABASE_URL + CRON_SECRET\nnpm run db:push              # create the tables\nnpm run dev\n```\n\nWithout `RESEND_API_KEY` alerts are **printed to the console** rather than sent,\nso the whole flow is drivable with no third-party signup. Trigger a poll by hand:\n\n```bash\ncurl -H \"authorization: Bearer $CRON_SECRET\" localhost:3000/api/cron/poll\n```\n\n### Scheduling in production\n\n`.github/workflows/poll.yml` hits `/api/cron/poll` hourly. Vercel's Hobby cron\nonly fires **once per day**, which can burn most of a ~5-day pendingDelete\nwarning, so the schedule lives in GitHub Actions instead. Set the `POLL_URL` and\n`CRON_SECRET` repository secrets. GHA cron is best-effort and can lag a few\nminutes — fine for a multi-day warning, and nothing here depends on being\npunctual.\n\n## Tests\n\n```bash\nnpm test\n```\n\n`lib/rdap-status.test.ts` and `lib/cadence.test.ts` are pure. `lib/poll.test.ts`\ndrives the real database with a **fake availability provider** — `pendingDelete`\nand `redemptionPeriod` are too rare in the wild to find on demand, and they're\nexactly what this feature exists to catch. It skips when `DATABASE_URL` is unset.\n\n## Getting started\n\n```bash\nnpm install\nnpm run dev      # http://localhost:3000\n```\n\n**Search** works with **no configuration** — availability (RDAP) and rule-based\ngeneration need no keys. To enable AI brainstorming, copy `.env.example` to\n`.env.local` and set an AI Gateway key:\n\n```bash\ncp .env.example .env.local\n# then set AI_GATEWAY_API_KEY=...\n```\n\nThe **watchlist** is the only part that needs a database; search keeps working\nwithout one (`getDb()` is lazy and only throws when a watch route calls it).\nSee [Watchlist](#watchlist) above.\n\n## API\n\n`POST /api/search`\n\n```jsonc\n// request\n{ \"query\": \"vegan meal delivery\", \"tlds\": [\".com\", \".io\"], \"useAi\": true }\n\n// response\n{ \"query\": \"...\", \"results\": [ { \"domain\": \"...\", \"score\": 92, \"availability\": {...}, ... } ],\n  \"meta\": { \"generated\": 45, \"checked\": 45, \"aiUsed\": false, \"availabilityProvider\": \"rdap\", \"tookMs\": 1200 } }\n```\n\n## Notes / limitations\n\n- **TLD coverage.** IANA's RDAP bootstrap covers ~1199 of 1438 zones and skews\n  gTLD; the gap is ccTLDs, which is exactly what domain hacks use. Verified\n  overrides are in `OVERRIDES` (`lib/availability.ts`): `.io`/`.me`/`.sh`/`.ac`,\n  `.de`, `.us`. No reachable endpoint was found for `.co`/`.es`/`.at`/`.gg`, so\n  those report `unknown` rather than guess. Add more if you find them.\n- **Parking detection is high-precision, low-recall.** It reliably flags\n  ordinary domains parked on Sedo/Bodis-style nameservers, but misses premium\n  ones — most sit behind Cloudflare/AWS where the signal is invisible. Absence\n  of `parked` proves nothing. Real aftermarket data needs a paid source.\n- **RDAP tells you _registered vs not_ — not price.** Pricing/buy needs a\n  registrar API (the interface is ready for it). \"Buy\" links currently point to\n  a Namecheap search; `backorderUrl()` points at DropCatch for dropping domains.\n  Swapping in affiliate links is a one-line change in `app/page.tsx`.\n- **You can't win a drop by polling this app.** Professional drop-catchers hold\n  hundreds of registrar connections. The useful build is a watchlist that alerts\n  and hands off to a backorder service — see the wiki for the design.\n\n## License\n\nSource-available under the **Elastic License 2.0** (`SPDX-License-Identifier:\nElastic-2.0`). You may inspect, self-host, and modify the code freely. What\nyou may **not** do is provide it to third parties as a hosted or managed\nservice that exposes a substantial set of its features — i.e. running it as a\ncompeting hosted service is not permitted. Full terms in [LICENSE](LICENSE).\n",
  "bytes": 10254,
  "sha": "6be6a15466a06f014b78fd7386c28084bc6728029356d02edd6904469cf1e281",
  "repo_slug": "sra1phani/domain-finder",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_sra1phani_domain_finder_b1ea64a1/readme"
}