guide

Credits

Let people buy credits and spend them in your app. Credits are a balance each person holds and your product spends; Gemmein keeps the balance.

Example: a 100-credit pack and a 20-credit tool

On the Payments page, add a product named "100 credits", set how it is sold (a Stripe Payment Link, or a relay for another provider) and set Grants credits to 100. Each confirmed purchase adds 100 to the person's balance; two packs make 200. A full refund claws back what is still unspent, floored at zero; a partial refund moves nothing. The app shows the balance and sells the pack:

const { balance } = await g.credits.balance()          // the signed-in person's balance now
if (balance === 0) await g.payments.buy("100 credits")   // the pack is a product like any other

Each chat through the AI tool then costs the credits set on it, 20 in this case. For a spend of your own, such as an export or a render, your server spends by person id with a reason and a dedupe key:

const r = await gemmeinServer(process.env.GEMMEIN_SECRET_KEY).spendCredits(person.id, {
  amount: 1, reason: "export", key: `export:${jobId}`
})
// { ok: true, spent: 1, deduped: false, balance: { before: 12, after: 11 } }
// GemmeinError 402 credits_exhausted: "this person has 0 credits — the spend needs 1"
  • Your secret key needs the "Spend a person's credits" permission, ticked when you create the key.
  • Repeat a spend with the same dedupe key (key) and it answers deduped: true, spent: 0 and the same event. Nothing moves.
  • A dedupe key is scoped to the person and namespaced, so one order id reused for two people charges both. Rotating the secret key never charges twice.
  • holdings.credits is { balance } on verifySession() and holdings(), so one verify answers who the person is, what they hold, and how many credits they have.

What it is. A number on each person, kept by Gemmein and separate from access. Every movement is one line on the person's ledger, and a spend never takes the balance below zero. A refunded or downgraded plan can leave a debt, which the next renewal or pack pays off first.

What it does.

  • Grant. The balance goes up when a person buys a pack, when a plan they pay for renews, when a relay grants some, or when you comp them from the dashboard (see Where credits come from). A plan period starts only with a period's own invoice: a proration, a one-off invoice or a zero-amount invoice starts none. A full refund of a period's invoice claws back what is left of that period's credits; the default plan carries none.
  • Spend. Your server spends by person id, with a reason and a dedupe key so a retry never spends twice. An AI tool call costs what you priced that tool at: a fixed price per call by default, or, on a tool priced per token (unit: "tokens"), a price per 1,000 tokens by default, settled at the provider's usage, up to a ceiling reserved before the call. See AI tools.
  • Ledger. The dashboard shows the meter, the grants and the ledger on the person's page, and lets you comp by hand, with an expiry date if you want one. A relay can run when someone runs low.

What it does not do.

  • Spend below zero. A spend larger than the balance is refused whole, never partly.
  • Open access. A locked collection still asks for the plan or the product, and a person holding 500 credits and no plan is still refused.
  • Seats or quotas. There are none.

When you need something else.

  • Access that switches on with a plan: see Payments.
  • A bill at the end of a period, by token or by minute: Gemmein does not do this. Sell packs up front.
  • A spend from the browser: nothing spends there. Your server, a relay or an AI tool moves credits, so put the spend behind your server or an AI tool.

Example. A 100-credit pack and a 20-credit tool, above.

Where credits come from

SourceHowLedger line
A purchaseA product with "Grants credits" (1 to 1,000,000). Added once per confirmed payment, in the same step that grants the product's access; a purchase by an email that has never signed in creates the person and credits thempurchase, the product's name as the reason
A refundA full refund claws back what that purchase's own ledger line granted, at most what is still unspent, whatever the product says today. Partial refunds move nothingrefund_clawback, actor system
A plan"Credits each period" on the plan, granted on each billing cycle's paid invoice (or, for a plan sold through a relay, each payment reference the relay's ref names), once per period, with your rollover rule: carry (the default), none, or carry up to a capgrant, "<plan> plan"; an earlier period that ends under the rule is one expire line per grant
The free planThe free (default) plan can carry an allowance too: "Credits each period" grants once per person per calendar month (UTC), at their first spend that month — never by a job over everyone — and every balance read shows it as spendable before it lands; a "welcome" allowance grants once, only to a person who has never held a paid plangrant, "Free plan · <month>" or "Free plan · welcome"
The dashboard"Grant credits" on the person's page, up to 100,000 per action, with a note and an optional expiry dategrant, your note, attributed to you
A relaygrant_credits { amount (1 to 10,000), reason?, expiresInDays? }, once per event; a promotional grant, the first to burngrant, "relay: <reason>"
An expiryWhat is left on a grant past its date, written when the person next moves or by the sweep soon after. What a run has reserved is safe past an expiry; only what comes back unused ends with its grantexpire, the grant's reason
Your serverspendCredits(personId, { amount?, reason, key? }), 1 to 10,000 per callspend, your reason, attributed to the key; the person's page shows the credits spent in the last 30 days, not each line
An AI toolThe credits you set for the named tool, one by default, spent before the request is forwarded; refunded when the provider fails before the first bytespend with reason the tool's label (ai_call for the default tool); spend_refund on a refund

Spend lines are kept for 90 days in the cloud. Purchases, grants, expiries and clawbacks are kept. The person's page lists the live grants with their expiries and the newest 50 non-spend lines. An account erasure removes the person's grants, balance and ledger with everything else.

Grants and expiry

  • Every credit a person holds sits on a grant: a pack, a plan period, a comp, a promo, a refund. The balance is what is left on the live ones.
  • A plan allowance with no rollover ends with its period: at the period end a Stripe invoice names, otherwise when the next period's allowance lands.
  • A relay's promotional grant ends after the days the relay names. A comp ends on the date you set. A pack never expires.
  • Spends burn comps and promos first, then allowances, then packs, and within each the soonest to end first. Bought credits are the last to go.
  • Each ledger line names the grants it moved. An expiry is one line per grant, for what was left.

API

SurfaceCallAnswer
Browser, signed ing.credits.balance() (GET /auth/credits){ balance, reserved, expiring }: spendable now, held by runs and per-token AI calls in flight, and the soonest grant that will end as { credits, at } or null
Server, secret keyspendCredits(personId, { amount?, reason, key? }) (POST /server/people/:id/credits/spend){ ok, spent, deduped, balance: { before, after }, event: { id, reason, actor } }; a repeat with the same dedupe key answers spent: 0 and the same event
Server, secret keyverifySession(token) and holdings(personId)holdings.credits = { balance, reserved, expiring }, the same meter
Relay{ "type": "grant_credits", "amount": 10, "reason": "…", "expiresInDays": 30 }The person's balance goes up by the amount, once per event; with expiresInDays what is left ends that many days later
Relay trigger{ "kind": "credits_low", "below": 20 }Runs once each time a person's spendable credits fall from 20 or more to under 20 (a spend, a clawback, an expiry), never on a rise; the person is the event's person; {{event.balance}} in templates

Numbers

  • A person holds at most 1,000,000,000, spendable and reserved together.
  • A pack grants 1 to 1,000,000. A comp is up to 100,000 per action. A relay grants 1 to 10,000. A server spend is 1 to 10,000 per call.

Codes

CodeStatusMeaning · what to do
credits_exhausted402The balance is below the spend. The message carries the balance: "this person has {balance} credits — the spend needs {amount}". Show the pack; never retry the same spend
session_required401g.credits.balance() without a signed-in person; sign in first
capability_required403spendCredits() on a key without "Spend a person's credits"; create one with it ticked
person_not_found404No person with this id in this app and environment; ids come from verifySession() or the dashboard
invalid_amount400amount outside 1 to 10,000, or not a whole number
invalid_reason400reason missing, not text, or over 200 characters
invalid_key400key not text, empty, or over 200 characters
dedupe_conflict409The dedupe key already names a different movement, another kind or another person; a dedupe key is one movement, reused only to retry that same one
credits_ceiling409A credit that would carry what the person holds past 1,000,000,000; nothing was added

What happens if…

If…What happensWhat you do
A spend is more than the balanceRefused whole with 402 credits_exhausted, carrying the balance. Nothing movesShow the pack. Don't retry the same spend
The same spend is sent twice with the same keyIt is spent once. The repeat answers deduped: true with the first eventNothing
Many spends for one person arrive at onceEach is checked against the balance as it lands, so no more than the balance is ever spentNothing
One key is sent for two different peopleBoth are charged: a key belongs to one personNothing, or derive the key from the person's own job
A grant would take a person past 1,000,000,000Refused with credits_ceiling; nothing is added. A pack paid through Stripe is recorded and Stripe is not asked to send it againFor a pack, refund it in Stripe
A pack is refunded in full after some credits were spentWhat is left of that purchase's credits is taken back, never below zero. A partial refund moves no creditsNothing
Stripe sends the same pack payment twiceThe credits are added onceNothing
An AI tool's provider fails before its first byteThe spend is refunded: the credits go back onto the grants they came from. A grant that has ended since comes back as a new refund grantNothing
A plan period endsWith no rollover, what is left of that period's allowance ends. Carry keeps it, and carry up to n ends the oldest excess. Packs and comps are untouchedNothing
Credits reach their end dateWhat was left ends, and Logs records whose credits, how many, and the balance before and afterNothing
A run holds credits and never finishesThe hold is released back to the balance on its ownNothing
A person's balance falls below a credits_low lineThe relay runs once for that crossing. A top-up and a second fall runs it againNothing
Your server spends with a key that lacks "Spend a person's credits"Refused with 403 capability_required, quoting the checkboxCreate a secret key with it ticked

Local development

gemmein dev keeps the same ledger on your machine, with snapshot and restore. Its pay simulator applies each product's "Grants credits" from gemmein/payments.json (gemmein payments setup asks for it). You can buy a pack and spend from it before a payment provider is connected. The local secret key holds "Spend a person's credits".

Was this page helpful?

Last updated