Files
names/CLAUDE.md
Tony Moon 9489ce5708 docs: add CLAUDE.md — deploy procedure and the guards that must not be relaxed
Captures what isn't derivable from the code: build_app times out at the MCP
layer while continuing to build (poll for the image, don't retry), the app
doesn't exist to build_app until platform CI goes green, and the three
generator guards (prefix-only hint matching, -er-only syncopation, truncation
rejection) each exist because of specific bad output.

Also records why lookups must not route through rdap.org: its redirector
returns a bare 404 both for "unregistered" and "I don't serve this TLD", which
would report every .io as available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JChWdFJPCRxMBxErb8kUVK
2026-08-10 22:58:54 -05:00

4.4 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.

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.