Records the v0.6.0 invariants as a house rule: 36/page, exclude already-shown names before ranking (a new seed alone reshuffles the same pool), appends reuse the current runId and look up only new domains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
7.7 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/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: 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:
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.
Paging appends — never re-look-up
36 names per page (NAME_COUNT), and "Show me more" appends: Finder passes the
names already on screen as exclude, and generateNames drops them before ranking.
A new seed alone mostly reshuffles the same pool — that was the old behaviour and it
re-checked domains the visitor had already seen. Two invariants: an append reuses the
current runId (so earlier cards keep resolving) and only looks up the NEW domains;
changing keywords/style/extensions starts a fresh list. When the pool is exhausted the
UI says so rather than silently returning nothing.
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.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.
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.
- Platform gateway (default, needs OTM sign-in). The platform registers
this app
needsAnthropicand injectsANTHROPIC_BASE_URL(its metered gateway) +ANTHROPIC_API_KEY(this app's gateway tokenotmapp_names_…, NOT an sk-ant key — seeplatform/apps/otm-admin/lib/first-party-ai.ts).app/api/ai-namescalls 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/aiis 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. - Bring your own key (override).
localStoragenames.aiKey, called browser-direct — same pattern asrpo's Google Vision key. Theanthropic-dangerous-direct-browser-accessheader 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/authorize
→ app/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.
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.