Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
Meters & resources
Features & routes
Capabilities & entitlements
Plans & pricing
The Manifest IR
Bring your own backend
Transport modes
Metering & verification
Runtime tokens
Frontend SDK
Root & data components
Auth & sessions
Entitlement gates
Connect Stripe
Subscriptions & usage
Plan changes & grandfathering
Billing strategies
Apply & deploy
Environments
Migrations
Docs versions & archive
Operate with an agent
Operation classes
MCP server
End-to-end via CLI/MCP
CLI reference
@farthershore/product
@farthershore/backend
@farthershore/farthershore-js
Environment variables
Response & deny codes
Add a metered capability
Edit the @Product classReport the value from your backendBuild, push, verifyVerify it worksRelated
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Cookbook/Add a metered capability

Add a metered capability

Meter a new dimension, charge for it on a route, and grant it on a plan.

PreviousResponse & deny codesNextGate a feature

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.

Edit the @Product class

All 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.

Report the value from your backend

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.

Build, push, verify

# 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

Verify it works

  • farthershore build succeeds and the IR lists a compute meter.
  • A POST /v1/cron-jobs call is allowed and compute rises by the reported value.
  • requests still increments by 1 on every metered route.
  • The Pro invoice charges compute above 100,000 included units at $0.000050/unit.

Related

  • Meter AI tokens — the same reports + withUsage loop for LLM tokens.
  • Prepaid credits — meter a dimension down against a balance instead of overage.
  • Gate a feature — restrict the route to subscribers who hold the capability.