Skip to content

Prepaid credits

import {
Billing,
Credits,
Offer,
Purchase,
consumeCredits,
createCheckout,
getCreditBalance,
getCreditHistory,
processPaymentNotification,
registerOffers,
releaseCredits,
reserveCredits,
} from "@byfungsi/sapa";
const credits = yield * Credits.pack({ unit: "ai.credit", quantity: 1_000n });
const creditPurchase =
yield *
Purchase.parse({
key: "credits.balance",
revision: 1,
provides: [credits],
});
const creditBilling =
yield *
Billing.oneTime({
price: { currency: "IDR", amount: 100_000n },
});
const creditPack =
yield *
Offer.parse({
key: "credits.pack",
revision: 1,
purchase: creditPurchase,
billing: creditBilling,
});
yield * registerOffers([creditPack]);
yield *
createCheckout({
type: "offer",
offerKey: creditPack.key,
offerRevision: creditPack.revision,
quantity: null,
provider: "midtrans",
customer,
successUrl,
cancelUrl,
clientIp: null,
idempotencyKey: "credit-pack-42",
});
yield *
processPaymentNotification({
provider: "midtrans",
payload: rawProviderPayload,
});
const query = { customerId: "customer-42", unit: "ai.credit" };
const before = yield * getCreditBalance(query);
const reservation =
yield *
reserveCredits({
...query,
quantity: 10n,
idempotencyKey: "generation-job-88",
});
// After the external work is known to have consumed the units:
yield * consumeCredits({ reservationId: reservation.id });
// For a different reservation whose work definitely did not run:
const unused =
yield *
reserveCredits({
...query,
quantity: 5n,
idempotencyKey: "generation-job-89",
});
yield * releaseCredits({ reservationId: unused.id });
const after = yield * getCreditBalance(query);
const movements = yield * getCreditHistory(query);

Consume and release are alternative terminal outcomes for one reservation. Never release in a generic cleanup block when an external result is uncertain; reconcile first. Reservations expire after the installation’s bounded window, and all quantities are exact units—not currency.