Model-agnostic by design
Every model sits behind the same interface. The call site asks for a task, not a model. Changing a route is one line in the routing configuration; adding a provider is one case in the dispatch function.
How the model works
The engine
The churn prediction in Churnora is a logistic regression running in application code. It takes eight behavioral features computed from a customer's usage data, standardizes them, multiplies by fitted weights, adds a bias term, and passes the result through a sigmoid to get a probability between 0 and 1. It does not call a language model. It does not use a prompt. The score is deterministic: the same inputs always produce the same number.
The language model enters at one step only: after the score is final, it receives the top behavioral factors the ML model computed for that account and translates them into plain language. It also drafts the save play: the email, the offer suggestion, and the call notes. It does not produce the score, it does not adjust the score, and it does not have access to the fitted weights.
This split is not a marketing claim; it is the architecture. The two parts live in separate files. The ML model is inlib/churn/model.ts. The language layer is inlib/ai/provider.ts. They are called in sequence, not together.
Customer data
Usage fields from your CSV
8 features
Engineered behavioral signals
ML model
Logistic regression, code
Score + factors
0 to 1, deterministic
Language model
Translates, drafts, does not predict
Save play
Email, offer, call notes
The eight features
| Feature | Computed from | Why it matters |
|---|---|---|
Sign-in recency recency | log(1 + days since last sign-in) | The longer since anyone on the account last signed in, the weaker the usage signal and the stronger the churn signal. |
Sign-in volume volume | log(1 + sign-ins last 30 days) | Accounts that sign in more often in the current 30-day window are embedded in their workflow. Fewer sign-ins means shallower engagement. |
Usage trend trend | (logins30 - loginsPrev30) / loginsPrev30, clamped | A fall in sign-ins from the previous 30-day window to the current one is a leading indicator. The account may still be active but it is becoming less so. |
Feature breadth breadth | log(1 + distinct features used, 30 days) | Customers who use more distinct product features have more invested in the product and more switching cost if they leave. |
Seat utilization seatUtil | seats active / seats paid | Low utilization of paid seats means the contract is not reaching the team. Unused seats are easy to cut at renewal. |
Support load tickets | log(1 + support tickets, 90 days) | A high ticket count over 90 days suggests friction. It can mean the product is hard to use, or that something is not working, and either increases churn risk. |
Payment health payFail | log(1 + failed payments, 90 days) | Failed or late payments over 90 days are a direct signal of trouble, either financial difficulty or declining priority. |
Tenure tenure | log(1 + months since signup) | Newer accounts are more likely to churn than established ones. Tenure captures the customer's depth of history with the product. |
The fitted model
The model was fitted with full-batch gradient descent and L2 regularization on a behavioral training set of 3,200 customers, with 800 held out for evaluation. The held-out AUC is 0.9382, meaning the model ranks a churner above a non-churner about 94 times out of 100. The training churn base rate was 15.3%.
The fitting script ships in the product at scripts/train-model.ts. Scale customers can refit the model on their own labeled history to match their customer base.
How to read a logistic coefficient
Each weight multiplies the standardized (z-scored) feature value. A positive weight means the feature pushes the log-odds of churn up; a negative weight pushes it down. The magnitude tells you how much: a weight of 1.03 on recency means one standard deviation more days since last sign-in shifts the log-odds by about 1.03 toward churn.
| Feature | Fitted weight | Direction |
|---|---|---|
Sign-in recency recency | +1.035066 | Raises risk |
Sign-in volume volume | -0.025068 | Lowers risk |
Usage trend trend | -0.459060 | Lowers risk |
Feature breadth breadth | -0.297414 | Lowers risk |
Seat utilization seatUtil | -0.363465 | Lowers risk |
Support load tickets | +0.510559 | Raises risk |
Payment health payFail | +0.336266 | Raises risk |
Tenure tenure | -0.471533 | Lowers risk |
| Bias | -3.253455 |
Generated by scripts/train-model.ts
Bands and flags
The training churn rate was 15.3%. The bands are set relative to that number rather than to arbitrary round thresholds. An account is at-risk when its score reaches roughly three times the base rate, where the model is saying the account is meaningfully more likely to churn than the average customer.
Accounts under two months old are flagged as new. The model scores them, but the score is presented with a caveat: there is not yet enough behavioral history for it to be as reliable as it is for established customers.
At-risk
45% and above
Roughly three times the training base rate. These accounts need attention this week.
Watch
10% to 44%
Around the training base rate. Not an emergency, but worth a look this week if the trend is moving up.
Healthy
Below 10%
Below the training base rate. The model does not see strong churn signals in this account's behavior.
New
Under 2 months tenure
Scored, but flagged. Not enough behavioral history for a reliable estimate. Score is shown with a caveat.
The language layer
After the ML model scores an account and identifies the top behavioral factors, the language model receives those factors and does two things. It translates the factors into plain language a customer success rep can act on, and it drafts the save play: the outreach email, the retention offer suggestion, and the call notes.
The language model never sees the fitted weights. It never adjusts the score. It never produces a churn probability. It receives a completed scorecard and writes words around it. If no language model key is configured, Churnora falls back to code-written factor phrasing, clearly labeled as generated without a language model.
The language model output is zod-validated JSON. If the output does not match the expected schema, the app does not pass it to the user. It retries or falls back to the code-written phrasing. A malformed completion is never shown as if it were a valid save play.
The behavioral fields of one account are sent to the language model provider only to write that account's explanation and save play, under API agreements that prohibit training on that traffic. Your data is not used to train any shared model.
Routing
| Task | Model | Provider | Tier | USD per M tokens |
|---|---|---|---|---|
| Translate an account's factors and draft its save play | gpt-4.1-mini | openai | balanced | $1.60 |
| Write the short read of a whole book after scoring | gpt-4.1-mini | openai | balanced | $1.60 |
| Work out the shape of an imported book | gpt-4.1-nano | openai | fast | $0.40 |
Every model sits behind the same interface. The call site asks for a task, not a model. Changing a route is one line in the routing configuration; adding a provider is one case in the dispatch function.
If the routed model fails or returns an empty completion, the call walks a chain of candidates from other tiers and other providers before giving up. One provider being unavailable does not block an explanation.
Providers are declared with real model names and switch on when their API key is present. In this deployment the OpenAI adapter is wired live; others are one key away.
Declared models
gpt-4.1
frontieropenai · $8.00/MTok
gpt-4.1-mini
balancedopenai · $1.60/MTok
gpt-4.1-nano
fastopenai · $0.40/MTok
claude-sonnet
frontieranthropic · $9.00/MTok
gemini-flash
fastgoogle · $0.50/MTok
llama-open
openmeta · $0.30/MTok
Honest limits
A score of 90% means the model sees strong churn signals in the behavioral data for this account, not that this account will definitely churn. The score should inform a human decision, not replace one. Churnora says this plainly in the product.
If your customer data does not include login counts, the model cannot use login frequency. If payment failure records are missing, that feature scores as zero. Incomplete data leads to incomplete signals, and incomplete signals lead to scores that are less reliable.
A customer whose team is offline for two weeks will show low sign-in recency and a sharp trend decline. The model will score them as higher risk. It cannot distinguish a temporary absence from the start of disengagement.
Accounts under two months old do not have enough behavioral history for the tenure and trend features to carry their normal weight. Scores for new accounts are flagged. They may still be accurate, but the signal is thinner.
The shipped model was trained on a behavioral dataset of 3,200 customers. Your customer base may have a different base rate, different plan economics, or different behavioral signals. Scale customers can refit the model on their own labeled history using the training script in the product.
More on data handling: Care stance and Security.
Free for up to 200 customers a month. The full model, every factor, no card required.