Skip to content

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.

Pick it in the setup wizard:

Terminal window
pnpm create extstart --billing-model hybrid-credits # or credits-only

hybrid-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.

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.

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.

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 wall
refunded 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.

  • Firestore → customers/{uid}: creditsRemaining, creditsAllowance, packCreditsRemaining, written by the webhook and /credits/consume only. Rules deny all client writes, including the credit_ledger subcollection.
  • GET /credits/balance (Bearer token) → { balance, allowance }, the read path for tooling outside the extension.
  • pnpm doctor verifies the pack/metered lookup keys referenced by your site.config.ts exist with their credit metadata, and that the webhook is subscribed to invoice.paid.
  • STRIPE_SECRET_KEY=sk_test_… pnpm test:lifecycle includes 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.