This page covers the customer-operations governance area of Oshun V1: how a
customer's plan and feature access (entitlements) are defined, how money
turns into entitlement (billing, settled crypto-first), how usage is metered
and capped, how lapses and refunds are handled (dunning and self-serve),
and how human help reaches the customer (support cases, SLAs, copilots). It
serves paying customers, institutional contract owners, support agents, and the
platform operators who run the billing and support machinery. In the V1
layering, the entitlement/billing/support core is real, shipped, deterministic
domain code in libs/oshun/billing-support (package @oshun/billing-support,
§23), composed by the product surfaces; the canonical crypto settlement that
funds those entitlements lives in the Aje domain (libs/aje/) and is
documented on
Crypto Payments — Non-Custodial Entitlement Settlement.
The backlog for this area is §23; the feature hub is
../features.md; the companion architecture page is
Support, Billing, and Crypto.
Where the billing logic actually lives — and why the docs were hard to navigate. Both ../features.md (the crypto-settlement path, via
libs/aje/andlibs/oshun/payments-bridge/) and the architecture page (the fiat adapter, vialibs/shared/inbound-integrations/src/payment.ts) describe payment paths that genuinely exist in code. But neither names the library that holds the entitlement, dunning, metered, self-serve, tax, and support-case state machines a reader is actually looking for. That islibs/oshun/billing-support. This page treats that library as the source of truth and wires the payment paths into it explicitly, so the entitlement code is findable from the prose for the first time.
What This Area Is, Concretely#
@oshun/billing-support is a pure-function domain library — deterministic state
machines and policy tables, no I/O. Its barrel (src/index.ts) re-exports
exactly eight modules, and a ninth single-file bridge:
| Module | Path | Responsibility |
|---|---|---|
entitlements |
src/entitlements/entitlements.ts |
Entitlement classes, feature map, subscription state machine, proration, feature-gate, intro offers |
paywalls |
src/paywalls/paywalls.ts |
Web/mobile paywall decision and recommended-upgrade computation |
dunning |
src/dunning/dunning.ts |
Payment-failure handling, dunning-stage progression, grace windows, trial-conversion scheduling |
self-serve |
src/self-serve/self-serve.ts |
Refund eligibility, pause/resume quotas, promo/gift-code redemption |
tax-billing |
src/tax-billing/tax-billing.ts |
VAT/GST/sales-tax application, EU reverse charge, locale-correct receipts |
metered |
src/metered/metered.ts |
Per-dimension usage accrual, included/soft-cap/hard-cap, usage meter cards |
support-cases |
src/support-cases/support-cases.ts |
Case intake/routing, SLAs, support copilot, CSAT |
billing-aje-bridge |
src/billing-aje-bridge.ts |
Maps billing classes → canonical product tier; advances subscription from a settled Aje payment |
The honest boundary: these are pure cores. The enforcement — charging a real chain, persisting an immutable invoice, sending a dunning email — is the runtime's job and the payment adapters'. The library decides; the surrounding services act.
Settlement Is Crypto-First and Non-Custodial#
V1 settlement is crypto-first and non-custodial. The canonical payment
surface is documented in full on
Crypto Payments — Non-Custodial Entitlement Settlement,
covering Bitcoin (on-chain + Lightning), Monero, and EVM stablecoins (USDC and
DAI on Base, Arbitrum, and Optimism). The chain implementations live under
libs/aje/chains/. Note a documentation correction here:
../features.md and the architecture notes describe "five new
chain modules" (monero, litecoin, ton, ergo, tron) — and all five do
exist with real, non-test source — but the actual libs/aje/chains/ set is
eight directories: those five plus cardano, solana, and
abstraction. The "five new" count is stale relative to the present chain set.
Release: V1.1. Fiat acceptance is built but not part of V1.0. V1.0 charges through the Aje crypto rail only; the fiat rails ship with the native apps in V1.1. The BFF refuses
/v1/payments/fiat/*withpayment_rail_deferred_to_v1_1— except/v1/payments/fiat/stripe/webhook, which stays open so settlement callbacks for charges made before the cut still land. Everything described below is real and stays in the tree.
Fiat acceptance is implemented as of 2026-07-04: Stripe Billing, plus
Apple Pay and Google Pay (both as Stripe-tokenized payment methods and
as fully-implemented merchant-decrypted wallet tokens), live in
libs/oshun/fiat-payments/ with BFF routes under /v1/payments/fiat/* — see
Fiat Payments — Stripe Billing, Apple Pay, and Google Pay.
The older libs/shared/inbound-integrations/src/payment.ts adapter remains the
generic multi-tenant connector framework (now with capture-bounded refunds).
Crypto and fiat terminate in the same place twice over: a settlement that
billing-aje-bridge translates into a subscription-state transition, and
identical-schema entitlement-bus events distinguished only by the rail
field. The fiat routes are fail-closed without deployment credentials
(STRIPE_SECRET_KEY, webhook secret, Apple merchant identity certificate) —
they refuse rather than pretend.
The Aje Bridge — One Source of Truth for "Are You Entitled?"#
The single most important fact the prose docs omit entirely is the
billing-aje-bridge, which closes a long-standing duplication. Two parallel
entitlement vocabularies had drifted apart: the six billing classes in
@oshun/billing-support, and the three canonical product tiers
(OshunEntitlementTier from @oshun/auth-client) that actually gate the
product. The bridge collapses the former onto the latter via TIER_BY_CLASS:
Billing class (EntitlementClass) |
Canonical product tier (OshunEntitlementTier) |
|---|---|
free |
free |
starter |
pro |
plus |
pro |
pro |
premium |
scholar |
premium |
institutional |
premium |
toOshunEntitlementTier(class) does that mapping;
entitlementTierForSubscription(sub, nowUnixSeconds) is time-aware (since
the 2026-07-04 audit remediation) and delegates its window decision to
featureGate, so the coarse tier and the fine-grained feature gate can never
drift: an active/restored subscription entitles only while the current
period covers now; trial requires an unexpired trialEndsAt; grace
requires an unexpired graceEndsAt; past-due keeps access within the
current period (dunning retries don't instantly cut a paying customer off);
canceled keeps access inside its paid-through window (canceledEffectiveAt);
everything else — including any state whose window has lapsed — returns free.
subscriptionPermitsFeature(sub, featureKey, now) is the feature-level
authority (a scholar subscription passes premium-tier checks but is still
denied voice.cloning).
The other half is applyPaymentSettlementToSubscription, which takes a settled
Aje payment — normalized from the confirmation ladder into an
AjePaymentSettlement with status: 'confirmed' | 'failed' | 'refunded' and an
opaque reference (the Aje receipt/tx id, for audit) — and advances the
subscription. It never invents a transition: it computes a target state and then
delegates to transitionSubscription, so only legal moves apply, and a
settlement with no transition for the current state is a no-op
(changed: false). The mapping:
confirmed→trial/past-due/grace/paused/restoredbecomeactive;canceled/lapsedbecomerestored; an already-activesubscription is a no-op.failed→ anactivesubscription drops topast-due; other states have no failed-payment transition.refunded→active/paused/trial/restoredmove tocanceled;grace/past-duemove tolapsed(a refund during dunning or grace ends the entitling state immediately — previously a silent no-op that let a refunded customer keep access until the window expired).
Entitlement Classes, Features, and the Subscription State Machine#
ENTITLEMENT_CLASSES is the six-class ladder: free, starter, plus, pro,
scholar, institutional. Each class owns a set of FEATURE_KEYS resolved by
featuresFor(class). The full key set is twelve entries, including the
premium-gated voice.cloning, voice.premium-narration, avatar.cloning,
avatar.premium, generated.video, generated.high-rate, and the
institution-only institutional.gradebook:
| Class | Notable feature keys it unlocks (cumulative) |
|---|---|
free |
assistant.basic |
starter |
+ study.basic |
scholar |
assistant.deep, study.adaptive, persona.premium, voice.premium-narration (narration, not cloning) |
plus |
assistant.deep, study.adaptive, persona.premium |
pro |
everything plus voice.cloning, avatar.cloning, generated.video, generated.high-rate |
institutional |
the plus set plus voice.premium-narration and institutional.gradebook |
The deliberate asymmetry — scholar and institutional grant
voice.premium-narration but not voice.cloning or avatar.cloning — is a
governance choice: classroom and institutional contexts get premium narration
without handing students cloning tools.
A Subscription carries entitlementClass, a state from
SUBSCRIPTION_STATES (trial, active, past-due, grace, paused,
canceled, lapsed, restored), the current period bounds, and the timestamps
that make trial/cancellation/grace/intro-offer behavior decidable
(trialEndsAtUnixSeconds, canceledEffectiveAtUnixSeconds,
graceEndsAtUnixSeconds, introOfferAppliedThroughUnixSeconds). State moves
are guarded by an explicit VALID_TRANSITIONS table inside
transitionSubscription; illegal moves return an illegal-transition error
rather than silently applying.
Feature Gating Is Time- and State-Aware#
featureGate({ subscription, featureKey, nowUnixSeconds }) is the single point
a surface asks "may this customer use this feature right now?" It returns
either { granted: true, via: 'entitlement' | 'trial' | 'grace' } or
{ granted: false, reason: 'not-in-plan' | 'subscription-lapsed' | 'paused' }.
The logic is genuinely time-aware, not a flat boolean:
- A
lapsedsubscription is denied; apausedone is denied withpaused. - A
canceledsubscription still grants untilcanceledEffectiveAtUnixSecondsand only while the current period coversnow— cancellation is honored as paid-through-period, then de-entitles. - A
trialgrants viatrialonly whilenow <= trialEndsAtUnixSecondsand the period coversnow. - A
gracesubscription grants viagraceonly untilgraceEndsAtUnixSeconds. - Otherwise the feature must be in the class's feature set and the current
period must cover
now.
Proration and Intro Offers#
computeUpgradeProration charges (or refunds) the price delta scaled by the
fraction of the billing period remaining:
charge = round(fractionRemaining * (toPrice - fromPrice)). A positive charge
means effectiveNow: true (an upgrade applies immediately); a negative one (a
downgrade refund) defers. applyIntroOffer is one-time-per-user — it rejects
with already-used if introOfferAppliedThroughUnixSeconds is already set,
class-not-eligible if the subscription's class isn't in
offer.applicableClasses, and invalid-offer for a malformed
discount/duration; on success it stamps the offer through
now + durationDays * 86400.
Paywalls and Upgrade Surfaces#
decidePaywall powers the web and mobile
('web' | 'mobile-ios' | 'mobile-android') paywalls and entitlement-aware
gating. Given the customer's current class and the requested feature, it returns
a PaywallDecision:
- If the current class already includes the feature →
{ showPaywall: false, upsellAngle: 'feature-included' }. - If no class includes the feature →
{ upsellAngle: 'graceful-degradation' }(nothing to upsell to). - Otherwise it computes the minimum class that includes the feature
(
minimumClassFor, walking aCLASS_RANKorderfree < starter < scholar < plus < pro < institutional), and if that target outranks the current class returns{ showPaywall: true, recommendedClass, priceAnchor, upsellAngle: 'feature-locked' }, with the price anchor pulled per-class from the suppliedPaywallPriceCatalog.
For the crypto path the surface renders a per-invoice address or BOLT11 string plus a QR code and never redirects to a custodial processor — that rendering and the rate-locking detail live on Crypto Payments.
Metered Billing — Usage, Caps, and Meter Cards#
METERED_DIMENSIONS is five dimensions, two of which the prose docs miss
entirely (gpu-minutes, and the unit name agentic-cost-units):
['agentic-cost-units', 'voice-seconds', 'avatar-seconds',
'storage-bytes-hours', 'gpu-minutes']
accrueUsage is the metered core. It first rejects mismatched input
(dimension or subscription mismatch, non-positive quantity, an out-of-period
timestamp, or a soft-cap above the hard-cap). It then applies the included
allowance: only usage past includedQuantityPerPeriod is billable, computed
as the marginal slice max(0, next - included) - max(0, used - included) and
priced at perUnitCentsAfterIncluded. Caps gate the result:
- crossing
softCapQuantityPerPeriodraisesalert: 'soft-cap-warn'; - being at/over
hardCapQuantityPerPeriodrejects withhard-cap-reached(accepted: false), pinningnewUsedQuantityto the hard cap so usage never silently exceeds it.
buildUsageMeterCard produces the per-user UI meter — used,
includedQuantity, caps, fractionOfHardCap (clamped to 1), and a status of
'ok' | 'soft-cap' | 'hard-cap' — which drives the usage-meter and budget-cap
surfaces. Metered top-ups settle through the same invoice contract as everything
else.
Dunning, Grace, and Trial Conversion#
DUNNING_STAGES is the five-stage ladder
first-warning → second-warning → final-warning → grace → lapse.
recordPaymentFailure enters or advances the dunning state and schedules the
next retry at now + retryBackoffSeconds; recordPaymentSuccess clears it.
advanceDunningStage walks the ladder against a DunningPolicy whose
DEFAULT_DUNNING_POLICY is concrete: 3 retries per stage, a 1-day (86400 s)
retry backoff, 3 days between first→second and second→final warnings, and a
7-day grace window. The ladder progresses on elapsed time and retry count —
final-warning advances to grace only after retryCount >= retriesPerStage,
and grace advances to lapse only once now >= graceEndsAtUnixSeconds. On
lapse, degradedEntitlementOnLapse() returns 'free': graceful feature
degradation, not a hard cutoff. trialPromptSchedule fires three conversion
prompts — pre-expiry (3 days before), at-expiry, and post-expiry (1 day
after).
Self-Serve Refunds, Pause/Resume, and Codes#
SELF_SERVE_ACTIONS enumerates refund-request, credit-issued,
plan-change, pause, resume, redeem-gift-code, redeem-promo-code.
evaluateRefundEligibility is a real risk gate, not a rubber stamp. It
auto-approves only reason codes in a fixed table — first-7d-cancel (≤
$50.00), duplicate-charge (≤ $1,000.00), service-outage (≤ $50.00) — and
routes everything else to manual-review with a specific reason:
reason-not-auto-approvable, amount-exceeds-auto-cap, or
refund-velocity-throttle once a customer has had 3+ refunds in the month.
Crypto refunds additionally require the customer to supply a destination address
(Monero has no on-chain return path; Bitcoin and EVM default to the original
sender, with explicit override) — see Crypto Payments.
startPause/endPause enforce a maxPauseDaysPerYear quota
(pause-quota-exceeded when daysPausedInLast365 >= maxPauseDaysPerYear).
redeemPromoCode validates a PromoCode against expiry, maxUses/usesSoFar,
and applicableClasses, returning a { percent, cents } discount or a typed
rejection (expired, max-uses-reached, class-not-eligible, unknown-code,
invalid-code).
Tax-, Region-, and Currency-Aware Billing#
applyTax computes VAT/GST/sales-tax/HST/PST against a per-jurisdiction
TaxRate. Its non-trivial case is EU cross-border B2B reverse charge: when
a VAT-rated, business customer with a VAT number transacts cross-border between
two EU jurisdictions (matched against the explicit 27-member EU_JURISDICTIONS
set), tax is shifted to the customer (taxCents: 0,
reverseChargeApplied: true). formatCurrency and buildReceiptSummary
produce locale-correct receipts via Intl.NumberFormat (with a graceful
CUR 0.00 fallback), and the receipt explicitly annotates a reverse-charge
line. Crypto invoices additionally show fiat-equivalent at invoice time plus the
locked rate and slippage tolerance — detailed on
Crypto Payments.
Support Cases — Intake, Routing, SLAs, Copilots#
The concrete support-case state machine lives in
src/support-cases/support-cases.ts (the architecture page describes SLAs as
"monitored by libs/shared/queue/src/sla-monitor.ts patterns" and points the
SupportCase contract at the backlog, but never names this implementation —
that is where the real logic is).
CASE_ROUTING_QUEUES is the seven-queue set
['general', 'billing', 'technical', 'safety', 'privacy', 'institutional', 'crisis'],
and CASE_STATES runs
received → triaged → in-progress → awaiting-customer → resolved → reopened → escalated.
routeCase applies a priority order: a crisis signal routes to crisis first,
then institutional to institutional, then any safety/privacy tag wins (so a
safety concern is never buried under a billing tag), then the first tag's mapped
queue, else general.
SLAs are per-queue, with the safety and crisis queues mirroring Trust & Safety's urgency:
| Queue | First-response budget | Resolution budget |
|---|---|---|
crisis |
5 min | 1 h |
safety |
30 min | 24 h |
privacy |
4 h | 14 d |
institutional |
4 h | 5 d |
billing |
12 h | 3 d |
general / technical |
24 h | 7 d |
evaluateCaseSla reports firstResponseBreached / resolutionBreached and the
seconds remaining against each budget. answerWithCitations is the support
copilot: it is grounded-by-construction — the caller supplies retrieved
passages, and the copilot abstains and escalates to a human when there are
no valid passages or the mean relevance of its top-3 passages is below 0.4,
rather than guessing. captureCsat accepts a 1–5 score only on a resolved
case, and aggregateCsat rolls up the mean. A SupportCase also records a
memoryPrivacyMode ('no-access' | 'session-only' | 'with-consent') so an
agent never reads memory without the corresponding privacy posture.
How This Fits the Broader Governance Area#
Support, entitlements, and billing are one of four interlocking governance cores. The others — Trust & Safety (policy taxonomy, severity SLAs, the appeal/decision taxonomy, and the cross-surface crisis-frame cascade) and Privacy (consent, DSAR, retention/deletion, residency, and regulatory compliance) — are documented on the sibling pages Review, Compliance, and Trust & Safety and Privacy, Consent, Data Portability, and User Controls. They connect directly to billing in several places, and three connections are worth making concrete because they cross the area boundary:
- The crisis queue.
routeCasesends a crisis-signalled support case to thecrisisqueue with the harshest SLA (5 min first response). On the Trust & Safety side this is backed by a real cross-surface cascade: the eventLILITH_CRISIS_FRAME_ACTIVATED_EVENT = 'lilith.crisis_frame.activated'is published (via a port — the domain stays free of@oshun/event-busby design) and fanned out toCRISIS_FRAME_SURFACES = ['psyche', 'lilith-video', 'tara', 'iris', 'assistant']by per-surface projectors, withcrisis-frame-worker.tsbinding the real Redis@oshun/event-bus. A crisis frame always setshaltSynthesisandsuspendMemoryWrites— non-overridable. See Review, Compliance, and Trust & Safety. - Severity SLA budgets. Trust & Safety's
SLA_BY_SEVERITYis a realSlaBudgettable keyed by['P0','P1','P2','P3']— P0: 5-min triage / 15-min action,minReviewerTier: 'crisis-trained',requiresParallelIncidentandrequiresPostIncidentReview; P1: 30-min / 2-h,'senior'; P2: 8-h / 48-h,'standard'; P3: weekly aggregate only. Abilling-action-reversalis an appealable decision kind, which is why billing reversals issued as safety decisions still flow back through the appeal machinery. - Privacy retention of billing data. Privacy's
RETENTION_DAYStable pinsbillingandauditdata to365 * 7(seven years),raw-chatto 30 days,summarized-profileto'durable', andgenerated-artifactto'per-artifact-policy', withSOFT_DELETE_DEFAULT_SECONDS = 30 * 86400and avalidateSoftDeleteWindowbound (24 h min, 90 d max). That is why a full account erasure does not wipe seven-year billing/audit records — they are retained under regulatory policy. See Privacy, Consent, Data Portability, and User Controls.
A note on
libs/maat. Some grounding lists citelibs/maatas governance code. It is not Oshun V1 governance:@maat/*is a separate Ghana B2B intelligence product (COMPLIANCE_LIBRARY = '@maat/compliance', with a Ghana-data-protection compliance engine, regulatory-filing automation, and ESG reporting). The Oshun review/T&S/privacy/billing implementation islibs/oshun/trust-safety,libs/oshun/privacy, andlibs/oshun/billing-support— notlibs/maat.
Honest Status#
The entitlement, paywall, dunning, self-serve, tax, metered, support-case, and
Aje-bridge logic in @oshun/billing-support is real, deterministic, typed, and
tested (billing-support.test.ts, billing-aje-bridge.test.ts). Nothing in
this core fabricates a result. The boundaries are honest and live elsewhere by
design: actually charging a chain and confirming settlement is the Aje domain's
job (libs/aje/, Crypto Payments); fiat acceptance is
V1.x optional through libs/shared/inbound-integrations/src/payment.ts;
persisting immutable invoices/audit and sending dunning communications is the
runtime's. The library decides who is entitled, what to bill, when to dun, and
how to route a case — it does not pretend to have performed the I/O it hands
off.
Related#
- Crypto Payments — Non-Custodial Entitlement Settlement
— the canonical settlement surface that funds these entitlements; chains under
libs/aje/chains/. - Review, Compliance, and Trust & Safety — policy
taxonomy, severity SLAs, decision/appeal taxonomy, and the crisis-frame
cascade that the
crisissupport queue feeds. - Privacy, Consent, Data Portability, and User Controls — retention/deletion of billing and audit data, consent, DSAR, and residency.
- Tenant, Institution, and Operator Toolkit — tenant
seats and institutional contracts that the
institutionalclass and tenant-scoped invoicing serve. - Generation Audience Tiers and Surface Boundaries
— the generated/voice/avatar features that
proandinstitutionalgate. - Admin Products — Web and Mobile — the operator surfaces that run billing, dunning, and support queues.
- Architecture, Platform Foundations, and Security — platform isolation and audit context.
- Companion architecture: Support, Billing, and Crypto.
- Subsystem Glossary and Product Surfaces.
- Hub: ../features.md. Backlog: §23.