# names.poweredbyotm.com Free business-name finder: type a few keywords, get brandable name ideas, and see which domains are actually free — checked against the registries themselves. - **Stack:** Next.js 15 (App Router, `output: "standalone"`), React 18, no DB, no auth, public. Name generation runs **client-side**; availability is a server route because browsers can't do DNS and RDAP servers send no CORS headers. - **Repo:** `tonym/names` on `git.poweredbyotm.com`. - **Deploy:** OTM platform — first-party app (`/apps`), built + deployed via the platform MCP, not docker-compose-from-this-repo. ## How availability works — and what it costs (nothing) There is no paid domain API here and no key to manage. 1. **RDAP** (RFC 7482), the registries' own replacement for WHOIS. IANA publishes a bootstrap file at `data.iana.org/rdap/dns.json` mapping every TLD to its authoritative RDAP server. We cache it for 24h, resolve the TLD ourselves, and query that server directly — `404` means unregistered, `200` means registered. Covers ~1,200 TLDs including `.com/.net/.org/.ai/.dev/.app`. 2. **DNS `NS` lookup**, for the TLDs that publish no RDAP server at all — `.io`, `.co`, `.me`, `.sh`, `.gg`, `.us`, `.de`. NXDOMAIN strongly suggests unregistered, but a registered domain with no delegated nameservers looks identical. These report `unverified-available` and the UI shows them in amber as *probably free*, never as a confirmed green. **Do not "simplify" this by pointing everything at `rdap.org`.** Its redirector returns a bare `404` both for "this domain is free" and for "I have no RDAP server for this TLD" — the two cases are indistinguishable from the status code, which is exactly the bug that would report every `.io` as available. Manners, since we're an anonymous client against other people's public endpoints: 60 domains max per request, 8-wide concurrency, a per-IP token bucket, and a 10-minute result cache. ## Name generation `src/lib/generate.ts` is pure and deterministic — the same `(keywords, style, seed)` always yields the same list, so re-renders don't reshuffle results and a shared URL reproduces what the sender saw. Eight strategies (compound, suffix-word, action, blend, coined, clipped, root, domain hack) over the vocabulary in `src/lib/wordbank.ts`. The strategies are cheap and dumb on purpose — **quality comes from `scoreName`**, so tune the scorer before adding more words. Three guards exist because of specific bad output, and removing them brings it straight back: - `expandKeywords` matches category hints by **prefix, never substring** — plain containment let `repair` match the `ai` category (rep-**ai**-r) and drag neural-network vocabulary into an auto-shop brief. - `syncopate` only drops the schwa from a final `-er` (`flicker` → `flickr`). The original "drop the last vowel" produced `crema` → `crem`, `kettle` → `kettl`, `security` → `securit`. - `isTruncation` rejects any candidate that is a strict prefix of a word the generator knows. This is what keeps stumps out of the results. `diversify()` caps each strategy at ~28% of the returned list. Without it the top 20 came back as eighteen coinages — scoring alone clumps badly. ## AI names Sign in with a free OTM account (the link sits next to the generate button) and an **AI names** toggle appears: Claude-generated ideas are merged in ahead of the built-in list, checked for availability like everything else. The call goes `browser → /api/ai-names → OTM's metered Anthropic gateway → Anthropic`; the app holds a per-app gateway token, never a real Anthropic key, and the route refuses anonymous requests and caps each account at roughly 20 requests an hour. The platform's monthly AI cap is the backstop. Sign-in is OTM SSO: `/sso/authorize?aud=app:names` on the control plane hands back a 60-second single-use ticket to `/api/auth/otm-sso`, which mints a small HMAC-signed cookie (`names.session`). There are no user accounts in this app. ## Bring-your-own AI key Optional. `/settings` stores an Anthropic or Gemini key in `localStorage` (`names.aiKey`) and the browser calls that provider **directly** — the key never touches our server. Same pattern as `rpo`'s Google Vision key, for the same reason: the platform has no per-app env tool, and this is a free public tool that shouldn't hold a metered credential. The `anthropic-dangerous-direct-browser-access` header is correct here and would be wrong in a product that owns the key. The distinction is whose key it is. ## Saved names Every card has a ☆. Pressing it keeps the name in a **Saved** panel above the results, so you can shortlist while you keep generating. The list is `localStorage` only (`names.wishlist`, capped at 60) — there are no accounts and this doesn't need one. Each entry snapshots the availability it last saw, and the panel's **Re-check** button re-queries every saved domain (batched through the same `/api/availability` route, so it obeys the same manners) and **Copy list** puts a plain-text `domain — status` list on the clipboard. ## Registrar links `src/lib/registrars.ts`. Links are plain today — every `affiliateCode` is `""`. Fill one in and `HAS_AFFILIATE_LINKS` flips, which turns on the footer disclosure automatically. Keeping both in one file is deliberate: a disclosure that can drift out of sync with whether links actually pay is worse than none. ## Local development ```bash npm install npm run dev # http://localhost:3000 npm run typecheck npm run build ``` No env vars are needed to run it. Sign-in and platform AI light up only when the platform injects `AUTH_SECRET`, `OTM_SSO_SECRET`, `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY`; without them `/api/auth/session` reports `platformAi:false` and the UI hides the feature. Tailwind is present **only** to style the shared `@otm/account-panel` widgets (the bottom-right feedback chat-pop) — the app's own UI is handwritten CSS, so `preflight` is off and the config scans `node_modules/@otm/account-panel/src`. The app is always dark, so `` pins the widget's `dark:` variants on. The suggestions lightbulb degrades to a quiet "not configured" state unless `GITEA_URL` / `GITEA_TOKEN` / `GITEA_REPO` are set (the platform injects those at deploy time).