Dilshat Rakhimov
May 2026 · 14 min read

A banking SuperApp has a classic growth trap. The team starts with a sensible goal: bring in more registered users, get them through KYC, and prove that ads can scale the user base. That is a normal first step. But if optimization lives for too long on sign_up, registration_complete, or kyc_approved alone, the ad system learns to find people who go through onboarding cheaply. It is not obliged to find people who later make transfers, open a deposit, take out a loan, or buy services inside a WebView.

For a Halyk-class SuperApp the problem is especially visible. One app holds products with very different economics: daily transfers, payments, loan applications, cards, deposits, investments, insurance, tickets, utility bills, mobile top-up, eGov-like services, and partner marketplaces. If you measure all of this by a single registration metric, marketing looks effective, but product teams see something else: users arrived, KYC was passed, and yet product turnover and margin are missing.

The right answer is not to swap KYC for some random purchase. You need a system where KYC stays a high-quality entry filter and optimization gradually moves to events with value: not to the loan amount, not to the deposit balance, and not to the transfer turnover, but to the expected business value of an action.

SuperApp promotion architecture

Short version

A SuperApp should be promoted in two layers.

The first layer is acquisition and onboarding. It is responsible for registration, first login, KYC, consents, setting the payment PIN, linking a card, and the first clear product interest. tCPA or Maximize conversions usually fit here, because the goal is to give the algorithm enough quality user flow.

The second layer is product value growth. It is responsible for money and behavior: transfers, payments, service purchases, loan disbursements, funded deposits, activated products. Here you already need value and, once history has accumulated, tROAS or Maximize conversion value.

The mistake is asking the first layer to do the second layer's job. KYC by itself does not explain to the ad system who will bring margin. It only says: the user reached the threshold from which the SuperApp can sell products.

What breaks if you optimize only for registration and KYC

If a campaign optimizes for registration_complete, it minimizes the cost of a registration. If it optimizes for kyc_approved, it hunts for people who are most likely to pass verification. That is useful, but a signal like that has four limits.

First, KYC does not differentiate product intent. A user who passed KYC for a free transfer, a user who needs a cash loan, and a user who is about to open a deposit look identical to the ad system.

Second, KYC carries no value. In Google Ads, tROAS works with conversion value: the algorithm has to understand how much value an action brings, not just the fact that it happened. If every KYC has the same value, the system will not see the difference between a cheap user with no further action and an expensive user with high LTV.

Third, KYC is too early for some products. For a loan, what matters is not the application but the approval, the disbursement, and the risk-adjusted expected margin. For a deposit, what matters is not opening the form but the funded deposit: money actually placed. For a WebView service, what matters is not the click on the storefront but the confirmed purchase or commission.

Fourth, a SuperApp quickly turns into a political dispute between products. The credit team wants its own optimization, deposits want their own, payments want volume, the marketplace wants purchases. If you slice campaigns along the org chart, every campaign may end up without a learning signal. If you collapse everything into a single event without a proper value model, the algorithm may start buying only the product whose value looks the highest on paper, even if it is worse in profit or risk.

Event taxonomy: a product grammar, not a list of screens

For a SuperApp, events should be designed not by screens but by the business grammar. There will be many screens, even more WebView services, product teams will rename things, partners will come and go. The semantic stages have to be stable:

  1. Who is the user and can they buy the product.
  2. Which product they are exploring.
  3. Which step of the product funnel they took.
  4. What expected value that action produced.
  5. Whether that event can be fed into ad optimization.

SuperApp event taxonomy

The base layer can be set up like this:

LayerEventsWhy you need them
Identitysign_up, login, kyc_start, kyc_submitted, kyc_verified, first_login_after_kycSeparate new users from real KYC users and give a warm-up signal
Product discoveryview_item, select_item, product_view, product_startShow that the user is not just registered but is looking at a specific product
Funnelbegin_checkout, generate_lead, application_submit, payment_start, service_order_submitProvide mid-funnel events for products with a long cycle
Valueproduct_value, purchase, loan_approved_value, deposit_funded_value, transfer_success_valueFeed tROAS and Maximize conversion value
Qualityloan_rejected, deposit_closed_7d, refund, chargeback, kyc_failedNot always sent to Ads, but needed for BigQuery QA and value-model correction

GA4 and Firebase already have recommended events such as sign_up, login, generate_lead, purchase, begin_checkout, view_item, and select_item. Use them where the meaning matches. But a banking SuperApp will almost always need custom events: for example kyc_verified, product_value, loan_disbursed_value, or deposit_funded_value.

The main rule: the event name should be stable and product detail should live in parameters.

{
"event_name": "product_value",
"value": 850.0,
"currency": "KZT",
"product_type": "loan",
"product_id": "cash_loan",
"funnel_stage": "approved",
"value_model": "expected_margin",
"source_surface": "native_app",
"event_id": "9b1d0c4b-..."
}

A contract like this avoids spawning dozens of events per SKU and still preserves product-level reporting. Separate events should exist not because a product team wants its own name in Firebase, but because the economics, eligibility, regulation, or bidding goal really are different.

How to compute value: do not send principal as revenue

The most dangerous mistake in banking tROAS is sending the loan amount, the deposit balance, or the transfer volume as value. The algorithm reads that as conversion value. But KZT 1,000,000 of a loan is not KZT 1,000,000 of bank revenue. KZT 1,000,000 of a deposit is not revenue. KZT 100,000 of a transfer is not margin.

What value should hold is the expected contribution value: commission, gross profit, expected margin, risk-adjusted value, or predicted LTV over a chosen window.

ProductEvent triggerWhat to put in valueBidding note
Registration / KYCkyc_verifiedUsually 0 or a small proxy valueGood for tCPA / warm-up, bad as a final goal
Transfertransfer_success_valueCommission + activation proxy or retention valueDo not use the transfer amount as revenue
Loanloan_approved_value or loan_disbursed_valueApproval probability × expected margin - risk - bonusBetter to have a separate model and often a separate campaign
Depositdeposit_funded_valueBalance × net margin over 30/90 days × retention probability - incentive costDo not send the whole balance as value
WebView servicepurchase or service_purchase_valueCommission, take rate, or gross marginIf the bank earns a commission, value = commission, not GMV
Card / subscriptioncard_activated_value, subscription_startedPredicted margin or LTVImportant to subtract cashback and welcome bonus

Minimal formula for a value model:

expected_value =
expected_revenue_or_margin
- incentive_cost
- risk_cost
- servicing_cost

For a loan:

loan_expected_value =
approval_probability
* disbursal_probability
* expected_net_interest_margin
- expected_loss
- acquisition_bonus

For a deposit:

deposit_expected_value =
funded_balance
* net_interest_margin_90d
* retention_probability_90d
- promo_bonus

For a WebView service:

service_purchase_value =
gross_order_value
* partner_take_rate
- discount_cost

These formulas do not have to be perfect on day one. They have to be more honest than principal-as-revenue. Even a rough margin proxy is usually better for tROAS than a clean but false transaction amount.

How to send value into Firebase

In Firebase/GA4 an event can have a value parameter, and for monetary events currency must be sent alongside. In the Android SDK that is FirebaseAnalytics.Param.VALUE and FirebaseAnalytics.Param.CURRENCY. For ecommerce events like purchase, GA4 also expects transaction-level value, currency, and an items array if the purchase truly looks like a product or service purchase.

For a SuperApp I would split events into two types.

The first type is the canonical value event. This is a single stable custom event, for example product_value, that fits most products:

fun logProductValue(
firebaseAnalytics: FirebaseAnalytics,
productType: String,
productId: String,
funnelStage: String,
expectedValue: Double,
eventId: String
) {
val params = Bundle().apply {
putDouble(FirebaseAnalytics.Param.VALUE, expectedValue)
putString(FirebaseAnalytics.Param.CURRENCY, "KZT")
putString("product_type", productType) // loan, deposit, transfer, service
putString("product_id", productId) // controlled dictionary, not free text
putString("funnel_stage", funnelStage) // approved, funded, completed
putString("value_model", "expected_margin")
putString("event_id", eventId)
}
firebaseAnalytics.logEvent("product_value", params)
}

The second type is the recommended ecommerce event, used when the user actually buys a service: mobile top-up, ticket, insurance, subscription, partner service in a WebView. Here purchase works because the meaning matches GA4 ecommerce.

fun logServicePurchase(
firebaseAnalytics: FirebaseAnalytics,
serviceId: String,
serviceCategory: String,
commissionValue: Double,
transactionId: String
) {
val item = Bundle().apply {
putString(FirebaseAnalytics.Param.ITEM_ID, serviceId)
putString(FirebaseAnalytics.Param.ITEM_CATEGORY, serviceCategory)
putDouble(FirebaseAnalytics.Param.PRICE, commissionValue)
putLong(FirebaseAnalytics.Param.QUANTITY, 1)
}
val params = Bundle().apply {
putString(FirebaseAnalytics.Param.TRANSACTION_ID, transactionId)
putDouble(FirebaseAnalytics.Param.VALUE, commissionValue)
putString(FirebaseAnalytics.Param.CURRENCY, "KZT")
putParcelableArray(FirebaseAnalytics.Param.ITEMS, arrayOf(item))
putString("surface", "webview")
putString("value_model", "commission")
}
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.PURCHASE, params)
}

In a banking app, value should not be computed only on the client. The client can send a fast product signal, but the final value event should be confirmed by a backend ledger: KYC really approved, the loan really issued or approved on the right status, the deposit really funded, the WebView purchase really paid, and no refund coming in.

A practical pattern:

  1. The app SDK logs early events: sign_up, kyc_start, product_view, begin_checkout.
  2. The backend creates an authoritative event after confirming the financial status.
  3. A value service computes expected_value from the product model.
  4. Firebase/GA4 receive the event with value, currency, and stable product params.
  5. Google Ads imports the required Firebase/GA4 events as conversion actions.
  6. BigQuery checks that value is not inflated, not duplicated, and not in conflict with the actual economics.

Check custom dimensions and metrics separately. Parameters like product_type, funnel_stage, value_model are needed for reporting, but they have to live in a controlled dictionary. If product_id ends up holding free text from partners or dynamic WebView service names, reporting cardinality will explode and become unmanageable.

One purchase for everything or separate events per product?

The idea of "let's make a single purchase event and put the product in custom variables" sounds right because Smart Bidding likes a dense signal. But for a SuperApp it is not always safe.

A single unified event is good when:

  1. The products share a similar audience.
  2. Value is normalized and comparable across products.
  3. Budget and conversions are not enough to support separate campaigns.
  4. Creative can be split into ad groups but the campaign goal stays unified.
  5. You accept portfolio optimization: the algorithm distributes demand between products by value on its own.

Product-specific events are better when:

  1. The product has very different economics: for example loans versus transfers.
  2. The product has different eligibility: not all KYC users can take a loan.
  3. There are regulatory restrictions or a separate approval flow.
  4. The value model is not comparable with the other products.
  5. There is enough volume for the campaign to learn without starving.

My default for a banking SuperApp: start with product_value as a single value event but bake in product_type, product_id, funnel_stage, value_model, surface from the beginning. Then split events and campaigns only where economics and volume justify it.

For a Halyk-like SuperApp this usually leads to a hybrid structure:

  1. Loans - often separate event/campaign logic, because risk, approval, disbursement, and margin do not behave like payments.
  2. Deposits / investments - a separate campaign only when funded-event volume is sufficient; otherwise an ad group inside the unified value campaign.
  3. Transfers / payments - can live inside a daily banking value campaign, since this is high-frequency behavior and a good engagement signal.
  4. WebView marketplace services - use purchase or service_purchase_value; if there are many services, split ad groups by category rather than spinning up a campaign per service.

Campaign: how to move from KYC to tROAS

The move to tROAS should not be abrupt. Google Ads, in its official guidance for App campaigns, recommends first verifying conversion tracking, making sure Firebase/GA4 events are imported correctly, and accounting for cold start: tROAS needs historical value data. If campaigns previously ran on tCPA, it is reasonable to build up 3-4 weeks of conversion value history; for new app campaigns, start with Maximize conversions or tCPA until volume stabilizes.

SuperApp campaign structure decision tree

A working structure can look like this.

Campaign 1: New users / KYC approved

Goal: not profit, but a quality base.

Event: kyc_verified or first_login_after_kyc.

Bid strategy: tCPA or Maximize conversions.

Whom to show: new users, lookalike/automated targeting, broad acquisition, excluding existing KYC users where possible.

Why it exists: feed the app a stream of users who actually can buy banking products. Do not try to make this campaign a report on credit margin.

Campaign 2: Daily Banking Value

Goal: first product actions after KYC.

Event: product_value with product_type = transfer, payment, service, sometimes deposit.

Bid strategy: Maximize conversion value, then tROAS once stable value history has accumulated.

Ad groups: transfers, payments, services, deposits-lite. Each group has its own assets and deep links: card-to-card transfer, mobile top-up, utility payment, 2-minute deposit, tickets, insurance.

Why it exists: do not splinter early product growth into too-small campaigns. Daily banking provides a high-frequency signal and helps the algorithm learn which KYC users start using the SuperApp as their financial hub.

Campaign 3: Loans Value

Goal: applications, approvals, disbursements, but with a risk-adjusted value.

Event: better loan_approved_value or loan_disbursed_value, not just loan_application_submit. If the cycle is long, you can use two goals: a mid-funnel generate_lead for volume and a final value conversion for quality assessment.

Bid strategy: tROAS only after you have verified the value model. Until then - tCPA on qualified application or Maximize conversion value with a cautious budget.

Why it is separate: loan economics are very different from payments. The algorithm should not be comparing KZT 300 of transfer commission against a risky loan margin when the value model is still raw.

Campaign 4: Deposits / Savings Value

Goal: funded deposit, not just an opened screen.

Event: deposit_funded_value.

Value: expected net interest margin over the chosen window, multiplied by retention probability, minus bonuses.

Structure: a separate campaign if funded-event volume is there. If volume is low, start as an ad group inside Daily Banking Value and analyze post-click behavior in BigQuery.

Campaign 5: App engagement for existing KYC users

Goal: bring users who already installed the app back to a specific product screen.

Events: product-specific value events or purchase.

Infrastructure: deep links, audiences from Firebase/GA4, exclusion of users who already bought the product, and a separate communication frequency so the SuperApp does not turn into push/ads spam.

It is important here not to mix acquisition and re-engagement in the same management report. A new user who just passed KYC and an existing KYC user who needs to see a credit offer require different messages, different deep links, and different CPA/ROAS expectations.

When to split campaigns and when to keep ad groups

Splitting campaigns is not a reflection of the bank's org chart. It is a decision about where the algorithm needs its own learning pool.

Keep one value campaign with product ad groups if:

  1. At the product level you have fewer stable value conversions than you need for learning.
  2. Users and creative overlap heavily.
  3. Value is already normalized and does not give one product an artificial advantage.
  4. Product teams can look at breakdowns via GA4/BigQuery and do not insist on a separate campaign budget at any cost.

Split into separate campaigns if:

  1. There are enough conversions and budget for each campaign.
  2. The product has distinct economics and a separate target ROAS.
  3. Different geo, language, age eligibility, legal disclaimers, or approval rules are required.
  4. Different conversion windows are needed: a loan can mature longer than a mobile top-up.
  5. You are ready to manage learning phases and not retune tROAS every two days.

In practice a good compromise looks like this:

Campaign A: Acquisition / KYC
goal: kyc_verified
bid: tCPA
Campaign B: Daily Banking Value
goal: product_value
ad groups: transfers, payments, services, deposits starter
bid: Max conversion value -> tROAS
Campaign C: Loans Value
goal: loan_approved_value or loan_disbursed_value
bid: tCPA on qualified lead -> tROAS after value history
Campaign D: Savings Value
goal: deposit_funded_value
bid: only if volume is enough; otherwise ad group in Campaign B
Campaign E: Existing Users Engagement
goal: product-specific value events
targeting: installed / KYC / eligible audiences

WebView services: a separate risk zone

In a SuperApp many services live inside WebView or partner flows. That is convenient for the business but risky for measurement.

The typical problems:

  1. The WebView sends web events, the native app sends Firebase events, the backend confirms payment separately.
  2. The same order can fire as purchase from the WebView and as service_purchase_value from native/backend.
  3. Partner GMV ends up in value, even though the bank only earns commission.
  4. transaction_id is not stable: the partner has one ID, the bank another, the payment a third.
  5. Service categories change faster than marketing can update the campaign structure.

The fix: the WebView should pass an order context, not a "marketing event", to native/backend. The final event should be assembled after the payment is confirmed.

webview order submitted
-> native bridge receives service_order_id
-> backend validates payment status
-> value service calculates commission_value
-> Firebase logs purchase with transaction_id
-> BigQuery deduplicates by transaction_id / event_id

If the services look like ecommerce, use purchase and items. If it is a banking product or an application, use generate_lead, product_value, or a product-specific custom event. Do not call everything purchase just for a clean report: the event has to describe the real action.

QA: what to check before importing into Google Ads

Before promoting an event to a Primary conversion action and turning on tROAS, run a technical QA.

  1. value is always numeric, not a string.
  2. currency is always sent together with a monetary value.
  3. The currency is stable: for example KZT, not a mix of KZT/USD without a clear conversion.
  4. value is not equal to principal, balance, or turnover unless that truly is revenue.
  5. event_id or transaction_id makes it possible to find duplicates.
  6. The WebView and the backend do not send the same final action twice.
  7. product_type is drawn from a controlled dictionary.
  8. Events do not carry PII, IIN, phone number, card number, contract number, or sensitive free-text fields.
  9. The event is visible in DebugView/Realtime and then in the BigQuery export.
  10. Google Ads imports exactly the event that should participate in bidding.
  11. Secondary conversions stay for diagnostics; Primary is only for optimization goals.
  12. The conv. value / cost report is reviewed before tROAS is launched.

Be especially careful with banking data. The ad system does not need the contract number, the IIN, the account number, or the raw application payload. For optimization, value, product category, funnel stage, and permitted technical identifiers are enough. Everything else should stay inside the bank's perimeter and BI.

Operating cadence: how to roll this out without chaos

Week 1: taxonomy and dictionaries.

Agree on the lists for product_type, product_id, funnel_stage, value_model, surface. Remove free text. Decide which events are analytics-only and which may become Ads conversions.

Week 2: Firebase events and backend ledger.

Add the early client-side events and the final backend-confirmed events. Check DebugView, BigQuery export, duplicates, and currency. Do not enable tROAS before value is verified.

Week 3: import into Google Ads.

Link Firebase/GA4 to Google Ads, import events, configure Primary/Secondary conversion actions. Keep KYC as an acquisition goal. Prepare product_value or product-specific value events for value bidding.

Week 4-6: value calibration.

Compare the value you sent with the actual product economics. Check whether the algorithm is buying only one product. Check lag: loans and deposits mature longer than payments and services.

After enough history has accumulated: tROAS.

Start with a conservative target ROAS based on historical conv. value / cost, do not change the target every day, and do not split campaigns before they have volume. Most tROAS failures in a SuperApp are not caused by the algorithm but by bad value, duplicated events, or campaigns split too early.

One-line recommendation

For a Halyk-class SuperApp I would not choose between "one campaign for everything" and "a separate campaign per product". I would go hybrid: KYC as a warm-up tCPA, a unified product_value for daily banking and services, separate loans value logic because of risk and margin, deposits split out only when funded volume is enough, and WebView purchases sent as purchase with a commission-based value and strict dedup.

That gives the ad system a signal dense enough to learn from, and at the same time it stops the bank from turning into a black box where registration looks cheap and product economics stay outside of optimization.

Sources

FAQ

Why is optimizing for registration and KYC not enough?

KYC is a floor, not a goal. The business needs transfers, loans, deposits and purchases inside WebView services, and if those events never reach the platform the algorithm optimizes for a milestone that does not pay.

When can app campaigns be switched to tROAS?

After conversion tracking is verified and Firebase/GA4 events import correctly, and only once there is value history. Google's App-campaign guidance warns about cold start — collecting roughly 3–4 weeks of conversion value is a sensible gate. New campaigns start on Maximize conversions or tCPA until volume is stable.

One purchase event for every product, or a separate event per product?

It depends on economics. Loans usually need their own event and campaign because risk, approval and margin behave nothing like payments. Transfers and payments can share a daily-banking value campaign. Deposits split out only when funded volume justifies it — below that, an ad group.