How CryptoScope Protected Paid Plans from Cannibalization
We break down part of CryptoScope’s plan configuration: base access, webhook events, grace period, downgrade, and P2P trading risk warnings.

A base plan can eat into a paid subscription faster than any discount.
At CryptoScope, the plan structure was designed for a crypto analytics platform with 2,500+ subscribers. So the question was not how to name three cards on a landing page. The goal was to draw a boundary: the base plan should be useful, but it should not remove the reasons to move to a paid one.
The trickiest part is not pricing. The trickiest part is access rights after payment, after subscription expiration, and after plan downgrade. If these states are scattered through the code, plan cannibalization shows up on its own: someone forgets to revoke access somewhere, grace period becomes indefinite somewhere else, and the P2P risk warning is shown only to paid users even though it depends on jurisdiction, not on the plan.
One configuration fragment everything revolves around
In broad terms, without internal feature names, the structure looked like this:
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
What matters here is not the field names, but the separation of responsibilities.
plans is responsible only for which entitlements are available on a given plan. The base plan is not empty: the analytics core stays in it. Otherwise, the user does not understand what they are paying for. But the separate conversion_entitlements group is kept apart — that is what should not accidentally get pulled into the base plan along with the “useful” features.
webhooks do not change rights directly in different parts of the application. They move the subscription between states: activate the plan, start the grace period, roll back to base, recalculate access. That is less exciting than writing a couple of if statements in a payment handler, but later it becomes possible to understand why the user sees a particular set of features.
risk_warnings are kept outside the plans. P2P trading risk warnings depend on the user’s jurisdiction. They cannot be treated as a paid feature or a bonus. This is a separate display rule that applies to all plans.
Why grace period cannot be mixed with paid access
Grace period is needed so the user does not lose access the moment the subscription expires. But for a plan model, this is a dangerous state: technically access still exists, commercially the subscription is no longer active.
If you store only plan = paid, the application will not distinguish between the paid period and the grace period. Then strange bugs start appearing: analytics treat the user as paid, the interface offers to renew the subscription, the webhook has already reported expiration, and access still looks like a full paid one.
That is why grace period is better kept as a separate state. Then the rules become testable:
- payment succeeded — activate the current plan;
- subscription expired — start grace period;
- grace period ended — downgrade to base;
- the user chose a lower plan — recalculate entitlements.
The longest discussion was not about plan wording, but about the boundary of base access. If the base is too poor, it becomes a stub. If it is too generous, the paid plan loses its meaning. In the config, this is just one hold_back line, but that is exactly where the product tradeoff lives.
Downgrade should be an event, not a side effect
Plan downgrade is a separate scenario. It should not be handled by the same code as subscription expiration, because the reason is different: the user did not disappear and did not miss a payment; they chose a smaller set of capabilities.
That is why subscription.downgraded leads to recalculate_entitlements. This is not “take away anything extra somewhere later,” but an explicit recalculation of access rights. That approach helps avoid mixing three similar states:
- the subscription is active;
- the subscription is in grace period;
- the subscription has been downgraded.
For CryptoScope, this is especially important because of the size of the audience. With 2,500+ subscribers, even a small mismatch between billing state and entitlements quickly turns into support load, manual checks, and distrust in the plan structure.
The next layer after such a configuration is state transition tests: paid → grace, grace → base, paid → downgraded, plus a check that P2P warnings are shown based on jurisdiction and do not depend on subscription tier.