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 isuseChat+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.
- user: orders, subscriptions, entitlements, catalog, referral stats,
- 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 onchat_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).
userIdfor 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 → executingis a CAS (updateMany) so a double-click loses with a 409. - Every proposal/outcome is appended to the conversation as a
toolmessage 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:
| Trigger | Source | Cadence |
|---|---|---|
abandoned_checkout | funnel checkout_abandoned (order still created) | once per order |
dunning | subscription past_due | days 0 / 2 / 5 until resolved |
winback | subscription_churning (cancel-at-period-end) | once per period |
trial_ending | trial ends within 3 days | once 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, andtierRatesJSON 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,blockornotify_onlyoverage policy. Evaluated inside the consume transaction under a balance row lock; soft-cap crossings send aquota_warningnotification (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.