osr-customer-success
Grow the number of people who trust and keep using the OpenSmartRoute platform. Improve customer onboarding and activation - the signup flow, the getting-started checklist and its first-value milestones, the welcome and verification e-mails, the first request in the playground; measure the activation funnel and its success rate (signed up, confirmed, first route, a target answered, an answer reported good, returned, paying), time to value, retention and conversion per acquisition source; find workspaces stuck before their first request and reach out; strengthen the trust and proof-of-value surfaces (live status, savings ledger, public rankings, traces, governance report, SLA, changelog, adopters); close the loop with feedback, support tickets, usage reports and alerts. Use when the task is about customer experience, onboarding, activation, adoption, trust, confidence, growth, retention or customer success on the hosted platform.
- Package
- .claude/skills/osr-customer-success
- Compatibility
- OpenSmartRoute repository, Python >= 3.11, Node >= 22, PostgreSQL for the platform test-suite
- License
- Apache-2.0
- Domains
- customer_support marketing coding general
- Quality prior
- 0.85
- Tags
- opensmartroute customer success onboarding activation funnel retention trust growth adoption
Install by copying .claude/skills/osr-customer-success/ into the skills folder of your coding assistant. To load every package as a routing target: osr --skills .claude/skills route "..." --plan.
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
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 asuser.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 >= ?(noIN (...)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 whenreadiness()fails) behindStatusPill/SystemStatusCardand the public page/status(components/marketing/status-page.tsx: the checks polled every 15 s next to the deployment-widestats/publicfigures, linked from the footer);/readyzreports 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'sDecisionPanelandComparePanelshow 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-wideGET /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(/opportunityredirects). 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,ExtensionSettingsforce-install policy, thedata-osr-extensionmarker) and the one-timeExtensionNudgeon the dashboard bring people to it. - Impact -
/platform/dashboard/impactis 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/appsputs 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 leastOSR_PLATFORM_RANKINGS_MIN_SAMPLESsamples),/rankings, the leaderboard results underexamples/leaderboard/resultsanddocs/USE_CASES.md(executable:tests/test_use_cases.pyruns every example). - Governance and audit -
GET /api/v1/governance, the evidence packGET /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.mdwith dates and compare links,ADOPTERS.md(only real production users),docs/ROADMAP.mdreadiness table computed byscripts/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 / monthlyworkspace_reportdigests (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 withList-Unsubscribeand 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/feedbackfeeds the learners and the rankings;/dashboard/learningshows what the workspace's outcomes changed. A customer who sees their thumbs-up move a ranking reports the next one. - Support (
support.py): ticketsOSR-nnnnn(open -> answered -> resolved -> closed), live updates throughGET /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): inquiriesOSR-S-nnnnn(new -> contacted -> qualified -> won -> lost) fromSalesInquiryForm, worked at/platform/admin/leads;docs/sales/DISCOVERY_GUIDE.md,DEMO_SCRIPT.md,CASE_STUDY_AND_QBR.mdare the playbooks. - Growth engine (
growth.py, docs/PLATFORM.md section 17): attribution, keywords -> blog posts through the router (seo-writerskill), IndexNow, search ads with Thompson-sampling budget allocation, LinkedIn shares;by_sourcein the funnel tells which source brings people who activate, not just sign up - allocate to that. - Newsletter (
content.pyCurator,/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=falsehides both forms (newsletterinGET /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 incampaign_sendsbefore composing. Adding one: a candidate query, a template inemail_templates.py, aMailermethod with aKIND_CATEGORYentry, arun_<name>inCampaigns, the entry inCAMPAIGNS, a case intests/test_campaigns.py, and the docs table under "Lifecycle campaigns".
Changing the journey: the recipe
- Pick the stage with the largest drop or the longest median time in the funnel; write the hypothesis.
- 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()inapi.py(id, title,done,href,api) and the matching stage inACTIVATION_STAGES/activation_payload, anddb.onboarding_countsif a new count is needed. New e-mail ->email_templates.py(_shell,manage_url/unsubscribe_url), sent throughmail.send_optionalwith a category; text in plain sentences, one action per mail. - Document:
docs/PLATFORM.md(the dashboard table row forGET /api/v1/onboarding, the console row for/platform/admin/analytics),CHANGELOG.mdunder[Unreleased]naming the metric expected to move. - Regenerate
platform/api/openapi.json(python -X utf8 platform/api/scripts/export_openapi.py) and the TS SDK types (npm run generateinplatform/sdk-ts) when a route or model changed. - Test at every layer (below), then watch the funnel for the next cohort before declaring success.
Validation
# 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 productionBefore 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_countssemantics on purpose. - Do not gate the trust surfaces (status, trace, savings) behind a paid plan.