Meters & measures
Declare what your backend measures, the dimensions it measures under, and where those measurements are reported.
Declare what your backend measures, the dimensions it measures under, and where those measurements are reported.
A meter is a measurement, never a price. It names the measures your backend observes (tokens, jobs, rows) and the dimensions those observations are produced under (model, modality, cache status). Money lives in a pricing catalog that references the meter; a plan binds the catalog. That separation is what lets one measurement be rated differently per plan, contract, and release without a backend change.
import * as fs from "@farthershore/business";
const requests = fs.requests();
const input = fs.measure("input_tokens");
const output = fs.measure("output_tokens");
const model = fs.dimension("model");
const modelUsage = fs.meter("model_usage", {
measures: [input, output],
dimensions: [model],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const chat = fs.route("/v1/chat", {
post: { backend: api, costs: [requests.fixed(1)], reports: [modelUsage] },
});
fs.meterRoutes("chat-model-usage", chat, {
reports: [modelUsage],
maxOutputUnits: output.atMost(8192),
});
fs.plan("free", {
kind: fs.plan.kind.free,
grants: [chat],
limits: [requests.perMinute(60)],
});
export default fs.business();
fs.measure(key) — one observed quantity. Its key is what the backend sends
under values.fs.dimension(key) — one selector axis. dimension.value("text") mints a
catalog selector; dimension.is("fast") is a modifier condition.fs.provider(key) and provider.model(key) — an owned provider namespace and
its models, used as catalog items (provider, model, modality?).fs.meter(key, { measures, dimensions? }) — the meter: a set of measures the
backend reports together, plus the dimensions it may report them under. A
meter may carry several measures (input and output tokens on one report).fs.requests() — the platform-managed request counter used for structural
bounds (requests.perMinute(n)) and gateway-known fixed costs. It is
admission policy, not a rated measurement.Keys are plain strings at the wire boundary: the backend reports
{ meter: "model_usage", values: { input_tokens: 1200 }, dims: { model: "acme-4" } }
and the gateway validates every key and value against the served release's
measurement schema. Unknown measures or dimensions are rejected loudly, never
silently dropped.
Routes are unattached by default. fs.meterRoutes(key, route, options) binds a
meter to a route with an author-supplied stable key — the key survives releases
so agreements and admission bounds can reference it:
fs.meterRoutes("chat-model-usage", chat, {
reports: [modelUsage],
maxOutputUnits: output.atMost(8192),
});
Options:
reports — the meters this route's backend reports.maxOutputUnits: measure.atMost(n) — a declared bound on an unbounded output
measure. The gateway clamps the request's output knob (max_tokens and its
aliases) to the bound before signing it upstream, so billing truncation and
product truncation are the same event. Required on prepaid plans for any
unbounded measure.chunkPolicy: { bound: measure.atMost(n), chunkUnits } — the alternative to a
hard bound: reserve in chunks of chunkUnits up to a cumulative per-operation
ceiling. Exclusive with maxOutputUnits.caps: [measure.atMost(n)] — finite admission bounds for measures the client
cannot declare.postStream: { settlementMax: [measure.atMost(n)] } — declares that the
authoritative measurement arrives after the response is on the wire, with a
finite settlement maximum per measure (required for prepaid plans).A route that reports no meter creates no rated usage for that dimension, even when a plan binds a catalog that could price it. The build reports such operations.
The target shape selects the meterRoutes API:
| Target | Purpose | Options |
|---|---|---|
One RouteRef directly | Commerce binding with a stable key and admission bounds | Nonempty reports of meters with measures; optional maxOutputUnits, adapter, caps, chunkPolicy, postStream |
| Group, wildcard path, or target array | Structural metering overlay across matching operations | Structural reports/costs/status policy; not commerce admission bounds |
A direct route binding attaches its reports to every operation declared on that
route ref. If only POST should report usage or have an output bound, use a
separate route ref for POST; do not accidentally bind a combined GET/POST ref.
A one-element array is still an overlay target, not a concrete commerce binding.
On a concrete binding, put fixed costs and onStatusCodes on the route's
operation; those are not accepted as concrete-binding options. Group/wildcard
overlays do not replace the stable concrete bindings needed for commerce
admission. Wildcards select already declared routes; they do not create routes.
measure.atMost(n) maximum must be a positive safe integer and refer
to a measure in this binding's reported meters.caps may name each measure once and cannot duplicate maxOutputUnits.chunkPolicy is mutually exclusive with maxOutputUnits. Its bound cannot
duplicate a cap, and chunkUnits must be a positive safe integer no greater
than its cumulative maximum. Invalid chunk combinations reject with
ADMISSION_CHUNK_POLICY_INVALID.postStream.settlementMax may name each reported measure once. The SDK's
shape validation is not the whole admission proof: prepaid/x402 compatibility
also depends on the compiler's economic-mode and measurement-timing checks.maxOutputUnitsAdapter requires maxOutputUnits. The supported adapter
names are knob: "max_output_units" and
parser/mutator: "json_body_max_output_units_v1"; this is not an arbitrary
JSON-path mapping API.The chunk ceiling is a cumulative per-operation maximum, not permission to produce unlimited output while reserving only the first chunk. Keep request clamping, actual backend output, reported measurements, and settlement bounds consistent. See Monetary admission.
Every measurement reaches the platform through one verb on the verified
context: req.fartherShore.report({ meter, values, dims?, quote? }). Before the
response is sent it rides signed response headers; after (streams, background
jobs) the SDK routes through the post-stream channel automatically. See
Metering & verification.
Backends report measurements, never money. The single exception is the
bounded quote channel for catalog rules declared fs.rate.backendQuoted(...).
fs.requests() limits bound the request rate; fs.resource() limits bound
persistent inventory; capacity bounds one request. None of these price
anything. A rated meter is commercially bounded by its plan's funding and spend
policy; add a structural bound only when use must stop regardless of money.
By default, usage is associated with successful responses. Use
onStatusCodes on a route only when the commercial contract intentionally
counts a different explicit set or range.