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
Gate a feature
Change a price
Prepaid credits
Add a prepaid planMeter usage down against the balanceShow the balance in the UICreate it from the CLI insteadVerify it worksRelated
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Cookbook/Prepaid credits

Prepaid credits

Sell a credit balance up front and meter usage down against it.

PreviousChange a priceNextMeter AI tokens

You want customers to buy a balance and spend it as they use the product — a prepaid wallet. In Farther Shore this is a knob combination on a @Plan: a credit grant seeds the balance, and a metered dimension prices the usage that draws it down. Billing math is handled by Stripe Billing at invoice time: bill = recurring_fee + max(0, total_meter_cost − applied_credit_balance).

CronCloud's running example bills cron jobs by request; here we add a prepaid plan: $50 of one-time credit, then $0.001 per request, no recurring fee.

Add a prepaid plan

// product/product.config.ts
@Plan("prepaid", {
  name: "Prepaid",
  // No `price` → no recurring fee. A one-time `credit` grant seeds the wallet:
  // a credit with no `recurs` is one-time; add `recurs: true` for a balance
  // that refreshes each period (included usage).
  grants: [{ kind: "credit", amount_cents: 5000 }], // $50 starting balance
  // Per-unit usage price in micro-dollars. 1000 micros = $0.001 / request.
  meter: { requests: { micros: 1000 } },
  // Publishing requires every plan to carry a rate limit (burst protection).
  limits: { requests: { rate: 600, interval: "minute", enforcement: "enforce" } },
})
prepaid!: unknown;

The grants array is the only credit surface, and it has two grant kinds:

Grant kindFieldsUse
creditamount_cents; optional recurs (refresh each period), label + expires_after_days (promotional)The wallet balance — one-time, recurring, or a labelled campaign credit.
top_upsku, label, price_cents, credit_centsA buy-extra-credit SKU surfaced in checkout.

Rollover and auto-recharge are not grants — they're a separate credit policy (creditPolicy). To let a balance refill itself and roll over what's left, keep the grant and add a policy:

@Plan("prepaid", {
  name: "Prepaid",
  grants: [{ kind: "credit", amount_cents: 5000 }],
  raw: {
    creditPolicy: {
      rollover: { percent: 100 },
      auto_recharge: { threshold_cents: 500, refill_cents: 5000 },
    },
  },
  meter: { requests: { micros: 1000 } },
  limits: { requests: { rate: 600, interval: "minute", enforcement: "enforce" } },
})
prepaid!: unknown;

A one-time credit (no recurs) plus a meter builds a prepaid wallet; a recurs: true credit plus a meter builds included usage (a monthly allowance

  • overage). The CLI exposes friendlier sugar — --grant one_time:5000 and --grant recurring:2000 — which both compile down to a credit entry.

Meter usage down against the balance

Plain request counting is platform-managed — @Requests() already makes every metered route draw on the wallet at the plan's per-request price. For a wallet priced in credit units rather than raw request count, report creditUnitsConsumed from the backend with withUsage:

import { fartherShore, withUsage } from "@farthershore/backend";

const fs = fartherShore.initFromEnv();

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 runJob(await request.json());
  return withUsage(
    request,
    Response.json(result),
    { requests: 1 },
    { creditUnitsConsumed: { credits: result.creditsUsed } },
  );
}

creditUnitsConsumed is a numeric map validated locally before signing; the gateway settles it against the subscriber's balance on the response path.

Show the balance in the UI

The @Frontend manifest has a credit_balance component for the wallet and a usage_card for draw-down over time:

import { Frontend } from "@farthershore/product";

@Frontend({
  pages: [
    {
      path: "/billing",
      title: "Billing",
      requiresAuth: true,
      components: [
        { component: "credit_balance" },
        { component: "usage_card", props: { meter: "requests" } },
      ],
    },
  ],
})

Create it from the CLI instead

Every part of the plan is expressible on plan create — --grant one_time:5000 seeds the wallet, --meter requests:1000 prices the draw-down:

farthershore plan create croncloud \
  --key prepaid --name "Prepaid" \
  --grant one_time:5000 \
  --meter requests:1000 \
  --format json

Verify it works

  • farthershore build succeeds and the plan shows a one-time grant + a meter.
  • A new prepaid subscriber starts with a $50 balance (credit_balance shows it).
  • Each request reduces the balance by the metered price (or creditUnitsConsumed).
  • Once the balance is exhausted, overage bills at $0.001/request (or auto_recharge tops it up).

Related

  • Add a metered capability — add more dimensions for the wallet to draw on.
  • Meter AI tokens — price a credit wallet by tokens.
  • Change a price — adjust the credit amount or per-unit price safely.