Krizaka
All building blocks

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

  • hold the estimate before the work, then settleMeasured or release.
  • 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.

The flamingo's rule

Decisions and trade-offs

  1. 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.

  2. 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.

  3. 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.

  4. We chose

    A no-op adapter when krizaka.billing.enabled is 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

javaRenderService.java
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

xmlpom.xml · application.yml
<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