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

112 lines
5.3 KiB
Markdown

# 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:
```sh
ssh debian@51.81.80.250 'docker images platform-names --format "{{.Repository}} {{.CreatedSince}}"'
```
Verify a deploy against the LIVE site, never the deploy log:
```sh
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` (`flicker``flickr`).
The original "drop the last vowel" gave `crema``crem`, `kettle``kettl`,
`security``securit`.
- `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.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.