Pricing model
Pricing decides what a usage event costs. Bursar models it in four layers: an
operation declares the inputs, a rate card names a pricing table, a price rule
selects a charge, and the PricingEngine evaluates the charge in exact
decimal arithmetic.
Operations, measures, and dimensions
An operation is one named billable action — completion, execution,
image_generation, whatever your product meters. Each operation declares the
inputs its price can reference:
| Concept | Role | Example |
|---|---|---|
| Measure | A numeric quantity with a unit | input_tokens (unit token) |
| Dimension | A typed selector, not priced | model (string) |
pricing:
operations:
completion:
measures:
input_tokens: { unit: token }
output_tokens: { unit: token }
cache_read_tokens: { unit: token }
dimensions:
model: { type: string }
Dimensions keep provider-specific attributes (model names, region, batch flags) out of the schema. Measures are non-negative exact numbers; a required dimension must be present on every event for the operation.
Rate cards
A rate card is a named pricing table keyed by operation. Cards can inherit
unpriced operations from a parent card via extends, and each plan references
exactly one card (plan.rate_card). When a plan selects the card, callers
never need to name one; a standalone engine requires rate_card only when
more than one card exists.
Price rules and unmatched policies
Each operation on a card lists ordered rules. A rule's when block matches
dimensions with typed operators, and the first matching rule wins:
| Operator | Dimension type | Match |
|---|---|---|
eq | any | Exact equality |
in / not_in | any | Membership / non-membership |
prefix | string | String prefix |
range | number | Bounds gt / gte / lt / lte |
When no rule matches, the unmatched policy decides: action: reject
refuses the event, or action: charge applies a fallback charge. The
canonical config uses both — gpt-4o and gpt-4o-mini get per-million-token
rates, anything else falls back to an expression; execution only prices
gpt-4o and rejects the rest:
rate_cards:
standard:
operations:
completion:
rules:
- when: { model: { op: in, values: [gpt-4o, gpt-4o-mini] } }
charge:
type: sum
components:
- {
type: per_unit,
measure: input_tokens,
rate: "0.0025",
unit_size: "1000000",
}
- {
type: per_unit,
measure: output_tokens,
rate: "0.0100",
unit_size: "1000000",
}
- {
type: per_unit,
measure: cache_read_tokens,
rate: "0.00125",
unit_size: "1000000",
}
unmatched:
action: charge
charge:
{
type: expression,
formula: input_tokens * 0.005 + output_tokens * 0.015,
}
execution:
rules:
- when: { model: { op: eq, value: gpt-4o } }
charge: { type: per_unit, measure: jobs, rate: "0.04" }
unmatched: { action: reject }
Charge types
A charge computes the credit cost for the event:
| Type | What it does | Example |
|---|---|---|
flat | A constant fee per event | { type: flat, amount: "1.00" } |
per_unit | Rate per unit_size units — the basis for per-million-token pricing | { type: per_unit, measure: input_tokens, rate: "0.0025", unit_size: "1000000" } |
package | Price per fixed block, rounded up/down/nearest | { type: package, measure: jobs, units: "10", amount: "0.10", rounding: ceil } |
graduated | Marginal rates per tier | { type: graduated, measure: input_tokens, tiers: [{ up_to: "1000", rate: "0" }, { rate: "0.005" }] } |
volume | One rate selected by total volume | { type: volume, measure: input_tokens, tiers: [{ up_to: "100000", rate: "0.004" }, { rate: "0.003" }] } |
expression | An arbitrary formula over the operation's measures | { type: expression, formula: input_tokens * 0.005 + output_tokens * 0.015 } |
sum | Sum of sub-charges (each a charge) | The completion rule above |
per_unit with unit_size: 1000000 is how per-million-token rates stay
readable: the rate is what one million tokens cost. graduated tiers must
end with exactly one open-ended tier, in strictly increasing order; package
defaults to rounding up. Graduated and volume tiers differ: graduated applies
each tier's rate to the marginal units within it, volume applies the selected
tier's rate to the whole amount.
The PricingEngine
PricingEngine is the stateless, database-free evaluation core. Both SDKs
construct it from the same canonical document:
- Python
- TypeScript
from bursar.engine import PricingEngine
from bursar.metrics import UsageMetrics
engine = PricingEngine.from_dict(config) # validates the whole config
cost = engine.calculate(
UsageMetrics(
operation="completion",
measures={"input_tokens": 1000, "output_tokens": 500, "cache_read_tokens": 200},
dimensions={"model": "gpt-4o"},
),
rate_card="standard",
)
import { PricingEngine } from "@zonastery/bursar";
const engine = PricingEngine.fromDict(config);
const cost = engine.calculate(
{
operation: "completion",
measures: {
input_tokens: 1000,
output_tokens: 500,
cache_read_tokens: 200,
},
dimensions: { model: "gpt-4o" },
},
{ rateCard: "standard" },
);
The engine:
- validates measures and dimensions against the operation definition —
undeclared names, missing required dimensions, and wrong dimension types
raise
ConfigError; - selects the first matching rule, or the unmatched policy, or rejects;
- evaluates every charge in exact decimal and quantizes the result to six
decimal places with
ROUND_HALF_UP; - rejects negative or non-finite costs, and never truncates a sub-credit charge to zero.
calculate_batch(metrics, rate_card=...) / calculateBatch(metrics, {rateCard})
evaluates a list of events with the same card selection.
get_rate_card_for_plan(plan_key) returns the card a plan references.
Evaluation contract
The engine receives one declared operation, its non-negative measures, typed dimensions, and optional caller metadata. It returns an exact CostBreakdown with the selected rule, rate card, input evidence, and quantized total.
Use the Python pricing reference or TypeScript pricing reference for field-level input and output contracts.
Worked example
One gpt-4o completion with 1,000 input, 500 output, and 200 cache-read tokens
against the standard card:
| Component | Rate | Cost |
|---|---|---|
| input | 0.0025 per 1M | 0.0025 × 1000 / 1000000 = 0.0000025 |
| output | 0.0100 per 1M | 0.0100 × 500 / 1000000 = 0.0000050 |
| cache read | 0.00125 per 1M | 0.00125 × 200 / 1000000 = 0.00000025 |
| Total | 0.00000775 → 0.000008 |
The engine charges exactly 0.000008 credits — the unquantized sum
0.00000775 rounded half-up at six decimal places. The same config returns
the same number in Python and TypeScript.
Related
- Expressions: the safe formula language for
expressioncharges - API reference: PricingEngine (Python) and PricingEngine (TypeScript)
- Configuration and catalog revisions: where operations and rate cards live