Как в CryptoScope защищали платные тарифы от каннибализации
Разбираем кусок конфигурации тарифов CryptoScope: базовый доступ, webhook-события, grace period, downgrade и предупреждения о рисках P2P-торговли.

Базовый тариф может съесть платную подписку быстрее, чем любая скидка.
В CryptoScope тарифная сетка проектировалась для крипто-аналитической платформы с 2500+ подписчиками. Поэтому вопрос был не в том, как красиво назвать три карточки на лендинге. Нужно было провести границу: базовый тариф должен давать пользу, но не закрывать причины перехода на платный.
Самое неприятное место — не цена. Самое неприятное место — права доступа после оплаты, истечения подписки и понижения тарифа. Если эти состояния размазать по коду, каннибализация тарифов появится сама: где-то забыли снять доступ, где-то grace period стал вечным, где-то предупреждение о рисках P2P показывается только платным пользователям, хотя оно зависит от юрисдикции, а не от тарифа.
Один фрагмент конфигурации, вокруг которого всё крутится
Условно, без внутренних названий фич, структура выглядела так:
plans:
base:
allow:
- analytics_core
- p2p_risk_warnings
hold_back:
- conversion_entitlements
paid:
allow:
- analytics_core
- conversion_entitlements
- p2p_risk_warnings
webhooks:
payment.succeeded:
action: activate_plan
subscription.expired:
action: start_grace_period
grace_period.ended:
action: downgrade_to_base
subscription.downgraded:
action: recalculate_entitlements
risk_warnings:
p2p_trading:
source: user_jurisdiction
required_for_all_plans: true
Здесь важны не названия полей, а разделение ответственности.
plans отвечает только за то, какие entitlement доступны на тарифе. Базовый тариф не пустой: в нём остаётся ядро аналитики. Иначе пользователь не понимает, за что вообще платить. Но отдельная группа conversion_entitlements вынесена отдельно — это то, что нельзя случайно протащить в базу вместе с «полезными» возможностями.
webhooks не меняют права напрямую в разных местах приложения. Они переводят подписку в состояние: активировать тариф, запустить grace period, откатить на базовый, пересчитать доступы. Это скучнее, чем написать пару if в обработчике платежа, зато потом можно понять, почему пользователь видит тот или иной набор функций.
risk_warnings вынесены за тарифы. Предупреждения о рисках P2P-торговли завязаны на юрисдикцию пользователя. Их нельзя считать платной фичей или бонусом. Это отдельное правило показа, которое применяется на всех тарифах.
Почему grace period нельзя смешивать с платным доступом
Grace period нужен, чтобы пользователь не терял доступ в момент истечения подписки. Но для тарифной модели это опасное состояние: технически доступ ещё есть, коммерчески подписка уже не активна.
Если хранить только plan = paid, приложение не отличит оплаченный период от льготного. Потом начинаются странные баги: аналитика считает пользователя платным, интерфейс предлагает продлить подписку, webhook уже прислал истечение, а доступ всё ещё выглядит как полноценный paid.
Поэтому grace period лучше держать отдельным состоянием. Тогда правила становятся проверяемыми:
- оплата прошла — активируем текущий тариф;
- подписка истекла — запускаем grace period;
- grace period закончился — понижаем до базового;
- пользователь сам выбрал тариф ниже — пересчитываем entitlement.
На обсуждении дольше всего спорили не про формулировки тарифов, а про границу базового доступа. Если база слишком бедная, она превращается в заглушку. Если слишком щедрая, платный тариф теряет смысл. В конфиге это выглядит как одна строка hold_back, но за ней как раз и лежит продуктовый компромисс.
Downgrade должен быть событием, а не побочным эффектом
Понижение тарифа — отдельный сценарий. Его нельзя обрабатывать тем же кодом, что истечение подписки, потому что причина другая: пользователь не пропал и не просрочил оплату, он выбрал меньший набор возможностей.
Поэтому subscription.downgraded ведёт к recalculate_entitlements. Это не «забрать всё лишнее где-нибудь потом», а явный пересчёт прав доступа. Такой подход помогает не смешивать три похожих состояния:
- подписка активна;
- подписка в grace period;
- подписка понижена.
Для CryptoScope это особенно важно из-за размера аудитории. При 2500+ подписчиках даже небольшой разъезд между billing-состоянием и entitlement быстро превращается в поддержку, ручные проверки и недоверие к тарифной сетке.
Следующий слой после такой конфигурации — тесты на переходы состояний: paid → grace, grace → base, paid → downgraded, плюс проверка, что P2P-предупреждения показываются по юрисдикции и не зависят от уровня подписки.