Pricing catalogs
fs.pricing() — versioned catalogs of exact rates, structured items, selectors, modifiers, tiers, and bounded backend quotes.
fs.pricing() — versioned catalogs of exact rates, structured items, selectors, modifiers, tiers, and bounded backend quotes.
fs.pricing(key, { meter, catalog }) declares an immutable, versioned family of
rates for one meter. Plans bind a family through pricing.current(),
pricing.withContractTerms(), or pricing.fixedVersion(n); the platform
resolves that binding — plus any economic-agreement terms — into a rating
context, the immutable input the rating engine prices measurements against.
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const input = fs.measure("input_tokens");
const out = fs.measure("output_tokens");
const providerName = fs.dimension("provider");
const model = fs.dimension("model");
const modality = fs.dimension("modality");
const cacheStatus = fs.dimension("cache_status");
const mode = fs.dimension("mode");
const text = modality.value("text");
const uncached = cacheStatus.value("uncached");
const cached = cacheStatus.value("cached");
const acme = fs.provider("acme");
const acme4 = acme.model("acme-4");
const modelUsage = fs.meter("model_usage", {
measures: [input, out],
dimensions: [providerName, model, modality, cacheStatus, mode],
});
const llmPricing = fs.pricing("llm", {
meter: modelUsage,
catalog: [
fs.rate.perMillion(fs.money.usd(3)).for(input, acme4, text, uncached),
fs.rate.perMillion(fs.money.usd(15)).for(out, acme4, text, uncached),
fs.rate.perMillion(fs.money.usd(0.3)).for(input, acme4, text, cached),
fs.rate.perMillion(fs.money.usd(15)).for(out, acme4, text, cached),
fs.modifier.multiplier(3, 2).when(mode.is("fast")),
],
});
const chat = fs.route("/v1/chat", { post: {} });
fs.meterRoutes("chat-model-usage", chat, {
reports: [modelUsage],
maxOutputUnits: out.atMost(8192),
});
fs.plan("enterprise", {
kind: fs.plan.kind.usage,
usagePricing: llmPricing.current(),
grants: [chat],
limits: [requests.perMinute(600)],
});
export default fs.business();
Authors write human units; the compiler serializes exact rationals
({ num: "3", den: "1000000" }) and the rating engine never touches a float.
| Constructor | Meaning |
|---|---|
fs.rate.perUnit(fs.money.usd(0.01)) | $0.01 per unit. |
fs.rate.per(1000, fs.money.usd(5)) | $5 per 1,000 units — half a cent per unit, exactly. |
fs.rate.perMillion(fs.money.usd(3)) | $3 per 1,000,000 units. |
fs.rate.rational(1, 3, fs.money.usd(1)) | $1/3 per unit — reserved for genuinely non-dyadic rates. |
fs.rate.graduated([{ upTo, rate }, ...]) | Tiered: each unit rated by the tier its cumulative window position falls in. |
fs.rate.volume([{ upTo, rate }, ...]) | Tiered, retroactive: every unit in the window rated at the tier the window total selects at close. |
fs.rate.backendQuoted({ min, max }) | The backend proposes a per-unit rate per report; the platform clamps it into [min, max]. |
Tier brackets use upTo as the inclusive cumulative upper bound and end with
one open upTo: null tier:
fs.rate.graduated([
{ upTo: 1_000_000, rate: fs.rate.perMillion(fs.money.usd(3)) },
{ upTo: null, rate: fs.rate.perMillion(fs.money.usd(2)) },
]);
Each finite tier boundary must be a positive safe integer, strictly greater than the previous boundary. Only the final tier may be open-ended, and it must be open-ended. Tier rates must be SDK-created flat rates; do not nest a tier table or backend-quoted rate inside a tier.
Graduated position carries across rating segments; volume-retroactive selection is computed per segment at window close, and a late measurement into a closed window becomes a correction posting that reruns the same deterministic selection.
.for(measure, ...selectors) binds a rate to a measure and a catalog tuple:
acme.model("acme-4")) becomes the item
(provider, model); models are declared inside their provider's namespace, so
a selector can never match another provider's model;modality.value("text"), cacheStatus.value("cached"))
become where conditions on the entry.A single-measure meter may carry an unbound default rate
(fs.rate.perUnit(...) with no .for); a multi-measure meter must bind every
rate with .for(measure).
At rating time the backend's dims select the entry: { model: "acme-4", modality: "text", cache_status: "cached" } resolves to the $0.30/M input rate.
Selector match sets are snapshotted per rating context — adding a catalog entry
applies forward only, never retroactively.
For a given measure, matching entries have this precedence:
Adding more conditions does not create another specificity level. Two
overlapping entries at the same level are rejected with
CATALOG_ENTRY_AMBIGUOUS; neither the first nor last array entry wins. For
example, a mode=fast rule and a cache_status=cached rule can both match the
same report. Give them disjoint selectors or model the shared behavior as a
modifier instead of relying on declaration order.
No matching entry is not a zero-price fallback. Cover all intended report
combinations and test uncovered combinations explicitly. An explicit unbound
default is available for a single-measure meter; for multiple measures, declare
each default with .for(theMeasure).
.for() takes one measure, at most one provider-owned model, and at most one
value for each dimension. Extra measures and duplicate dimension selections
are rejected.fs.modifier.multiplier(numerator, denominator).when(dimension.is(value))
scales every matching charge by an exact rational. multiplier(3, 2) is 1.5x
without a float. Contract discounts from economic agreements compose on top of
modifiers; the true-up base for minimums is post-modifier rated usage.
For rules that only the backend can price (dynamic upstream resale, bespoke jobs), declare bounds in the repo:
const jobs = fs.measure("jobs");
const jobUsage = fs.meter("jobs", { measures: [jobs] });
const jobPricing = fs.pricing("jobs", {
meter: jobUsage,
catalog: [
fs.rate.backendQuoted({
min: fs.rate.perUnit(fs.money.usd(0.5)),
max: fs.rate.perUnit(fs.money.usd(50)),
}),
],
});
The backend then passes quote: { currency: "usd", amountNanos } to
report() — a per-unit rate in nanodollars, never a total. Out-of-range
quotes are clamped and dispute-flagged; the ledger records only core-rated
charges. A quote sent against a rule that is not backend-quoted is ignored.
Both bounds must be SDK-created flat rates and min must not exceed max.
Tier tables and nested quote rules are not valid bounds. Invalid bounds reject
with BACKEND_QUOTE_BOUNDS_REQUIRED; do not repair them by trusting an
unbounded amount from the backend.
| Binding | Rating context | Moves when |
|---|---|---|
pricing.current() | The catalog version active in the served release. | Every activated release, forward. |
pricing.withContractTerms() | The current catalog plus the subject's agreement terms. | Release activation and agreement amendment. |
pricing.fixedVersion(n) | Exactly version n. | Never — require_amendment by definition. |
withContractTerms and fixedVersion are set on subjects through
economic agreements; repo plans normally bind
current().
Every material catalog change mints a new immutable pricing-policy version and a new rating-context version in the next commercial release.
A catalog edit does not mint a new compiled plan, and it is not meant to. The compiled plan freezes the plan's route grants, structural limits and its own recurring fee — the contract a live subscriber is enforced against — and a rate edit changes none of those. So the identity of a price-only change is the pair: the release's membership for the plan keeps the same compiled-plan id and moves to a new rating-context version. A repricing that re-sealed the compiled plan would rebuild every live subscriber's enforcement contract for a change that never touched it.
farthershore commercial-release diff lists rating-context versions added and
removed — that, not a compiled-plan entry, is where a price change shows up. Overlays and bindings carry author-supplied keys (fs.meterRoutes's
first argument, the pricing family key), so reordering declarations never
rebinds a pinned rate.