Prepaid credits
Sell a credit balance up front and meter usage down against it.
Sell a credit balance up front and meter usage down against it.
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.
// 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 kind | Fields | Use |
|---|---|---|
credit | amount_cents; optional recurs (refresh each period), label + expires_after_days (promotional) | The wallet balance — one-time, recurring, or a labelled campaign credit. |
top_up | sku, label, price_cents, credit_cents | A 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
--grant one_time:5000 and
--grant recurring:2000 — which both compile down to a credit entry.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.
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" } },
],
},
],
})
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
farthershore build succeeds and the plan shows a one-time grant + a meter.credit_balance shows it).creditUnitsConsumed).auto_recharge tops it up).