Add a metered capability
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 @Meter, a @Feature
route that reports it, and an overage price on a plan. This is the most common
change you'll make: it touches three members of your @Product class and nothing
else.
The running example is CronCloud, the product used throughout these recipes.
It already declares @Requests() (the platform-managed request meter), a
cron-jobs feature, and starter/pro plans. Here we add a compute meter
measured in milliseconds, report it from the create route, and bill it as
overage on the Pro plan.
@Product classAll product state lives in one decorated class in product/product.config.ts.
There is no YAML — you edit TypeScript and push.
// product/product.config.ts
import {
Product,
Requests,
Meter,
Resource,
Capability,
Feature,
Plan,
capabilityGrant,
} from "@farthershore/product";
@Product({
name: "croncloud",
origin: "https://api.example.com",
displayName: "CronCloud",
description: "Managed cron jobs",
})
export default class CronCloud {
@Requests()
requests!: unknown;
// 1. Declare the new dimension. `estimate` is the pre-request admission
// value the gateway reserves before the upstream reports the real number.
@Meter("compute", { display: "Compute", unit: "ms", estimate: 250 })
compute!: unknown;
@Resource("cron_jobs", { display: "Cron jobs", countSource: "action_inferred" })
cronJobs!: unknown;
@Capability("managed-cron", {
title: "Managed Cron Jobs",
includesFeatures: ["cron-jobs"],
})
managedCron!: unknown;
@Feature("cron-jobs", {
description: "Cron job CRUD",
plans: ["starter", "pro"],
routes: {
"GET /v1/cron-jobs": {},
// 2. The create route reports `compute`. The upstream sends the real
// value with @farthershore/backend (see below); `requests = 1` is
// still inherited automatically.
"POST /v1/cron-jobs": { reports: "compute" },
"DELETE /v1/cron-jobs/{id}": {},
},
})
cronJobsFeature!: unknown;
@Plan("starter", {
name: "Starter",
price: { amount: 2900, currency: "usd", interval: "month" },
grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 10 } })],
limits: {
requests: { rate: 600, interval: "minute", enforcement: "enforce" },
},
})
starter!: unknown;
@Plan("pro", {
name: "Pro",
price: { amount: 19900, currency: "usd", interval: "month" },
grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 100 } })],
// 3. Bill the new dimension. `micros` is the per-unit rate in
// micro-dollars; `includedUnits` is a free allotment per period.
meter: { compute: { micros: 50, includedUnits: 100_000 } },
limits: {
requests: { rate: 6000, interval: "minute", enforcement: "enforce" },
},
})
pro!: unknown;
}
A meter that a route lists under reports is a dynamic meter: the upstream
reports its value per request. A meter listed under cost (e.g.
{ compute: 5 }) is a fixed cost the gateway knows without the upstream. A
meter cannot be both on the same route — the SDK throws at build time.
A reports meter needs the upstream to send the measured value on the response.
Use withUsage from @farthershore/backend — it
signs the usage into the gateway response path with no extra network call.
import { fartherShore, withUsage } from "@farthershore/backend";
const fs = fartherShore.initFromEnv(); // derives everything from FS_RUNTIME_TOKEN
export async function POST(request: Request) {
const url = new URL(request.url);
const body = new Uint8Array(await request.clone().arrayBuffer());
await fs.verifyRequest({
method: request.method,
path: url.pathname,
query: url.search,
headers: request.headers,
body,
});
const result = await createCronJob(await request.json());
return withUsage(request, Response.json(result), { compute: result.computeMs });
}
If result.computeMs exceeds the route estimate you set, the gateway reconciles
the difference on the response path.
# Compile the manifest locally to confirm the edit is valid.
farthershore build --format json
build runs the same deterministic compile the platform does. Push product/**
and the GitHub bot validates and applies it; then confirm the new meter shows up
on real traffic:
# After traffic flows, the new dimension appears in the usage summary.
farthershore usage summary croncloud --format json
farthershore build succeeds and the IR lists a compute meter.POST /v1/cron-jobs call is allowed and compute rises by the reported value.requests still increments by 1 on every metered route.compute above 100,000 included units at $0.000050/unit.reports + withUsage loop for LLM tokens.