Billing & Credits
The billing feature handles Stripe checkout, credit balance management, pre-operation credit validation, and billing error detection.
Checkout
Hook: useCheckout (lib/hooks/useCheckout.ts)
Orchestrates the Stripe subscription checkout flow:
- User selects a plan (and optional promo code)
- Hook determines whether to use company or personal checkout
- Redirects to Stripe Checkout session
- On return, subscription is activated
Method: startCheckout(planId, promoCodeId?)
Requires an authenticated user.
Credit Check
Hook: useCreditCheck (lib/hooks/useCreditCheck.ts)
Pre-operation credit balance validation. Called before AI operations to ensure sufficient credits:
| Option | Type | Purpose |
|---|---|---|
estimatedCost | number | Expected credit cost of the operation |
customMessage | string | Custom warning message |
Returns: CreditCheckResult with balance, warningThreshold, and showWarning flags.
Billing Error Detection
Hook: useBillingError (lib/hooks/useBillingError.ts)
Intercepts GraphQL errors and maps them to user-friendly billing messages:
| Error Code | HTTP Status | Meaning |
|---|---|---|
INSUFFICIENT_CREDITS | 402 | Not enough credits for the operation |
PLAN_LIMIT_EXCEEDED | 403 | Feature limit reached on current plan |
SUBSCRIPTION_INACTIVE | -- | No active subscription |
INVALID_PLAN_PRICING | -- | Plan pricing configuration error |
Methods: detectBillingError(), showBillingToastIfNeeded()
The BillingErrorToastHandler component (in the provider hierarchy) automatically intercepts these errors and shows toast notifications.
Credit Balance
Hook: useCreditBalance (lib/hooks/useCreditBalance.ts)
Fetches the current credit balance, automatically routing to the correct balance (company or personal) based on the authenticated context.
Other Billing Hooks
| Hook | File | Purpose |
|---|---|---|
useBillingConfig | useBillingConfig.ts | Fetch billing configuration and pricing |
useCreditPurchase | useCreditPurchase.ts | Credit pack purchase flow |
useSubscriptionStatus | useSubscriptionStatus.ts | Check subscription status and feature eligibility |
Components
| Component | File | Purpose |
|---|---|---|
| PromoCodeInput | components/reusable/payments/PromoCodeInput.tsx | Promo code entry with validation |
| BillingErrorToastHandler | In provider hierarchy | Auto-intercepts billing errors |
Pages
The credits/usage UI is not a standalone route. It lives in app/(shared)/userprofile/creditsPage.tsx and is rendered as the credits settings tab through the dynamic app/(shared)/userprofile/(settings)/[tab]/page.tsx host (route /userprofile/credits). The former app/company/credits/ and app/jobseeker/credits/ routes no longer exist — both roles share the one settings tab, which switches scope via an inline Personal/company profile switcher.
| Route | Purpose |
|---|---|
/userprofile/credits | Credit balance, usage insights, transactions, and Buy/Transfer actions. Rendered by creditsPage.tsx via the (settings)/[tab] host. Personal + company scope from one page |
/userprofile/plans · /userprofile/invoices · /userprofile/subscriptions | Subscription/billing tabs, rendered by app/(shared)/userprofile/payment/page.tsx (billing-tab kind in the [tab] host) |
app/jobseeker/pricing/ | Public subscription plan selection |
Usage Insights
The credits tab surfaces AI-operation usage alongside the credit balance. All of it is driven by a single period selector and reads from aiOperationUsageSummary (cost-by-feature) and the paged aiOperationLogs query.
Period selector
The Breakdown tab carries a preset selector plus a custom date range. The chosen period drives the Cost-by-Feature panel, the usage chart, and the admin team-member panel together.
| Preset | Range |
|---|---|
| Today | Local midnight → now |
| 7d | Last 7 days |
| 30d | Last 30 days |
| This month | 1st of the current month → now (default) |
| Last month | Full previous calendar month |
| 1y | Rolling last 12 months (buckets into 12 calendar months) |
| Custom | User-picked start/end date range |
Cost by Feature
aiOperationUsageSummary (via GET_AI_OPERATION_USAGE_SUMMARY) is queried in both Personal and company scope — a companyId is passed only when a company profile is selected. Results are sorted by spend, rendered as a per-feature bar list with a "top feature" callout.
The usage figures carry the caption: "Metered from AI operations — the basis for the monthly invoice. On pay-as-you-go plans these aren't recorded as credit transactions, so the ledger below won't match." On POSTPAID (enterprise) plans, usage is invoiced rather than deducted, so there are no CONSUMPTION credit-transaction rows to reconcile against.
Because aiOperationUsageSummary is company-scoped on the backend, it throws a 403 on a Personal profile with no company selected. The Apollo error link (graphql/apollo/apolloClient.ts) suppresses the generic 403 toast for GetAIOperationUsageSummary — the panel already renders a clean "No usage in this period" empty state, so the 403 is not user-actionable.
Usage chart
A bar chart of credits over time, honoring the active period. It buckets by local calendar day for short ranges and by calendar month for spans beyond ~2 months (so the 1y view shows 12 month bars), with per-bar value labels. In company scope the chart sources from aiOperationLogs (so it matches the Cost-by-Feature total exactly and works for POSTPAID companies with no consumption rows); in Personal scope it falls back to CONSUMPTION credit transactions until a personal-scope op-log resolver lands.
Period boundaries come from local midnight, so both the axis and the rows are keyed in local calendar time. An earlier bug keyed the axis by UTC day, which for users ahead of UTC pushed today's bucket out of range and silently dropped the day's usage.
By Team Member (admin only)
Rendered only when the caller has admin-tier canManage and a company profile is selected (useCompanyRole().canManage && selectedProfileId). It pages through aiOperationLogs(filter: { status: SUCCESS }) — the backend hard-caps each request at 100 rows, so the panel fetches up to 5 pages (500 rows) and aggregates client-side into a per-member table (operations, credits, % of team), with an expandable per-feature drill-down and a "top user" insight callout.
When the 5×100 cap is hit, an amber banner reads "Showing the most recent 500 operations for this period…" — older activity in the same period isn't included until a native per-user backend query lands (handoff #13). The panel filters status: SUCCESS, so its totals can be lower than the unfiltered Cost-by-Feature figure.
Companies You Own
On the Personal profile, an owner sees a Cost-by-Feature rollup for every company where they are the billing owner (company.billingOwnerId === user.id), for the selected period — no profile switch required. Each company card uses the same aiOperationUsageSummary query with an explicit companyId; the panel ranks companies by spend and surfaces the leader. Per-company balance is not shown here (creditBalance takes no companyId); switch into the company profile for its ledger.
Transfer Credits
A single Transfer Credits button sits in the hero card top-right (next to Buy Credits). A previously-duplicated button that the sticky page header also rendered was removed once the (settings) layout began mounting its own PageBlock. Transfer stays visible on enterprise/POSTPAID plans (the account can still hold prepaid credits to move); only Buy Credits hides there.
For the backend ledger mechanics of transfers and the metering that feeds these panels, see Credit System.