Core concepts
Credit systems
Name your own credits, decide when they mint, and let the gateway spend them down.
Seams meters every call in money. Your credit system is the product vocabulary on top: what one credit means, who gets how many, and what happens when they run out. Everything below is scoped to one application, the active sk_, not shared across products.
Two paths: wallet top-up vs plan allotment
These are different nouns. Mixing them up is the usual way to mis-price a pack or attach the wrong unit.
| Wallet top-up | Plan allotment | |
|---|---|---|
| What it is | Money on the end user's wallet | An entitlement pool minted from a bundle include |
| Unit on the wire | Always micros (dollars on the CLI) | micros or credits, set on the include |
| How you mint | seams users grant / grants.issue | seams bundles include … |
| Tied to a bundle? | No. Optional --bundle only picks who gets credited | Yes. The include lives on that plan |
| When it refills | When you grant again (checkout, support, trial) | Each period, daily, or once, per the include |
grants.issue has no unit field and never credits a named-credit pool. If your UI sells “credits” as a wallet pack, convert to micros at a fixed rate before you call it.
CLI: top up a wallet
Amount is dollars. Recipients are named one way: specific end users, everyone on a bundle, or everyone in the application.
seams users grant 20.00 --user ada --reason top_up
# --bundle here means "everyone on pro", not "credit in pro's unit"
seams users grant 5.00 --bundle pro --reason apology
seams users grant 2.00 --all --reason welcomeawait seams.grants.issue({
endUsers: ["ada"],
amountUnits: "20000000", // $20.00
reason: "top_up",
idempotencyKey: stripeEvent.id,
})seams.grants.issue(
end_users=["ada"],
amount_units="20000000", # $20.00
reason="top_up",
idempotency_key=stripe_event.id,
)CLI: attach an allotment to a plan
The include is the template. Subscribing an end user to that bundle opens the pool; the gateway spends entitlement pools before the money wallet.
seams bundles include pro monthly-credits \
--name "Monthly credits" --unit credits --amount 500
# optional pack sold separately (once), still defined on the bundle
seams bundles include pro pack-500 \
--name "500 credits" --unit credits --amount 500 \
--price 5.00 --no-recurringawait seams.bundles.include("pro", {
slug: "monthly-credits",
displayName: "Monthly credits",
unit: "credits",
amount: "500",
price: null,
thenOnDemand: false,
recurring: true,
})seams.bundles.include(
"pro",
models.IncludeGrantRequest(
slug="monthly-credits",
display_name="Monthly credits",
unit="credits",
amount="500",
price=None,
then_on_demand=False,
recurring=True,
),
)Named credit currencies
A named credit currency is defined once for your organization, then attached to a bundle. One bundle sells in at most one named currency; several includes on that bundle (monthly allotment, bonus, purchasable pack) share it. Across bundles you choose: reuse the same currency everywhere, or give each plan its own.
| Setup | What it means |
|---|---|
| Share one currency | starter and pro both point at the same unit, one “credit” means the same spend everywhere |
| Different per plan | Each bundle has its own unit (or its own value override), “credit” on Pro can be worth more than on Free |
| Several includes, one currency | One bundle can mint many allotments; they all burn in that bundle’s currency |
| Money beside credits | The wallet is always micros and independent of the plan currency; top-ups never inherit the bundle’s unit |
The meter still prices usage in sell-micros. Named credits convert with the rate on the bundle (or the currency’s default). Customers should never see micro-dollars.
What you design
| Choice | What it decides |
|---|---|
| Unit | Dollars on the wallet, or a named currency on the plan |
| Mint | At sign-up, on a plan period, after a pack purchase, or by support |
| Spend order | Plan allotments first, then wallet top-ups, then overage if allowed |
| Ceiling | A key or bundle cap so a loop cannot empty the account |
| Empty state | A 402 your product turns into top-up, wait, or upgrade |
UI credits that grant money
When you sell “credits” but land them on the wallet, pick one rate and keep it in one module. The portal can show dollars; your product shows credits.
// 1 credit = $0.01 of usage → micros = credits × 10_000
const MICROS_PER_CREDIT = 10_000n
export function grantCredits(userId: string, credits: number, paymentId: string) {
return seams.grants.issue({
endUsers: [userId],
amountUnits: String(BigInt(credits) * MICROS_PER_CREDIT),
reason: `pack:${credits}`,
idempotencyKey: paymentId,
})
}When the balance is gone
| Code | What your product should do |
|---|---|
insufficient_credit | Offer a pack, upgrade, or wait for the next period |
spend_limit_exceeded | Stop the run; the period or key cap is the limit, not the wallet |
Pass your payment event id as idempotencyKey on every grant after checkout. Retries must not mint twice.
Pricing strategies
Usage-based, prepaid, plans, outcomes.
Worked example
A writing product with welcome credit, packs, and a monthly plan.
Prepaid credits
Markup at top-up and the Stripe webhook shape.
Subscriptions and overage
Recurring grants and then-on-demand.
Wallets and holds
available = balance − reserved.