← All guides

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=100

A user with both memberships resolves an ai-credits cap of 12020 (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>