Add metered routes
Meter a new dimension, charge for it on a route, and grant it on a plan.
Meter a new dimension, charge for it on a route, and grant it on a plan.
You want to add a billable dimension to a product — a new measure and meter, a
pricing catalog for it, a fs.meterRoutes() binding on the route that reports
it, and a plan that binds the catalog. This is the most common change you'll
make: it pairs the business declaration change with backend reporting and a
preview settlement check.
The running example is CronCloud, the product used throughout these recipes.
It already declares fs.requests() (the platform-managed request counter), a
cron-jobs route, and starter/pro plans. Here we add a compute measure
in milliseconds, report it from the create route, and bill it as overage on
the Pro plan after a $5 included allowance.
One route reports a new measurement, and the Pro plan prices that measurement after an included allowance.
@farthershore/backendProduct state lives in the composable business/ program. It may span modules,
but the folder must contain exactly one default-exported fs.business() result. There is no
YAML: edit TypeScript and push.
import * as fs from "@farthershore/business";
const requests = fs.requests();
const computeMs = fs.measure("compute_ms");
const compute = fs.meter("compute", { measures: [computeMs] });
const computePricing = fs.pricing("compute", {
meter: compute,
catalog: [fs.rate.per(1000, fs.money.usd(0.05))],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests, compute],
default: true,
});
const createJob = fs.route("/v1/cron-jobs", {
post: { backend: api, costs: [requests.fixed(1)], reports: [compute] },
});
fs.meterRoutes("cron-jobs-compute", createJob, {
reports: [compute],
caps: [computeMs.atMost(60_000)],
});
fs.plan("pro", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(49).monthly(),
usagePricing: computePricing.current(),
funding: { buckets: [fs.included(fs.money.usd(5))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(computePricing.current()),
},
grants: [createJob],
limits: [requests.perMinute(600)],
});
export default fs.business();
fs.rate.per(1000, fs.money.usd(0.05)) is $0.00005 per millisecond, exactly.
The $5 included bucket covers the first 100,000 ms each period; overage is
rated at the same catalog. caps declares a finite per-request bound the
gateway uses when reserving spend.
A meter bound with fs.meterRoutes() is a reported measurement: the
upstream sends its value per request. costs: [requests.fixed(1)] is a
gateway-known structural count used for rate limits. Money never appears on
either — it lives in the pricing catalog.
A bound meter needs the upstream to send the measured value. Use
ctx.report() from @farthershore/backend —
before the response is sent it signs the usage into the platform response path
with no extra network call.
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv(); // derives everything from FS_RUNTIME_TOKEN
// Use the scaffold's raw-body capture before fs.middleware(), then JSON parsing.
// The middleware supplies the response adapter for in-band reporting.
app.post(
"/v1/cron-jobs",
fs.handler(async (ctx, req, res) => {
const result = await createCronJob(req.body);
const report = await ctx.report({
meter: "compute",
values: { compute_ms: result.computeMs },
});
if (!report.ok) console.error("Usage delivery failed", report.reason);
res.json(result);
}),
);
The platform rates result.computeMs from the signed response evidence under
the release the request was admitted with.
For a Fetch handler, use the response-sink example.
Calling verifyRequest() without that adapter chooses post-stream delivery;
returning a Response does not attach reporting headers automatically.
# Validate the manifest locally to confirm the edit is valid.
farthershore build --format json
build runs the same deterministic validation the platform does. Push
business/** and the GitHub bot validates and applies it. Wait until env list
contains the target preview before creating or binding its matching api row;
if automatic branch-prefix creation did not occur, create the preview explicitly
first. Then confirm the new meter shows up on real traffic:
farthershore env list croncloud --format json
farthershore backend create croncloud --env <environment> \
--name "Preview API" --slug api --transport direct \
--idempotency-key <persisted-backend-create-attempt-key> \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list croncloud --format json
# After traffic flows, the new dimension appears in the usage summary.
farthershore usage summary croncloud --format json
Filter the structured backend list by the preview's environment id.
farthershore build succeeds and the validated business lists a compute
meter with a compute_ms measure.POST /v1/cron-jobs call is allowed and compute_ms rises by the reported
value.requests increments by 1 because the route explicitly attaches its fixed
cost.receivableNanos growing at exactly $0.00005 per millisecond.Stop test traffic and revert the meter, route binding, and plan price together in preview. If incorrect usage reached billing, diagnose it before replaying or adjusting any event.
Add a
computemeter with acompute_msmeasure to the existing create route, price it in a catalog, bind it on Pro as a hybrid plan with a $5 included allowance and overage, and report actual usage throughctx.report(). Build and verify one preview request and its usage delta. Do not publish or change live billing.
meterRoutes + report() loop for LLM tokens.