Initial release: business-name finder with live domain availability

Free, no-account tool for naming a business and finding a domain you can
actually register.

Availability uses no paid API and no key. IANA's RDAP bootstrap maps ~1,200
TLDs to their authoritative registry servers (404 = free, 200 = taken); the
handful with no RDAP at all (.io, .co, .me, .sh, .gg, .us) fall back to a DNS
NS lookup and are reported as "probably free" rather than confirmed, because a
registered-but-undelegated domain is indistinguishable that way.

Name generation is pure, deterministic, and client-side — eight strategies over
a curated word bank, ranked by a scorer that does the real quality work. Three
of its guards (prefix-only hint matching, -er-only syncopation, truncation
rejection) exist because of specific bad output and should not be relaxed.

Optional AI suggestions use the visitor's OWN Anthropic/Gemini key from
localStorage, called browser-direct, so this stays free to run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JChWdFJPCRxMBxErb8kUVK
This commit is contained in:
2026-08-10 13:40:26 -05:00
commit 78460d35c9
29 changed files with 3644 additions and 0 deletions

107
src/app/settings/page.tsx Normal file
View File

@@ -0,0 +1,107 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { loadAiSettings, saveAiSettings, type AiProvider } from "@/lib/ai";
export default function SettingsPage() {
const [provider, setProvider] = useState<AiProvider>("anthropic");
const [key, setKey] = useState("");
const [saved, setSaved] = useState(false);
const [hasStored, setHasStored] = useState(false);
useEffect(() => {
const existing = loadAiSettings();
if (existing) {
setProvider(existing.provider);
setKey(existing.key);
setHasStored(true);
}
}, []);
const onSave = () => {
const trimmed = key.trim();
saveAiSettings(trimmed ? { provider, key: trimmed } : null);
setHasStored(Boolean(trimmed));
setSaved(true);
window.setTimeout(() => setSaved(false), 2500);
};
const onClear = () => {
saveAiSettings(null);
setKey("");
setHasStored(false);
setSaved(true);
window.setTimeout(() => setSaved(false), 2500);
};
return (
<>
<h1>Settings</h1>
<p className="tagline">
<Link href="/"> Back to the name finder</Link>
</p>
<h2>Bring your own AI key (optional)</h2>
<p className="tagline">
The name finder works fully offline without this &mdash; the built-in generator needs no
key and costs nothing. Adding your own API key gets you a second, usually more
imaginative set of suggestions alongside it.
</p>
<p className="notice info">
Your key is stored in <strong>this browser only</strong> and the request goes straight
from your browser to {provider === "anthropic" ? "Anthropic" : "Google"}. It is never sent
to our server and never appears in our logs. Usage is billed to your own account, so use
a key with a spending limit set.
</p>
<div className="field">
<label htmlFor="provider">Provider</label>
<select id="provider" value={provider} onChange={(e) => setProvider(e.target.value as AiProvider)}>
<option value="anthropic">Anthropic (Claude)</option>
<option value="gemini">Google (Gemini)</option>
</select>
<p className="hint">
{provider === "anthropic" ? (
<>
Create a key at{" "}
<a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer">
console.anthropic.com
</a>
. Starts with <code>sk-ant-</code>.
</>
) : (
<>
Create a key at{" "}
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">
aistudio.google.com
</a>
. Gemini has a free tier that is plenty for this.
</>
)}
</p>
</div>
<div className="field">
<label htmlFor="key">API key</label>
<input
id="key"
type="password"
value={key}
autoComplete="off"
placeholder={provider === "anthropic" ? "sk-ant-…" : "AIza…"}
onChange={(e) => setKey(e.target.value)}
/>
</div>
<div className="controls">
<button className="primary" onClick={onSave}>
Save key
</button>
{hasStored ? <button onClick={onClear}>Remove key</button> : null}
{saved ? <span style={{ color: "var(--ok)", fontSize: 14 }}>Saved to this browser.</span> : null}
</div>
</>
);
}