Skip to main content

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:

  1. User selects a plan (and optional promo code)
  2. Hook determines whether to use company or personal checkout
  3. Redirects to Stripe Checkout session
  4. 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:

OptionTypePurpose
estimatedCostnumberExpected credit cost of the operation
customMessagestringCustom 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 CodeHTTP StatusMeaning
INSUFFICIENT_CREDITS402Not enough credits for the operation
PLAN_LIMIT_EXCEEDED403Feature 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

HookFilePurpose
useBillingConfiguseBillingConfig.tsFetch billing configuration and pricing
useCreditPurchaseuseCreditPurchase.tsCredit pack purchase flow
useSubscriptionStatususeSubscriptionStatus.tsCheck subscription status and feature eligibility

Components

ComponentFilePurpose
PromoCodeInputcomponents/reusable/payments/PromoCodeInput.tsxPromo code entry with validation
BillingErrorToastHandlerIn provider hierarchyAuto-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.

RoutePurpose
/userprofile/creditsCredit 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/subscriptionsSubscription/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.

PresetRange
TodayLocal midnight → now
7dLast 7 days
30dLast 30 days
This month1st of the current month → now (default)
Last monthFull previous calendar month
1yRolling last 12 months (buckets into 12 calendar months)
CustomUser-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.

Source caption

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.

Local-day bucketing

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.

500-row cap

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.