B2CDROID-282 · analytics reference

Monetization & Paywall Event Catalog

Payments run on Razorpay (no Google Play Billing anywhere in the codebase). Every event below is traced to its Kotlin class and fire site.
Two sources, kept separate. Event rows (class, properties, fire site) come from origin/staging @ 0a9262e3c code exploration. Volumes come from Mixpanel Equal Production, last 30 days.
33
distinct monetization events
35 / 18
paywall.* events, staging vs prod
3
navigate(PaywallRoute) call sites
2 / 3
lock surfaces measured (summary missing)
401
purchases, 30d
paywall.api.purchase_success
01

Live production funnel

Prod volumes, 30 days. Bars are log-scaled so the tail stays legible; drop-off between steps is labelled on the right.
touchpoint.exposed 132,495 screen.view 20,287 15.3% of exposed api.checkout_initiated 4,188 20.6% of view coupon.apply_success 1,120 side path api.purchase_success 401 9.6% of checkout view to purchase, end to end 2.0%

Lock-surface exposure, 30d

chat.transcript 369,322 view · 7,168 click · 1.9% conversations.lock_row 241,528 view · 3,269 click · 1.4% chat.recording_player 119,211 view · 9,739 click · 8.2% solid = clicks, faded = views (linear). Recording player converts 4x the others.
summary card
No event exists
The fourth lockable surface emits nothing: no view, no click, no property on chat.screen.view. It is invisible in every report. See finding 02.
02

Key findings

Summary-card lock is unmeasurable, confirmed three ways. Code has a summaryLocked: Boolean display input (it blurs text) but no tap handler and no analytics callback. No chat.summary.lock_* event class exists. Backend never emits a Placement that locks SUMMARY (all summary wire strings are provisional, pending Ayush). chat.screen.view carries is_recording_locked and is_transcript_locked but no is_summary_locked.
Manage-subscription entry_point is misattributed. bug manage_subscription.screen.view.entry_point is hardcoded "settings" (ManageSubscriptionViewModel:135) even though deep-link entry already ships (MainActivity:1097). Deep-link-originated views report as settings.
Naming collision in the staging Lexicon. Both paywall.status_sheet.shown and paywall.status_sheet_shown exist (dot vs underscore). Verify only one is emitted by current code; likely a stale duplicate.
Scroll-before-coupon is already instrumented. paywall.coupon.apply_click carries scrolled_before_apply:Bool and max_scroll_depth_pct:Int. The real gap: no standalone scroll event, and no scroll capture on non-coupon paths (users who dismiss or checkout without touching a coupon).
New entry points are not in prod volume yet. new The 1107 build adds source push_notification, trigger promo_push, and preview surface promo_push. None appear in prod 30d. Expected, the build just shipped.
Dead taxonomy. no writer paywall.coupon.apply_failed, DismissalReason.CTA_SKIP, and FailureReason.CANCELLED are defined with no emitter.
03

Paywall core events

paywall/domain/PaywallAnalytics.kt. Common bucket on every event: plan_tier, paywall_surface, session_id?, placement, variant, plan_experiment_id.
EventExtra propertiesFired atTrigger
paywall.touchpoint.exposedsource_screenTriggerHost:299 / 540Locked surface resolves, assignment set
paywall.screen.viewtrigger, source_screen, paywall_heading, conversation_id, price_variantPaywallVM:441Sheet painted, not subscribed
paywall.api.plans_successplan_count:Int, price_variantPaywallVM:534/plans 2xx non-empty
paywall.api.plans_failederror_code, price_variantPaywallVM:483/plans failure
paywall.coupon.apply_clickcoupon_code, apply_method, scrolled_before_apply:Bool, max_scroll_depth_pct:Int, price_variantPaywallVM:166applyCoupon(), manual and auto
paywall.coupon.apply_successcoupon_code, discount_amount:Int(paise), effective_price:Int(paise), apply_methodPaywallVM:201Sync after click (V1)
paywall.coupon.apply_failed deadcoupon_code, error_codenonereserved for V2 validate API
paywall.button.clickselected_plan_id, coupon_applied:Bool, price_confirmed:Int, time_on_paywall_ms:LongPaywallVM:660Top of confirmContinue()
paywall.api.checkout_initiatedorder_id, selected_plan_idPaywallVM:721/subscriptions/initiate 200
checkout.screen.viewplan_id, effective_price:IntPaywallVM:752Before Razorpay launcher
paywall.api.purchase_successorder_id, plan_id, effective_price:Int, razorpay_payment_id(masked), razorpay_subscription_id(masked)PaywallVM:859/subscriptions/verify 200
subscription.system.unlockedplan_id, effective_price:Int, time_to_confirm_ms:Long?PaywallVM:1120inactive to active transition
paywall.api.active_subscription_recoveredplan_idPaywallVM:328initiate 409 ACTIVE_EXISTS
paywall.api.pending_subscription_detectedplan_idPaywallVM:344initiate 409 PENDING_EXISTS
paywall_success.screen.viewplan_id, effective_price:IntStatusSheet:140Welcome sheet mount
paywall.overlay.viewstatus(confirmation|confirmed|pending|failed|welcome), order_idStatusSheet:123Each status overlay render
paywall.api.purchase_failedorder_id, failure_reason(enum)PaywallVM:783..980Razorpay Failure, or verify/initiate error
paywall.screen.dismisstrigger, dismissal_reason, order_id, time_on_paywall_ms:LongPaywallVM:1091Dismiss without subscribing
paywall.canary.shown_to_subscribed_user P0 tripwireentitlement_status, source_screenPaywallVM:388Paywall for isActive=true user, must stay 0
paywall.network.offline_blocked_initiatesurfacePaywallVM:632CTA tapped offline
paywall.network.offline_shown_no_planssurfacePaywallVM:416 / 504Opened offline, /plans failed
04

Manage subscription and cancel flow

paywall/ui/ManageSubscriptionViewModel.kt. Cancel is POST /subscriptions/{id}/cancel, always cancel_at_cycle_end=true. No restore event. Self-heal is via 409 recovery plus refreshOnResume().
EventExtra propertiesAtTrigger
manage_subscription.screen.viewplan_id, entry_point (hardcoded "settings"), subscription_status:127Screen mount
manage_subscription.button.cancel_clickplan_id, subscription_age_days:Int:246Cancel CTA tapped
paywall.network.offline_blocked_cancelsurface="manage_subscription":260Cancel tapped offline
paywall.cancel.feedback_shownsubscription_id(masked):288Feedback sheet opened
manage_subscription.overlay.viewsubscription_id(masked), feedback_length:Int:309Draft to Confirm step
manage_subscription.button.cancel_confirmedsubscription_id(masked), reason(ellipsized), cancel_at_cycle_end:Bool, days_remaining_in_cycle:Int, subscription_age_days:Int:196POST /cancel 200
manage_subscription.button.cancel_dismissedsubscription_id(masked), abort_step, abort_method:335Backs out of cancel flow
manage_subscription.api.cancel_failedsubscription_id(masked), failure_kind(http|network), http_code:Int(-1 network), feedback_length:Int:385POST /cancel non-2xx or throw
05

Content-lock surfaces (modular gating)

Gate is the server Placement from GET /api/v2/experiment?type=paywall. LockPredicate checks !entitlement.isActive && placement.locks(surface).
InputAudioTranscriptSummary
Display lock flagAudioLockState (sealed)Boolean lockedBoolean summaryLocked
Placement surfaceAUDIO (shipped)TRANSCRIPT (shipped)SUMMARY (never emitted)
Tap handleronLockTaponUnlockTapmissing
Lock-view analytics cbonLockViewonLockView (dedupe by msgId)missing
View eventchat.recording_player.lock_viewchat.transcript.lock_viewnone
Click eventchat.recording_player.lock_clickchat.transcript.lock_clicknone
Preview surfaceCallAudioSummaryLockTranscriptChatLockn/a
Fail-closed defaulttrue (force-lock)true (force-lock)false (never)
Chat lock events carry conversation_id(masked), call_type("unknown"), locked_element. Recording-lock view is not deduped (re-fires per composition); transcript-lock view is deduped per messageId.

Conversations-list row lock (distinct, index-based)

EventExtraAtTrigger
conversations.lock_row.viewconversation_id, row_position:Int, call_typeTriggerHost:324Locked row into viewport (dedupe by convId)
conversations.lock_row.clicksame:113Tap locked row
Row gate: rowIndex at or above freeConversationLimit (default 4, from Statsig paywall_lock_config). Only for Placement.CONVERSATION.
07

Entry points into the paywall

Three navigate(PaywallRoute) sites, plus two navigate(ManageSubscription). All lock surfaces converge on one destination (CallScreensNavGraph:306); trigger and source are derived from PaywallPreviewSurface in PaywallScreen.kt:174-185.
Conversations · locked row Transcripts · sticky CTA Transcripts · transcript bubble Transcripts · audio scrubber Promo push / deep link · new PaywallRoute NavGraph:306 trigger + source
#EntryPreview surfacetrigger / source_screenNav site
1Conversations list, tap locked rowConversationsLockhistory_pagination_boundary / conversations_listNavGraph:903
2Transcripts, sticky CTATranscript or CallAudioSummary (derived)transcript_open or audio_play_tap / transcripts_screen_v2NavGraph:292
2bTranscripts, tap blurred transcript bubbleTranscriptChatLocktranscript_open / transcripts_screen_v2same route
2cTranscripts, tap locked audio scrubberCallAudioSummaryLockaudio_play_tap / transcripts_screen_v2same route
3 newPromo push / deep link to paywallPromoPushpromo_push / push_notificationMainActivity:1071
MS1Settings to Manage Subscriptionn/aentry_point="settings"SettingsNavGraph:348
MS2 bugDeep link manage-subscriptionn/aentry_point still "settings"MainActivity:1097
08

Enums and wire values

Trigger / source / preview surface

GroupWire values
PaywallTriggerhistory_pagination_boundary, audio_play_tap, transcript_open, promo_push
PaywallSourceScreenconversations_list, transcripts_screen_v2, push_notification
PaywallPreviewSurfaceconversations_lock, transcript_lock, recording_lock, promo_push (unknown falls back to conversations_lock)
CouponApplyMethodmanual, auto
SubscriptionStatuscreated, authenticated, active, past_due, paused, cancelled, completed, abandoned

Placement to locked surfaces

EnumWireLocks
CONVERSATIONconversationnone (row-index)
CALL_RECORDINGcall_recordingAUDIO
RECORDING_AND_TRANSCRIPTrecording_and_transcriptAUDIO, TRANSCRIPT
RECORDING_TRANSCRIPT_SUMMARYrecording_transcript_summaryAUDIO, TRANSCRIPT, SUMMARY (provisional)
TRANSCRIPT_AND_SUMMARYtranscript_summaryTRANSCRIPT, SUMMARY (provisional)
RECORDING_AND_SUMMARYrecording_summaryAUDIO, SUMMARY (provisional)
TRANSCRIPT_ONLYtranscriptTRANSCRIPT (provisional)
SUMMARY_ONLYsummarySUMMARY (provisional)

failure_reason / dismissal_reason

Enumvalue to wire
FailureReasonSIGNATURE_MISMATCH, VERIFY_FAILED to invalid_signature. RAZORPAY_FAILURE to bank_declined. NETWORK, UNKNOWN, INITIATE_FAILED, WRITES_DISABLED to network_error. CANCELLED to user_cancelled no writer
DismissalReasonback_press, timeout, cta_skip no writer
09

Action items

Summary lock. Confirm the SUMMARY Placement wire with Ayush, then add a tap handler, chat.summary.lock_view / click, and is_summary_locked on chat.screen.view. Display machinery is wired; interaction and measurement are absent.
Manage-sub entry_point. Thread a route arg so deep-link opens report entry_point="deeplink" instead of "settings".
Naming collision. Reconcile paywall.status_sheet.shown vs paywall.status_sheet_shown in the Lexicon; keep one.
Scroll on non-coupon paths. Scroll props exist on apply_click; add scroll capture on dismiss and checkout so users who never touch a coupon are covered.
Conversation touchpoint removed. Add a user-property to measure old-conversation access (median days back, percent of base) before revisiting the touchpoint.
Dead taxonomy. Wire or delete paywall.coupon.apply_failed, CTA_SKIP, and FailureReason.CANCELLED.
10

Full event table

The complete machine-readable catalog: meta, prod volumes, all 46 events as a filterable table, backed by the embedded JSON. Same payload served at /monetization-event-catalog.json.
Rendered from the embedded JSON. status flags DEAD_NO_WRITER and P0_TRIPWIRE_MUST_BE_ZERO are highlighted. Raw payload at /monetization-event-catalog.json.