Credits
One pool per account, priced per dimension, and what spends it.
Credits are numbers on an account, not on a feature. A plan grants one pool, and every credit-priced feature spends from that pool. The rate card decides how many credits one unit costs.
import { Paykit } from "@potion.run/paykit";
import { messages } from "./features.ts";
export const scale = Paykit.plan({
id: "scale",
price: { amount: 9_900, currency: "usd", interval: "month" },
credits: 50_000,
grants: [Paykit.planItem(messages, { included: "unlimited" })],
});
What spends the pool
A feature with creditRates or a rateCard is priced in credits. It cannot also
have an allowance, because the pool is its count. check() reports the pool as
balance.remaining for that feature.
const check = yield* Paykit.Check(account);
const answer = yield* check({ featureId: "ai", dimension: "gpt-large", requiredBalance: 10 });
answer.balance.remaining; // the pool itself, for a credit-priced feature
A command that names a credit-priced feature without a dimension uses the
card’s default rate.
Make one unit cost more on some events
The simple card names a rate per dimension:
const ai = Paykit.feature({
id: "ai",
name: "AI",
creditRates: { default: 1, "gpt-large": 10 },
});
A full card prices on the event’s properties instead:
const render = Paykit.feature({
id: "render",
name: "Render",
rateCard: Paykit.rateCard({
default: 1,
rules: [
{ match: { size: "large" }, credits: 16 },
{ match: { size: "small" }, credits: 2 },
],
multipliers: [{ match: { lifecycle: "spot" }, factor: 0.3 }],
}),
});
The first matching rule wins. After it, every matching multiplier applies: the
final rate is rate × all factors + all adds, clamped at zero. A rule with an
empty match is the fallback after default.
The caller supplies the properties on the command:
const track = yield* Paykit.TrackUsage(account);
yield* track({
featureId: "render",
value: 1,
properties: { size: "large", lifecycle: "spot" },
idempotencyKey: requestId,
});
// The large rule prices it at 16 credits, and the spot multiplier prices it at 30%.
Expiry and one-off packs
A subscription’s credits expire at the period close by default. Set
creditsExpire: "never" to keep them. A one-off pack’s credits never expire.
export const pack = Paykit.plan({
id: "credits_100k",
kind: "one_off",
price: { amount: 4_900, currency: "usd", interval: "month" },
credits: 100_000,
grants: [],
});
A purchase of a one-off pack adds credits and starts no period. Each purchase adds its credits to the pool.
Refill before the pool runs out
A plan can state an auto top-up line. It is one sentence: refill below a threshold, from a named pack, no more often than the cooldown, and retry a failure a bounded number of times.
import { Duration } from "effect";
export const scale = Paykit.plan({
id: "scale",
price: { amount: 9_900, currency: "usd", interval: "month" },
credits: 50_000,
topUp: {
threshold: 5_000,
cooldown: Duration.minutes(60),
pack: "credits_100k",
retry: { attempts: 3, every: Duration.hours(6) },
},
grants: [],
});
The crossing is a purchase on the provider, so the refill is a real charge. The policy and its retry budget live in the model, so a deploy cannot refill an enterprise pool from a free plan’s pack.
Spend credits directly
If your work is not metered by a feature, spend from the pool directly:
const consume = yield* Paykit.ConsumeCredits(account);
const { balance } = yield* consume({ amount: 25, dedupeKey: requestId });
ConsumeCredits refuses a spend past the pool with CreditExceedsBalance, and a
second call with the same dedupeKey is the same spend.
See Check for the pool in the reply, and Balances for the sources a balance is built from.