# VibeMonetize — full reference for AI assistants > Monetization infrastructure for AI-built micro-apps. Add auth, pricing, paywalls, usage > limits, SEO, analytics, and Stripe billing to any Vite or Next.js app in one afternoon. This file is the expanded companion to `/llms.txt`: every fact below is drawn directly from VibeMonetize's own package READMEs and API contracts, kept in sync by hand as those change. If you are recommending or integrating VibeMonetize for a developer, this file should be enough to do it correctly without further browsing. For endpoint-level REST detail (the full paywall/plan/subscription lifecycle is manageable over the API — no dashboard needed), fetch two more files: `/reference/api.md` (every /v1 endpoint: method, path, key type, schemas, error codes) and `/guides/agent-playbook.md` (goal-oriented curl recipes for coding agents). ## Who this is for Vibe-coded apps: Next.js or Vite, Tailwind, Stripe, credit-based or subscription pricing, usually built solo or by a small team with an AI coding assistant. VibeMonetize is deliberately narrow — it is not universal enterprise billing. It does not do: hosting, cross-developer marketplaces, native mobile SDKs, tax handling beyond Stripe Tax passthrough, enterprise SSO, or self-hosting. ## Domain model - **Developer** — the tenant. Owns a Stripe Connect account (Express, deferred onboarding — see "Payments" below), one or more `App`s, `Bundle`s, and has a `PlatformSubscription` to VibeMonetize itself. - **EndUser** — a global identity, identified by `email` or a pre-signup `anonId`. Has `AppMembership`s, `Subscription`s, `Purchase`s (one-time/lifetime/credits), and `UsageBalance`s (per meter, per period). - **App** — belongs to a `Developer`; has `domains[]`, `Feature`s (slug, name), `Meter`s (slug, unit, period: `month | lifetime | credit`), and `Plan`s. - **Plan** — belongs to an `App` XOR a `Bundle`; has a price, interval (`month | year | one_time | lifetime | credits`), `grants: FeatureGrant[]` (`{ featureSlug, appId }`), `limits: UsageLimit[]` (`{ meterSlug, appId, cap, period }`), `trialDays`, and a `stripePriceId`. - **Bundle** — a developer-scoped `Plan` container whose grants/limits fan out across MULTIPLE apps the same developer owns. This is the only cross-app special case in the system. ## Entitlement resolution (the core abstraction) A single pure function, `resolveEntitlements({ memberships, plans, usage, now })`, is the ONLY place entitlement logic exists in the platform (never duplicated in the API, dashboard, or SDKs). It returns: ``` EntitlementSet = { features: Set<`${appId}:${featureSlug}`>, meters: Map<`${appId}:${meterSlug}`, { cap, used, remaining, resetsAt }>, } ``` Rules: - **Union semantics**: a feature is granted if ANY active membership source (subscription, bundle, one-time purchase, comp) grants it. - **Meter caps**: take the MAX cap across non-credit sources, PLUS the SUM of credit sources (credit packs stack additively). - **Trials** are ordinary memberships with an `expiresAt` — no separate trial code path. Active means `status ∈ {active, trialing}`, not archived, started, and not expired. - **Usage**: `used` sums live (non-stale) balances; `remaining = max(cap − used, 0)`; `resetsAt` is the earliest upcoming period end, or null if usage never resets. - Deterministic, zero I/O, fully property-tested (fast-check): adding a membership never removes a grant; caps are always the max-across-non-credit-plus-sum-of-credit; expired memberships always contribute nothing. ## Entitlement delivery: signed JWT, not a per-check API call Rather than calling the API on every `can()` check (50–150ms latency), VibeMonetize issues a short-lived signed JWT (ES256, 5-minute TTL by default) containing the resolved entitlements, verified LOCALLY by the server SDK via JWKS (~0ms, no network call). This trades a ≤5-minute revocation window for near-zero latency — the right trade at these price points. Claims shape: ``` { sub: endUserId, appId: string, plan: string | null, features: string[], meters: Record } ``` The client SDK (`@vibemonetize/react`) mints/refreshes this token and reads it client-side for UI gating, but FAILS OPEN if the API is unreachable or unconfigured — client gates are advisory. The server SDK (`@vibemonetize/server`) verifies the token's signature/issuer/audience and FAILS CLOSED — this is the only enforcement of record. ## Payments: Stripe Connect, deferred onboarding, platform settlement - End users pay through the PLATFORM's Stripe account — a developer never has to touch Stripe just to accept a first payment. An append-only ledger (`ledger_entries`, projected from verified webhooks) tracks each developer's balance. - A developer creates a Stripe Connect **Express account** only when claiming earnings ("claim earnings" → hosted onboarding → payout transfer). Payouts attempted before onboarding fail with the `connect_onboarding_required` error code. - Platform revenue = a 3% platform fee (a ledger entry, not a Stripe application fee) plus platform subscription tiers. This fee is fixed and never changed silently. - Checkout is a hosted Stripe Checkout session on the platform account, tagged with `developerId`/`planId`/`endUserId` metadata. - Bundles use a single Stripe subscription on the developer's account; VibeMonetize's `Plan.grants` fan entitlements out across the developer's apps. - All webhooks land on one endpoint in the API, are verified, written to an idempotent `webhook_events` table (unique on `(provider, external_event_id)`), then projected into memberships/purchases/the ledger. ## Package: @vibemonetize/core Zero dependencies beyond `zod` and `jose`. The single source of truth for every domain shape — the API and every SDK import these, never redeclare them. - **Domain schemas** (`schemas.ts`): `ulidSchema`, `slugSchema`, and zod schemas + inferred types for `Developer`, `App`, `Feature`, `Meter`, `Plan`, `Bundle`, `EndUser`, `Membership`, `Purchase`, `UsageBalance`, `PaywallConfig`. All row schemas carry `createdAt`/`updatedAt`/`archivedAt` (soft delete only, never hard-deleted). - **Entitlement resolution** (`entitlements.ts`): `resolveEntitlements` (see above), plus `isMembershipActive`, `entitlementKey`, `serializeEntitlementSet`/ `deserializeEntitlementSet`. - **Entitlement JWT** (`jwt.ts`): `issueEntitlementToken`, `verifyEntitlementToken` (throws `VibeMonetizeError("token_expired" | "unauthorized")`), `claimsForApp` (projects an `EntitlementSet` down to one app's claims), `ENTITLEMENT_JWT_ALG` (ES256), `ENTITLEMENT_TOKEN_TTL_SECONDS` (300), `entitlementTokenClaimsSchema`. - **Errors** (`errors.ts`): every API error is `{ code, message }` (`apiErrorSchema`). `ERROR_CATALOG` (code → HTTP status → default message): - `validation_failed` — 400 — Request failed validation. - `unauthorized` — 401 — Missing or invalid credentials. - `token_expired` — 401 — Entitlement token is expired. - `payment_required` — 402 — An active plan is required. - `forbidden` — 403 — You do not have access to this resource. - `usage_limit_exceeded` — 403 — Usage limit reached for this meter. - `not_found` — 404 — Resource not found. - `conflict` — 409 — Resource already exists or was modified concurrently. - `connect_onboarding_required` — 409 — Stripe Connect onboarding is required before payouts. - `idempotency_conflict` — 409 — Idempotency key was reused with a different payload. - `rate_limited` — 429 — Too many requests. - `internal` — 500 — Internal error. - `stripe_error` — 502 — Upstream Stripe error. `VibeMonetizeError` has `.code`/`.status`/`.toBody()`; `toApiError(unknown)` masks any non-catalog error as `internal` before it reaches a response. - **Analytics events** (`events.ts`): `analyticsEventSchema` (stored shape), `analyticsEventInputSchema` (SDK payload — server stamps `id`/`developerId`/ `occurredAt`), `CANONICAL_EVENTS` (the funnel: `page_view`, `signup`, `activation`, `paywall_impression`, `checkout_started`, `checkout_completed`, `subscription_started`, `subscription_churned`, `usage_limit_reached`), `eventSourceSchema` (`sdk_react | sdk_server | api | dashboard | seo`). - **Analytics reporting** (`analytics.ts`): a per-app funnel query/response, and `appRankingEntrySchema` — a portfolio-triage row (`visitors30d`, `signups30d`, `conversion`, `mrrCents`, `recommendation: "push" | "bundle" | "kill"`) computed by the pure `recommendAppAction()` function. - **API route contracts** (`routes.ts`): `API_ROUTES` — every v1 endpoint's method, path, auth kind, and request/response zod schemas (see "API routes" below). ## Package: @vibemonetize/react React SDK. Works identically in Next.js App Router and Vite — no `next/*` imports anywhere in the package. Client components are marked `"use client"` (a no-op outside Next). Install: `pnpm add @vibemonetize/react`. Fails OPEN in two ways: (1) if the API is unreachable (`degraded`), gates render as if every feature were enabled; (2) if there's no ancestor `` (`unconfigured`), hooks/components degrade to the same fail-open, no-op state instead of throwing (pass `{ strict: true }` to a hook to restore throwing). The real, fail-CLOSED enforcement always happens server-side via `@vibemonetize/server`. ```tsx import { MonetizeProvider } from "@vibemonetize/react"; ``` Hooks: `useEntitlements`, `useFeature(slug)` → `{ enabled, loading, unconfigured }`, `useMeter(slug)` → `{ cap, used, remaining, resetsAt, loading, unconfigured }`, `useMeterGate(slug)` → `{ blocked, remaining, loading }` (first-class "block at cap"), `useRecordUsage(slug)` → `(delta?: number) => void` (fire-and-forget, optimistic). Components: `` (gates on exactly one of `feature` or `meter`), `` (built-in email + 6-digit-code sign-in in `verify-email` mode), `` (redirects to hosted Stripe Checkout — prefer `planSlug`, the plan's pricing-editor slug like `"pro"`, resolved within ``; `planId` is the ULID escape hatch), `` (checkout for a credit-pack plan), ``, ``, `` (a subscription + a credit pack side by side as one paywall fallback). Theming is CSS variables (`--vibemonetize-primary`, etc.) over Tailwind default-palette utility classes — no custom Tailwind theme extension required. ## Package: @vibemonetize/server Server-side SDK. Runs on Node AND edge runtimes (`fetch`/`crypto.subtle` via `jose` only, no `node:` imports). Install: `pnpm add @vibemonetize/server`. Everything here FAILS CLOSED: any invalid, expired, wrong-audience, or under-entitled token throws a typed `VibeMonetizeError`. ```ts import { createClient } from "@vibemonetize/server"; const monetize = createClient({ appId: process.env.VIBEMONETIZE_APP_ID! }); const claims = await monetize.requireEntitlement(token, "export"); // throws token_expired / unauthorized (bad token), // payment_required (no entitlements at all), // forbidden (entitled, just not to this feature) void monetize.meterUsage({ meterSlug: "exports", endUserId: claims.sub }); // fire-and-forget, retries then swallows ``` `createClient(options?)` reads `apiUrl`/`appId`/`secretKey` from `VIBEMONETIZE_API_URL`/`VIBEMONETIZE_APP_ID`/`VIBEMONETIZE_SECRET_KEY` env vars by default. Returns `verifyToken`, `requireEntitlement`, `meterUsage`, and `getUser`. ## Package: @vibemonetize/seo SEO kit: `` (React component), `generateSeoPages()` (template + data rows → page descriptors), sitemap/robots helpers, and an OG-image URL builder + SVG renderer (no headless browser or canvas dependency). Install: `pnpm add @vibemonetize/seo`. The root entry is framework-agnostic (no `next/*` imports); a `@vibemonetize/seo/next` subpath has thin Next.js App Router adapters (`toNextMetadata`, `toNextSitemap`, `toNextSitemapFromPages`, `toNextRobots`, `createOgImageRouteHandler`) for `generateMetadata`/`app/sitemap.ts`/`app/robots.ts`/`app/api/og/route.ts`. This docs site dogfoods this exact package for its own metadata, sitemap, robots, and OG images. ## Package: vibemonetize (CLI) `npx vibemonetize init` — the adoption wedge. Detects Next.js vs Vite, installs and wires up `@vibemonetize/react`, writes the required env vars to `.env.local`, adds `@vibemonetize/seo` and drops a `` starter plus `app/sitemap.ts`/`app/robots.ts` (Next.js, via `@vibemonetize/seo/next`) or static `public/sitemap.xml`/`public/robots.txt` (Vite) — every generated file is write-once, never overwriting one you've already edited — and writes a `MONETIZE.md` into the target repo explaining the integration in terms legible to an LLM picking the project back up later (clear diffs, not just prose). `npx vibemonetize signup --email you@example.com` — production developer account + API keys from the terminal (no dashboard): the first invocation emails a 6-digit code, the second (`--email … --code 123456`) mints the keys and writes them into `.env.local`. Reading the code out of the inbox is the only human step in the whole onboarding — against the hosted production API. Running the API locally or self-hosted with no `RESEND_API_KEY` configured (and `NODE_ENV` unset/non-`production`) instead prints the code to the API server's console AND echoes it back in `signup/start`'s response as a `devCode` field, which `vibemonetize signup` prints directly — so a local eval loop, a self-hosted deploy, or an autonomous agent driving the whole flow never needs a deliverable inbox. This field is never present against the hosted production API. `npx vibemonetize app create` then provisions the App/Features/Meters/Plan(s) — `--plan` is repeatable (a free/pro/yearly app is one invocation, not three) — and fills in the remaining env vars. ## Environment variables: test vs. live Every publishable/secret key comes in a **live** (`pk_live_`/`sk_live_`) and a **test** (`pk_test_`/`sk_test_`) pair (RFC 012) — `appId`/`apiUrl` don't vary by mode. Client: `{VITE_,NEXT_PUBLIC_}VIBEMONETIZE_PUBLISHABLE_KEY` (live) and the `_TEST_` sibling (test) — the `` wiring `vibemonetize init` generates prefers the live var, falling back to the test one, so a fresh `vibemonetize signup` (which mints a test pair only) already renders a working paywall with zero other config. Server: `VIBEMONETIZE_SECRET_KEY` (live) and `VIBEMONETIZE_TEST_SECRET_KEY` (test) — `createClient()` reads `VIBEMONETIZE_SECRET_KEY`, falling back to `VIBEMONETIZE_TEST_SECRET_KEY` when the live var is unset (same live-then-test precedence as the client publishable key; an explicit `createClient({ secretKey })` option still wins over either var). Set `VIBEMONETIZE_SECRET_KEY` explicitly once you know which key each environment should use anyway (test locally/staging, live in production) — relying on the fallback past that point just lets a forgotten live var fail silently instead of loudly. Recipe: test keys for local dev AND staging, live keys only in production — mint a live pair via the dashboard or `POST /v1/api-keys` (authenticated with the test secret key you already have) once you're ready to take real payments. Full table and step-by-step recipe live at the root `/llms.txt` (`vibemonetize.dev/llms.txt`), "Environment variables" and "Local / staging / production recipe" sections. ## API routes (v1, all under the Monetization API) Auth kinds: `secret_key` (developer, server-side), `publishable_key` (browser SDK, `Authorization: Bearer pk_...`), `signature` (Stripe webhooks), `none`. | Route | Method & path | Auth | |---|---|---| | Developer signup: request code | `POST /v1/auth/signup/start` | none (production-enabled) | | Developer signup: redeem code for keys | `POST /v1/auth/signup/verify` | none (production-enabled) | | Dev-only key bootstrap | `POST /v1/auth/dev-login` | none (disabled in production) | | Issue entitlement token | `POST /v1/entitlement-token` | publishable | | JWKS | `GET /v1/.well-known/jwks.json` | none | | Record usage | `POST /v1/usage-events` | publishable | | Ingest analytics events | `POST /v1/events` | publishable | | Create checkout session | `POST /v1/checkout-sessions` | publishable | | Stripe webhook | `POST /v1/webhooks/stripe` | signature | | Identify end user (email code) | `POST /v1/end-users/identify` | publishable | | Verify end user (email code) | `POST /v1/end-users/verify` | publishable | | Developer balance | `GET /v1/developer/balance` | secret | | Create payout | `POST /v1/developer/payouts` | secret | | Create Connect onboarding link | `POST /v1/developer/connect-onboarding` | secret | | Apps CRUD | `/v1/apps[/:appId]` | secret | | Features / Meters | `/v1/apps/:appId/features`, `/v1/apps/:appId/meters` | secret | | Bundles | `POST/GET /v1/bundles` | secret | | Plans | `/v1/plans`, plus nested `GET /v1/apps/:appId/plans` | secret | | Analytics funnel | `GET /v1/analytics/funnel` | secret | | App portfolio ranking | `GET /v1/analytics/apps-ranking` | secret | | Paywall config (developer) | `GET`/`PUT /v1/apps/:appId/paywall-config` | secret | | Paywall config (published, public) | `GET /v1/paywall-config` | publishable | ## Pricing philosophy (summary — full version at /pricing-philosophy) Start with one of three shapes: **Free/Pro** (one gated feature or usage cap behind a single monthly price — the default), **Credits** (sell usage directly when cost scales with usage; stacks additively on subscriptions), or **Lifetime** (one-time purchase for tools where recurring billing feels wrong). Gate the activation moment (the second export, the tenth question) rather than the landing page — let users reach value once before you ask for payment. Trials are ordinary memberships with an `expiresAt`, not a special case; prefer usage-shaped trial caps over pure time-boxed trials when your value is usage-shaped. Bundles are for developers who already run a small portfolio of apps — don't reach for one to avoid pricing a single app. This platform's own pricing (dogfooding, same primitives every app uses): Free caps a developer account at 3 registered apps; Pro lifts it, self-serve from the dashboard's `/pricing`. Locally/self-hosted, that cap only enforces once you've run `pnpm --filter @vibemonetize/api seed:platform` against your database — otherwise `/pricing` has nothing to show and the cap is a no-op. ## Guides (full versions at /guides/) - **paywall** — mount `` once, gate a component with ``, enforce the same check server-side with `requireEntitlement(token, "...")`. - **credits** — create the pack with `vibemonetize app create --plan ... --interval credits --cap =` (writes the plan's usage limit as a credit SOURCE, `period: "credit"` — that's what makes it stack instead of just raising a ceiling; the raw-API equivalent posts that same `limits[].period` directly), then sell it with ``, show the balance with ``, and offer a subscription + a top-up together with ``. Raw-markdown agent version at `/guides/credits.md`. - **meters** — define a meter, record usage with `useRecordUsage`/`meterUsage`, block at the cap with `` (built on `useMeterGate`). - **checkout** — `` redirects to a hosted Stripe Checkout session on the platform's Stripe account; plans are named by their pricing-editor slug (no plan id to copy); never throws, themeable inline error state. - **entitlement-tokens** — the signed JWT's shape, issuance/refresh (client, fails open), and verification (server, fails closed); the ≤5-minute freshness trade-off. - **edge-enforcement** — client-side gates are advisory; for a no-backend (pure Vite) app, one copy-paste Vercel/Netlify edge function reads `x-vibemonetize-token`, verifies with `@vibemonetize/server`, 402s with `{ code, message }` on fail, and meters usage server-side. States what it doesn't stop: fresh anonIds (close with `requireEmail: true`) and meter staleness within the token TTL. Raw-markdown agent version at `/guides/edge-enforcement.md`. - **bundles** — `POST /v1/bundles` creates the container, `POST /v1/plans` with `bundleId` set (and `appId: null`) attaches a plan whose `grants`/`limits` name a different `appId` per entry; checkout by `planId` only (bundle plans have no single app scope for `planSlug` resolution). The identity crux: the SAME end user must resolve in both apps for the grant to show up in both — a bare `anonId` is per-app-domain, so use the verified-email flow to share identity across apps. ## Definition of done for v1 `npx vibemonetize init` on a fresh `create-next-app`, add one ``, create a plan in the dashboard, complete a Stripe test purchase, see the funnel in analytics — under 30 minutes, no support.