diff --git a/README.md b/README.md
index 9e951a1..ddf1e63 100644
--- a/README.md
+++ b/README.md
@@ -1411,9 +1411,18 @@ Things I chose while building, with the reasoning:
pre-blurred wallpaper — the scene is clearly visible through the glass, and the
pixel-contrast scan still passes.
+82. **First run offers the choice: free Ollama or a paid provider.** The gate (user-key installs)
+ now shows Ollama with a "Free" badge as the headline option, and below it an "or · Use a paid
+ provider instead" button that opens the same provider rows as Settings — with the framing that
+ those are billed by the provider directly (Freezmo never sees a card) and that keys stay in the
+ browser. Saving a provider key on that screen lists its models and switches study generation to
+ it automatically, so the gate steps aside without an Ollama key at all — pinned end to end in
+ the providers suite with a second, user-key-mode app instance.
+
81. **Bring-your-own keys for cloud providers.** Besides the server's Ollama, Settings → Providers takes
- keys for OpenAI, Anthropic (their official OpenAI-compatibility layer), DeepSeek, Groq, OpenRouter
- and any custom OpenAI-compatible endpoint. One adapter (`lib/openai-compat`) covers them all —
+ keys for OpenAI, Anthropic (their official OpenAI-compatibility layer), Google Gemini (its
+ OpenAI-compatible endpoint), DeepSeek, Groq, OpenRouter and any custom OpenAI-compatible
+ endpoint. One adapter (`lib/openai-compat`) covers them all —
model listing, streaming chat, JSON generation and vision — and every model flow can run on a
provider: the chat picker lists each enabled provider's models (labelled, remembered across
visits), and a "Study generation" choice moves decks, homework, grading and attachment
diff --git a/components/ApiKeyGate.tsx b/components/ApiKeyGate.tsx
index fe2f1a7..7dfdb89 100644
--- a/components/ApiKeyGate.tsx
+++ b/components/ApiKeyGate.tsx
@@ -17,6 +17,9 @@ import {
import { Spinner } from "@/components/ui";
import { LOCALES, useI18n } from "@/lib/i18n";
import { verifyOllamaKey } from "@/lib/client-api";
+import { ProvidersSection } from "@/components/ProvidersSection";
+import { PROVIDERS } from "@/lib/providers";
+import { modelsFor, setGeneration, useProviders } from "@/lib/providers-client";
import { saveSettings, settingsStore } from "@/lib/settings";
import { useHealth } from "@/lib/use-health";
import { cn } from "@/lib/utils";
@@ -40,13 +43,26 @@ export function ApiKeyGate({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const { health } = useHealth();
+ const providers = useProviders();
// Server-key and local-daemon installs never need a browser key.
const needsKeyMode = health?.authMode !== "local" && health?.authMode !== "server";
+ // A paid provider is a valid first-run choice too. As soon as a key is saved
+ // the study generation switches to that provider — Ollama stays untouched —
+ // and the gate steps aside (the provider's models were listed on save).
+ const firstEnabled = PROVIDERS.find((provider) => providers.keys[provider.id]);
+ useEffect(() => {
+ if (!firstEnabled || providers.generation) return;
+ const models = modelsFor(providers, firstEnabled.id);
+ if (models.length === 0) return;
+ setGeneration({ provider: firstEnabled.id, textModel: models[0], visionModel: models[0] });
+ }, [firstEnabled, providers]);
+ const providerReady = Boolean(providers.generation && providers.keys[providers.generation.provider]);
+
// Until hydration finishes, render the page as usual so a key holder never
// sees the gate flash. After that, a missing key replaces the app.
- if (!mounted || storedKey || !needsKeyMode) return <>{children}>;
+ if (!mounted || storedKey || providerReady || !needsKeyMode) return <>{children}>;
return