Fourteen States, Fourteen Voices
The same event fires for two users at the same second: app.foreground.
User A is on day two of a trial. The right surface: nothing. The trial is running; let them use it.
User B is on day three of a grace period after a billing failure. The right surface: a quiet, administrative reminder to update the card. No countdown. No discount. No urgency theater.
If the engine treats those two app.foreground events the same way, both users get the wrong experience. Send A the billing reminder — confusion, they have no billing issue. Send B the trial silence — they lose access in 13 days and the system never told them why.
One event. Two states. Two different right answers. The engine is wrong by default.
What AMORE Is
AMORE — Acquisition, Monetization, Onboarding, Retention, Engagement — is the retention engine I built for a B2C health app. The first article covers grace, the second covers dormant, the third covers fatigue. Each is a different state and a different voice. This article zooms out to all fourteen.
State is the first thing the engine knows about a user and the last thing the engine forgets. Every other decision — flow, channel, copy, timing — is downstream.
The Fourteen
1. free-new install, no paywall seen yet
2. free-onboarded onboarding done, no purchase
3. free-engaged uses features daily, many locked
4. free-churned 14-30 days quiet, no purchase
5. trial-active trial running
6. trial-expiring 24h or 1h to trial end
7. trial-expired trial ended, no conversion
8. paid-active subscription active
9. paid-grace billing failed, in retry window
10. paid-canceled canceled but still entitled
11. paid-expired canceled and lapsed
12. lifetime bought outright
13. refunded Apple/Google refunded
14. dormant 60+ days quiet, token still alive
Each state has a tone. Each tone is a different voice the engine speaks in.
| state | voice |
|---|---|
| free-new | friendly: “welcome, try premium” |
| free-onboarded | soft: “you forgot about premium” |
| free-engaged | feature-gate at each touch |
| free-churned | aggressive discount |
| trial-active | silence + Live Activity countdown |
| trial-expiring | urgent + discount |
| trial-expired | hard winback paywall |
| paid-active | upsell, otherwise quiet |
| paid-grace | administrative: “update your card” |
| paid-canceled | soft winback: “stay with us” |
| paid-expired | aggressive winback: 50-70% off |
| lifetime | silent, maybe an addon |
| refunded | 30-day cooldown, then soft entry |
| dormant | last-resort weekend daytime touch |
Same engine. Same event triggers. Fourteen mouths.
The Default Wrong Frame
The first version of any monetization engine has one voice. It uses if-else on subscription status — is_paid vs is_free. Two voices, sometimes three if there is a separate trial branch.
That works while there are two voices. The user is either a customer or not. The copy in those two buckets is roughly right.
It collapses the moment a user spends real time in the system. A free user on day one is not the same person as a free user on day 60. A canceled user with three days of entitlement left is not the same person as one who lapsed last month. A grace-period user is not a sales target; a trial-expired user is.
Treating those as “free” or “paid” forces the engine into copy that is technically defensible and behaviorally wrong. The user reads “limited time offer” while their card is being silently retried in the background and concludes the app is opportunistic. They read “we miss you” while still entitled and conclude the app is confused.
Wrong voice in the wrong state is worse than no voice at all. Silence is recoverable. The wrong message rewrites the user’s model of what the app is.
CBT, Strapped to Billing
CBT — cognitive behavioral therapy — has a concept that maps exactly onto state-aware copy: the therapeutic alliance. The same intervention is different depending on where in the alliance you deliver it.
Confrontation in session three: rupture. Confrontation in session twenty: breakthrough. Same words, opposite outcome. The variable is not the intervention. It is the relationship state.
A monetization engine is the same protocol, applied to a relationship between a user and an app, with billing as the consequence. The right offer at the wrong moment is a rupture. The right reminder at the right moment is care. The thing that determines which one fires is the state, not the offer.
This is not metaphor. The state machine in AMORE is literally a CBT protocol with a payments backend grafted on. Onboarding is intake. Trial is treatment trial. Grace is a session where the alliance is fragile. Dormant is a former client you have not seen in a year, where the question “are you still interested in working with me” has to be asked carefully or not at all.
The engine cannot do CBT. The engine can avoid the most obvious therapeutic failures by checking the state before it speaks.
One Event, Two States, Two Right Answers
A worked example. Event: app.foreground.
Trial-active, day 2:
flow: —
channel: Live Activity (countdown only, no notification)
copy: none
The trial is fresh. The user is exploring. Silence is the gift. The Live Activity on the lock screen lets them know the trial is running, in case they want to know, without demanding their attention.
Paid-grace, day 3 of 16:
flow: flow.grace.update_card
channel: push (9-21 local only) + in-app banner on open
copy: "Your payment could not be processed. Update your card to keep your access."
The user is entitled, the card retry is pending, and they almost certainly do not know there is a problem. The administrative tone — no discount, no urgency theater — matches what is actually happening. The push respects the daytime-only window from the grace article.
Trial-expiring, 1h to end:
flow: flow.trial.soft_1h
channel: paywall on open + push (regardless of quiet hours, near-end exception)
copy: "1 hour left in your trial. Continue with 50% off the first year."
Same event. Different state. The trial is ending in sixty minutes. The system has earned the right to interrupt. The offer exists, the discount is real, the urgency is not invented.
Dormant, day 67 quiet:
flow: flow.dormant.welcome_back (if open in 30 days from last push)
channel: in-app welcome surface (push not allowed mid-week)
copy: "Welcome back. Here is what changed since you were here."
Same app.foreground. The user just walked back in after two months of silence. The right move is not a paywall. The right move is a re-onboarding. They get fourteen days of welcome_back flag before the engine resumes normal upsell behavior.
Same event. Four states. Four different right answers. None of them is “show paywall”.
What Goes Wrong When State Is Stale
The engine knows state from two sources: backend authoritative (App Store Server Notifications, Google RTDN) and local hint (StoreKit2 transaction updates, AppVisitCounter). Backend wins. Local hints are used to bridge the seconds-to-minutes lag.
When backend is slow or local is wrong, the state is stale. The user is in paid-active according to local — they just bought a minute ago — but backend has not caught up. Or the user is in paid-grace and a subscription.renewed event fired two minutes ago, putting them in paid-active, but the local cache still says grace.
A stale state means a wrong voice. The engine apologizes for billing problems the user does not have. The engine pitches premium to someone who just paid for it. The user reads the dissonance as the app being broken.
The fix is not “make state never stale” — that is impossible. The fix is to make the engine fail to the most conservative voice when state confidence is low: silence. If the engine is not sure whether the user is paid-grace or paid-active, it says nothing. Better to under-message than to message in the wrong voice.
This is one of the few places where “do nothing” is the explicit best action.
The Voice Library Is Not Templates
The wrong way to implement fourteen voices is to put them in copy templates indexed by state. That sounds clean. It does not survive contact with reality.
Voices are not strings. A voice is a combination of:
- the flow to run (or skip)
- the channels allowed (push? in-app? Live Activity? email? none?)
- the quiet hours (default 22-08, or grace 9-21 only, or dormant weekend-daytime only)
- the cooldown after dismissal (30 min? 24h? 48h? lifetime?)
- the copy register (administrative? warm? urgent? curious?)
- the offers permitted (none? specific list? all?)
A voice is six dimensions, not one string. Templates capture the last one. The other five live in the policy engine, the channel rules, and the capping system.
The reason these articles can talk about “the dormant voice” or “the grace voice” as if it is one thing is that AMORE bundles all six dimensions per state. Every flow inherits the voice of its triggering state by default. Overrides exist; they are rare and reviewed.
The Trade-Off
Cost: fourteen states is fourteen branches of logic, fourteen sets of copy, fourteen channels-and-quiet-hours rows in the config. The configuration surface is large. Onboarding a new flow requires deciding which states it applies in, which is more work than “everyone sees this”.
Cost: states drift. Backend and local fight each other on the seconds-to-minutes scale. Multi-device users have state computed independently on each device until the next sync. Shadow-mode rollouts of new flows have to be run through every state, not just the happy path, to catch the case where the flow misfires for refunded users.
Benefit: every message the engine sends is in a voice that matches the user’s actual situation. The same user gets a warm push, an urgent push, an administrative push, and a silence — each at the moment that voice is correct.
The wrong tone in the wrong state is the bug everyone in growth claims they do not have and quietly ships every quarter. State-aware voices are the only systemic fix.
Takeaway
A monetization engine that speaks in one voice is loud and wrong.
Fourteen states. Fourteen voices. Each voice is six dimensions, not one template. State is the first decision; everything else is downstream.
Silence when state confidence is low. Speak when the alliance allows it. The relationship is the product.