Skip to main content
Version: 2.x

:::info Executable tutorial

This page is generated from a tested Jupyter notebook. Open it in Google Colab or view the source notebook.

:::

Publish your first pricing configuration

Bursar uses one validated document for operations, rates, credit buckets, plans, and commerce policy. This tutorial examines the shared example, publishes an immutable revision, activates it, and verifies that invalid fields cannot enter the catalog.

Learning objectives

After completing this tutorial, you can:

  • Identify each top-level configuration section
  • Validate, publish, and activate a configuration revision
  • Explain how canonicalization and digests prevent duplicate revisions
  • Diagnose strict-schema validation errors

Prerequisites

  • Complete the setup tutorial or install the Bursar Python development environment
  • Start the notebook server from samples/python/notebooks/

The document at a glance

Every bursar document is a strict dict with a fixed top-level shape: catalog (which plan is the default), pricing (operations and rate cards), credits (buckets and the default bucket), entitlements (feature definitions), admission (concurrency policies), plans, and commerce (providers, offers, auto-recharge). The cell below lists the sections the canonical demo config declares.

from shared import base_config, start_postgres_store, cleanup, publish_config
from bursar.config import load_config_from_dict, ConfigError

config = base_config()
print("top-level sections:", sorted(config.keys()))

pricing.operations: what you sell

An operation declares the measures it bills on, each with a unit, and the dimensions that select a price. The shared tutorial configuration declares two operations:

  • completion — measures input_tokens, output_tokens, cache_read_tokens (unit token), dimension model;
  • execution — measures jobs (unit job) and compute_seconds (unit second), dimension model.

Measures are quantities, dimensions are selectors. Later the engine charges any combination of measures — one you do not report counts as zero.

for name, operation in config["pricing"]["operations"].items():
measures = ", ".join(f"{m} ({unit['unit']})" for m, unit in operation["measures"].items())
dimensions = ", ".join(operation["dimensions"])
print(f"{name}: measures=[{measures}] dimensions=[{dimensions}]")

pricing.rate_cards: what it costs

A rate card prices every operation with an ordered list of rules. Each rule has a when clause that matches on dimensions — here model in ["gpt-4o", "gpt-4o-mini"] — and a charge built from building blocks. The completion charge is a sum of three per_unit components: each multiplies a measure by a rate and divides by a unit_size, which is how "per 1M tokens" is expressed. Rates are decimal strings, never floats.

The unmatched policy decides what happens when no rule matches: charge applies a fallback (here an expression over the operation's measures), while reject refuses the request. The base config rejects unknown models for execution — an unpriced job is a bug, not a discount.

card = config["pricing"]["rate_cards"]["standard"]
completion = card["operations"]["completion"]
rule = completion["rules"][0]

print("when:", rule["when"])
print("charge type:", rule["charge"]["type"])
for component in rule["charge"]["components"]:
print("component:", component)
print("unmatched completion:", completion["unmatched"]["action"])
print("unmatched execution:", card["operations"]["execution"]["unmatched"]["action"])

credits, plans, entitlements, admission, commerce

The rest of the document answers product questions:

  • credits.buckets — where money sits. promotional (priority 1) spends first; purchased (priority 10) is where purchases land and is the default bucket.
  • plansfree (rank 0, completion only, 10,000-credit monthly calendar allowance) and pro (rank 1, completion + execution, voice_mode and max_context features, a 500,000 output-token/day blocking quota, and admission policy default).
  • entitlements.features — the catalogue of features plans can turn on (voice_mode boolean, max_context integer).
  • admission.policies — the default policy caps concurrent in-flight operations at 4.
  • commerce — the pro_monthly subscription (50,000-credit cycle grant) and credits_10k top-up, plus auto-recharge guardrails: recharge when the balance drops to 2,000 credits, at most 5 purchases per day, max 5,000 minor units per charge.
print(
"buckets:", {k: bucket["priority"] for k, bucket in config["credits"]["buckets"].items()},
"| default:", config["credits"]["default_bucket"],
)

for key, plan in config["plans"].items():
summary = {
"rank": plan["rank"],
"rate_card": plan["rate_card"],
"operations": plan["allowed_operations"],
"features": plan.get("features", {}),
"allowance": plan.get("credit_allowance", {}).get("amount"),
"quotas": list(plan.get("quotas", {}).keys()),
"admission_policy": plan.get("admission_policy"),
}
print(f"plan {key}:", summary)

print("offers:", sorted(config["commerce"]["offers"].keys()))
print("admission policies:", sorted(config["admission"]["policies"].keys()))
# load_config_from_dict is the gatekeeper: it parses the document into
# typed models and validates every cross-reference — rate cards exist,
# plans reference real operations, quotas reference real measures, offer
# buckets exist, and every pricing expression is safe. Publishing runs
# the same validation, so a document that fails here never reaches the
# live catalog.
parsed = load_config_from_dict(base_config())
print("parsed plans:", list(parsed.plans.keys()))
print("parsed operations:", list(parsed.pricing.operations.keys()))
print("parsed rate cards:", list(parsed.pricing.rate_cards.keys()))
print("free plan operations:", parsed.plans["free"].allowed_operations)
print("pro plan features:", parsed.plans["pro"].features)

Publish, activate, and advance

Publishing writes the validated document as a new immutable catalog version and activates it; catalog.get_active() returns the live version. Then we exercise the loop that matters: raise the gpt-4o output rate from 0.0100 to 0.0200 credits per million tokens, publish again, and the catalog advances to version 2. Version 1 is not overwritten — it stays in history for audit and rollback — and the active document read back through the facade shows the new rate.

store, pgdata = start_postgres_store()
bursar = publish_config(store, base_config(), label="notebooks")
print("active version:", bursar.catalog.get_active().version)

raised = base_config()
raised["pricing"]["rate_cards"]["standard"]["operations"]["completion"]["rules"][0]["charge"]["components"][1]["rate"] = "0.0200"
bursar.catalog.publish_and_activate(raised, label="raise gpt-4o output rate")
print("active version after change:", bursar.catalog.get_active().version)

active = bursar.catalog.get_config()
components = active.pricing.rate_cards["standard"].operations["completion"].rules[0].charge.components
print("active gpt-4o output rate:", components[1].rate)

Bad configs are rejected

Validation is strict by design. A typo in a section name hits extra_forbidden instead of being silently dropped, and a malformed rate fails type validation. Both surface as ConfigError. The try/excepts below let the notebook keep running; in production they stop a broken configuration before it ever reaches users.

bogus = dict(base_config())
bogus["bogus_top_level_key"] = True
try:
load_config_from_dict(bogus)
except ConfigError as error:
print("unknown key ->", type(error).__name__)

malformed = base_config()
malformed["pricing"]["rate_cards"]["standard"]["operations"]["completion"]["rules"][0]["charge"]["components"][0]["rate"] = "not-a-number"
try:
load_config_from_dict(malformed)
except ConfigError as error:
print("bad rate ->", type(error).__name__, "|", str(error).splitlines()[0])

cleanup(pgdata)