Files
names/CLAUDE.md
tonym c7d6812466 feat: saved-names wishlist (#1); style the shared feedback chat-pop with Tailwind (#2)
- ☆ on every card saves it to a browser-only shortlist (localStorage
  names.wishlist, cap 60) shown as a Saved panel with snapshotted
  statuses, Re-check and Copy list.
- SuggestionLightbulb from @otm/account-panel is Tailwind-styled and
  names had no Tailwind, so it rendered as a bare button. Added Tailwind
  (preflight off, dark class pinned), scoped element CSS to .wrap and
  mounted the widget outside it.
- v0.2.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
2026-08-16 18:14:40 -05:00

5.3 KiB

names — Business Name Finder

Free business-name generator with live domain availability, at names.poweredbyotm.com. A first-party Powered by OTM app.

  • Stack: Next.js 15 (App Router, output: "standalone"), React 18, no DB, no auth, public. Generation runs client-side; availability is a server route (browsers can't do DNS, and RDAP servers send no CORS headers).
  • Repo: tonym/names on git.poweredbyotm.com.
  • Deploy: platform MCP — build_app("names") then deploy_app("names"). NOT docker-compose-from-this-repo. Prod deploys need Tony's explicit OK.

README.md explains the mechanisms in full. This file is the house rules.

Deploy

Registered in platform/apps/otm-admin/lib/first-party-apps.ts as publicAccess: true, needsDb: false, needsAuthSecret: false, needsSuggestions: true, uploads: "/app/public/uploads".

Adding this app to the registry required an otm-admin release (0.113.0) — and because the auto-deploy webhook is gated on green CI, names does not appear in /apps until platform CI passes and otm-admin redeploys (~2.5 min). build_app before that point fails on an unknown app id.

build_app times out at the MCP layer and keeps building. That's normal, not a failure. Poll for the image instead of retrying the call:

ssh debian@51.81.80.250 'docker images platform-names --format "{{.Repository}} {{.CreatedSince}}"'

Verify a deploy against the LIVE site, never the deploy log:

curl -s https://names.poweredbyotm.com/api/health
curl -s -X POST https://names.poweredbyotm.com/api/availability \
  -H 'content-type: application/json' \
  -d '{"domains":["google.com","zzq7xk4m.io"]}'   # expect taken + unverified-available

Availability — the trap

Never route lookups through rdap.org. Its redirector returns a bare 404 both for "this domain is unregistered" and for "I have no RDAP server for this TLD" — the two are indistinguishable from the status code. Routing through it would report every .io as available. We resolve the TLD ourselves from IANA's bootstrap (data.iana.org/rdap/dns.json, cached 24h) and query the authoritative server directly, so a 404 can only mean unregistered.

The TLDs with no RDAP at all (.io .co .me .sh .gg .us .de) fall back to a DNS NS lookup. That path can only ever return unverified-available, shown amber as probably free — a registered-but-undelegated domain looks identical. Do not "upgrade" it to a green check.

We're an anonymous client against other people's public endpoints. Keep the manners: 60 domains/request, 8-wide concurrency, per-IP token bucket, 10-minute result cache.

Generation — three guards that must not be relaxed

src/lib/generate.ts is pure and deterministic. The strategies are deliberately cheap and dumb; quality lives in scoreName — tune the scorer before adding vocabulary. Each of these exists because of specific bad output, and removing it brings that output straight back:

  • expandKeywords matches category hints by prefix, never substring. Plain containment let repair match the ai category (rep-ai-r) and dragged neural-network words into an auto-shop brief.
  • syncopate only drops the schwa from a final -er (flickerflickr). The original "drop the last vowel" gave cremacrem, kettlekettl, securitysecurit.
  • 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 list. Without it the top 20 came back as eighteen coinages — raw score ranking clumps badly.

Shared OTM widgets need Tailwind

The bottom-right feedback chat-pop is SuggestionLightbulb from @otm/account-panel, styled with Tailwind utilities. This app's own UI is plain CSS, and for a while it had no Tailwind at all — so the widget rendered as a bare unstyled button (issue #2). Tailwind is configured with preflight: false (it must not reset the handwritten CSS) and its content includes node_modules/@otm/account-panel/src. Element-level rules in globals.css are scoped to .wrap, and the widget mounts outside .wrap, so neither side can restyle the other. Keep it that way when adding global CSS.

Wishlist is browser-only

src/lib/wishlist.tslocalStorage key names.wishlist, capped at 60. It snapshots the last-seen status per domain; the Finder keeps snapshots fresh from live lookups and Re-check re-queries through the normal availability route. No server state, on purpose.

Keys stay in the browser

Optional AI names use the visitor's own Anthropic/Gemini key from localStorage (names.aiKey), called browser-direct — same pattern as rpo's Google Vision key, for the same reason: the platform has no per-app env tool, and a free public tool must not 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.

src/lib/registrars.ts. Links are plain today (every affiliateCode is ""). Fill one in and HAS_AFFILIATE_LINKS flips the footer disclosure on automatically. Keep the code and the disclosure in that one file — a disclosure that can drift out of sync with whether links actually pay is worse than none.