From 577b089780404b6356e0c8b3de61fa6c0f75cf64 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 13 Jun 2026 13:28:17 +0700 Subject: [PATCH] docs: add brainstorm, plan, and red-team review for openrouter key giveaway site --- .../phase-01-tos-approval-gate.md | 55 ++++ .../phase-02-scaffold-and-config.md | 56 ++++ .../phase-03-github-oauth-auth-flow.md | 49 +++ .../phase-04-key-provisioning.md | 59 ++++ .../phase-05-pages-and-ui.md | 47 +++ .../phase-06-test-and-deploy.md | 56 ++++ .../plan.md | 76 +++++ ...-openrouter-key-giveaway-website-report.md | 81 +++++ ...openrouter-key-limits-workspaces-report.md | 144 ++++++++ ...pport-email-acceptable-use-key-giveaway.md | 54 +++ ...penrouter-github-tos-feasibility-report.md | 307 ++++++++++++++++++ 11 files changed, 984 insertions(+) create mode 100644 plans/260613-1144-openrouter-key-giveaway-website/phase-01-tos-approval-gate.md create mode 100644 plans/260613-1144-openrouter-key-giveaway-website/phase-02-scaffold-and-config.md create mode 100644 plans/260613-1144-openrouter-key-giveaway-website/phase-03-github-oauth-auth-flow.md create mode 100644 plans/260613-1144-openrouter-key-giveaway-website/phase-04-key-provisioning.md create mode 100644 plans/260613-1144-openrouter-key-giveaway-website/phase-05-pages-and-ui.md create mode 100644 plans/260613-1144-openrouter-key-giveaway-website/phase-06-test-and-deploy.md create mode 100644 plans/260613-1144-openrouter-key-giveaway-website/plan.md create mode 100644 plans/reports/brainstorm-summary-260613-1144-openrouter-key-giveaway-website-report.md create mode 100644 plans/reports/from-researcher-to-brainstorm-openrouter-key-limits-workspaces-report.md create mode 100644 plans/reports/openrouter-support-email-acceptable-use-key-giveaway.md create mode 100644 plans/reports/researcher-260613-1119-openrouter-github-tos-feasibility-report.md diff --git a/plans/260613-1144-openrouter-key-giveaway-website/phase-01-tos-approval-gate.md b/plans/260613-1144-openrouter-key-giveaway-website/phase-01-tos-approval-gate.md new file mode 100644 index 0000000..aac051e --- /dev/null +++ b/plans/260613-1144-openrouter-key-giveaway-website/phase-01-tos-approval-gate.md @@ -0,0 +1,55 @@ +--- +phase: 1 +title: "ToS Approval Gate" +status: pending +priority: P1 +effort: "1-5 days (mostly waiting)" +dependencies: [] +--- + +# Phase 1: ToS Approval Gate + +## Overview +Blocking precondition. Confirm OpenRouter permits a free, BYOK-backed, per-user key giveaway before writing any code. If rejected → cancel plan (or pivot to Org Accounts / proxy in a new plan). + +## Requirements +- Functional: written reply from OpenRouter support on acceptable use under ToS §7.4. +- Non-functional: no build work starts until a clear answer is received. + +## Architecture +N/A — process gate, not code. Decision branch on the reply. + +## Related Code Files +- Read: `plans/reports/openrouter-support-email-acceptable-use-key-giveaway.md` (drafted email) +- Read: `plans/reports/from-researcher-to-brainstorm-openrouter-key-limits-workspaces-report.md` + +## Technical verification questions (ASK OpenRouter here, not "verify at build time") +These are load-bearing assumptions the whole architecture rests on. Get them answered in the same support thread (or by a throwaway test key) BEFORE Phase 2: +1. **BYOK economics:** Does BYOK function at a **zero credit balance**, or does the 5%-of-cost platform fee (deducted from OpenRouter credits) cause requests to fail once the monthly free-request allowance is exhausted? (Docs: BYOK fee = 5%, waived first ~1M req/mo, default fallback to paid shared endpoints.) +2. **Limit cost-basis:** With `include_byok_in_limit: true`, does the per-key `limit` count the **5% fee** or the **full model-equivalent cost**? (If only the fee, `limit:10` permits ~$200/day of real usage.) +3. **Daily reset:** Confirm `limit_reset:daily` resets at **midnight UTC** (not rolling 24h). +4. **MiniMax M3 BYOK:** Is MiniMax actually a supported **BYOK provider** (not just available on OpenRouter)? BYOK provider docs historically name only OpenAI/Azure/Bedrock/Vertex. +5. **Delete handle:** Confirm the create-key response identifier used for `DELETE /api/v1/keys/:hash` is `data.hash` (the plan corrects `data.id` → `hash`). +6. **Per-key model restriction:** Can a provisioned key be restricted to one provider/model? (If no, raw-key cannot honor any "model allowlist" condition — see design-fatal note below.) +7. **Fallback control:** Can paid-credit fallback be disabled ("Always use this provider") so requests **fail** rather than silently bill? + +## Implementation Steps +1. Fill bracketed blanks in the drafted email (name, OpenRouter account email, repo URL). Append the Technical verification questions above. +2. Send from the same email as the OpenRouter account (so support can tie request to it). +3. Await reply. Record outcome + answers to all 7 questions in this phase file. +4. Branch on outcome: + - **Approved (as-is) AND BYOK questions answered favorably:** proceed to Phase 2. + - **Approved with a model-restriction or per-user-identity condition:** **DESIGN-FATAL for raw-key.** Provisioning API can't restrict models per key — do NOT treat as a Phase 4 tweak. Pivot to server-side proxy or Org Accounts in a new plan; re-confirm with user. + - **Rejected:** STOP. Cancel plan. Optionally open a new plan for Org Accounts or server-side proxy. +5. **Binding rule:** No Phase 2 commit until a written, account-tied reply is recorded here. **Silence = rejection.** + +## Success Criteria +- [ ] Email sent from OpenRouter account address, with the 7 verification questions +- [ ] Written reply received and recorded here, including BYOK economics + limit-basis answers +- [ ] All 7 verification questions answered (or empirically confirmed via a test key) +- [ ] Go / no-go decision documented; conditions classified as benign-tweak vs design-fatal + +## Risk Assessment +- **No reply / slow reply:** follow up once after ~5 business days; do not build on silence (silence ≠ permission). +- **Conditional approval (model restriction / per-user identity):** design-fatal for raw-key → forces proxy/Org-Accounts pivot, not a Phase 4 tweak. Re-confirm with user. +- **BYOK not free / breaks at $0 balance:** if confirmed, the "strictly free + empty balance" model is invalid — re-decide architecture before building. diff --git a/plans/260613-1144-openrouter-key-giveaway-website/phase-02-scaffold-and-config.md b/plans/260613-1144-openrouter-key-giveaway-website/phase-02-scaffold-and-config.md new file mode 100644 index 0000000..57d3af2 --- /dev/null +++ b/plans/260613-1144-openrouter-key-giveaway-website/phase-02-scaffold-and-config.md @@ -0,0 +1,56 @@ +--- +phase: 2 +title: "Scaffold and Config" +status: pending +priority: P1 +effort: "0.5d" +dependencies: [1] +--- + +# Phase 2: Scaffold and Config + +## Overview +Stand up the Next.js (App Router, JS + JSDoc) project, Supabase clients, the dedicated `llmapikey` Postgres schema + `api_keys` table + RLS, and Vercel env wiring. No feature logic yet. + +## Requirements +- Functional: app boots locally and on Vercel; Supabase reachable; schema migrated. +- Non-functional: JS + JSDoc (no TypeScript); secrets server-only; files < 200 LOC. + +## Architecture +- Next.js App Router, plain JS with JSDoc typedefs (`@typedef`, `@param`, `@returns`). +- Supabase: two clients — browser (anon key, for **auth UI only** — never queries `api_keys`) and server (service-role, server-only). Use `@supabase/ssr` for cookie-based sessions. +- **Access model = server-only.** All `api_keys` reads/writes go through the service-role server client. The `api_keys` table is NEVER exposed to the anon role: do NOT add `llmapikey` to PostgREST exposed schemas. RLS = **deny-all** (enable RLS, no permissive policies → anon/authenticated get nothing; service-role bypasses RLS). This avoids the forgeable-`user_metadata` RLS trap (user_metadata is end-user-mutable per Supabase docs) and removes dead RLS machinery. +- **Schema isolation:** all objects in dedicated `llmapikey` schema. NOTE (blast radius): the service-role key bypasses RLS for the **entire shared project**, not just `llmapikey` — schema isolation does not contain a service-role compromise. Recommendation: prefer a dedicated Supabase project for this spending-key registry; if shared is kept (user's choice), enforce server-only via the `server-only` npm package on the service-role module (compile-time guard, not just convention). +- Secrets in Vercel env (and `.env.local` for dev): `OPENROUTER_PROVISIONING_KEY`, `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `GITHUB_OAUTH_*` (configured in Supabase, not app), `NEXT_PUBLIC_REPO_URL`. `OPENROUTER_WORKSPACE_ID` only if Phase 1 confirms workspaces are used — otherwise omit (no maybe-vars). + +## Related Code Files +- Create: `package.json`, `next.config.mjs`, `jsconfig.json` +- Create: `app/layout.js`, `app/page.js` (placeholder) +- Create: `lib/supabase/server-client.js` (imports `server-only`), `lib/supabase/browser-client.js` +- Create: `supabase/migrations/0001_llmapikey_schema_and_api_keys.up.sql` + matching `.down.sql` +- Create: `.env.example` (names only, no secrets), `.gitignore` already present +- Create: `README.md` (update existing — setup + env doc) + +## Implementation Steps +1. `npx create-next-app` (JS, App Router, no TS). Strip boilerplate. +2. Add `@supabase/supabase-js` + `@supabase/ssr`. Create browser + server client modules with JSDoc. +3. Write migration SQL (`.up.sql`): + - `create schema if not exists llmapikey;` + - `create table llmapikey.api_keys (id uuid pk default gen_random_uuid(), github_user_id text unique not null, github_username text not null, openrouter_key_hash text, status text not null default 'pending', created_at timestamptz default now());` — note: `openrouter_key_hash` (the OpenRouter delete handle, NOT `id`), nullable, default status `pending` to support reserve-then-mint (Phase 4). Add a `key_hint` column for last-4 display. + - `alter table llmapikey.api_keys enable row level security;` with **NO policies** → deny-all to anon/authenticated; service-role bypasses. Do not write a `user_metadata`-based policy. + - Write paired `.down.sql` (drop table + schema) for rollback. +4. Apply `.up.sql` to a Supabase **branch/staging DB first**, then production (SQL editor or `supabase db push`). Shared project → never run untested DDL straight at prod. +5. Add `.env.example`; document each var in README. +6. Deploy skeleton to Vercel; set env vars; confirm build green. + +## Success Criteria +- [ ] App runs locally (`next dev`) and deploys green on Vercel +- [ ] `llmapikey.api_keys` exists with unique `github_user_id`, `openrouter_key_hash` column, RLS enabled with deny-all (no policies) +- [ ] Server client uses service-role, imports `server-only`, and is never imported into client components +- [ ] `llmapikey` schema NOT added to PostgREST exposed schemas +- [ ] `.up.sql` + `.down.sql` both present; migration applied to staging before prod +- [ ] `.env.example` documents all vars; no secret committed; no `OPENROUTER_WORKSPACE_ID` unless Phase 1 confirmed workspaces + +## Risk Assessment +- **Service-role blast radius (shared project):** service-role bypasses RLS for ALL apps in the shared project. Mitigate with `server-only` import guard (compile-time) + recommend dedicated project. RLS deny-all is defense-in-depth, not the primary control — the server action scoping by session identity IS the control. +- **Untested DDL on shared prod DB:** always apply to staging/branch first; keep `.down.sql` for rollback (Vercel rollback does not revert DB). diff --git a/plans/260613-1144-openrouter-key-giveaway-website/phase-03-github-oauth-auth-flow.md b/plans/260613-1144-openrouter-key-giveaway-website/phase-03-github-oauth-auth-flow.md new file mode 100644 index 0000000..8ecc9aa --- /dev/null +++ b/plans/260613-1144-openrouter-key-giveaway-website/phase-03-github-oauth-auth-flow.md @@ -0,0 +1,49 @@ +--- +phase: 3 +title: "GitHub OAuth Auth Flow" +status: pending +priority: P1 +effort: "0.5d" +dependencies: [2] +--- + +# Phase 3: GitHub OAuth Auth Flow + +## Overview +GitHub sign-in via Supabase Auth (GitHub provider). Establish server-side session; expose `github_user_id` + `github_username` to server actions. This identity is the one-key-per-account key. + +## Requirements +- Functional: user signs in with GitHub, returns authenticated; sign-out works; session readable server-side. +- Non-functional: OAuth scope minimal (`read:user`; `public_repo` only if a future star-read is added — soft nudge needs no star read, so `read:user` suffices). + +## Architecture +- Supabase Auth GitHub provider. GitHub OAuth App registered with callback `https://.supabase.co/auth/v1/callback`. +- Next.js: `signInWithOAuth({ provider: 'github' })` from a client button → Supabase callback → app `/auth/callback` route exchanges code for session (cookie via `@supabase/ssr`). +- **Identity anchor (pinned, not "TBD"):** `github_user_id = user_metadata.provider_id` (the **numeric, immutable** GitHub id) and `github_username = user_metadata.user_name`. Do NOT use `user_name` as the id (it's the mutable login) and do NOT use `sub` (that's the Supabase user UUID, not the GitHub id). Resolve by logging the full `user_metadata` from one real GitHub session in a Phase 3 spike, then assert the chosen field is numeric. Note `user_metadata` is end-user-mutable, so it is used only server-side to scope queries — never as an RLS authorization claim (see Phase 2 deny-all RLS). + +## Related Code Files +- Create: `app/auth/callback/route.js` (code exchange) +- Create: `app/auth/sign-out/route.js` +- Create: `components/sign-in-with-github-button.js` +- Create: `lib/auth/current-github-identity.js` (server helper → `{ githubUserId, githubUsername }`) +- Modify: `app/layout.js` (session-aware header) + +## Implementation Steps +1. Register GitHub OAuth App (decide owner — see Unresolved). Add client id/secret to Supabase Auth GitHub provider config (NOT app env). +2. Enable GitHub provider in Supabase dashboard. +3. Build sign-in button (client component) calling `signInWithOAuth`. +4. Implement `/auth/callback` route to exchange code → set session cookie. +5. **Spike:** sign in once, log full `session.user.user_metadata`, confirm `provider_id` is the numeric GitHub id. Pin it in `current-github-identity.js`. +6. Implement `current-github-identity.js` server helper → `{ githubUserId: provider_id, githubUsername: user_name }`. +7. Implement sign-out. + +## Success Criteria +- [ ] Sign in with GitHub → authenticated session (cookie set) +- [ ] Server helper returns `githubUserId` = numeric `provider_id` (asserted numeric), `githubUsername` = `user_name` +- [ ] Test fails if stored `github_user_id` equals the login string instead of the numeric id +- [ ] Sign-out clears session +- [ ] Works on Vercel domain (callback URLs correct) + +## Risk Assessment +- **Wrong identity field:** GitHub `user_name`/login is mutable → renamed account could mint a second key. Anchor on numeric `provider_id`; guard with the numeric-id assertion test above. +- **Callback URL mismatch:** Supabase callback vs app redirect — set both Supabase Site URL and additional redirect URLs for localhost + Vercel domain. diff --git a/plans/260613-1144-openrouter-key-giveaway-website/phase-04-key-provisioning.md b/plans/260613-1144-openrouter-key-giveaway-website/phase-04-key-provisioning.md new file mode 100644 index 0000000..25d4921 --- /dev/null +++ b/plans/260613-1144-openrouter-key-giveaway-website/phase-04-key-provisioning.md @@ -0,0 +1,59 @@ +--- +phase: 4 +title: "Key Provisioning" +status: pending +priority: P1 +effort: "1d" +dependencies: [3] +--- + +# Phase 4: Key Provisioning + +## Overview +Core logic: authenticated user generates exactly one OpenRouter key ($10/day cap) via the Provisioning API; key stored in `llmapikey.api_keys`; returning users see existing key (masked). One key per `github_user_id`, enforced by DB unique constraint + pre-check. + +## Requirements +- Functional: first generate → mint + store + return raw key once; second attempt → return existing (no new mint); concurrent double-submit → only one key. +- Non-functional: provisioning key server-only; raw key shown once, never persisted (store only `key_hint` = last 4). + +- Server action `generateKey()` — **reserve-then-mint** to make the unique constraint guard the *mint*, not just the row: + 1. Resolve `{ githubUserId, githubUsername }` from session (Phase 3 helper). + 2. **Reserve:** `insert` a `pending` row `{ github_user_id, github_username, status:'pending' }` with `ON CONFLICT (github_user_id) DO NOTHING`. If 0 rows inserted (conflict) → a row already exists → return masked hint + "already issued" and STOP (never mints). This makes concurrent double-submits safe: only the winning insert proceeds to mint. + 3. **Mint:** `POST https://openrouter.ai/api/v1/keys` with `Authorization: Bearer `, body `{ name: "llmapikey:", limit: 10, limit_reset: "daily", include_byok_in_limit: true, expires_at: <+90d ISO> }`. NOTE: name uses the opaque numeric id, NOT the username (avoid leaking PII into OpenRouter-side logs). + 4. **Persist:** parse response — raw key is top-level `key` (shown once); the delete handle is `data.hash`. `update` the reserved row → `{ openrouter_key_hash: data.hash, key_hint: last4, status:'active' }`. + 5. **Compensate on failure:** if mint fails → delete the pending row (free the reservation). If persist (update) fails after a successful mint → best-effort `deleteKey(data.hash)` to avoid an orphaned billable key, then surface a retryable error. + 6. Return raw key to client for one-time display. **Never log the raw key or the full mint response.** +- OpenRouter create response: `{ key: "sk-or-v1-…" (once), data: { hash, … } }`. The `hash` is the identifier for `DELETE /api/v1/keys/:hash` — there is no `data.id`. +- **Sybil/abuse guard:** env-driven `MAX_TOTAL_KEYS` kill-switch (stop minting past N active keys); optional min GitHub account age check via the session/GitHub API. Per-key $10/day bounds each key; the owner's BYOK subscription cap bounds total $ exposure (user-confirmed ceiling). +- **No revoke & regenerate in v1.** Raw key is shown once; if lost, that's acceptable for a free key. (Revoke/regenerate deferred to v2 — it adds a destructive multi-step delete+remint flow with several partial-failure modes; not worth it for MVP.) + +## Related Code Files +- Create: `app/actions/generate-key.js` (server action, imports `server-only`) +- Create: `lib/openrouter/provisioning-client.js` (create/delete key wrappers, JSDoc) +- Create: `lib/keys/api-keys-repository.js` (Supabase CRUD for `api_keys`) +- Create: `scripts/reconcile-keys.js` (list OpenRouter keys vs DB rows; flag orphans + dangling rows) + +## Implementation Steps +1. Build `provisioning-client.js`: `createKey({name, limitUsd, resetPeriod, includeByok, expiresAt})` returning `{ key, hash }`; `deleteKey(hash)` (treat 404 as success/idempotent). Handle non-2xx with typed errors. Confirm request field casing is **snake_case** (`limit_reset`, `include_byok_in_limit`) per API ref. +2. Build `api-keys-repository.js`: `reserve(githubUserId, githubUsername)` (insert pending, ON CONFLICT DO NOTHING, returns whether reserved), `activate(id, {hash, hint})`, `deletePending(id)`, `findByGithubUserId`. +3. Build `generate-key.js` server action implementing reserve→mint→persist→compensate above. +4. Mask helper: store/display last 4 chars only. +5. Build `reconcile-keys.js`: `GET /api/v1/keys`, diff against DB by hash; report orphaned OpenRouter keys (cost leak) and DB rows whose key was deleted out-of-band. +6. Manual test: generate, re-generate (idempotent — no second mint), concurrent double-submit (exactly one OpenRouter key), verify cap + BYOK-included + daily reset in OpenRouter dashboard. + +## Success Criteria +- [ ] First generate mints a key with `limit:10, limit_reset:daily, include_byok_in_limit:true, expires_at` — **verified in OpenRouter dashboard** (not just that the field was sent) +- [ ] `openrouter_key_hash` stores `data.hash`; a create→delete round-trip on a throwaway key succeeds +- [ ] Second attempt returns existing key, does NOT mint +- [ ] Concurrent double-submit yields exactly ONE OpenRouter key (verified in dashboard, not just one DB row) +- [ ] Mint-fail leaves no pending row; persist-fail deletes the orphan key +- [ ] Raw key returned once; only `key_hint` + `hash` persisted; raw key never logged +- [ ] Provisioning + service-role keys never reach the client bundle +- [ ] `MAX_TOTAL_KEYS` kill-switch halts minting when reached + +## Risk Assessment +- **Raw key unrecoverable (v1):** shown once; no in-app recovery. Prominent "copy now" warning (Phase 5). Revoke/regenerate deferred to v2. +- **Model surface too wide:** raw key can call any model on the account; Provisioning API can't restrict per key (confirmed: create-keys has no model field). The "empty credit balance" idea is UNVERIFIED and may break BYOK — do not rely on it until Phase 1 answers Q1/Q2/Q6. Document intended model `minimax/minimax-m3`; if Phase 1 says per-key model restriction is impossible AND support requires it → proxy pivot. +- **BYOK economics:** 5% fee to credits + paid fallback (Phase 1 Q1/Q2). "$10/day" cost-basis unverified — could permit far more real usage if it counts only the fee. Gate on Phase 1 answers. +- **Orphaned keys:** mitigated by reserve-then-mint + compensation + `reconcile-keys.js`. +- **Provisioning rate-limit:** handle OpenRouter 429 explicitly (back off, surface "try later", do NOT retry-loop into more mints). diff --git a/plans/260613-1144-openrouter-key-giveaway-website/phase-05-pages-and-ui.md b/plans/260613-1144-openrouter-key-giveaway-website/phase-05-pages-and-ui.md new file mode 100644 index 0000000..0457d17 --- /dev/null +++ b/plans/260613-1144-openrouter-key-giveaway-website/phase-05-pages-and-ui.md @@ -0,0 +1,47 @@ +--- +phase: 5 +title: "Pages and UI" +status: pending +priority: P2 +effort: "1d" +dependencies: [4] +--- + +# Phase 5: Pages and UI + +## Overview +The three user-facing pages: landing (`/`), dashboard (`/dashboard`), usage docs (`/docs`). Soft star nudge (CTA, not enforced). Keep it clean and minimal — this is a small giveaway site. + +## Requirements +- Functional: landing explains purpose + available model + how-it-works + optional star CTA; dashboard gates on auth and shows generate/existing-key; docs show how to call the API. +- Non-functional: responsive, accessible labels, no secret in client. Components < 200 LOC. + +## Architecture +- `/` — server component. Hero (purpose), models list (MiniMax M3 now; "more coming"), 3-step how-it-works, soft "⭐ Star the repo" link to `NEXT_PUBLIC_REPO_URL` (purely optional copy: "star if it helps you"), sign-in CTA. +- `/dashboard` — auth-gated. If unauthenticated → prompt sign-in. If no key → "Generate my key" button (calls Phase 4 action) + soft star nudge. If key exists → masked hint + usage snippet. (No revoke/regenerate in v1.) +- `/docs` — static usage guide: base URL `https://openrouter.ai/api/v1`, `Authorization: Bearer `, model id `minimax/minimax-m3`, curl + JS fetch examples, note on $10/day cap. + +## Related Code Files +- Modify: `app/page.js` (landing) +- Create: `app/dashboard/page.js` +- Create: `app/docs/page.js` +- Create: `components/key-display.js` (one-time raw key + copy; masked thereafter — interactive) +- Create: `components/generate-key-panel.js` (interactive) +- Inline (NOT separate files): star nudge + how-it-works are static copy blocks — write as JSX directly in `page.js`/`dashboard`. Extract later only if a file nears 200 LOC. + +## Implementation Steps +1. Landing page sections + soft star CTA (no gating), how-it-works inline. +2. Dashboard: auth check (redirect/prompt), wire generate-key panel to server action, one-time key display with copy button + prominent "shown once — copy now" warning. +3. Docs page with copy-paste examples for `minimax/minimax-m3`. +4. Basic styling (Tailwind or minimal CSS — keep deps lean). + +## Success Criteria +- [ ] Landing clearly states purpose, model, how-it-works, optional star +- [ ] Dashboard gates on auth; generate works; key shown once with copy + prominent warning +- [ ] Existing-key state shows masked hint, not raw key +- [ ] Docs examples are correct and runnable against a real key +- [ ] Star CTA present but never blocks access + +## Risk Assessment +- **One-time key UX confusion:** make the "copy now, shown once" warning prominent. No recovery path in v1 (revoke/regenerate is v2) — acceptable for a free key. +- **Accidental star-gating creep:** keep star strictly cosmetic — no code path checks star state (avoids GitHub AUP issue). diff --git a/plans/260613-1144-openrouter-key-giveaway-website/phase-06-test-and-deploy.md b/plans/260613-1144-openrouter-key-giveaway-website/phase-06-test-and-deploy.md new file mode 100644 index 0000000..94cf61a --- /dev/null +++ b/plans/260613-1144-openrouter-key-giveaway-website/phase-06-test-and-deploy.md @@ -0,0 +1,56 @@ +--- +phase: 6 +title: "Test and Deploy" +status: pending +priority: P2 +effort: "0.5d" +dependencies: [5] +--- + +# Phase 6: Test and Deploy + +## Overview +Validate the end-to-end flow, lock in the critical invariants (one-key-per-account, cap, secret isolation), and ship to Vercel production. + +## Requirements +- Functional: full flow works on the production Vercel domain with real GitHub + OpenRouter. +- Non-functional: no secret in client bundle; build green; basic regression tests for the provisioning logic. + +Tests should be written alongside Phases 4-5 (not test-last). This phase is the consolidation + verification gate + production promotion. Mocked tests give false confidence for API-shape and concurrency bugs — the load-bearing invariants are verified by real E2E + dashboard, not mocks. + +## Architecture +- **Thin unit tests** (node test runner) for genuinely pure logic only: mask/last-4 helper; provisioning-client request-body shaping (snake_case fields). Skip a mocked-Supabase repository test — it tests the mock, not the unique constraint. +- **Real verification** (the parts that actually matter): + - RLS negative test against a real Supabase: anon/authenticated client cannot SELECT any `api_keys` row. + - Create→delete round-trip on a throwaway OpenRouter key (confirms `data.hash` is the right handle). + - Manual E2E on Vercel: sign-in → generate → call MiniMax M3 → re-generate (no second mint) → concurrent double-submit (exactly one key in OpenRouter dashboard). + - Dashboard verification: minted key shows `limit:10`, daily reset, BYOK-included. + - Raw-key redaction: grep Vercel function logs to confirm no raw key / full mint response logged. + +## Related Code Files +- Create: `tests/mask-helper.test.js` +- Create: `tests/provisioning-client.test.js` (mock fetch — request shaping only) +- Create: `tests/rls-deny-all.test.js` (real Supabase anon client → expects empty/denied) +- Modify: `package.json` (test script) + +## Implementation Steps +1. Write the thin unit tests (mask, provisioning-client shaping). +2. RLS deny-all test: anon client SELECT on `llmapikey.api_keys` returns nothing. +3. Create→delete round-trip test on a throwaway key. +4. Verify no secret in client bundle (grep build output) AND no raw key in function logs. +5. Manual E2E on Vercel preview (see Architecture); confirm dashboard config + concurrent double-submit yields one key. +6. Promote to production; smoke test. Keep `.down.sql` ready for DB rollback (Vercel rollback won't revert the migration). + +## Success Criteria +- [ ] Thin unit tests pass (mask, provisioning-client shaping) +- [ ] RLS deny-all verified: anon cannot read `api_keys` +- [ ] Create→delete round-trip succeeds (`data.hash` is correct handle) +- [ ] No secret in client bundle; no raw key in function logs +- [ ] E2E on production: sign-in → generate → call MiniMax M3 succeeds +- [ ] Re-generate idempotent; concurrent double-submit = exactly one OpenRouter key (dashboard-verified) +- [ ] Dashboard shows $10/day + daily reset + BYOK-included + +## Risk Assessment +- **Real-money E2E:** calling MiniMax M3 spends BYOK budget — keep test calls tiny. +- **Mocks mask real bugs:** API-shape (hash) and concurrency invariants are only real at the OpenRouter/Postgres layer — verify via round-trip + dashboard + double-submit, not mocked tests. +- **Secret leak:** bundle grep + log grep are the gates; fail the phase if any server secret or raw key appears. diff --git a/plans/260613-1144-openrouter-key-giveaway-website/plan.md b/plans/260613-1144-openrouter-key-giveaway-website/plan.md new file mode 100644 index 0000000..26ffa7e --- /dev/null +++ b/plans/260613-1144-openrouter-key-giveaway-website/plan.md @@ -0,0 +1,76 @@ +--- +title: "llmapikey — Free OpenRouter Key Giveaway Site" +description: "" +status: pending +priority: P2 +branch: "master" +tags: [] +blockedBy: [] +blocks: [] +created: "2026-06-13T04:50:23.160Z" +createdBy: "ck:plan" +source: skill +--- + +# llmapikey — Free OpenRouter Key Giveaway Site + +## Overview + +Next.js (App Router, JS + JSDoc) site on Vercel. Gives developers a $10/day-capped OpenRouter API key — one per GitHub account — minted from owner's master Provisioning key, backed by owner's BYOK monthly subscription. Supabase only (dedicated `llmapikey` schema in a shared project, server-only access) for auth + key records. No proxy: OpenRouter enforces the cap. Soft star nudge (not enforced). + +**Cost model caveat (verify in Phase 1):** "free to users" — owner pays. OpenRouter BYOK still bills ~5% of model cost to OpenRouter credits and can fall back to paid endpoints, so "strictly free for the owner" is UNVERIFIED. The "$10/day" cap basis and zero-balance BYOK behavior are open questions for OpenRouter support (Phase 1). Owner's subscription cap is the global $ ceiling. + +**Source:** `plans/reports/brainstorm-summary-260613-1144-openrouter-key-giveaway-website-report.md` + +**GATING:** Phase 1 (ToS Approval Gate) blocks all build phases. If OpenRouter support rejects the free key-giveaway model, the plan is **cancelled** — do not build. + +**Scope OUT (v1):** proxy, payments, GLM/Kimi, per-model restriction, admin analytics, star enforcement. + +## Phases + +| Phase | Name | Status | +|-------|------|--------| +| 1 | [ToS Approval Gate](./phase-01-tos-approval-gate.md) | Pending | +| 2 | [Scaffold and Config](./phase-02-scaffold-and-config.md) | Pending | +| 3 | [GitHub OAuth Auth Flow](./phase-03-github-oauth-auth-flow.md) | Pending | +| 4 | [Key Provisioning](./phase-04-key-provisioning.md) | Pending | +| 5 | [Pages and UI](./phase-05-pages-and-ui.md) | Pending | +| 6 | [Test and Deploy](./phase-06-test-and-deploy.md) | Pending | + +## Dependencies + +None (greenfield, no cross-plan dependencies). + +## Red Team Review + +### Session — 2026-06-13 +**Findings:** 15 (15 accepted, 0 rejected) — 4 reviewers (security, failure-mode, assumptions, scope) +**Severity breakdown:** 2 Critical, 4 High, 9 Medium +**Verification:** OpenRouter create/delete/BYOK + Supabase RLS claims fetched against official docs. + +| # | Finding | Severity | Disposition | Applied To | +|---|---------|----------|-------------|------------| +| 1 | Delete handle is `data.hash`, not `data.id` (revoke would silently fail) | Critical | Accept | P2, P4, P6 | +| 2 | BYOK not "free": 5% fee to credits + paid fallback; empty-balance mitigation unverified/likely false | Critical | Accept (harden P1) | P1, P4, plan | +| 3 | Mint-before-insert race leaks orphaned billable keys | High | Accept | P4 | +| 4 | RLS bypassed by service-role + `user_metadata` policy is forgeable | High | Accept | P2 | +| 5 | Identity anchor unverified — pin numeric `provider_id` | High | Accept | P3 | +| 6 | `$10/day` cost-basis (fee vs full) + midnight-UTC reset unverified | High | Accept (P1 question) | P1, P4 | +| 7 | No global ceiling / Sybil via free GitHub accounts | Medium | Accept (light guards; subscription cap = $ ceiling) | P4 | +| 8 | Revoke®enerate is planner-introduced scope creep + unsafe | Medium | Accept (cut to v2) | P4, P5, P6 | +| 9 | Raw-key hardening: `expires_at`, no PII in name, no-log redaction | Medium | Accept | P4, P6 | +| 10 | Shared-Supabase service-role blast radius (all apps) | Medium | Accept (recommend dedicated project + `server-only`) | P2 | +| 11 | Phase 1 gate not binding; conditional approval may be design-fatal | Medium | Accept | P1 | +| 12 | Forward-only migration; RLS untested on shared DB | Medium | Accept (`.down.sql` + RLS negative test + staging) | P2, P6 | +| 13 | Mocked tests give false confidence; fold tests into build | Medium | Accept | P6 | +| 14 | Over-decomposed static components | Medium | Accept (inline) | P5 | +| 15 | Orphan reconcile script; decide WORKSPACE_ID; drop markRevoked | Medium | Accept | P2, P4 | + +**Decision-touching findings surfaced (not silently flipped):** #2 (strictly-free/BYOK — folded into P1 verification, awaiting OpenRouter reply), #7 (Sybil ceiling — user's subscription cap retained as the $ bound, light app guards added), #10 (shared Supabase — user's choice kept, dedicated project recommended). + +### Whole-Plan Consistency Sweep +- `data.id` → `openrouter_key_hash`/`data.hash` reconciled across P2 (schema), P4 (flow), P6 (round-trip). No `data.id` references remain. +- Revoke®enerate removed from P4 flow, P5 UI, P6 E2E; `markRevoked` dropped from P4 repo. +- RLS: P2 deny-all + server-only is consistent with P3 (metadata used only server-side) and P4 (service-role queries). +- "Strictly free" softened in plan overview + P4 + P1; consistent. +- No unresolved contradictions. diff --git a/plans/reports/brainstorm-summary-260613-1144-openrouter-key-giveaway-website-report.md b/plans/reports/brainstorm-summary-260613-1144-openrouter-key-giveaway-website-report.md new file mode 100644 index 0000000..ca10c29 --- /dev/null +++ b/plans/reports/brainstorm-summary-260613-1144-openrouter-key-giveaway-website-report.md @@ -0,0 +1,81 @@ +# Brainstorm Summary — llmapikey (Free OpenRouter Key Giveaway Site) + +**Date:** 2026-06-13 +**Status:** Design approved by user. Pending OpenRouter support approval before build. +**Related research:** +- `researcher-260613-1119-openrouter-github-tos-feasibility-report.md` +- `from-researcher-to-brainstorm-openrouter-key-limits-workspaces-report.md` +- Support email draft: `openrouter-support-email-acceptable-use-key-giveaway.md` + +--- + +## Problem Statement + +Build a website that gives developers free trial access to flagship models (currently MiniMax M3; later GLM, Kimi if demand). Each user gets one OpenRouter API key, capped $10/day, minted from owner's master account via Provisioning API, backed by owner's BYOK monthly subscription (already capped). Access gated by GitHub login; star is an optional nudge. Site = intro + key self-service. + +## Requirements (locked) + +- **Expected output:** Deployed Next.js site (Vercel) where a GitHub-authed user generates exactly one OpenRouter key ($10/day) and sees usage docs. +- **Acceptance criteria:** GitHub login works; one key per `github_user_id` (enforced); key minted via OpenRouter Provisioning API with `limit:10, limit_reset:daily`; returning user sees existing key; landing page explains purpose + models + soft star CTA. +- **Scope OUT (v1):** Proxy, payments, GLM/Kimi, per-model restriction, admin analytics, star enforcement. +- **Non-negotiable constraints:** Next.js + JS/JSDoc; Supabase only (dedicated `llmapikey` schema — shared project); Vercel; no proxy; raw-key delivery. +- **Touchpoints:** New repo (empty). Shared Supabase project. OpenRouter master account + provisioning key + BYOK subscription + workspace. + +## Decisions (user-confirmed) + +| Decision | Choice | Rationale | +|---|---|---| +| Access gate | **Soft star nudge** | Star-for-access violates GitHub AUP; gate on GitHub login only, star optional | +| Key delivery | **Raw provisioned key** (no proxy) | User accepts; OpenRouter auto-enforces cap; BYOK bounds cost | +| Monetization | **Strictly free** | Strengthens non-reselling position under ToS §7.4 | +| Datastore | **Supabase only** | Postgres + GitHub OAuth + RLS; one service | +| DB isolation | **Dedicated `llmapikey` schema** | Supabase project shared with other Vercel apps | +| Spend ceiling | **BYOK subscription cap** | Owner's monthly subscription already capped; per-key $10/day on top | +| ToS gating | **Support email first; cancel if rejected** | Removes account-ban ambiguity before any build | + +## Approaches Evaluated + +1. **Raw-key distribution (CHOSEN)** — mint per-user key, hand to user. Pro: simplest, native cap enforcement, no infra. Con: ToS §7.4 gray; full account liability; key usable off-site; cannot restrict models per key. +2. **Server-side proxy (declined)** — real key stays server-side; forward requests. Pro: ToS-"allowed" shape, model allowlist + content control + instant revoke. Con: more build effort (streaming gateway). +3. **Org Accounts / Authorized Users (fallback)** — if support rejects raw-key, OpenRouter's intended multi-user pattern. + +## Recommended Solution (final) + +Next.js (App Router, JS+JSDoc) on Vercel. Supabase dedicated `llmapikey` schema. GitHub OAuth via Supabase Auth (scope `public_repo`). + +**Flow:** land `/` → Sign in with GitHub → lookup `github_user_id` → has key (show masked + docs) / no key (soft star CTA + "Generate my key") → server action `POST /api/v1/keys {name:, limit:10, limit_reset:"daily", include_byok_in_limit:true}` → store → show key once + usage snippet (`minimax/minimax-m3`). + +**Data model — schema `llmapikey`, table `api_keys`:** `github_user_id` (unique), `github_username`, `openrouter_key_id`, `key_hint`, `created_at`, `status`. RLS: user reads only own row. + +**Pages:** `/` landing · `/dashboard` (key/generate) · `/docs` (usage). + +## Implementation Considerations & Risks + +| Risk | Severity | Mitigation | +|---|---|---| +| ToS §7.4 reselling/competing clause | High | Support email (drafted); BYOK; free; soft gate. **If rejected → abort.** | +| Account liability for end-user abuse | High | Owner responsible; revoke key via Provisioning API; keep model surface = BYOK only | +| Raw key can call unintended models | Medium | Keep OpenRouter credit balance empty → only BYOK subscription models usable; document allowed model | +| Key leakage / off-site use | Medium | Accepted; bounded by $10/day + BYOK cap | +| Master/provisioning key secret mgmt | Medium | Vercel env vars only; never client-exposed; server actions only | +| Shared Supabase collisions | Low | Dedicated `llmapikey` schema isolates objects/RLS | + +## Success Metrics / Validation + +- GitHub login → key generated end-to-end on Vercel. +- Second generate attempt by same account returns existing key (unique constraint holds). +- New key enforces $10/day (verify via OpenRouter dashboard). +- Landing clearly states purpose + models + optional star. + +## Next Steps & Dependencies + +1. **Phase 0 (gating):** Send OpenRouter support email; await approval. If rejected → cancel. +2. On approval: `/ck:plan` → scaffold Next.js, Supabase schema + Auth, provisioning server action, pages. +3. Provide secrets: OpenRouter provisioning key, BYOK subscription, Supabase URL/keys, GitHub OAuth app creds. + +## Unresolved Questions + +1. **Model allowlist:** Raw keys can't be model-restricted. Confirm "keep credit balance empty so only BYOK models work" is acceptable, or accept users may reach other models until balance hits $0. +2. **GitHub OAuth app:** Owner to create OAuth app (callback = Vercel domain) — confirm who registers it. +3. **BYOK setup:** Confirm BYOK subscription already connected in OpenRouter and which provider/models it covers (does it cover MiniMax M3?). +4. **Star revocation:** Since star is non-enforced, no revocation logic needed — confirm. diff --git a/plans/reports/from-researcher-to-brainstorm-openrouter-key-limits-workspaces-report.md b/plans/reports/from-researcher-to-brainstorm-openrouter-key-limits-workspaces-report.md new file mode 100644 index 0000000..0fc0579 --- /dev/null +++ b/plans/reports/from-researcher-to-brainstorm-openrouter-key-limits-workspaces-report.md @@ -0,0 +1,144 @@ +# OpenRouter API Key Limits, Workspaces & ToS Analysis +**Date:** 2026-06-13 | **Research Type:** Technical feasibility for provisioned key distribution to public end-users + +--- + +## Executive Summary + +The developer's plan to mint per-user API keys via Provisioning API, cap each at $10/day, and distribute to anonymous public users via GitHub login faces **ToS prohibition** on reselling/competing services. Workspaces are isolated project containers with unified billing. Key creation has **no documented account-level cap**, but **per-key cost all bills to master account**. Distributing raw keys to anonymous third parties **violates ToS §7(4)** unless the service itself (not the keys) is the product. + +--- + +## Findings + +### 1. Max Number of API Keys Per Account/Workspace + +**NO documented cap on API key count.** OpenRouter's documentation does not specify a maximum number of API keys per account or per workspace. + +- Provisioning API docs ([openrouter.ai/docs/features/provisioning-api-keys](https://openrouter.ai/docs/features/provisioning-api-keys)) focus on use cases (SaaS customer isolation, key rotation, usage monitoring) but omit numeric limits. +- DataStudios comprehensive limits guide ([usage-limits-explained](https://www.datastudios.org/post/openrouter-usage-limits-explained-rate-limits-spending-controls-provider-errors-fallbacks-byok)) treats keys as "budget containers" but does not mention per-account caps. +- FAQ ([openrouter.ai/docs/faq](https://openrouter.ai/docs/faq)) mentions management keys enable "programmatic key management" without cap details. +- Search across official docs and third-party analyses yields **no rate limit on the key-creation endpoint itself**. + +**Verdict:** No limit found; likely unlimited, but verify with support@openrouter.ai. + +--- + +### 2. Workspaces: Definition, Relation to Provisioning, Per-Workspace Limits + +**Workspaces = isolated project containers** for segmentation, not permission/quota boundaries. + +**Definition:** +- Workspaces organize projects into separate environments with independent API keys, routing defaults, guardrails, and observability ([openrouter.ai/docs/guides/features/workspaces](https://openrouter.ai/docs/guides/features/workspaces)). +- Each workspace inherits account-level policies; can impose stricter rules via guardrails. + +**Relation to Provisioning Keys:** +- API keys are **workspace-scoped resources** ([workspaces doc](https://openrouter.ai/docs/guides/features/workspaces)). +- When creating a key via Provisioning API, you specify `workspace_id` parameter ([search result](https://openrouter.ai/docs/api/api-reference/api-keys/create-keys)). +- Members can create their own keys in workspaces they access; admins can create system-owned keys for workspace isolation. + +**Per-Workspace Limits & Quotas:** +- **No per-workspace rate limits or key caps documented.** Guardrails ([openrouter.ai/docs/guides/features/guardrails](https://openrouter.ai/docs/guides/features/guardrails)) allow **spending caps** (daily/weekly/monthly reset) and **model/provider allowlists** per guardrail, but do not segment quota per workspace. +- Free-tier per-minute and daily limits ([API Rate Limits doc](https://openrouter.ai/docs/api/reference/limits)) apply account-wide, not per workspace. + +**Billing Rollup:** +- **Unified billing across all workspaces** ([workspace blog announcement](https://openrouter.ai/announcements/introducing-workspaces)): all charges aggregate at account level; org admins manage billing centrally. +- Can filter logs and activity by workspace for cost breakdown, but all usage bills to one master credit pool. + +**Verdict:** Workspaces are **logical containers for access control**, not cost silos. All provisioned keys' charges roll up to master account's single credit balance. + +--- + +### 3. ToS: Distributing Provisioned Keys to Anonymous Third Parties + +**PROHIBITED.** OpenRouter ToS §7(4) explicitly forbids **"accessing the Site or Service for purposes of reselling API access to Models or otherwise developing a competing service."** ([openrouter.ai/terms](https://openrouter.ai/terms)) + +**Analysis:** + +| Scenario | Verdict | Evidence | +|----------|---------|----------| +| **Raw API keys to anonymous public users** | Prohibited | §7(4) "reselling API access" + account holder liability for key confidentiality (§3). Hacker News discussion: ["not allowed to expose the access to end users"](https://news.ycombinator.com/item?id=47702048) | +| **"Strictly free" exemption** | NO exemption | ToS uses "reselling API access" language; free does not negate access-distribution violation. Payment intent is irrelevant to the prohibition. | +| **SaaS with provisioned keys** | Allowed | OpenRouter rep confirmed: "build applications powered by OpenRouter (SaaS products, AI assistants)" is permitted. Caveat: "as long as you don't create a proxy that simply passes through API access." | +| **Gray zone: app offering OpenRouter's models as a service, no proxy** | Unresolved in ToS | Hacker News commenter raised: "SaaS product wrapping OpenRouter's models + charging users per token." OpenRouter did not directly answer; this remains **gray**. | + +**Key Quote:** +> "OPENROUTER IS NOT RESPONSIBLE FOR THE ACCURACY OR QUALITY OF ANY OUTPUT YOU RECEIVE." +> Account holder must "maintain the confidentiality of your account and password" and accept "responsibility for all activities that occur under your account." ([openrouter.ai/terms](https://openrouter.ai/terms), §3, §6) + +**Per Hacker News:** +OpenRouter has "strengthened account-level regional restrictions" and enforces upstream provider rules (e.g., OpenAI/Anthropic geoblocking). Account-level enforcement signals → master account is liable for all key activity. + +**Verdict:** **Prohibited.** Distributing raw keys to anonymous public users = "reselling API access." Free service does not change this. Building a service that *uses* OpenRouter (with your own keys) is allowed; giving out your own keys to end-users is not. + +--- + +### 4. Account-Level Liability & Abuse Mitigation + +**Master account holds liability for all provisioned key activity.** No per-key content filtering or liability firewall exists. + +**Liability:** +- ToS §6.7: OpenRouter "may, however, at any time and without prior notice, screen, remove, edit, or block any Inputs that in our sole judgment violates these Terms or is otherwise objectionable or illegal." +- **No exemption for master account:** if a provisioned key generates prohibited content, enforcement applies to the account/key pair. No documented consequence cap (suspension, account loss possible). +- Account holder is "responsible for all activities that occur under your account" (§3). + +**Abuse Mitigation (Per-Key Controls):** + +| Control | Available | Details | +|---------|-----------|---------| +| **Spending cap** | ✅ Yes (via guardrails) | Daily/weekly/monthly reset; requests rejected at limit ([guardrails doc](https://openrouter.ai/docs/guides/features/guardrails)). | +| **Model allowlist** | ✅ Yes (via guardrails) | Restrict key to specific models/providers; intersection logic if multiple guardrails apply. | +| **Content filtering** | ⚠️ Partial | Regex-based prompt injection detection; custom content filters with redact/block actions; NLP-based sensitive info detection ([guardrails doc](https://openrouter.ai/docs/guides/features/guardrails)). **Does not cover output (model response) filtering.** | +| **Per-key logging disable** | ❓ Undocumented | No per-key opt-out from logging found in docs. Account-level privacy controls exist. | + +**Verdict:** **Guardrails reduce but do not eliminate risk.** Spending caps + model allowlists + prompt filtering mitigate financial + injection risk. **Output monitoring is OpenRouter's responsibility, not user-configurable.** Anonymous users remain a liability vector; upstream provider (OpenAI, Anthropic) abuse bans may cascade to master account. + +--- + +### 5. Key Creation Requirements: Payment Method, Credit Balance, Minimum + +**No per-key payment method required.** All charges bill to **master account's credit pool.** + +**Key Creation via Provisioning API:** +- Does **NOT** require a separate payment method per key. +- Each provisioned key **shares the master account's credit balance** ([provisioning API doc](https://openrouter.ai/docs/features/provisioning-api-keys)). +- Billing is **unified across all keys** in all workspaces. + +**Master Account Requirements:** +- **Credit balance**: OpenRouter uses prepaid credits. Account must have credit balance to serve requests; 402 errors returned if balance exhausted ([API Rate Limits doc](https://openrouter.ai/docs/api/reference/limits)). +- **Minimum purchase**: No hard minimum documented; "usually $5" mentioned in 3rd-party guides, but API docs state no floor ([pricing info](https://costgoat.com/pricing/openrouter)). +- **Negative balance:** If balance goes negative, requests fail with 402 Payment Required, even for free models ([API Rate Limits doc](https://openrouter.ai/docs/api/reference/limits)). + +**Rate Limiting & Credits:** +- Rate limit (requests/second) is **per-account**, not per-key, and scales with master account credit balance: 0 credits → 1 req/s, $5 → 5 req/s, up to 200 req/s max ([API Rate Limits doc](https://openrouter.ai/docs/api/reference/limits)). +- Per-key spending cap (via guardrails) is enforced independently; total of all keys' spending must fit within master account balance. + +**Verdict:** Each $10/day-capped provisioned key will cost nothing to create; **all usage charges bill to master account's single credit balance.** If 100 users each hit $10/day, master account must have $1000/day credit float to avoid 402 errors. + +--- + +## Unresolved Questions + +1. **Exact rate limit on key-creation endpoint:** Search found none. Provision API docs are 404 or minimal. Confirm with OpenRouter support: "Max requests/minute to POST /api/v1/keys?" + +2. **Account-level consequence specifics:** ToS allows OpenRouter to "remove, edit, or block" inputs and (implicitly) suspend keys/accounts. No SLA on "banned key" recovery or escalation path. Clarify: "What happens to master account if a provisioned key generates CSAM or other severe violation?" + +3. **Output filtering vs. input filtering:** Guardrails cover prompt injection, regex, sensitive-info detection. OpenRouter's own output monitoring (inference endpoint blocks) is not user-configurable. Gap: anonymous users generating harmful outputs → no per-key control. + +4. **Free service + "reselling" interpretation:** ToS says "reselling." Legal analysis of whether non-profit/free distribution of raw API keys still = "reselling" is **gray**. "Reselling" typically = exchange-for-money, but ToS language does not explicitly carve out free services. Recommend: ask OpenRouter directly whether free, non-commercial key distribution is allowed under provisioning. + +5. **Workspace member + provisioning key scope:** Docs state "members can create their own keys in workspaces they belong to." Unclear: can a provisioning key create keys for a different workspace than its own, or is it confined to its originating workspace? + +--- + +## Sources + +- [OpenRouter Terms of Service](https://openrouter.ai/terms) +- [OpenRouter Workspaces Documentation](https://openrouter.ai/docs/guides/features/workspaces) +- [OpenRouter Provisioning API Keys](https://openrouter.ai/docs/features/provisioning-api-keys) +- [OpenRouter Guardrails](https://openrouter.ai/docs/guides/features/guardrails) +- [OpenRouter API Rate Limits](https://openrouter.ai/docs/api/reference/limits) +- [OpenRouter FAQ](https://openrouter.ai/docs/faq) +- [Hacker News: "Not Allowed to Expose Access"](https://news.ycombinator.com/item?id=47702048) +- [DataStudios: OpenRouter Usage Limits Explained](https://www.datastudios.org/post/openrouter-usage-limits-explained-rate-limits-spending-controls-provider-errors-fallbacks-byok) +- [OpenRouter Blog: Introducing Workspaces](https://openrouter.ai/announcements/introducing-workspaces) diff --git a/plans/reports/openrouter-support-email-acceptable-use-key-giveaway.md b/plans/reports/openrouter-support-email-acceptable-use-key-giveaway.md new file mode 100644 index 0000000..3bfe162 --- /dev/null +++ b/plans/reports/openrouter-support-email-acceptable-use-key-giveaway.md @@ -0,0 +1,54 @@ +# OpenRouter Support Email — Acceptable-Use Confirmation (Free Key Giveaway) + +> Copy the body below into an email to OpenRouter support (support@openrouter.ai or the in-app support/Trust channel). Fill the `[bracketed]` blanks before sending. + +--- + +**To:** support@openrouter.ai +**Subject:** Acceptable-use check — free giveaway of per-user, daily-capped API keys (no reselling) + +--- + +Hi OpenRouter team, + +I'm building a small **non-commercial, free** community project and want to confirm it's acceptable under your Terms (esp. §7.4 on reselling / competing services) **before** I launch. I'd rather ask first than risk my account. + +**What it is** +A simple website that lets developers try flagship models they may not otherwise have access to (currently MiniMax M3; possibly GLM and Kimi later). It's purely a "try it out" giveaway — I am **not selling tokens, not charging users anything, and not running a paid or competing API service.** + +**How it works technically** +- One OpenRouter account (mine), with a **Provisioning/Management key**. +- Usage is backed by **BYOK** — I've connected my own monthly provider subscription, which already has its own spending cap. So I'm routing through my own paid subscription, not reselling your metered credits. +- For each user I mint a separate API key via the Provisioning API, each hard-capped at **$10/day** (`limit: 10`, `limit_reset: daily`). +- Users authenticate with **GitHub login** so I can enforce **one key per GitHub account**. (Starring my repo is an optional nudge only — not required for access.) +- The key is handed to the user so they can call the models directly from their own tools. + +**My questions — acceptable use** +1. Is this free, non-commercial giveaway of per-user, daily-capped keys (backed by my own BYOK subscription) acceptable under your Terms? +2. If not, is there a preferred pattern you'd recommend for this use case — e.g. **Organizational Accounts** with Authorized Users instead of distributing raw keys? +3. Are there any guardrails you'd require of me (model allowlist, logging, abuse handling) given I'd be the responsible account holder for end-user activity? + +**My questions — technical (these decide whether the design is even viable)** +4. Does BYOK function at a **zero credit balance**, or does the 5% platform fee (deducted from credits) cause requests to fail once the monthly free-request allowance is exhausted? +5. With `include_byok_in_limit: true`, does a key's `limit` count the **5% fee** or the **full model-equivalent cost**? (Determines what "$10/day" actually permits.) +6. Does `limit_reset: daily` reset at **midnight UTC**? +7. Is **MiniMax** a supported **BYOK provider** (not just a model available on OpenRouter)? +8. Can a provisioned key be **restricted to a single provider/model**, or can it call any model on my account? +9. Can paid-credit **fallback be disabled** so requests fail rather than silently bill my credits? +10. Confirm the create-key response identifier used for `DELETE /api/v1/keys/:hash` is `data.hash`. + +I'm happy to adjust the design to whatever keeps things compliant. Thanks for building OpenRouter — this project only exists because your platform makes multi-model access easy. + +Best regards, +[Your name] +[Project name / repo URL: https://github.com/tiennm99/llmapikey] +[Your OpenRouter account email] + +--- + +## Notes (not part of the email) + +- **Why this email matters:** ToS §7.4 prohibits "reselling API access … or otherwise developing a competing service." A free, BYOK-backed giveaway is arguably outside that clause, but it's ambiguous. Written confirmation removes the account-ban risk. +- **Strongest points to lean on:** (1) no money changes hands, (2) BYOK = your own subscription, not resale of OpenRouter credits, (3) per-key $10/day hard cap, (4) one-key-per-identity control. +- **If they say no:** the fallback is Organizational Accounts (Authorized Users) or a server-side proxy — both keep the raw key out of users' hands. Ask them which they prefer. +- **Send from** the same email as your OpenRouter account so they can tie the request to it. diff --git a/plans/reports/researcher-260613-1119-openrouter-github-tos-feasibility-report.md b/plans/reports/researcher-260613-1119-openrouter-github-tos-feasibility-report.md new file mode 100644 index 0000000..53620e6 --- /dev/null +++ b/plans/reports/researcher-260613-1119-openrouter-github-tos-feasibility-report.md @@ -0,0 +1,307 @@ +# OpenRouter + GitHub API: ToS & Technical Feasibility Research + +**Date:** 2026-06-13 +**Status:** Complete research, multiple sources verified +**Scope:** OpenRouter provisioning API, spending controls, ToS restrictions; GitHub star verification API & acceptable use + +--- + +## 1. Provisioning / Programmatic API Key Creation + +**Verdict: YES — Full CRUD available** + +OpenRouter exposes Management API Keys specifically for programmatic key management: + +### API Endpoint +- **POST** `/api/v1/keys` — Create new API key [https://openrouter.ai/docs/api/api-reference/api-keys/create-keys] +- **DELETE** `/api/v1/keys/{key_id}` — Delete a key [https://openrouter.ai/docs/api/api-reference/api-keys/delete-keys] +- List, read, update operations implied via Management API [https://openrouter.ai/docs/guides/overview/auth/provisioning-api-keys] + +### Authentication Required +- **Bearer token** in Authorization header +- Must use a **Management API Key** (special key type, separate from user keys) +- Management keys are **NOT usable for model completions** — only key management operations + +### Creation Parameters +- `name` (required): string +- `expires_at` (optional): ISO 8601 timestamp +- `limit` (optional): USD spending cap +- `limit_reset` (optional): `daily`, `weekly`, or `monthly` +- `include_byok_in_limit` (optional): bool, include BYOK usage in limit +- `creator_user_id` (optional): associate with specific user +- `workspace_id` (optional): UUID for workspace assignment + +### Response +- Returns the **key string itself** (one-time display) +- Includes metadata: creation time, expiration, limit info, workspace + +**Use case alignment:** Perfect for SaaS multi-tenant, per-customer key creation + rotation [https://openrouter.ai/docs/cookbook/administration/organization-management] + +--- + +## 2. Per-Key Spending Limits + +**Verdict: YES — But **DAILY/WEEKLY/MONTHLY only, no "per-request" limit** + +### Hard Spending Caps Available +- Yes, each key can have a **`limit`** in USD +- Paired with **`limit_reset`** = `daily`, `weekly`, or `monthly` +- Once limit is reached, key is subject to OpenRouter's rate limits (20 RPM for free models) + +### Limitation: Enforcement +- **Lifetime-only is NOT available** (only rolling reset) +- **Per-request limits are NOT available** (only per-key aggregate) +- To implement "$10/day cap": use `limit: 10`, `limit_reset: daily` + +### Real-Time Enforcement +- Limit status available via Management API responses: `limit_remaining` field +- Can poll for real-time spend tracking +- BYOK (Bring Your Own Key) usage **can be included/excluded** from the limit via `include_byok_in_limit` flag [https://openrouter.ai/docs/api/reference/limits] + +**Verdict for "$10/day":** YES — use `limit_reset: daily` + `limit: 10`. Enforcement is automatic. + +--- + +## 3. Per-Key Usage Tracking + +**Verdict: YES — Available via API, near-real-time** + +### Usage Endpoints +- **Activity Dashboard** — real-time usage display in web UI [https://openrouter.ai/docs/faq] +- **`/api/v1/generation` endpoint** — query stats for specific request ID (token counts, cost) after completion [https://costgoat.com/pricing/openrouter] +- **Broadcast/observability** — OpenRouter sends token usage (prompt, completion, total tokens) in response headers [https://openrouter.ai/docs/guides/features/broadcast] + +### Real-Time Latency +- API response includes immediate cost/token counts (within request) +- Generation stats endpoint available post-completion +- Not confirmed as instant; assume ~1-5 second propagation to tracking system + +### Per-Key Granularity +- Per-key usage logs available via Activity tab +- Per-key spending visible in dashboard +- **No explicit per-key API endpoint documented**, but management API returns key metadata with usage stats + +**Limitation:** No documented REST endpoint for "query usage for key X in last 24h" — would require either (a) parsing Activity dashboard, or (b) polling generation endpoints for each request. + +--- + +## 4. Model Availability & Pricing + +**Verdict: YES for GLM; YES for Kimi; PARTIAL for MiniMax** + +### MiniMax Models (Confirmed) +- **MiniMax M3** (multimodal): $0.30/M input, $1.20/M output, 1M token context [https://openrouter.ai/minimax/minimax-m3] +- **MiniMax M2.7**: $0.25/M input, $1.00/M output, 204K context [https://openrouter.ai/minimax/minimax-m2.7] +- **MiniMax M2**: $0.255/M input, $1.00/M output, 197K context [https://openrouter.ai/minimax/minimax-m2] +- ✅ All available on OpenRouter + +### GLM / Z.ai Models (Confirmed) +- **GLM-5.1** (Apr 2026): $0.98/M input, $3.08/M output, long-context coding [https://openrouter.ai/z-ai/glm-5] +- **GLM-5** (Feb 2026): $0.60/M input, $1.92/M output, 202K context [https://openrouter.ai/z-ai/glm-5-1] +- **GLM-4.7** (Jan 2026): $0.40/M input, $1.75/M output [https://openrouter.ai/z-ai/glm-4.7] +- **GLM-4.5**: $0.60/M input, $2.20/M output +- **GLM-4.5 Air** (free tier): 0 cost [https://openrouter.ai/z-ai/glm-4.5-air:free] +- ✅ 12 GLM variants available on OpenRouter + +### Kimi / MoonshotAI Models (Confirmed) +- **Kimi K2.7 Code** (Jun 2026): $0.95/M input, $4.00/M output, 262K context [https://openrouter.ai/moonshotai/kimi-k2.7-code] +- **Kimi K2.6** (Apr 2026): $0.68/M input, $3.41/M output, 262K context [https://openrouter.ai/moonshotai/kimi-k2.6] +- **Kimi K2.6 (free)**: 0 cost [https://openrouter.ai/moonshotai/kimi-k2.6:free/pricing] +- **Kimi K2.5**: $0.35/M input, $1.89/M output [https://openrouter.ai/moonshotai/kimi-k2.5] +- ✅ 10 Kimi variants available on OpenRouter + +--- + +## 5. ToS — Reselling / Sharing Keys / Sub-Accounts + +**Verdict: NO — Reselling explicitly prohibited. Key sharing ambiguous. Provisioning for end-users is legally murky.** + +### Reselling Prohibition (Section 7.4) +**Quote:** "access the Site or Service for purposes of reselling API access to Models or otherwise developing a competing service" **is prohibited** [https://openrouter.ai/terms] + +**Implication:** Creating a marketplace where you resell OpenRouter API access as your own service **violates ToS.** + +### API Key Sharing +**Quote (Section 3):** "you are solely responsible for maintaining the confidentiality of your account and password, and you accept responsibility for all activities that occur under your account" [https://openrouter.ai/terms] + +**Implication:** Sharing a key with end-users is NOT explicitly forbidden, but you assume liability for their abuse. + +### Sub-Accounts / Authorized Users (Section 3) +- OpenRouter allows **Organizational Accounts** with Admin User + Authorized Users +- **Quote:** "Authorized Users may only use the Service as configured by the Admin User" +- Admin can restrict logging, data retention, model access per user +- This is an **intended use case** [https://openrouter.ai/terms] + +### Provisioning Keys for End-Users +- **Provisioning API is documented** and officially supported [https://openrouter.ai/docs/features/provisioning-api-keys] +- Creating keys per customer (SaaS multi-tenant) is listed as a common use case +- **However:** Creating keys and distributing them to end-users (not employees) is NOT explicitly blessed in ToS +- **Risk:** If you distribute keys + they violate upstream model provider ToS (e.g., content policy), OpenRouter holds you liable + +**Best practice:** Use OpenRouter's **Organizational Accounts** (not raw key provisioning) if you intend to offer API access to third parties — it ensures OpenRouter controls permissions + logging. + +--- + +## 6. ToS — Acceptable Use & End-User Responsibility + +**Verdict: YES — Clear responsibility clause. KYC not enforced; abuse responsibility is.** + +### End-User Responsibility (Section 5.6, 6.6, 7) +**Quotes:** +- "You are solely responsible for selecting the Models you use, configuring your account settings" +- "You are solely responsible for your Inputs and the consequences of providing Inputs" +- "Organizations are responsible for their Authorized Users' compliance with Model Terms and these Terms" [https://openrouter.ai/terms] + +### Account Registration / KYC +- OpenRouter **does not require KYC** (no ID verification, no business verification) +- Registration requires email only +- **Quote (Section 3):** "you agree that the information you provide is accurate and will be kept up-to-date" [https://openrouter.ai/terms] + +### Prohibited Conduct +- Section 7: User cannot violate upstream model provider ToS +- Cannot use models for illegal, harmful, or deceptive content +- **"must not...assist or permit any person in engaging in"** prohibited conduct +- OpenRouter maintains dynamic list of prohibited content, updated per upstream providers [https://openrouter.ai/terms] + +### Abuse Reporting +- Not detailed in ToS; managed via support tickets and Trust Center [https://trust.openrouter.ai/?tab=securityControls] + +**Implication for reselling:** If you create keys for end-users and they violate policy, OpenRouter will hold YOUR account responsible and may suspend all keys. + +--- + +## 7. Free Tier / Rate Limits + +**Verdict: YES — Generous free tier, good for MVP** + +### Free Models Available +- 28+ models (as of June 2026) including DeepSeek R1, Llama 3.3 70B, Qwen3 Coder, Gemini Flash, **GLM-4.5 Air (free)**, **Kimi K2.6 (free)** [https://costgoat.com/pricing/openrouter-free-models] +- **No credit card required** + +### Rate Limits (free models, IDs ending in `:free`) +- **20 requests per minute (RPM)** — hard cap +- **Daily limit depends on account history:** + - <$10 purchased lifetime: 50 requests/day + - ≥$10 purchased at any point: 1,000 requests/day +- **Failed requests count toward quota** [https://openrouter.zendesk.com/hc/en-us/articles/39501163636379-OpenRouter-Rate-Limits-What-You-Need-to-Know] + +### Paid Models +- No enforced rate limits from OpenRouter (depends on upstream provider) +- Only limit is spending cap (if set) + +**Verdict for "free trial":** Yes, 20 RPM + 50–1,000 daily is usable for light testing. Upgrade to paid models removes RPM limits. + +--- + +## GitHub API: Star Verification + +**Verdict: YES — Endpoint exists, OAuth scope is `public_repo` or `repo`** + +### Endpoint +- **GET** `/user/starred/{owner}/{repo}` [https://docs.github.com/en/rest/activity/starring] +- **Response:** 204 (starred) or 404 (not starred) +- **Requires authentication** (401 if missing credentials) + +### OAuth Scope Required +- **`public_repo`** — minimum scope to check/star public repos [based on GitHub OAuth scope docs] +- **`repo`** — full access (public + private repos) + +### Note +- Endpoint determines "Whether the authenticated user has starred the repository" +- Works with Personal Access Tokens or OAuth tokens + +--- + +## GitHub ToS: Incentivized Stars + +**Verdict: NO — Prohibited. "Star-for-access" violates Acceptable Use Policy** + +### Prohibited Conduct (Acceptable Use Policy) +GitHub explicitly prohibits: +- **"inauthentic interactions, such as fake accounts and automated inauthentic activity"** +- **"rank abuse, such as automated starring or following"** +- **Starring incentivized by "cryptocurrency airdrops, tokens, credits, gifts or other give-aways"** [https://gigazine.net/gsc_news/en/20260421-github-fake-star-economy] [https://awesomeagents.ai/news/github-fake-stars-investigation/] + +### Enforcement +- Reactive (not proactive) +- No published enforcement metrics or transparency reports +- Fake star detection is ongoing investigation (6M+ suspected fake stars as of late 2025) [https://arxiv.org/html/2412.13459v2] + +**Implication for "star-for-API-access" plan:** +- **Offering API credits/access in exchange for stars = violation of GitHub ToS** +- GitHub may suspend your account if detected +- Better alternative: use "star" as optional metric, but don't tie it to access + +--- + +## Summary Verdicts + +| # | Finding | Verdict | +|---|---------|---------| +| 1 | Provisioning API (create/list/delete keys) | ✅ YES — Full CRUD via Management API | +| 2 | Per-key spending limits (daily cap) | ✅ YES — `limit` + `limit_reset: daily` works | +| 3 | Per-key usage tracking endpoint | ⚠️ PARTIAL — Dashboard + generation endpoint, no dedicated "usage by key" REST endpoint | +| 4 | MiniMax, GLM, Kimi availability + pricing | ✅ YES — All available, current pricing confirmed | +| 5 | ToS: Reselling/sharing keys prohibited? | 🚫 YES reselling prohibited (7.4). Sharing ambiguous (3). Provisioning for sub-accounts OK via Org API. | +| 6 | ToS: End-user responsibility clause? | ✅ YES — Clear liability on account owner. KYC not enforced. | +| 7 | Free tier / rate limits | ✅ YES — 28+ free models, 20 RPM, 50–1K daily. | +| — | GitHub: Star verification endpoint | ✅ YES — GET `/user/starred/{owner}/{repo}`, scope `public_repo` | +| — | GitHub: Incentivized stars OK? | 🚫 NO — ToS prohibits star-for-reward exchanges | + +--- + +## Architectural Implications + +### For a Reselling Marketplace +- **❌ Direct reselling is illegal** under OpenRouter ToS §7.4 +- **✅ Alternative:** Use OpenRouter Organizational Accounts + Management API + - Create org with per-customer authorization + - Each customer gets isolated access, controlled by your admin settings + - Billing rolls up to your master account + - OpenRouter assumes the intermediary risk, not you alone + +### For a "Free Trial with GitHub Star Incentive" +- **❌ Cannot tie API access to starred repo** +- **✅ Alternatives:** + - Use free models (no credit card) + free tier rate limits + - Offer paid access separate from star-checking + - Check star as optional metric (analytics only, no access gating) + +### For Usage & Billing Tracking +- **Use Management API + polling `limit_remaining` field** to track spend +- **Not production-grade:** No real-time usage API documented. Dashboard is GUI-only. +- **Recommended:** Generate reports from OpenRouter Activity export + locally track per-customer allocation + +--- + +## Unresolved Questions + +1. **Usage API endpoint:** Is there a documented REST endpoint to query "usage for key X in time range Y"? Current research found only dashboard + generation ID polling. Recommend: check OpenRouter support or API reference directly. + +2. **Organizational Account billing limits:** Can you set per-user spending limits within an Org, or only per-account master limit? Affects whether Org API is viable for reselling. + +3. **Key rotation + zero-downtime:** Does creating a new key + switching while old key still has requests "in flight" cause errors? Management API docs mention zero-downtime, but implementation not clear. + +4. **GitHub star verification cadence:** If star is removed, how long until GET `/user/starred` returns 404? Is it instant or cached? Affects "star-for-access" revocation logic. + +5. **OpenRouter content policy**: Full list of prohibited use cases not available in ToS snippet. Recommend reviewing Section 7 + upstream provider terms for your use case (e.g., agents, reselling). + +--- + +## Sources + +- [OpenRouter Provisioning API Keys](https://openrouter.ai/docs/features/provisioning-api-keys) +- [OpenRouter API Key Creation](https://openrouter.ai/docs/api/api-reference/api-keys/create-keys) +- [OpenRouter API Key Deletion](https://openrouter.ai/docs/api/api-reference/api-keys/delete-keys) +- [OpenRouter API Rate Limits](https://openrouter.ai/docs/api/reference/limits) +- [OpenRouter MiniMax M3](https://openrouter.ai/minimax/minimax-m3) +- [OpenRouter GLM-5](https://openrouter.ai/z-ai/glm-5) +- [OpenRouter Kimi K2.6](https://openrouter.ai/moonshotai/kimi-k2.6) +- [OpenRouter Terms of Service](https://openrouter.ai/terms) +- [OpenRouter Organization Management](https://openrouter.ai/docs/cookbook/administration/organization-management) +- [OpenRouter Rate Limits FAQ](https://openrouter.zendesk.com/hc/en-us/articles/39501163636379-OpenRouter-Rate-Limits-What-You-Need-to-Know) +- [OpenRouter Free Models (Jun 2026)](https://costgoat.com/pricing/openrouter-free-models) +- [GitHub REST API: Starring](https://docs.github.com/en/rest/activity/starring) +- [GitHub Fake Star Economy](https://gigazine.net/gsc_news/en/20260421-github-fake-star-economy) +- [GitHub Fake Stars Investigation](https://awesomeagents.ai/news/github-fake-stars-investigation/) +- [Six Million Suspected Fake Stars Study](https://arxiv.org/html/2412.13459v2)