Live demo — shared accounts, sample data, reset regularly. Don't enter anything real.Get the source

AI-Ready Module

AI-Ready Module

What it does

A thin, reusable wrapper around the Anthropic SDK (lib/ai.ts) plus one worked example built on top of it: an "Improve with AI" button on the contact form that rewrites a visitor's rough draft into a clearer message. Every call is logged to an ai_usage table with token counts and an estimated USD cost, and the feature is gated behind both a configured API key and a feature flag — see feature-flags.md.

How to configure it

VarPurpose
ANTHROPIC_API_KEYUnset → isAiConfigured is false and every AI feature no-ops (returns a friendly "not available" error, never crashes).
ANTHROPIC_MODELOptional override; defaults to claude-haiku-4-5-20251001 in lib/ai.ts (not in lib/env.ts, so a missing var reads as "use the default" rather than as an actual empty string in env output).

Also requires the ai_assistant feature flag enabled (off by default) — toggle it at /admin/feature-flags, see feature-flags.md.

How it works

  • lib/ai.ts — feature-agnostic on purpose: no DB dependency of its own. generateText({system, prompt, maxTokens}) calls the Anthropic Messages API and returns {text, model, inputTokens, outputTokens}; callers write their own ai_usage row tagged with their own feature name, so this file never grows call-site-specific branches as more AI features get added on top of it. getClient() lazily constructs the Anthropic client (not at module load) so importing this file never throws just because the key isn't set — same lazy-init pattern as lib/stripe.ts.
    • estimateCostUsd(result) — multiplies token counts by a hardcoded PRICING_PER_MILLION_TOKENS table (illustrative published rates, not your actual negotiated pricing) keyed by model name; an unrecognized model (e.g. you set ANTHROPIC_MODEL to something not in the table) still gets its token counts logged, just with costUsd: null.
  • lib/db/schema/app.ts's aiUsage table — one row per successful call: feature (a free-form string tag, e.g. "contact.improve_message"), model, inputTokens/outputTokens, nullable costUsd, nullable userId (onDelete: "set null" — usage history outlives the user being deleted).
  • lib/actions/ai.ts's improveContactMessage(raw) — the one shipped example. Checks isAiConfigured and the ai_assistant flag server-side before doing anything else — a request reaching this action with either gate off is either a stale client render or a direct call, and either way shouldn't spend an API call. Then: Zod-validates length (10–4000 chars), rate-limits by IP (lib/rate-limit.ts, 10/hour — tighter than lib/actions/contact.ts's own limit, deliberately, since this one costs real money per call), best-effort-attributes to a signed-in session if one exists (the contact page itself requires no login, so userId is often null), calls generateText, and writes the ai_usage row before returning the rewritten text.
  • components/marketing/contact-form.tsx — calls improveContactMessage directly as an async function (not a <form action>, since it fills a textarea rather than submitting), same pattern as lib/actions/waitlist.ts's markInviteUsed.

How to extend it

  • A new AI feature — call generateText() from a new Server Action, write your own ai_usage row with a distinctive feature tag, and gate it behind isAiConfigured (and optionally a new feature flag if you want it independently toggleable). No changes needed to lib/ai.ts itself.
  • Streaming responses — generateText uses messages.create (a single non-streaming call, appropriate for the current short-rewrite use case); swap in messages.stream() from the Anthropic SDK if you build something where perceived latency matters more.
  • Surfacing usage/cost — nothing currently reads ai_usage back out; it's write-only today. An admin view summing costUsd/inputTokens by feature or by day would be a natural /admin addition, following the same pattern as notifications.md's admin views.
  • A heavier default model — override per-deployment via ANTHROPIC_MODEL, or change DEFAULT_MODEL in lib/ai.ts if you want a different default across all deployments of a fork.

How to remove it

Delete lib/ai.ts, lib/actions/ai.ts, the aiUsage table from lib/db/schema/app.ts (regenerate a migration), the ai_assistant flag seed in lib/flags.ts, the "Improve with AI" button and its call in components/marketing/contact-form.tsx, @anthropic-ai/sdk from package.json, and the two ANTHROPIC_* vars from lib/env.ts.