Farther ShoreDocs
Go to Farther Shore
The fs.business() program
Meters & measures
Counted resources
Routes & access groups
Route groups & grants
Plans & pricing
The five controlsKinds and their controlsArchetypesPrice valuesGrantsLimits and capacityAvailability and retirementProduct boundsBefore releasing a change
The build output
Team RBAC
Tenancy & identity
Frontend integrations
@farthershore/business
Add metered routes
Add a resource limit
Add team RBAC
@farthershore/business exports
@farthershore/business/codegen exports
Status
Docs/Build your product/Plans & pricing

Plans & pricing

Declare a plan's kind and its five economic controls; grant access and bounds beside them.

fs.plan(id, options) declares one sellable plan. Every plan states its kind — fs.plan.kind.free | flat | usage | prepaid | hybrid | trial | custom — and only the economic controls that kind allows. The compiler validates the controls against the declared kind; a mismatch is a build error (PLAN_KIND_CONTROL_MISMATCH), never a silent reclassification.

ts
import * as fs from "@farthershore/business";

const requests = fs.requests();
const units = fs.measure("units");
const usage = fs.meter("api_usage", { measures: [units] });
const usagePricing = fs.pricing("api_usage", {
  meter: usage,
  catalog: [fs.rate.perUnit(fs.money.usd(0.01))],
});
const api = fs.backend("api", {
  transport: { mode: "direct" },
  default: true,
});
const search = fs.route("/v1/search", {
  get: { backend: api, costs: [requests.fixed(1)], reports: [usage] },
});
fs.meterRoutes("search-usage", search, { reports: [usage] });

fs.plan("pro", {
  kind: fs.plan.kind.hybrid,
  price: fs.money.usd(30).monthly(),
  usagePricing: usagePricing.current(),
  funding: { buckets: [fs.included(fs.money.usd(10))] },
  spendPolicy: {
    onExhaustion: fs.exhaustion.overage(usagePricing.current()),
  },
  grants: [search],
  limits: [requests.perMinute(600)],
});

export default fs.business();

That plan is $30/month, includes $10 of rated usage every period, prices everything past the allowance at the current api_usage catalog, and lets subscribers keep calling after the allowance is spent (overage). Access (grants) and structural bounds (limits) sit beside the economics but are independent mechanisms — see Entitlements vs economics.

The five controls

ControlTypeWhat it decides
pricefs.money.usd(n).monthly() / .yearly()Recurring economics — the fee a subscription pins when it is created.
usagePricingpricing.current() / .withContractTerms() / .fixedVersion(n)Which pricing catalog rates this plan's measurements, and how it is bound.
funding{ buckets: [fs.included(…), fs.prepaid(…), fs.promo(…), fs.referral(…)] }Value that pays for rated usage before anything is owed — see Funding & allowances.
lifecycle{ trialDays: n }Trial gating; during the trial no obligation accrues.
spendPolicy{ onExhaustion, disclosure?, rail? }What happens when funding is exhausted, and whether surfaces disclose rates.

Every value is an SDK constructor or ref: fs.plan.kind.*, fs.exhaustion.block / fs.exhaustion.overage(binding), fs.disclosure.opaque | transparent, fs.display.multiplier({ factor }), fs.rail.x402. Hand-written objects and bare strings are rejected at build time.

Kinds and their controls

KindRequiresAllowsTypical shape
freenothingnothing economicfs.plan("free", { kind: fs.plan.kind.free }) — bound it with limits.
flatpricepriceA $30/month subscription with no metered charges.
usageusagePricingspendPolicy only for rail.x402Postpaid pay-as-you-go against a catalog.
prepaidusagePricing, funding (only fs.prepaid buckets), spendPolicy with fs.exhaustion.blockthose threeA wallet: usage draws down purchased value and stops at zero.
hybridprice, usagePricing, funding (≥1 bucket), spendPolicy with fs.exhaustion.overage(...)those fourSubscription plus an included allowance plus overage.
trialprice, lifecycleusagePricingA trial that converts to the recurring price; usage pricing rates post-trial use.
customusagePricing, funding, spendPolicy (block or overage)all fivefs.plan.kind.custom — bespoke shapes that still select exactly one economic mode.

The overage binding on a hybrid plan must name the same pricing family as usagePricing; the compiler rejects a mismatch.

Archetypes

The fragments below illustrate economic controls, not complete buildable programs. Declare their referenced pricing first and add route grants and a rate-limit rule to every plan, then seal the program once. The complete example above shows those required pieces together. A plan that bills usage must bound it somehow — a limits rule, maxMonthlySpendCents, or spendPolicy: { onExhaustion: fs.exhaustion.block } — or the build fails with PLAN_UNBOUNDED_SPEND.

ts
fs.plan("free", { kind: fs.plan.kind.free });

fs.plan("flat", {
  kind: fs.plan.kind.flat,
  price: fs.money.usd(30).monthly(),
});

fs.plan("usage", {
  kind: fs.plan.kind.usage,
  usagePricing: usagePricing.current(),
});

fs.plan("prepaid", {
  kind: fs.plan.kind.prepaid,
  usagePricing: usagePricing.current(),
  funding: { buckets: [fs.prepaid(fs.money.usd(25), { topUp: true })] },
  spendPolicy: { onExhaustion: fs.exhaustion.block },
});

fs.plan("trial", {
  kind: fs.plan.kind.trial,
  price: fs.money.usd(30).monthly(),
  lifecycle: { trialDays: 14 },
});

An opaque-allowance plan (the "5x / 20x" shape) keeps rates hidden and shows subscribers only allowance remaining:

ts
fs.plan("max-5x", {
  kind: fs.plan.kind.hybrid,
  price: fs.money.usd(100).monthly(),
  usagePricing: usagePricing.current(),
  funding: {
    buckets: [
      fs.included(fs.money.usd(25), {
        display: fs.display.multiplier({ factor: 5 }),
      }),
    ],
  },
  spendPolicy: {
    disclosure: fs.disclosure.opaque,
    onExhaustion: fs.exhaustion.overage(usagePricing.current()),
  },
});

The multiplier's base is always amount / factor ($5 here) — an inconsistent display cannot be authored. Two plans on the same catalog with $25 and $100 allowances are exactly 5x and 20x of the same $5 base, so the marketing label is honest by construction. See the enterprise LLM archetype for multi-measure, multi-dimension pricing.

Price values

Create prices with the SDK so money is represented exactly:

ts
fs.money.usd(29).monthly();
fs.money.usd(290).yearly();

Amounts passed to fs.money.usd() are human major units with at most two decimal places. Sub-cent per-unit rates are authored on the catalog with fs.rate.per(1000, fs.money.usd(5)) (half a cent per unit) or fs.rate.perMillion(...), never as fractional dollars on the plan.

Grants

Plans accept route refs, group refs, and frontend-integration refs under grants. Grants are access declarations only; monetary values are rejected. A declaration is not customer-accessible merely because it exists — access comes from the subscriber's compiled plan grants, except for explicitly public routes.

Limits and capacity

Every plan needs at least one complete rate-limit rule at build time, including paid plans. A resource count cap alone does not satisfy that requirement.

Plan limits accept typed structural bounds:

ts
limits: [requests.perMinute(600), projects.max(25)];

fs.requests() provides per-window request bounds; fs.resource() provides inventory caps. capacity bounds one request (input tokens or payload bytes). Structural bounds are admission policy; they never change what a measurement costs. Prepaid and hybrid plans are additionally bounded by their funding: the gateway reserves each request's economic maximum against the subscriber's buckets before forwarding it (see Monetary admission).

Window strategies and compiler constraints

Temporal limits default to fixed_window. The SDK also accepts sliding_window and, for sub-day limits, token_bucket. Choose the algorithm explicitly when burst behavior matters; do not confuse a billing allowance with a request-rate limit.

Authored ruleCompiler behavior or rejection
Window shorter than 86,400 secondsEmits a rate-limit constraint
Window of 86,400 seconds or longerEmits a quota constraint, including custom windows
Quota-length window with token_bucketRejected; use fixed/sliding window or a sub-day token bucket
Token bucket without both bucketCapacity and refillRatePerSecondRejected by the SDK
Token bucket burst capacity greater than the limit capacityRejected; the burst must not widen the authored limit
Bucket fields on a non-token-bucket ruleRejected by the SDK
MAX, LATEST, or UNIQUE_COUNT with non-fixed accountingRejected for nonzero capacity; these aggregations are not additive over a rolling horizon

Bucket capacity and refill rate must be positive finite numbers. Zero limit capacity is a deny-all policy, not an unlimited sentinel; Core normalizes its algorithm to fixed-window accounting. This does not excuse invalid bucket fields or a token bucket on a quota-length window.

The SDK's named day/week/month durations are 86,400 / 604,800 / 2,592,000 seconds. A named month is a 30-day duration; do not assume it means a calendar month or the subscription's billing period. Keep structural windows and billing cadence distinct when explaining the product to subscribers.

Availability and retirement

selfServeEnabled: false removes a plan from customer self-service while preserving existing assignments. archive can schedule retirement and point at a successor plan ref.

Do not delete a plan declaration while subscribers still depend on it. Existing subscriptions stay pinned to the release they bought; see Commercial releases.

Product bounds

The active contract supports at most six plans. Keep the plan set legible; prefer clear plans over a matrix of tiny variations.

Before releasing a change

bash
farthershore build
farthershore commercial-release diff <from-release.json> <to-release.json>
git push
farthershore apply-timeline inspect <business> <commit-sha> \
  --env production \
  --format json

A published release affects new subscriptions. Existing subscriptions keep their recurring-price pin; subscribers bound to pricing.current() pick up the usage rates of each newly activated release, forward only — "current" means the catalog version in the release being served, not a rate that moves before you publish, and work already admitted is never rerated. For an active repository-managed business, production publication happens through a GitHub Release for the reviewed commit. farthershore business publish is only for first activation while the business is still DRAFT.

PreviousRoute groups & grantsNextThe build output

On this page

The five controlsKinds and their controlsArchetypesPrice valuesGrantsLimits and capacityWindow strategies and compiler constraintsAvailability and retirementProduct boundsBefore releasing a change