> ## Documentation Index
> Fetch the complete documentation index at: https://docs.odds-api.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Activation onboarding

# 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

```
Go API (v3)                     Reconciler cron                Loops
──────────                      ───────────────                ─────
every request:                  every 15 min:                  onboarding drip (dashboard):
 SETNX activation:first:{email} scan activation:* markers      trigger  = contact added
 SET   activation:last:{email}  → $min firstRequestAt          gate     = activated != true
 (persistent, no TTL)           → set  lastRequestAt (Mongo)   day 3    → setup-help email
                                → if newly activated:          day 7    → escalation email
                                    Loops contact activated=true  exit  = activated == true
                                    Loops event  api_activated
```

* **`v3/utils/usage_tracker.go`** — `TrackUsage` writes two persistent Redis keys on every request:
  `activation:first:{email}` (SETNX, first-ever) and `activation: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 into `api-tokens.firstRequestAt` / `lastRequestAt`, and the first time it observes an
  account activate it sets the Loops contact `activated=true` and sends an `api_activated` event
  (the drip's exit condition). `--dry-run` supported.

### Scheduling

Runs in-process via `worker.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:

1. Create two custom contact properties: `activated` (boolean), `apiPlan` (string). No signup code
   change is needed: new contacts simply have `activated` empty, and the loop filter below treats
   empty as "not activated". The reconciler sets `activated=true` on first request.
2. New Loop, trigger **"Contact added"** filtered to API subscribers.
3. **Wait 3 days** → **filter `activated` is not `true`** (matches empty too) → send *Day-3 setup*.
4. **Wait until day 7** → **filter `activated` is not `true`** → send the *Day-7 escalation* email.
5. The reconciler sets `activated=true` on first request, which drops them out of the loop.

Scope: this drip targets **new** signups only. We deliberately do not email established active,
non-cancelled customers who've gone quiet — that invites a cancel decision. Dormancy in a paying
account is a watch-signal (KPI), not an outreach trigger.

## 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 {{firstName}},
>
> You're set up on {{apiPlan}}, 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:
>
> 1. Your key + a copy-paste example: {{dashboardUrl}}
> 2. Endpoints & filters: {{docsUrl}}
>
> 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.

### Day 7 — escalation (only if still not activated)

> Subject: Still stuck getting started? I'll set it up with you
>
> Hi {{firstName}},
>
> You're a week into {{apiPlan}} and haven't made a call yet — I don't want you paying for
> something you're not using. Two options:
>
> * 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: {{callUrl}}
>
> If odds-api.io isn't the right fit, tell me — I'll help either way.

## 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.

Dec 2025 proved lifting survival from \~15% → \~45% is achievable. `scripts/_churn-analysis.ts`
(analysis, not committed) produces the cohort table; wire it into a weekly report once
`firstRequestAt` has accumulated data.
