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
The subscription feeRate limitsMetered usageSpend capsWhat customers seeRelated
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
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Monetize/Subscriptions & usage

Subscriptions & usage

How subscriptions, metered usage, and spend caps bill on a plan.

PreviousConnect StripeNextPlan changes & grandfathering

A @Plan is the unit you sell. It carries a recurring subscription fee, the rate limits the gateway enforces, and optional metered usage that bills per unit on top of the fee. Add a spend cap to bound what a subscriber can run up in a month. You author all of it in TypeScript on the decorated @Product class — Stripe products, prices, and meters are derived from the compiled Manifest IR; you never touch Stripe objects directly.

import { Product, Requests, Meter, Feature, Plan, capabilityGrant }
  from "@farthershore/product";

@Product({
  name: "croncloud",
  origin: "https://api.example.com",
  displayName: "CronCloud",
})
export default class CronCloud {
  @Requests()
  requests!: unknown;

  @Meter("compute", { display: "Compute", unit: "ms" })
  compute!: unknown;

  @Feature("cron-jobs", {
    plans: ["starter", "pro"],
    routes: {
      "GET /v1/cron-jobs": {},
      "POST /v1/cron-jobs": {},
      "DELETE /v1/cron-jobs/{id}": {},
    },
  })
  cronJobsFeature!: unknown;

  @Plan("starter", {
    name: "Starter",
    price: { amount: 2900, currency: "usd", interval: "month" },
    limits: {
      requests: { rate: 600, interval: "minute", enforcement: "enforce" },
    },
  })
  starter!: unknown;

  @Plan("pro", {
    name: "Pro",
    price: { amount: 19900, currency: "usd", interval: "month" },
    limits: {
      requests: { rate: 6000, interval: "minute", enforcement: "enforce" },
    },
  })
  pro!: unknown;
}

The subscription fee

price is the recurring fee, in integer cents, taken verbatim — no scaling. amount: 2900 is $29.00/month. The interval is "month" or "year". Omit price (or pass { free: true }) for a free plan.

@Plan("starter", {
  name: "Starter",
  price: { amount: 2900, currency: "usd", interval: "month" }, // $29/mo
})
starter!: unknown;

@Plan("free", { name: "Free", price: { free: true } })
free!: unknown;

currency must be "usd" and money is integer-only — 2900 cents, never 29.00. (See the money convention in @farthershore/contracts.)

Rate limits

Every plan must declare at least one rate limit — the platform refuses to publish a plan with no limits. A limit is a per-window capacity on a metered dimension, authored as { rate, interval, enforcement }:

  • rate — the capacity (a positive integer).
  • interval — second | minute | hour | day | week | month.
  • enforcement — "enforce" (the gateway rejects over-limit calls with 429, wire code rate_limited) or "track" (count only, never block).
limits: {
  requests: { rate: 600, interval: "minute", enforcement: "enforce" },
}

requests is the platform-managed request meter — declare it once with @Requests() and it counts 1 per successful request on every metered route, no backend code required. You can rate-limit any declared @Meter dimension the same way.

A plan with no rate-limit rule fails the publish gate with PLAN_RATE_LIMIT_REQUIRED. The hint shows the minimal shape: limits: { requests: { rate: 600, interval: "minute" } }.

Metered usage

A @Meter declares a billable dimension on top of the flat fee. There are two ways usage reaches the meter:

  • Fixed cost — the gateway knows the cost per call. Set it per route with cost, or product-wide with a meter's routeDefault.
  • Dynamic report — the upstream reports the real quantity after the call, with @farthershore/backend. Declare it on the route with reports, and give the meter an estimate so the gateway can admit the request before the actual number is known.
@Meter("tokens_used", { unit: "token", estimate: 500 })
tokensUsed!: unknown;

@Feature("runs", {
  routes: {
    // Gateway charges 10 api_credits up front:
    "POST /v1/export": { cost: { api_credits: 10 } },
    // Upstream reports tokens_used after the call; 500 is the admission estimate:
    "POST /v1/chat": { reports: "tokens_used" },
  },
})
runs!: unknown;

To price that usage, attach per-unit overage to the plan with meter. micros is the per-unit rate in micro-dollars (1,000,000 µ = $1.00); includedUnits is an optional free allotment before billing starts:

@Plan("pro", {
  name: "Pro",
  price: { amount: 19900, currency: "usd", interval: "month" },
  limits: { requests: { rate: 6000, interval: "minute", enforcement: "enforce" } },
  // $0.000002 per token after the first 1,000,000 tokens:
  meter: { tokens_used: { micros: 2, includedUnits: 1_000_000 } },
})
pro!: unknown;

The same meter cannot be both a fixed cost and a dynamic report on one route — authoring it both ways throws at compile time. See usage strategies for the full patterns.

Spend caps

maxMonthlySpendCents bounds total spend on a plan per billing month. Once the subscriber's metered usage would exceed the cap, further billable calls are denied with 402 instead of running up the bill. Pair it with overageBehavior to choose what happens at a limit: "block" (refuse) or "allow_and_bill" (let it through and charge).

@Plan("pro", {
  name: "Pro",
  price: { amount: 19900, currency: "usd", interval: "month" },
  limits: { requests: { rate: 6000, interval: "minute", enforcement: "enforce" } },
  meter: { tokens_used: { micros: 2 } },
  maxMonthlySpendCents: 50_000, // never bill more than $500/mo of usage
  overageBehavior: "allow_and_bill",
})
pro!: unknown;

minMonthlySpendCents sets a floor — a minimum monthly charge invoiced through Stripe even if usage is lower (useful for committed-spend plans).

Subscriber-adjustable cap

The subscriber can set their own cap inside the plan's ceiling. The Frontend SDK exposes it through fs.billing (and the zero-prop <SpendCapControl/> component):

await fs.billing.setSpendCap({ maxMonthlySpendCents: 20_000 }); // their $200/mo cap
const cap = await fs.billing.getSpendCap(); // → number | null

The subscriber-set cap is currently stored, not enforced at the edge (it rides in subscription metadata pending the rate-limit migration). Surface it as a preference, not a hard ceiling — the enforced ceiling is the plan's maxMonthlySpendCents.

What customers see

The platform tracks usage per subscriber and exposes it through the Frontend SDK — no reporting code on your side:

const usage = await fs.usage.snapshot();
usage.summary.tokens_used; // total of a product-defined meter this period
usage.events[0]?.dimensions; // per-event meter values

const sub = await fs.billing.subscription(); // plan, status, period, trial

The managed components <UsageCard/>, <BillingSummary/>, and <BillEstimator/> render this with zero props under <FartherShoreRoot>.

Related

  • Billing strategies — flat, usage, overage, prepaid shapes.
  • Plan changes & grandfathering — reprice without breaking existing subs.
  • Connect Stripe — the publish precondition.