Meters & resources
@Requests, @Meter, and @Resource — the billable and enforceable dimensions of a product.
@Requests, @Meter, and @Resource — the billable and enforceable dimensions of a product.
Meters are the dimensions you measure and bill on. A product declares them as class members
so features can report against them and plans can rate-limit
and price them. There are three member decorators: @Requests (the platform request meter),
@Meter (any other billable dimension), and @Resource (a counted thing, like cron jobs).
import { Product, Requests, Meter, Resource } from "@farthershore/product";
@Product({ name: "croncloud", origin: "https://api.example.com" })
export default class CronCloud {
@Requests()
requests!: unknown;
@Meter("compute", { display: "Compute", unit: "ms" })
compute!: unknown;
@Resource("cron_jobs", { display: "Cron jobs", countSource: "action_inferred" })
cronJobs!: unknown;
@Plan("starter", {
name: "Starter",
limits: { requests: { rate: 600, interval: "minute" } },
})
starter!: unknown;
}
@Requests() declares the platform-managed successful-request meter and applies a fixed
requests = 1 cost to every metered route. You do not need backend code for plain request
counting. It compiles to a meter keyed requests with sane defaults:
@Requests()
requests!: unknown;
emits, in product.metering.meters:
{
"key": "requests",
"display": "Requests",
"unit": "request",
"estimate": 1,
"enforcementType": "estimated_then_settled",
"aggregation": "COUNT"
}
Every metered route then inherits defaults: { requests: 1 }. You can override the display,
unit, estimate, window, or enforcementType via options (@Requests({ display: "API calls" })),
but the per-route cost is fixed at 1 — @Requests does not accept routeDefault.
@Meter(key, options) declares a billable or enforceable dimension you control — tokens,
compute milliseconds, credits, rows processed. The first argument is the meter key (the
string other declarations reference); display defaults to a title-cased version of the key.
@Meter("tokens_used", { unit: "token", estimate: 500 })
tokensUsed!: unknown;
| Option | Type | Meaning |
|---|---|---|
display | string | Human label. Defaults to a title-cased key (tokens_used → Tokens Used). |
unit | string | Unit of measure (token, ms, credit, …). |
estimate | number | Reusable pre-request estimate used for admission checks when this meter is a dynamic route report. |
routeDefault | number | A fixed cost applied to every metered route unless the route opts out. |
aggregation | "SUM" | "COUNT" | "MAX" | "UNIQUE_COUNT" | "LATEST" | How usage rolls up. Dynamic meters default to SUM. |
enforcementType | "exact_pre_request" | "estimated_then_settled" | "postpaid" | "strict_concurrency" | How the gateway admits requests against the meter. |
window | "minute" | "hour" | "day" | "month" | "billing_period" | Rolling window for windowed meters. |
A meter's routeDefault adds a reusable fixed cost to every metered route. Route-level
cost adds on top of the default:
// routeDefault: 2 is the product-wide default cost for api_credits.
@Meter("api_credits", { unit: "credit", routeDefault: 2 })
credits!: unknown;
@Feature("runs", {
routes: {
"POST /v1/runs": { cost: { api_credits: 10 } }, // default 2 + explicit 10
},
})
runs!: unknown;
compiles the route's metering to defaults: { api_credits: 12, requests: 1 }. See
features & routes for per-route cost, reports, unmetered, and
inheritDefaultMeters.
If a route lists a meter in its reports (the upstream reports actual usage after the
request), the gateway needs an estimate to admit the request before usage is known. Provide
it on the meter (estimate: 500) or per-route (estimates: { tokens_used: 750 }). A
reported meter with no estimate anywhere fails the build:
@Meter("tokens_used", { unit: "token" }) // no estimate
tokens!: unknown;
@Feature("chat", { routes: { "POST /v1/chat": { reports: "tokens_used" } } })
chat!: unknown;
// throws: meter "tokens_used" needs an estimate
Dynamic meters are reported at runtime by your backend with @farthershore/backend. The
SDK only declares the dimension and its estimate; the actual value is reported per request.
@Resource(name, options) declares a counted thing whose quantity a plan can cap (e.g. how
many cron jobs a subscriber may create). Resource caps land in a plan's capability_limits,
not in rate limits[].
@Resource("cron_jobs", { display: "Cron jobs", countSource: "action_inferred" })
cronJobs!: unknown;
| Option | Type | Meaning |
|---|---|---|
display | string | Human label. |
scope | "subscription" | "subject" | Whether the count is per-subscription or per-subject. |
subjectType | string | The subject type when scope: "subject". |
countSource | "reported" | "action_inferred" | action_inferred derives the count from create/delete actions; reported expects your backend to report it. |
With countSource: "action_inferred", the count is maintained from feature actions whose
resource effect is create or delete (see the cron-job actions on the
features page). A plan then caps it:
@Plan("starter", {
name: "Starter",
grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 10 } })],
limits: { requests: { rate: 600, interval: "minute" } },
})
starter!: unknown;
This compiles to capability_limits: { cron_jobs: 10 } on the starter plan. See
plans & pricing for limits, grants, and overage, and
capabilities & entitlements for how cron_jobs ties to the
managed-cron capability.
Meters are sorted by key in the emitted IR, so declaration order does not affect the
IR hash. Plan dimension records (limits, meter, caps) keep
declaration order, so their keys must not be integer-like — an integer-like key like "0"
throws at decoration time to prevent silent reordering.