diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..977a2ff --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# 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. + +## 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.