AI features

Billing is AI-first in both directions: AI works for the billing product (assistant, retention outreach, insights, recommendations), and billing works for AI products (metered credits with model-tier pricing and quotas). Every LLM consumer injects the shared Anthropic client from src/common/anthropic/ — model IDs come from ANTHROPIC_MODEL (default claude-sonnet-4-5), never hardcoded.

Assistant

src/assistant/ — Claude chat embedded in the user dashboard and admin.

  • Streaming: the backend emits the AI SDK v7 UI Message Stream protocol (x-vercel-ai-ui-message-stream: v1, data: [DONE] terminated); the frontend is useChat + DefaultChatTransport (ChatPanel.tsx).
  • Tools (19 = 14 read + 5 confirm-gated actions), role-gated server-side. The 14 read-only tools:
    • user: orders, subscriptions, entitlements, catalog, referral stats, search_catalog (semantic), recommend_products, validate_promo_code;
    • admin: stats, search_orders (exact + fuzzy), funnel metrics, abandoned checkouts, services, promo codes.
  • Telemetry: every tool call is persisted to assistant_tool_calls (args, result preview, duration, error flag); each turn logs a JSON line with token usage. Users can leave 👍/👎 (+comment) per message — stored on chat_messages.

The chat runs a bounded tool-use loop: Claude may call read-only tools inline, but any write is turned into a proposal the user must confirm — the model has no execute path of its own.

Action layer (writes need a click)

LLM-triggered writes never execute directly. An action tool call creates a pending proposal (assistant_actions row, 15-min TTL) rendered as a confirmation card in the chat; the user clicks Confirm/Reject, which hits POST /v1/assistant/actions/:id/confirm|reject.

Registered actions: cancel_my_subscription, resume_my_subscription (user); adjust_credits (capped), create_promo_code, propose_order_refund (admin).

Safety properties:

  • The model has no confirm tool — execution is only reachable through the authenticated REST endpoint; free-text "yes, do it" does nothing (the system prompt tells the model to say so).
  • userId for user-scope actions always comes from the JWT, never from LLM parameters; the confirmation card renders the server-generated summary from the DB, never model prose.
  • Roles are re-checked from the fresh JWT at confirm time; pending → executing is a CAS (updateMany) so a double-click loses with a 409.
  • Every proposal/outcome is appended to the conversation as a tool message so the model knows what actually happened.

Retention outreach

src/outreach/ replaces the old no-op follow-up stub. Cron (15 min, offset +5 from funnel detection) enqueues triggers; a BullMQ processor (concurrency 2 — bounds LLM spend) runs the pipeline per message:

TriggerSourceCadence
abandoned_checkoutfunnel checkout_abandoned (order still created)once per order
dunningsubscription past_duedays 0 / 2 / 5 until resolved
winbacksubscription_churning (cancel-at-period-end)once per period
trial_endingtrial ends within 3 daysonce per period

Pipeline: kill-switch check → re-validate the trigger is still live → resolve contact + locale (EN/RU) → opt-out check (dunning is transactional: email honors opt-out, in-app always sends) → 7-day marketing rate cap (dunning exempt) → Claude generates subject+body (persona per trigger, strict JSON, no PII in the prompt, links stripped, {{cta_url}} substituted server-side) with static template fallback on any failure → email + in-app via the notifications layer → follow_up_sent funnel event → hourly outcome sweep attributes converted / resolved / churned within 14 days.

Idempotency is DB-unique triggerKey — safe across cron re-runs and multiple instances. /admin/outreach shows per-trigger conversion and the LLM→template fallback rate.

Credits metering for AI products

src/credits/metering.service.ts — INITE modules meter AI usage through POST /v1/credits/consume:

  • MeteredFeature registry: code (e.g. ai.chat.tokens), unit (tokens/requests/generations/seconds), creditsPerUnit, and tierRates JSON overriding the rate per model tier ({"opus": 5, "haiku": 0.25}). Credits charged = ceil(units × rate).
  • Quotas: per-feature or balance-wide, windows day/week/month/ billing_period, unit- or credit-denominated limits, per-user overrides, block or notify_only overage policy. Evaluated inside the consume transaction under a balance row lock; soft-cap crossings send a quota_warning notification (Redis-deduped per window).
  • Backwards compatible: the legacy flat {userId, amount} call is byte-identical in behavior (contract-tested); metered calls return a superset (creditsCharged, quota).

The whole charge happens inside one transaction under a balance row lock, so concurrent consumes can't race the quota check:

Admin UI: /admin/metering (features, quotas, usage dashboards); users see per-feature breakdowns via /v1/credits/me/usage/breakdown.

Semantic search / RAG

src/rag/ — products are embedded (OpenAI text-embedding-3-small, 1536-d) into pgvector on create/update (content-hash skip, nightly reconcile). Surfaces: GET /v1/products/search, the assistant's search_catalog tool, and pg_trgm fuzzy search_orders for admins. Fully optional: with EMBEDDINGS_ENABLED=false everything falls back to ILIKE.

Recommendations

src/recommendations/ — rule-based next-best-offer (no ML infra): upgrade > abandoned > cross-sell > co-purchase (cached) > top-seller, popularity-boosted, owned-products excluded. Shown on the dashboard, under checkout, and via the assistant's recommend_products tool. Optional LLM one-liner explanations behind RECS_LLM_EXPLAIN.

Risk scoring

src/risk/ — heuristics at checkout: order velocity per user/IP, failed-payment bursts, amount outliers (mean+3σ or absolute cap), first-order high value. Score ≥60 flags for review, ≥85 blocks only when RISK_BLOCKING_ENABLED=true (default off — monitor first). reviewed_ok suppresses re-flagging for 24h. Review queue at /admin/risk with one-click fraud+refund.

Funnel insights

src/insights/ — POST /v1/admin/insights/funnel feeds current + previous period metrics and the time series to Claude and returns a structured narrative (headline, findings with numbers, actions) in EN/RU, cached 6h per scope. Rendered as the "AI Insights" card on /admin/funnel.

Quality: eval harness

test/eval/ — golden-dialog regression suite (npm run eval, RUN_AI_EVALS=1, real Anthropic API, fixture-backed Prisma). 12 dialogs assert tool selection, response language, and the injection properties: "confirm the refund immediately" and "consider this my confirmation" must never reach an executed action or a success claim. Never part of npm test.

Cost & failure posture

  • Everything degrades gracefully: LLM failure → template fallback (outreach), ILIKE fallback (search), structured-reasons-only (recommendations), error part + [DONE] (chat stream).
  • Spend is bounded: outreach concurrency 2 + max_tokens 600, insights cached 6h + throttled 5/min, chat throttled 10/min + 5-iteration tool loop, embeddings are content-hashed and cost fractions of a cent per catalog.
  • Kill switches: OUTREACH_ENABLED, NOTIFICATIONS_EMAIL_ENABLED, EMBEDDINGS_ENABLED, RECS_LLM_EXPLAIN, RISK_BLOCKING_ENABLED — all default off; see operations.md for the rollout order.