guide

Credits

A balance your customers hold and your product spends. A pack they buy, a comp you give, a call that costs one. Credits are a quantity beside access, and Gemmein keeps the ledger: every addition, every spend, every refund, and a floor of zero.

What it is. A number on each customer, kept by Gemmein. It goes up when they buy a pack, when a relay grants some, or when you comp them from the dashboard. It goes down when your server spends with a reason, or when the AI route makes a call on their behalf. Every movement is one line on the person's ledger, and the balance never goes below zero.

What it does. A product grants credits at purchase: set "Grants credits" on the product and each confirmed purchase adds that many. Your server spends them, by person id, with a reason, and with a key so a retry never spends twice. The AI route spends the credits the owner set for the named tool, one by default. A relay grants them. The dashboard shows the balance and the ledger on the person's page, and lets you comp by hand.

What it does not do. Credits do not expire. The balance does not go negative: a spend larger than the balance is refused whole, never partly. Credits are not priced by token; an AI tool call costs whatever the owner priced that tool at, not its length. Credits do not 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. Nothing spends from the browser; your server, a relay or the AI route moves credits. There are no seats and no quotas.

When you need something else. Access that switches on with a plan is an entitlement: see Payments. A bill after the fact, by token or by minute, is not something Gemmein does; sell packs up front. A spend that must happen in the browser has no home; put it behind your server or the AI route.

The worked case: a 100-credit pack, 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 buyer'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 then costs the credits set on the AI tool through the AI route — 20, in this case — which spends before it forwards. For a spend of your own, an export or a render, your server spends by person id with a reason and a 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"

The key needs "Spend a person's credits" ticked when it is minted. A repeat with the same key answers deduped: true, spent: 0 and the same event, and moves nothing; the key is scoped to the person and namespaced, so one order id reused for two people charges both, and rotating the secret key never re-charges. 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.

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 row granted, at most what is still unspent, whatever the product says today. Partial refunds move nothingrefund_clawback, actor system
The dashboard"Grant credits" on the person's page, up to 100,000 per action, with a notegrant, your note, attributed to you
A relaygrant_credits { amount (1 to 10,000), reason? }, once per eventgrant, "relay: <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
The AI routeThe credits the owner 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 on the cloud rail. Purchases, grants and clawbacks are kept. The person's page lists the newest 50 non-spend lines. An account erasure removes the person's balance and ledger with everything else.

Routes and verbs

SurfaceCallAnswer
Browser, signed ing.credits.balance() (GET /auth/credits){ balance }
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 key answers spent: 0 and the same event
Server, secret keyverifySession(token) and holdings(personId)holdings.credits = { balance }
Relay{ "type": "grant_credits", "amount": 10, "reason": "…" }The person's balance goes up by the amount, once per event

Numbers

  • No expiry. Floor zero. A refund claws back at most what is still unspent. A balance carries at most 1,000,000,000.
  • 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.
  • The AI route spends the credits the owner set for that tool; one by default.
  • Spend lines are kept 90 days; purchases, grants and clawbacks are kept.

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"; mint 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 key already names a different movement, another kind or another person; a key is one movement, reused only to retry that same one
credits_ceiling409A credit that would carry the balance past 1,000,000,000; nothing was added

Both rails

gemmein dev keeps the same ledger on your machine, with snapshot and restore, and its pay simulator honours the "Grants credits" a product declares in gemmein/payments.json (gemmein payments setup asks for it), so buying a pack and spending from it runs end to end before a payment provider is connected. The local secret key holds "Spend a person's credits".