Credits
Sell a subscription with a monthly credit allowance plus purchasable top-up packs: the model AI extensions like Monica and Sider run on. This page turns it on, creates the Stripe products, and puts your first feature on the meter.
Enable the model
Section titled “Enable the model”Pick it in the setup wizard:
pnpm create extstart --billing-model hybrid-credits # or credits-onlyhybrid-credits is subscription + allowance + packs; credits-only sells packs without a subscription. Both default the gate preset to metered, which raises a dismissible top-up wall the moment the balance hits 0.
Seed the metered prices
Section titled “Seed the metered prices”pnpm seed:stripe (part of the one-time billing setup) creates premium_metered_monthly (a subscription with a 1,000-credit monthly allowance) and the packs credits_pack_small / credits_pack_large.
Credit amounts live in Stripe price metadata: credits on packs, monthly_credits on the metered plan. At checkout the server copies them into session/subscription metadata, so the webhook can grant without an extra API call. The client never supplies an amount.
Meter a feature
Section titled “Meter a feature”Consume first, then work:
const result = await sendMessage("creditsConsume", { feature: "summarize" });if (!result.ok) return; // exhausted (top-up wall is up) or offline — stop// … do the metered work …The background attaches the ID token, calls POST /credits/consume, and mirrors the fresh balance into storage.local.entitlements. The gate engine raises the top-up wall when the balance reaches 0 and clears it when a pack purchase or the monthly reset raises it again. Accounts without credits (no metered plan, no packs) get ok: true; instrumented features simply run free under the other billing models.
How credits move
Section titled “How credits move”buy a pack checkout (lookup_key) → webhook grant (+N, idempotent by event id)monthly renewal invoice.paid → allowance reset (packs + fresh allowance)run a feature creditsConsume message → POST /credits/consume → Firestore transaction: decrement + ledger entry → 402 when exhausted → the metered preset raises the top-up wallrefunded pack charge.refunded → credits clawed back (clamped at 0)Consuming spends the allowance portion first. Packs roll over forever; unused allowance doesn’t (it’s replaced, not stacked, on each invoice.paid). Cancelling the subscription drops the remaining allowance but keeps pack credits.
Offline consumes are denied, not queued. A metered feature needs the backend to do its work anyway, and an offline queue would be a client-side free-usage lever.
Grants are webhook-written only, and POST /credits/consume can only ever lower a balance. Each consume writes a deterministic ledger entry (customers/{uid}/credit_ledger/consume_<idempotencyKey>), so a retried request replays its recorded outcome instead of double-spending.
Test it
Section titled “Test it”- Firestore →
customers/{uid}:creditsRemaining,creditsAllowance,packCreditsRemaining, written by the webhook and/credits/consumeonly. Rules deny all client writes, including thecredit_ledgersubcollection. GET /credits/balance(Bearer token) →{ balance, allowance }, the read path for tooling outside the extension.pnpm doctorverifies the pack/metered lookup keys referenced by yoursite.config.tsexist with their credit metadata, and that the webhook is subscribed toinvoice.paid.STRIPE_SECRET_KEY=sk_test_… pnpm test:lifecycleincludes a real test-clock scenario: the first invoice grants the allowance; a simulated month later the renewal resets it, packs surviving.
Price the model honestly. “Unlimited” plans with hidden fair-use caps are the most common credibility sinkhole in AI-extension reviews: show the CreditMeter, price the allowance for real usage, and let heavy users buy packs. Never gate the balance UI itself.
