Sergey Kopanev: you sleep — agents ship

Go Back
AMORE: Acquisition, Monetization, Onboarding, Retention, Engagement · Part 3

Each Touch Burns Conviction


A user opened the app on a Wednesday at 7pm.

She got a paywall. She closed it. A push fired forty minutes later about an unrelated offer. She opened the app to dismiss it. A banner sat on the home screen advertising a third thing. She scrolled. A toast popped up about a fourth.

Each of those four surfaces was within its individual cap. One paywall per session. Two pushes per day. One banner. One toast. The dashboard for that day showed nothing red.

She uninstalled the app the next morning.

What AMORE Is

AMORE is the retention engine for a B2C health app — Acquisition, Monetization, Onboarding, Retention, Engagement. It owns when and through which channel the system talks to a user. The first article covers grace; the second covers dormant. Both end with the same lesson: knowing what to say is half the job, knowing when to shut up is the other half.

Capping is the rules layer that says shut up. The first version had per-channel rules. They were not enough.

The Per-Channel Trap

The first capping system was textbook clean.

per_session / any_paywall / limit=1
per_day / category=entry-cancel / limit=2
per_week / flow_id=flow.winback.hard / limit=1
per_day / channel=push / limit=2
per_session / cooldown=120s / any

Every flow had its limit. Every channel had its limit. Every category had a cooldown. Logs were clean. The capping dashboard was green.

What the rules did not encode: a user does not experience flows or channels. A user experiences a phone, a notification tray, and an app that keeps interrupting her evening.

A paywall is not equivalent to a banner. A push is not equivalent to a toast. A banner is not equivalent to a paywall. Treating them like independent budgets is bookkeeping, not respect.

The four-touch evening above is what clean capping logs look like to a user.

The Glance Limit

The fix was an aggregate counter sitting above the per-channel ones.

Each surface gets a fatigue weight:

paywall   weight = 5
push      weight = 3
banner    weight = 1
toast     weight = 0

A toast is free. A banner is one unit. A push is three. A paywall — the heaviest demand on attention, the most disruptive, the one that demands a tap to dismiss — is five.

Sum across surfaces per user per day. If the sum crosses a threshold, non-critical channels block for 24 hours. The user still gets billing reminders. The user does not get the fourth optional sales nudge.

The Wednesday-evening user above scored 5 (paywall) + 3 (push) + 1 (banner) + 0 (toast) = 9. Threshold was set at 7. The toast should never have fired. The banner should never have rendered.

The aggregate counter is not a substitute for per-channel caps. It rides on top of them. Per-channel rules prevent any single surface from going feral. The glance limit prevents the union of well-behaved surfaces from being a worse experience than any of them individually would have been.

Counter-Conditioning

The behavioral name for what happens when you exceed the threshold: counter-conditioning.

The user is conditioned to associate notifications from this app with annoyance. Future notifications fire that association, regardless of content. A perfect “your card is about to expire” reminder, sent the day after the four-touch evening, gets dismissed without reading. Not because the user does not care. Because the user has learned that notifications from this app are noise.

This is not engagement metrics. This is the slow, quiet collapse of the channel as a medium.

Per-channel caps cannot see this. Per-channel caps see “we sent two pushes this week, well under the limit”. They cannot see that those two pushes shipped into a tray the user had already learned to ignore.

The aggregate weight is a crude proxy for “how much of the user’s patience are we burning”. It is wrong in the small. It is right in the large.

State Modulates the Threshold

A free-new user — first three days, no purchase — gets a higher threshold. Aggressive onboarding is allowed. The system is trying to get them to commit to a state where the app matters.

A free-engaged user — uses the app daily — gets a lower threshold. They are already convinced; do not poke them.

state              paywalls/day   threshold
free-new           3              12
free-onboarded     2              10
free-engaged       1              6
free-churned       1              4
paid-canceled      0 (winback)    8
dormant            0              n/a

free-engaged is the trap state. A user who is engaged and converting on their own is the most overlooked: the marketing instinct says “they are warm, push more.” The data says the opposite. Every push to an engaged user is risk without upside. Their conversion does not need help. Their tolerance for noise is finite.

Lowering caps for engaged users felt counter-intuitive. It pushed conversion-per-push down. It pushed long-term retention up. Different metric, different graph, different person looking at it.

The Critical-Channel Override

There is one carve-out. Billing reminders during a grace period — covered in the first article — bypass the aggregate threshold.

The user might be at fatigue 11 with a threshold of 7. Card declined. Push fires anyway.

The reasoning: a billing reminder is not a sales pitch. The fatigue threshold protects from sales pressure. Letting it block administrative messages would let the user lose access because we were too tired to interrupt them.

That carve-out is one bypass rule, written down, reviewed, and locked. Every other system that wanted to bypass — “but our flow is really important” — got told no. The list of exceptions stays short on purpose. The moment “critical” expands, the threshold means nothing.

What the Dashboard Stopped Showing

After the aggregate counter went in, the dashboard added one number: percentage of triggers blocked by glance-limit.

Per-flow capping blocks were 4–7%. Acceptable.

Glance-limit blocks were 22% in the first week. That number was the size of the gap between “no individual cap was violated” and “the user would have been carpet-bombed”.

22% of optional sales nudges that the old system would have shown got silently dropped. Conversion per shown surface went up — survivorship, mostly. Total conversion stayed roughly flat. Notification-permission revocations dropped sharply.

The metric the dashboard had been missing was the user’s breaking point. The aggregate counter is the proxy for it.

The Trade-Off

Cost: aggregate counters are a third storage path on top of per-channel and per-flow counters. They have to sync between local (fast decisions) and backend (cross-device). They are one more place where state can drift. The first two weeks after launch had bugs where local fired faster than backend, occasionally double-counting.

Cost: explaining to product why a “perfectly within caps” flow did not fire. The new answer — “the user already saw enough today across all surfaces” — does not show up in any single flow’s logs. It shows up in policy.aggregate_block events. People had to learn to look for them.

Benefit: fewer uninstalls during high-pressure campaigns. The aggregate counter throttles the campaign before the user does.

Takeaway

A user does not have a paywall budget, a push budget, and a banner budget.

A user has one attention budget. Each touch burns conviction. The cost is what they will tolerate from you tomorrow.

Cap the union, not just the parts.


Next: [coming soon]