Skip to content
Paykit
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Was this page helpful?