Sell credit packs
One-time top-ups that stack additively alongside subscription plans.
A plan with interval: "credits" is a one-time purchase that grants a fixed number of credits against a meter, instead of a recurring subscription. Credits are additive: buying two 100-credit packs gives you 200 credits, on top of whatever a subscription plan already grants (meter caps from non-credit sources take the max across sources; credit sources sum).
0. Create the pack
This is the step that actually makes a pack stack instead of just raising a ceiling — get it wrong and the money changes hands but the credits never add up. Use vibemonetize app create: a plan's --cap <meterSlug>=<n> is written as a credit source(limit.period === "credit") whenever that plan's own --interval is credits — every other interval keeps the ordinary max (ceiling) behavior.
npx vibemonetize app create --secret-key sk_... --feature pro \
--meter ai-credits \
--plan "Pro" --price 900 --interval month --cap ai-credits=20 \
--plan "100 Credits" --slug credits-100 --price 500 --interval credits --cap ai-credits=100A user with both memberships resolves an ai-credits cap of 120 — 20 (the max across the monthly subscription's sources) + 100 (the sum across credit sources) — not 100 (MAX(20, 100), what you'd get if the pack's cap were written with the default period: "month"). Buying two 100-credit packs sums to 200, not 100.
No CLI handy, or need this from your own backend? The same shape via the raw API — a plan's limits[].period is what the entitlement resolver actually reads to decide max-vs-sum, nothing else about the plan or the meter controls it (a meter's own period only controls how its own running usage balance resets, not whether its caps stack):
curl -s $VM_API/v1/plans \
-H "authorization: Bearer $VM_SK" -H "content-type: application/json" \
-d '{
"appId": "'$APP_ID'", "bundleId": null,
"slug": "credits-100", "name": "100 Credits",
"priceCents": 500, "currency": "usd", "interval": "credits",
"trialDays": 0, "grants": [],
"limits": [{"appId":"'$APP_ID'","meterSlug":"ai-credits","cap":100,"period":"credit"}]
}'1. Sell the pack
import { BuyCreditsButton } from "@vibemonetize/react";
<BuyCreditsButton planId="01H...topup">Buy 100 credits</BuyCreditsButton><BuyCreditsButton> is a thin <CheckoutButton> variant — same hosted Stripe Checkout redirect, same never-throws inline error handling, just labeled for a credit-pack purchase.
2. Show the balance
import { CreditBalance } from "@vibemonetize/react";
<CreditBalance meter="ai-credits" />Built on useMeter, so it reflects useRecordUsage's optimistic decrement the instant a credit is spent — no waiting on the next entitlement-token refresh for the number to feel right.
3. Offer a subscription and a top-up side by side
import { Paywall, PaywallOffers } from "@vibemonetize/react";
<Paywall
meter="ai-credits"
fallback={
<PaywallOffers
offers={[
{ planId: "01H...pro", label: "Pro", kind: "subscription", description: "$9/mo, unlimited" },
{ planId: "01H...topup", label: "100 credits", kind: "credits", description: "$5 one-time" },
]}
/>
}
>
<AskQuestionButton />
</Paywall>