A small web app that lets workshop attendees sign in and claim a personal, budget-capped, model-restricted LLM API key in under a minute. One YAML file per workshop. Admin page for visibility. Auto-expiring keys. No cron.
Read this blog post end-to-end — it explains the why for most design choices below and the gotchas that will otherwise burn your afternoon: https://blog.mehdio.com/p/running-an-ai-workshop-build-a-llm?r=1g8h3p&utm_campaign=post&utm_medium=web&triedRedirect=true
Especially internalise:
- OpenRouter Management API = mint sub-keys with
limit+expires_at - Model allowlist is NOT a per-key setting — it lives on a Guardrail
POST /keyssilently ignoresguardrail_ids; you MUST callPOST /guardrails/{id}/assignments/keysseparately- Store the minted key plaintext in the DB — OpenRouter shows it once, and blast radius is bounded by budget + expiry + guardrail
- Save usage to the DB periodically; live-pulling is inconsistent between OpenRouter's UI and API endpoints
- Auth provider: Auth0, Clerk, Supabase Auth, WorkOS, NextAuth + plain OIDC, etc. Must support Google + GitHub and expose a stable user id + verified email.
- Database: Postgres anywhere (Supabase, Neon, PlanetScale, RDS, local). SQLite is fine for dev. The schema is trivial (2 tables).
- Hosting: Vercel, Fly, Railway, Cloudflare Workers — whatever matches your stack.
- Framework: Next.js App Router is the blog's reference, but SvelteKit, Remix, or Nuxt are all fine. Server-side route/action for the mint call is non-negotiable (the management key never touches the browser).
- Workshop config = YAML file in the repo (
workshops/<slug>.yaml). PR + merge + deploy = new workshop. No admin CRUD. - Three-call mint flow on the server:
ensureGuardrail→createKey→assignKeysToGuardrail. If call 3 fails, surface it in the admin page as "unbound" with a retry button — don't fail the user-facing flow. - One Guardrail per workshop, named
workshop-<slug>. Fields:allowed_models(from YAML),limit_usd(aggregate workshop cap), monthly reset. - Claim window enforced server-side via
claims_open_at/claims_close_atin the YAML. No cron to toggle. - Schema (adapt names/types to your DB; keep the shape):
attendees(user_id PK, email, email_normalized, display_name, auth_provider, marketing_opted_in, marketing_opted_in_at, marketing_opted_in_workshop, first_seen_at, last_seen_at) workshop_attendance(user_id, workshop_slug, or_key_hash, or_key_plaintext, key_label, limit_usd, expires_at, or_guardrail_id, guardrail_attached_at, usage_usd, usage_synced_at, attended_at, PK (user_id, workshop_slug))