Gate a feature
The pattern for putting a paywall on the request path.
The shape is always the same: check, work, record.
import { Base316 } from "@base316/sdk";
const base316 = new Base316();
export async function generateReport(accountId: string, requestId: string) {
// 1. Ask before doing the work.
const decision = await base316.access.check({
externalId: accountId,
feature: "reports",
requested: 1,
});
if (!decision.allowed) {
throw new PaymentRequired(decision.reason);
}
// 2. Do the work.
const report = await buildReport(accountId);
// 3. Record it, only now that it succeeded.
await base316.events.record({
externalId: accountId,
event: "report_generated",
idempotencyKey: requestId,
});
return report;
}Turn a refusal into a useful error
reason is safe to log and safe to branch on. Map it to something your user can
act on:
const MESSAGES: Record<string, string> = {
USAGE_LIMIT_EXCEEDED: "You have used your monthly reports. Upgrade or add credits.",
HARD_CAP_REACHED: "You have reached this month's spending limit for reports.",
ENTITLEMENT_REQUIRED: "Reports are not included in your plan.",
SUBSCRIPTION_INACTIVE: "Your subscription is not active.",
NO_SUBSCRIPTION: "This account has no subscription.",
};
throw new PaymentRequired(MESSAGES[decision.reason ?? ""] ?? "Not available on your plan.");Warn before the wall
decision.usage carries remaining. Showing it is the difference between a
customer who upgrades and one who files a ticket:
const { used, limit, remaining } = decision.usage ?? {};
if (limit !== null && remaining !== null && remaining < limit * 0.1) {
notifyNearLimit(accountId, { used, limit, remaining });
}You can also subscribe to the usage.limit_reached webhook rather than
computing it yourself.
Overage: when the limit does not stop anyone
If the plan prices the feature's usage — a per-unit, tiered or package price on
it — the limit is soft. check keeps answering allowed: true past it and
the extra usage is billed. Tell the user when that starts, because from then on
every call costs them money:
if (decision.allowed && decision.usage?.overage) {
notifyBilledOverage(accountId, {
billable: decision.usage.billableOverage,
cap: decision.usage.hardCap,
});
}To stop runaway spend, give the entitlement a hard cap. Past it, check
refuses with HARD_CAP_REACHED — a different message from
USAGE_LIMIT_EXCEEDED, because the fix is different: raising a spending limit,
not upgrading the plan. Credits never raise the cap.
A plan with no usage price on the feature keeps a hard limit, refused with
USAGE_LIMIT_EXCEEDED exactly as before.
Gating on a permission as well
For a feature only some users in an account may use:
const decision = await base316.access.check({
externalId: accountId,
userId: actingUserId,
feature: "reports",
permission: "reports.export",
});Both must pass. The account must be entitled and the user must hold the permission.
Things to get right
- Check before, record after. Work that fails should cost nothing.
- Pass
requestedwhen you know how much you are about to use, so the refusal happens before the cost. - Do not treat
allowedas "within the plan". On a soft limit it can mean "allowed, and billed" — readusage.overage. - Never cache an allow across a request boundary. The point of the call is that the answer changes.
- Fail closed on a network error, or fail open deliberately — but decide. Silently allowing on a timeout is a decision you did not know you made.

