Appended to the shared prompt (lib/ai-prompt.ts) on both AI paths; capped at 300 chars client- and server-side; shown only when AI is on; kept in sessionStorage so it survives 'Show me more'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
130 lines
6.6 KiB
Markdown
130 lines
6.6 KiB
Markdown
# 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.
|
|
|
|
There's no chat: the AI is briefed from the same keywords box and Style chip
|
|
as the built-in generator (`src/lib/ai-prompt.ts` is the rulebook), and its
|
|
names are merged in front of the built-in list, tagged **AI**, and checked like
|
|
everything else. When AI is on, a **Tell the AI more** field appears — tone,
|
|
words to include or avoid, audience — which is appended to that brief (capped
|
|
at 300 chars, enforced client- and server-side). The built-in generator ignores it.
|
|
|
|
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. It overrides the
|
|
platform path above and works without signing in.
|
|
|
|
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 `<html
|
|
class="dark">` 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).
|