Oshun Platform · Features

Support, Entitlements, Billing, and Customer Operations

A focused page within the Oshun Platform Features documentation. The full map and every sibling page live in the Features hub.

12sections15 minread4tables

On this page

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/ and libs/oshun/payments-bridge/) and the architecture page (the fiat adapter, via libs/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 is libs/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/* with payment_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:

  • confirmedtrial/past-due/grace/paused/restored become active; canceled/lapsed become restored; an already-active subscription is a no-op.
  • failed → an active subscription drops to past-due; other states have no failed-payment transition.
  • refundedactive/paused/trial/restored move to canceled; grace/past-due move to lapsed (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 lapsed subscription is denied; a paused one is denied with paused.
  • A canceled subscription still grants until canceledEffectiveAtUnixSeconds and only while the current period covers now — cancellation is honored as paid-through-period, then de-entitles.
  • A trial grants via trial only while now <= trialEndsAtUnixSeconds and the period covers now.
  • A grace subscription grants via grace only until graceEndsAtUnixSeconds.
  • 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 a CLASS_RANK order free < 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 supplied PaywallPriceCatalog.

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):

text
['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 softCapQuantityPerPeriod raises alert: 'soft-cap-warn';
  • being at/over hardCapQuantityPerPeriod rejects with hard-cap-reached (accepted: false), pinning newUsedQuantity to 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 countfinal-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. routeCase sends a crisis-signalled support case to the crisis queue with the harshest SLA (5 min first response). On the Trust & Safety side this is backed by a real cross-surface cascade: the event LILITH_CRISIS_FRAME_ACTIVATED_EVENT = 'lilith.crisis_frame.activated' is published (via a port — the domain stays free of @oshun/event-bus by design) and fanned out to CRISIS_FRAME_SURFACES = ['psyche', 'lilith-video', 'tara', 'iris', 'assistant'] by per-surface projectors, with crisis-frame-worker.ts binding the real Redis @oshun/event-bus. A crisis frame always sets haltSynthesis and suspendMemoryWrites — non-overridable. See Review, Compliance, and Trust & Safety.
  • Severity SLA budgets. Trust & Safety's SLA_BY_SEVERITY is a real SlaBudget table keyed by ['P0','P1','P2','P3'] — P0: 5-min triage / 15-min action, minReviewerTier: 'crisis-trained', requiresParallelIncident and requiresPostIncidentReview; P1: 30-min / 2-h, 'senior'; P2: 8-h / 48-h, 'standard'; P3: weekly aggregate only. A billing-action-reversal is 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_DAYS table pins billing and audit data to 365 * 7 (seven years), raw-chat to 30 days, summarized-profile to 'durable', and generated-artifact to 'per-artifact-policy', with SOFT_DELETE_DEFAULT_SECONDS = 30 * 86400 and a validateSoftDeleteWindow bound (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 cite libs/maat as 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 is libs/oshun/trust-safety, libs/oshun/privacy, and libs/oshun/billing-support — not libs/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.