krizaka-billing
Billing & credits
Credits, wallets, plans, a versioned price book and metering: hold → settle → release.
The problem it removes
Debit after and the work ran on credit nobody had. Debit before and failures get billed.
Charging for expensive work goes wrong two ways: debit afterwards and the work ran on credit nobody had; debit up front and a failed job is billed. Then a "job done" message is redelivered and the same job is debited twice.
- A user who starts ten expensive jobs at once with credit for one.
- A failed render billed at its estimate.
- A redelivered settlement debiting twice.
What it does
holdthe estimate before the work, thensettleMeasuredorrelease.- Every debit carries an idempotency key: a redelivered message is charged once.
- Plans, subscriptions, packs and wallets around an append-only ledger.
A service and a typed client: a caller holds the estimated credits before expensive work, then settles the measured cost or releases the hold. Holds that are never settled expire. Plans, subscriptions, packs, wallets and usage analytics around it.
The flamingo gets paid for what was done, once — and nobody pays for work that failed. A failed job releases its hold; every debit carries an idempotency key.
Decisions and trade-offs
We chose
Hold → settle → release, with an expiry on holds.
We refused
Debit after the work, and prepay without reservation.
Because
Work never starts without the credit to cover it, and failure costs nothing.
What it costs you
One synchronous call (hold) on the request path; settle and release are off it.
We chose
An append-only ledger where every debit carries an idempotency key, on top of message deduplication.
We refused
Trusting deduplication alone.
Because
Two independent guards: a redelivery that gets past one still debits once.
What it costs you
The ledger only grows; corrections are new entries.
We chose
Prices and the hold lifetime are rows of a versioned price book; a hold pins the version it was priced at.
We refused
Prices in code or configuration.
Because
Repricing never changes what an in-flight job costs, and needs no deployment.
What it costs you
A price change is a data migration you review.
We chose
A no-op adapter when
krizaka.billing.enabledis false.We refused
if (billingEnabled)at every call site.Because
The same code runs metered and unmetered.
What it costs you
None worth naming.
In code
CreditHoldResponse hold = credits.hold(new CreditHoldCommand(
userId, BillableCapability.IMAGE, "sdxl", requestId, jobId, estimate));
// InsufficientCreditsException carries what a structured 402 needs
try {
Render render = renderer.run(job);
// The ledger prices what was measured, at the price-book version the hold pinned.
credits.settleMeasured(hold.holdId(), render.consumption(), jobId); // jobId = idempotency key
} catch (RuntimeException failed) {
credits.release(hold.holdId(), "render failed"); // a failed job is never billed
throw failed;
}Don't use it when
- You need invoices, taxes or card payments: billing counts credits; collecting money is yours (a top-up credits a wallet).
- Your capabilities are not AI work — today. The contract still names Orazaka's (CHAT, IMAGE, AUDIO, VIDEO, AGENT) and its measurements (tokens, GPU seconds…); see below.
Where it stands
0.1.0 on Maven Central (api, client). 0.2.0 — the outbox implements OutboxStore.append, every event has its JSON Schema — is merged and ships in November.
Published
In progress
- Decided: capabilities and units become keys declared in the price book, measurements a map of quantities; Orazaka keeps its enums in Orazaka. A reusable billing block cannot speak one product's vocabulary.krizaka-billing#7
- BOM 0.2.0 manages this block at an unpublished 0.2.0: declare 0.1.0 explicitly until BOM 0.3.0.krizaka-build#11
Adopt it
<dependency>
<groupId>com.krizaka</groupId>
<artifactId>krizaka-billing-client</artifactId>
<version>0.1.0</version>
</dependency>
<!-- krizaka.billing.enabled: false wires a no-op adapter: no call site branches on "is billing on" -->Tell us where it hurts.
A block is right when it survives your code base, not ours. Ask in the block's thread, propose a change as an idea, or report a bug on its repository — every decision on this page is open to a better argument.
The other blocks
- Platform kitAn event published after the commit is lost on the next crash.
- UsersSign-up looks like a weekend. Reset tokens, OAuth and JWTs make it a quarter.
- NotificationsAn e-mail sent inside a transaction that rolls back can't be unsent.
- Build, BOM & test kitParent POMs drift — and a Spring BOM silently overrides the Boot version you chose.
- Krizaka UIThree products, three token vocabularies, 1,153
light:overrides in one app.