Files
names/CLAUDE.md
2026-08-16 22:28:46 -05:00

7.1 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 local accounts, public. Sign in with OTM (SSO) exists only to gate AI. 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: true (only for OTM_SSO_SECRET/AUTH_SECRET — see below), needsSuggestions: true, needsAnthropic: 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.

Client layout

components/Finder.tsx is orchestration only (inputs → generate → lookup). Cards are NameCard.tsx, the Saved panel Wishlist.tsx, the AI switch AiControls.tsx; session and shortlist state are hooks/useOtmSession.ts and hooks/useWishlist.ts; the availability result shape + batch fetch is lib/availability-client.ts. Add UI to the piece it belongs to, not to Finder.

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.

AI — two paths, one brief

src/lib/ai-prompt.ts holds the prompt + parser both paths share; change the brief there, never in one path.

  1. Platform gateway (default, needs OTM sign-in). The platform registers this app needsAnthropic and injects ANTHROPIC_BASE_URL (its metered gateway) + ANTHROPIC_API_KEY (this app's gateway token otmapp_names_…, NOT an sk-ant key — see platform/apps/otm-admin/lib/first-party-ai.ts). app/api/ai-names calls it server-side, only with a session cookie, plus ~20 requests/hour per OTM account. Usage is metered against the "Powered By OTM" customer, so the platform's monthly cap on /settings/ai is the hard stop. The login gate is what makes it safe to hold a metered credential on a free public tool — do not add an anonymous path to /api/ai-names.
  2. Bring your own key (override). localStorage names.aiKey, called browser-direct — same pattern as rpo's Google Vision key. The anthropic-dangerous-direct-browser-access header is correct here and would be wrong in a product that owns the key. When a key is saved, the UI shows only this path.

Sign in with OTM — no accounts here

src/lib/otm.ts (app:names audience, bounce URLs) → OTM's /sso/authorizeapp/api/auth/otm-sso verifies the 60s ticket (src/lib/sso-ticket.ts, vendored verbatim from @otm/account-panel — keep in sync) and mints our own HMAC cookie (src/lib/session.ts, names.session, 12h, signed with AUTH_SECRET). jti replay guard is an in-memory Map — fine for one container and 60s tickets. No user rows, no DB. /api/auth/session tells the UI who's signed in and whether platform AI is wired (platformAi); if the env is missing the feature is simply absent.

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.