Activation tracking & onboarding retention
Why
WS-Starter (and API plans generally) churn at a chronic ~30-day cliff — median churner leaves at first renewal, and ~60% of every signup cohort is gone by day 45. The differentiator is activation: of currently-active WS-Starter accounts, ~79% make API requests and ~21% are dormant (paying, never integrated). Dormant-by-day-20 → churned-at-day-30. The lever is proactive onboarding of new signups — getting every new account to its first successful API call within days, while they’re still in the trial/first-month window and expecting setup help. This is deliberately scoped to new accounts: we do not email existing active, non-cancelled customers who’ve gone quiet — reminding a paying customer they’re not using the product invites a cancel decision (sleeping-dogs risk). Dormancy in an established paying account is a signal to watch (via the KPI), not to poke. This required (a) a durable activation signal — previously we only had Redis usage counters with a 31-day TTL, no “activated ever” — and (b) an automated first-week onboarding drip gated on activation.How it works
v3/utils/usage_tracker.go—TrackUsagewrites two persistent Redis keys on every request:activation:first:{email}(SETNX, first-ever) andactivation:last:{email}(SET, most-recent). No TTL, so “activated ever” is durably answerable.GetActivation(email)reads them.account-service/scripts/activation_reconcile.ts— cron (~every 15 min). Denormalizes the markers intoapi-tokens.firstRequestAt/lastRequestAt, and the first time it observes an account activate it sets the Loops contactactivated=trueand sends anapi_activatedevent (the drip’s exit condition).--dry-runsupported.
Scheduling
Runs in-process viaworker.ts (node-cron), same as the other account-service jobs — no separate
Railway cron to configure. Scheduled at 10,25,40,55 * * * * (every 15 min, offset from the other
jobs). Also runnable directly for a one-off: tsx --env-file=.env scripts/activation_reconcile.ts.
Loops setup (one-time, dashboard)
Onboarding is a Loop built in the Loops UI — no code, so timing/copy can be tuned without deploys:- Create two custom contact properties:
activated(boolean),apiPlan(string). No signup code change is needed: new contacts simply haveactivatedempty, and the loop filter below treats empty as “not activated”. The reconciler setsactivated=trueon first request. - New Loop, trigger “Contact added” filtered to API subscribers.
- Wait 3 days → filter
activatedis nottrue(matches empty too) → send Day-3 setup. - Wait until day 7 → filter
activatedis nottrue→ send the Day-7 escalation email. - The reconciler sets
activated=trueon first request, which drops them out of the loop.
Email copy
Day 3 — “Let’s get your first API call working” (only if not activated)
Subject: Your odds-api.io key is ready — here’s your first call Hi , You’re set up on , but I noticed you haven’t made your first API request yet. Most customers are pulling live odds within 10 minutes — here’s the fastest path:If anything’s in the way — auth, a specific bookmaker, WebSocket setup — just reply to this email and I’ll help directly. Happy to jump on a 15-min call.
- Your key + a copy-paste example:
- Endpoints & filters:
Day 7 — escalation (only if still not activated)
Subject: Still stuck getting started? I’ll set it up with you Hi , You’re a week into and haven’t made a call yet — I don’t want you paying for something you’re not using. Two options:If odds-api.io isn’t the right fit, tell me — I’ll help either way.
- Reply with what you’re trying to build and I’ll send working code for your exact use case, or
- Grab 15 minutes here and we’ll get you live on the call:
The KPI
Track per signup-month cohort, not monthly revenue:- Day-7 activation rate — % who made ≥1 request within 7 days.
- Day-45 survival — % still subscribed at day 45.
scripts/_churn-analysis.ts
(analysis, not committed) produces the cohort table; wire it into a weekly report once
firstRequestAt has accumulated data.