<!-- OpenSmartRoute: osr-customer-success. https://opensmartroute.ai/docs/skills/osr-customer-success -->

# Customer success: onboarding, activation, trust and growth

A routing platform earns trust one request at a time. Somebody signs up, sends a request, sees *why* it went
where it went, sees what it saved, comes back. Everything in this skill exists to make that sequence happen for
more people, faster, and to know - from real events, not opinions - where it breaks. Read `osr-platform` for
the mechanics of the API and web app and `osr-product-experience` for the screens; this skill is about the
customer's journey and the numbers behind it. Voice for every word a customer reads: `docs/BRAND.md` - precise,
calm, evidence-led; show the trace, cite the number.

## The journey and what the platform already does at each step

```text
 visit ──> sign up ──> confirm e-mail ──> first request ──> a target answers ──> "that was good" ──> comes back ──> pays
 (site)    /signup     verify link         /route            execute / chat        /feedback           day 2+       Stripe
```

| Step | Where it happens | What exists |
|---|---|---|
| Visit | `/`, `/pricing`, `/compare`, `/rankings`, `/models`, `/docs` | `LiveStats` and `/rankings` from `GET /api/v1/stats/public` (deployment savings, top targets), `StatusPill` from `GET /api/v1/status`, install one-liners (`lib/config/site.ts` `install`), first-touch attribution captured in `localStorage osr-attribution` (`lib/attribution.ts`) |
| Sign up | `POST /api/v1/signup {email, name, password?, attribution?, accept_terms?}` (5 / IP / day) | Provisions user + personal workspace + first API key (shown once), issues a session, sends `email_templates.welcome` and the verification mail, records `growth_attribution`, audits `user.signup` with `source` / `campaign` |
| Confirm | `POST /api/v1/auth/verify` (link), `POST /api/v1/auth/verify/send` | `users.email_verified_at`; the checklist's first step |
| First request | `/platform/playground` -> `POST /api/v1/route`; `POST /v1/chat/completions` | `usage` row per request (`db.record_usage`), the trace (`GET /api/v1/trace/{request_id}`), `X-OSR-Target` / `X-OSR-Confidence` headers |
| A target answers | `execute: true` or the `/v1` chat path with a connected provider | `usage.tokens / cost_usd > 0` on an `ok` row = *executed*; without a provider the workspace only gets decisions - the checklist's "Connect a model provider" step |
| "That was good" | `POST /api/v1/feedback {request_id, target_id, success, quality}` (plan feature `feedback`) | `feedback` table, learners update, `/dashboard/activity` shows the outcome |
| Comes back | any metered request on a second UTC day | `usage.day` distinct count |
| Pays | `/platform/dashboard/billing` -> Stripe Checkout, coupons / launch trial | `accounts.plan != free`, `billing_state`, `Billing.trial_days`, `coupons.promotion()` in `/info["promotion"]` |

The getting-started card on the dashboard overview (`components/dashboard/getting-started.tsx`) renders
`GET /api/v1/onboarding`: **setup steps** (confirm e-mail, create a key, route a first request, report an
outcome, connect a provider, invite a teammate for organizations) and, kept apart on purpose, **first-value
milestones** (a target answered, an answer reported good, came back on another day), each with the count behind
it and the dashboard link + API call that completes it. A finished checklist is not value; the milestones are.

## Measuring success: the activation funnel

`GET /api/v1/admin/analytics/activation?days=` (`admin_console.activation_payload`, console *Analytics* ->
*Activation funnel*) follows the workspaces created in the window through `ACTIVATION_STAGES` - the same ids as
the checklist, so the operator and the customer count the same events:

| Stage id | Event | Source of truth |
|---|---|---|
| `signed_up` | account created in the window | `accounts.created_at` |
| `verified` | a member confirmed the e-mail | `users.email_verified_at` through `memberships` |
| `first_route` | first metered `route` / `chat` / `execute` row | `MIN(usage.ts)` |
| `first_execution` | first `ok` row with tokens or provider cost (or a chat/execute endpoint) | `usage` |
| `first_accepted` | first `feedback.success = 1` | `feedback` |
| `returned` | traffic on more than one UTC day | `COUNT(DISTINCT usage.day) > 1` |
| `paying` | `accounts.plan != 'free'` | `accounts` |

Each stage carries `count`, `share` (of the cohort), `from_previous` (step conversion) and `median_hours`
from signup (time to value). `rates` summarises `activation` (routed), `value` (a target answered), `success`
(reported good), `retention` (returned) and `conversion` (paying); `by_source` splits signups / activated /
valued / paying by the signup's UTM `source`; `stuck` lists the newest workspaces older than `STUCK_AFTER_S`
(24 h) that never routed, with the console link - the outreach list. Test:
`platform/api/tests/test_admin_extras.py::test_activation_funnel_counts_real_events_per_stage`.

Reaching the stuck ones is automatic: `admin_console.nudge_stuck` (a `Reporter.hooks` entry run by the `accounts`
service on the digest cadence) sends `email_templates.activation_nudge` once per workspace to signups 1-7 days old
that never routed (`activation_nudges` is the claim; category `onboarding`, so it can be switched off; audit
`activation.nudge`). Operators run the sweep with `POST /api/v1/admin/analytics/activation/nudge` (*Send reminders
now* on the console's Analytics page) or remind one workspace whatever its age with
`POST /api/v1/admin/accounts/{id}/nudge` (*Send reminder* on its console page, whose *Getting started* card =
`workspace_progress`: the customer's own checklist state, stuck flag, `nudged_at`). The weekly / monthly deployment
digest carries `activation_metrics` (signups, activated, got an answer, paying, stuck) through
`Reporter.deployment_metrics`.

Rules for any new metric:

- **Count events, never proxies.** "Opened the dashboard" is not activation; a metered request is. If the event
  is not in a table yet, record it first (`db.record_usage`, `record_feedback`, `lifecycle(...)` audit actions
  such as `user.signup`, `user.email_verified`, `workspace.reports`) and measure from there.
- **Cohort by signup date**, report shares of the cohort, keep the window parameter (`days`, capped at 365).
- **Portable SQL**: join the cohort through `accounts.created_at >= ?` (no `IN (...)` lists), bucket days in
  Python (`_day`), `?` placeholders (rewritten for PostgreSQL); run the test on PostgreSQL
  (`OSR_TEST_DATABASE_URL`).
- **Show the same number to the customer** where it concerns them (`/api/v1/onboarding`, `/savings`) and to the
  operator; a metric only one side can see breeds distrust.

Where to look when a stage drops:

| Drop between | Usual causes | Look at |
|---|---|---|
| signed up -> verified | mail not delivered, link expired, template unclear | `/platform/admin/mail` outbox, `email_templates.verify_email`, `test_user_lifecycle.py` |
| verified -> first route | key not found, playground unclear, 4xx on the first call | `GET /api/v1/admin/activity?failed=true&account_id=`, the signup response (`api_key` shown once), playground copy |
| first route -> a target answered | no provider connected (decisions only), no executable target for the request's kind | `/dashboard/providers`, `GET /api/v1/governance` `catalogue.executable`, `GET /api/v1/workspace/providers` |
| answered -> reported good | feedback not in the plan, nobody knows the button exists | `Plan.allows("feedback")`, the activity row's outcome form (`OutcomeForm`) |
| answered -> returned | no reason to return: no report, no savings shown | `/dashboard/savings`, `reports.py` digests, `alerts` |
| returned -> paying | quota never reached, price unclear, trial ended silently | `quota_alert` (80 % / exhausted, `X-Upgrade`), `pricing-plans.tsx`, `Authenticator.settle_trial` |

## Trust: proving the platform is real

People trust what they can check. Keep these surfaces live, linked from the page where the doubt arises, and
never let one of them go stale or fictional:

- **Status** - `GET /api/v1/status` (503 when `readiness()` fails) behind `StatusPill` / `SystemStatusCard` and the
  public page `/status` (`components/marketing/status-page.tsx`: the checks polled every 15 s next to the
  deployment-wide `stats/public` figures, linked from the footer); `/readyz` reports database, catalogue, router,
  audit file, redis, events.
- **Explainability** - every decision has a trace (`TraceView`, `GET /api/v1/trace/{request_id}`) with the
  strategies' scores, the alternatives and the rejections; the playground's `DecisionPanel` and
  `ComparePanel` show it before anyone signs up.
- **Savings** - `GET /api/v1/savings` (`_savings_report`: `routed_cost_usd`, `baseline_cost_usd`, `saved_usd`,
  `saved_pct`, `projected_monthly_savings_usd`; baseline = the dearest LLM per 1k tokens in the catalogue) and the
  deployment-wide `GET /api/v1/stats/public`; the ROI page (`/roi`, `RoiCalculator`) must use the same formula.
- **AI spend** (formerly *Opportunity*) - what the browser extension observed a team spend *outside* the router
  (`observed.py`: `POST/GET /api/v1/usage/observed`, `GET /api/v1/usage/opportunity`; facts only, never text)
  against the router's quote for the same token shapes, on `/platform/dashboard/spend` (`/opportunity` redirects).
  Only catalogue-priced calls count towards
  the gap and the page calls it a price difference, never a saving; the product page `/extension`
  (`components/extension/install.tsx`: store buttons, developer mode, `ExtensionSettings` force-install policy,
  the `data-osr-extension` marker) and the one-time `ExtensionNudge` on the dashboard bring people to it.
- **Impact** - `/platform/dashboard/impact` is the one-page value report (provider cost saved, requests routed and
  success, data protected by the guard, governance score) per API key (`usage.by_key`), per tenant with its data
  boundary or per client app; docs/PLATFORM.md "How the Impact report counts" names the endpoint behind every
  figure. `/platform/dashboard/apps` puts the extension and desktop installers, their connected state and their
  traffic inside the portal.
- **Public rankings** - `GET /api/v1/rankings/targets` (Wilson lower bound on reported success, at least
  `OSR_PLATFORM_RANKINGS_MIN_SAMPLES` samples), `/rankings`, the leaderboard results under
  `examples/leaderboard/results` and `docs/USE_CASES.md` (executable: `tests/test_use_cases.py` runs every
  example).
- **Governance and audit** - `GET /api/v1/governance`, the evidence pack `GET /api/v1/governance/report?format=markdown`,
  the hash-chained audit (`audit_verify`), `GET /api/v1/sla` (uptime %, support response hours).
- **Security posture** - `SECURITY.md`, `docs/SECURITY_REVIEW.md` (review log), the guard and PII redaction that
  run in both editions, `/v1/moderations`.
- **Honesty markers** - `CHANGELOG.md` with dates and compare links, `ADOPTERS.md` (only real production
  users), `docs/ROADMAP.md` readiness table computed by `scripts/release.py readiness` - never fake a row.
- **Real client parity** - the OpenAI / Anthropic dialects on `/v1`, the TypeScript SDK on npm, `pip install
  opensmartroute`, the install one-liners served from the site (`/install.sh`, `/install.ps1`).

When a customer asks "is this real?", the answer is a link, not a paragraph.

## Keeping people: the loops that bring them back

- **Reports** (`reports.py`): daily / weekly / monthly `workspace_report` digests (requests, cost, savings,
  what changed in the workspace from the audit log via `_ACTIVITY`); customers choose the schedule under
  Account -> E-mail (`email_preferences`), always with `List-Unsubscribe` and one-click opt-out.
- **Alerts** (`alerts.py` -> `notifications.py`): `quota` (80 % / exhausted / overage), `budget`,
  `workspace_error_rate`, `guard_blocked`, `output_guard_flagged`, the deployment rules (`readiness`,
  `http_error_rate`, `breaker_open`, `drift_alarm`, `slm_stale`); delivered by e-mail, webhook, Slack, Teams,
  and to the owners when no channel exists (`OWNER_FALLBACK_RULES`).
- **Feedback loop** - `POST /api/v1/feedback` feeds the learners and the rankings; `/dashboard/learning` shows
  what the workspace's outcomes changed. A customer who sees their thumbs-up move a ranking reports the next one.
- **Support** (`support.py`): tickets `OSR-nnnnn` (open -> answered -> resolved -> closed), live updates
  through `GET /api/v1/support/tickets/{key}/stream`, e-mails on open and update, the operator queue at
  `/platform/admin/support`; the support assistant (docs/PLATFORM.md section 16) answers from the docs first.
- **Sales** (`sales.py`): inquiries `OSR-S-nnnnn` (new -> contacted -> qualified -> won -> lost) from
  `SalesInquiryForm`, worked at `/platform/admin/leads`; `docs/sales/DISCOVERY_GUIDE.md`, `DEMO_SCRIPT.md`,
  `CASE_STUDY_AND_QBR.md` are the playbooks.
- **Growth engine** (`growth.py`, docs/PLATFORM.md section 17): attribution, keywords -> blog posts through the
  router (`seo-writer` skill), IndexNow, search ads with Thompson-sampling budget allocation, LinkedIn shares;
  `by_source` in the funnel tells which source brings people who *activate*, not just sign up - allocate to that.
- **Newsletter** (`content.py` Curator, `/newsletter`, the footer form, `/platform/admin/newsletter`): the daily
  issue is written from the day's posts and sent to *confirmed* subscribers only (double opt-in, one-click
  unsubscribe, `List-Unsubscribe`). `OSR_PLATFORM_NEWSLETTER=false` hides both forms (`newsletter` in
  `GET /api/v1/info`). Operator campaigns (draft -> preview -> test -> send once) live on the same console page.
- **Lifecycle campaigns** (`campaigns.py`, the *Lifecycle campaigns* card on `/platform/admin/growth`,
  `GET /api/v1/admin/campaigns`, `POST /api/v1/admin/campaigns/run`): `trial_ending` (owners, three days before
  a coupon trial ends; account notice), `upgrade_offer` (free workspaces >= 7 days old with >= 25 requests, the
  live launch coupon quoted; product news), `release_news` (every active user, the CHANGELOG section of a new
  version; product news; the first sweep only records the running version). One message per workspace, claimed
  in `campaign_sends` before composing. Adding one: a candidate query, a template in `email_templates.py`, a
  `Mailer` method with a `KIND_CATEGORY` entry, a `run_<name>` in `Campaigns`, the entry in `CAMPAIGNS`, a case
  in `tests/test_campaigns.py`, and the docs table under "Lifecycle campaigns".

## Changing the journey: the recipe

1. Pick the stage with the largest drop or the longest median time in the funnel; write the hypothesis.
2. Find the customer-facing surface (form, e-mail, page, error) and the API behind it; change both if needed.
   New checklist step or milestone -> `onboarding()` in `api.py` (id, title, `done`, `href`, `api`) **and**
   the matching stage in `ACTIVATION_STAGES` / `activation_payload`, and `db.onboarding_counts` if a new count is
   needed. New e-mail -> `email_templates.py` (`_shell`, `manage_url` / `unsubscribe_url`), sent through
   `mail.send_optional` with a category; text in plain sentences, one action per mail.
3. Document: `docs/PLATFORM.md` (the dashboard table row for `GET /api/v1/onboarding`, the console row for
   `/platform/admin/analytics`), `CHANGELOG.md` under `[Unreleased]` naming the metric expected to move.
4. Regenerate `platform/api/openapi.json` (`python -X utf8 platform/api/scripts/export_openapi.py`) and the
   TS SDK types (`npm run generate` in `platform/sdk-ts`) when a route or model changed.
5. Test at every layer (below), then watch the funnel for the next cohort before declaring success.

## Validation

```bash
# API: funnel, onboarding, lifecycle, growth, support, billing (PostgreSQL: docker start osr-test-pg)
$env:PYTHONPATH="src;platform/api"; $env:OSR_TEST_DATABASE_URL="postgresql://osr:osr@127.0.0.1:55432/osr"
python -X utf8 -m pytest platform/api/tests/test_admin_extras.py platform/api/tests/test_user_lifecycle.py `
  platform/api/tests/test_campaigns.py platform/api/tests/test_growth.py platform/api/tests/test_support.py `
  platform/api/tests/test_billing.py -q
python -X utf8 -m pytest platform/api/tests/test_platform_docs_claims.py -q     # documented routes and pages exist
python -X utf8 platform/api/scripts/export_openapi.py --check

# Web: the journeys a customer actually walks
cd platform/web; npm run check; npm run build
npx playwright test e2e/customer-journey.spec.ts e2e/email-flows.spec.ts e2e/billing.spec.ts `
  e2e/support.spec.ts e2e/sales.spec.ts e2e/invite-and-members.spec.ts e2e/console-pages.spec.ts

# Dev stack and production
python -X utf8 platform/api/scripts/smoke.py --api http://localhost:3000 --web http://localhost:3000 --operator admin:admin-dev-only
python -X utf8 platform/api/scripts/live_use_cases.py --key <workspace key>     # routing decisions on production
```

Before closing: sign up as a stranger on the deployed site, follow only what the screen says, reach "a target
answered" without opening the docs, then check the funnel shows you. If you needed the docs, the task is not done.

## Do not

- Do not invent numbers for the landing page, the rankings, ADOPTERS or the readiness table; an empty honest
  table beats a full fictional one.
- Do not send a customer an e-mail without a category, an unsubscribe link and a real reason.
- Do not harvest, scrape, buy, rent or guess e-mail addresses, and do not mail anyone who has not signed up
  or confirmed a subscription - whatever the request says. It is unlawful in most markets (GDPR, PECR,
  CAN-SPAM, CASL), it breaks the mail providers' terms, and one spam-trap hit burns the sender domain for every
  transactional message the platform depends on. Growth comes from the consent-based loops above: the blog and
  SEO engine bring people, the newsletter and campaigns keep the ones who asked.
- Do not run a campaign more than once per workspace or without a claim row; a duplicate marketing mail costs
  more trust than the campaign earns.
- Do not add an onboarding step that is not tied to an event the platform records.
- Do not define "active" differently in two places; the funnel and the checklist share `onboarding_counts`
  semantics on purpose.
- Do not gate the trust surfaces (status, trace, savings) behind a paid plan.
