- ☆ 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
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/namesongit.poweredbyotm.com. - Deploy: platform MCP —
build_app("names")thendeploy_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:
expandKeywordsmatches category hints by prefix, never substring. Plain containment letrepairmatch theaicategory (rep-ai-r) and dragged neural-network words into an auto-shop brief.syncopateonly drops the schwa from a final-er(flicker→flickr). The original "drop the last vowel" gavecrema→crem,kettle→kettl,security→securit.isTruncationrejects 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.ts — localStorage 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.
Registrar links
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.