Flutter SDK - Ad Revenue Tracking

Ad Revenue Attribution

Track ad revenue from mediation platforms and attribute it to specific marketing campaigns that brought users to your app, enabling comprehensive ROI analysis.

Ad Revenue Attribution connects mobile app ad revenue to the marketing campaigns that generated users, combining campaign cost, in-app revenue, and ad revenue in unified reporting. This data also flows back to ad networks to optimize campaign performance.

Learn More: See Singular Ad Revenue Attribution FAQ for detailed information on attribution methodology and supported mediation platforms.


How Ad Revenue Attribution Works

Attribution Flow

Your mediation platform reports impression-level or user-level revenue data through callbacks, which you validate and forward to Singular for attribution analysis.

  • Campaign Attribution: Ties ad revenue to the specific campaigns that acquired each user, showing true ROI per campaign
  • Data Sources: Revenue data comes from your mediation platform at either user-level or impression-level granularity
  • Network Optimization: Singular passes revenue data back to ad networks to improve targeting and bidding strategies

Implementation Requirements

Ensure data accuracy before implementing ad revenue tracking, as incorrect revenue data cannot be corrected after transmission.

Critical Requirements:

  • Currency Codes: Use three-letter ISO 4217 codes (USD, EUR, INR). Most mediation platforms use USD by default
  • Data Validation: Always validate revenue values are positive and currency codes are non-empty before sending to Singular
  • Unit Conversion: Some platforms report revenue in micros (1,000,000 = $1.00). Convert to dollars before sending to Singular

Setup Steps

Follow these steps to implement ad revenue attribution with your mediation platform.

  1. Update SDK: Ensure you're running the latest version of the Singular Flutter SDK
  2. Configure Platform: Enable ad revenue reporting in your mediation platform dashboard (AdMob, AppLovin, etc.)
  3. Implement Callbacks: Add revenue event listeners from your mediation SDK to capture impression data
  4. Validate Data: Check revenue > 0 and currency is valid before forwarding to Singular
  5. Test Integration: Verify revenue data appears in Singular reporting within 24 hours

AdMob Integration

Prerequisites

Enable ad revenue reporting in your AdMob account and implement the Google Mobile Ads SDK for Flutter before integrating with Singular.

  • AdMob Setup: Enable impression-level ad revenue in your AdMob dashboard. See AdMob Support
  • Flutter Package: Install google_mobile_ads package. See Getting Started Guide

Platform Differences: AdMob reports revenue differently by platform. Android returns revenue in micros (5000 = $0.005), while iOS returns decimal values (0.005 = $0.005). Convert Android values by dividing by 1,000,000 before sending to Singular.


Implementation Steps

Set up the onPaidEvent callback when loading ads to capture revenue data from successful ad impressions.

  1. Load Ad: Create and load an ad unit (Rewarded, Interstitial, Banner, etc.)
  2. Set Callback: Assign onPaidEvent callback to capture AdValue when ad generates revenue
  3. Convert Units: Divide valueMicros by 1,000,000 to convert to dollars
  4. Validate: Check revenue > 0 and currency is non-empty
  5. Send to Singular: Call Singular.adRevenue() with validated data

Rewarded Ad Example

Capture ad revenue from rewarded video ads by setting the onPaidEvent callback after ad load completes.

Dart
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:singular_flutter_sdk/singular.dart';

const String adUnitId = 'YOUR_AD_UNIT_ID';

class AdManager {
  RewardedAd? _rewardedAd;

  void loadRewardedAd() {
    RewardedAd.load(
      adUnitId: adUnitId,
      request: AdRequest(),
      rewardedAdLoadCallback: RewardedAdLoadCallback(
        onAdLoaded: (RewardedAd ad) {
          _rewardedAd = ad;
          print('Rewarded ad loaded successfully');

          // Set up full screen content callback
          _rewardedAd?.fullScreenContentCallback = FullScreenContentCallback(
            onAdShowedFullScreenContent: () {
              print('Rewarded ad displayed');
            },
            onAdFailedToShowFullScreenContent: (AdError adError) {
              print('Rewarded ad failed to show: ${adError.message}');
            },
            onAdDismissedFullScreenContent: () {
              print('Rewarded ad dismissed');
              _rewardedAd = null;
            },
          );

          // Set up paid event callback for revenue tracking
          _rewardedAd?.onPaidEvent = (AdValue adValue) {
            // Convert revenue from micros to dollars
            double revenue = adValue.valueMicros / 1_000_000.0;
            String? currency = adValue.currencyCode;

            // Validate revenue and currency before sending
            if (revenue > 0 && currency != null && currency.isNotEmpty) {
              final adData = {
                'adPlatform': 'AdMob',
                'currency': currency,
                'revenue': revenue,
              };

              // Send ad revenue data to Singular
              Singular.adRevenue(adData);

              // Log for debugging
              print('Ad Revenue reported: $revenue $currency');
            } else {
              print('Invalid ad revenue: revenue=$revenue, currency=$currency');
            }
          };
        },
        onAdFailedToLoad: (LoadAdError loadAdError) {
          print('Rewarded ad failed to load: ${loadAdError.message}');
        },
      ),
    );
  }

  void showRewardedAd() {
    if (_rewardedAd != null) {
      _rewardedAd!.show(
        onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
          print('User earned reward: ${reward.amount} ${reward.type}');
        },
      );
    } else {
      print('Rewarded ad not ready');
    }
  }
}

Interstitial Ad Example

Track revenue from interstitial ads using the same onPaidEvent pattern.

Dart
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:singular_flutter_sdk/singular.dart';

const String interstitialAdUnitId = 'YOUR_INTERSTITIAL_AD_UNIT_ID';

class InterstitialAdManager {
  InterstitialAd? _interstitialAd;

  void loadInterstitialAd() {
    InterstitialAd.load(
      adUnitId: interstitialAdUnitId,
      request: AdRequest(),
      adLoadCallback: InterstitialAdLoadCallback(
        onAdLoaded: (InterstitialAd ad) {
          _interstitialAd = ad;
          print('Interstitial ad loaded');

          // Set paid event callback
          _interstitialAd?.onPaidEvent = (AdValue adValue) {
            double revenue = adValue.valueMicros / 1_000_000.0;
            String? currency = adValue.currencyCode;

            if (revenue > 0 && currency != null && currency.isNotEmpty) {
              Singular.adRevenue({
                'adPlatform': 'AdMob',
                'currency': currency,
                'revenue': revenue,
              });
              
              print('Interstitial revenue: $revenue $currency');
            }
          };

          // Set full screen callback
          _interstitialAd?.fullScreenContentCallback = FullScreenContentCallback(
            onAdDismissedFullScreenContent: () {
              _interstitialAd = null;
              loadInterstitialAd(); // Load next ad
            },
          );
        },
        onAdFailedToLoad: (LoadAdError error) {
          print('Interstitial ad failed to load: ${error.message}');
        },
      ),
    );
  }

  void showInterstitialAd() {
    _interstitialAd?.show();
  }
}

Banner Ad Example

Track revenue from banner ads displayed in your Flutter UI.

Dart
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:singular_flutter_sdk/singular.dart';

const String bannerAdUnitId = 'YOUR_BANNER_AD_UNIT_ID';

class BannerAdWidget extends StatefulWidget {
  @override
  _BannerAdWidgetState createState() => _BannerAdWidgetState();
}

class _BannerAdWidgetState extends State<BannerAdWidget> {
  BannerAd? _bannerAd;
  bool _isBannerAdReady = false;

  @override
  void initState() {
    super.initState();
    _loadBannerAd();
  }

  void _loadBannerAd() {
    _bannerAd = BannerAd(
      adUnitId: bannerAdUnitId,
      size: AdSize.banner,
      request: AdRequest(),
      listener: BannerAdListener(
        onAdLoaded: (Ad ad) {
          setState(() {
            _isBannerAdReady = true;
          });
          print('Banner ad loaded');

          // Set paid event callback
          (ad as BannerAd).onPaidEvent = (AdValue adValue) {
            double revenue = adValue.valueMicros / 1_000_000.0;
            String? currency = adValue.currencyCode;

            if (revenue > 0 && currency != null && currency.isNotEmpty) {
              Singular.adRevenue({
                'adPlatform': 'AdMob',
                'currency': currency,
                'revenue': revenue,
              });
              
              print('Banner revenue: $revenue $currency');
            }
          };
        },
        onAdFailedToLoad: (Ad ad, LoadAdError error) {
          ad.dispose();
          print('Banner ad failed to load: ${error.message}');
        },
      ),
    );

    _bannerAd?.load();
  }

  @override
  void dispose() {
    _bannerAd?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_isBannerAdReady && _bannerAd != null) {
      return Container(
        alignment: Alignment.center,
        width: _bannerAd!.size.width.toDouble(),
        height: _bannerAd!.size.height.toDouble(),
        child: AdWidget(ad: _bannerAd!),
      );
    }
    return SizedBox.shrink();
  }
}

AppLovin MAX Integration

Prerequisites

Implement the AppLovin MAX Flutter plugin and configure impression-level revenue tracking before integrating with Singular.

  • MAX Setup: Configure your app in the AppLovin MAX dashboard
  • Flutter Plugin: Install applovin_max package. See Getting Started Guide
  • Revenue API: Enable Impression-Level User Revenue API in MAX dashboard settings

Implementation Overview

Set up onAdRevenuePaidCallback listeners for each ad format to capture revenue when impressions generate earnings.

  1. Initialize MAX: Configure AppLovin MAX SDK with your SDK key
  2. Set Listeners: Add onAdRevenuePaidCallback for each ad format
  3. Extract Revenue: Get revenue from ad.revenue property
  4. Currency Handling: AppLovin typically reports in USD, but verify with ad.currency
  5. Forward to Singular: Send validated data to Singular.adRevenue()

Complete MAX Implementation

Capture revenue from all MAX ad formats (Rewarded, Interstitial, Banner, MREC) using a unified handler.

Dart
import 'package:flutter/material.dart';
import 'package:applovin_max/applovin_max.dart';
import 'package:singular_flutter_sdk/singular.dart';

class AppLovinMaxManager extends StatefulWidget {
  @override
  _AppLovinMaxManagerState createState() => _AppLovinMaxManagerState();
}

class _AppLovinMaxManagerState extends State<AppLovinMaxManager> {
  @override
  void initState() {
    super.initState();
    _initializeAppLovinMax();
  }

  Future<void> _initializeAppLovinMax() async {
    // Initialize AppLovin MAX SDK
    await AppLovinMAX.initialize('YOUR_SDK_KEY');

    // Set up revenue listeners for all ad formats
    _setupRewardedAdListeners();
    _setupInterstitialAdListeners();
    _setupBannerAdListeners();
    _setupMRecAdListeners();

    print('AppLovin MAX initialized with revenue tracking');
  }

  void _setupRewardedAdListeners() {
    AppLovinMAX.setRewardedAdListener(RewardedAdListener(
      onAdRevenuePaidCallback: (ad) {
        _handleAdRevenuePaid(ad, 'Rewarded');
      },
      onAdLoadedCallback: (ad) {
        print('Rewarded ad loaded');
      },
      onAdLoadFailedCallback: (adUnitId, error) {
        print('Rewarded ad failed to load: $error');
      },
    ));
  }

  void _setupInterstitialAdListeners() {
    AppLovinMAX.setInterstitialListener(InterstitialListener(
      onAdRevenuePaidCallback: (ad) {
        _handleAdRevenuePaid(ad, 'Interstitial');
      },
      onAdLoadedCallback: (ad) {
        print('Interstitial ad loaded');
      },
      onAdLoadFailedCallback: (adUnitId, error) {
        print('Interstitial ad failed to load: $error');
      },
    ));
  }

  void _setupBannerAdListeners() {
    AppLovinMAX.setBannerListener(AdViewAdListener(
      onAdRevenuePaidCallback: (ad) {
        _handleAdRevenuePaid(ad, 'Banner');
      },
      onAdLoadedCallback: (ad) {
        print('Banner ad loaded');
      },
      onAdLoadFailedCallback: (adUnitId, error) {
        print('Banner ad failed to load: $error');
      },
    ));
  }

  void _setupMRecAdListeners() {
    AppLovinMAX.setMRecListener(AdViewAdListener(
      onAdRevenuePaidCallback: (ad) {
        _handleAdRevenuePaid(ad, 'MREC');
      },
      onAdLoadedCallback: (ad) {
        print('MREC ad loaded');
      },
      onAdLoadFailedCallback: (adUnitId, error) {
        print('MREC ad failed to load: $error');
      },
    ));
  }

  void _handleAdRevenuePaid(Ad ad, String adType) {
    // Extract revenue and currency from ad object
    final double revenueValue = ad.revenue ?? 0.0;
    final String currency = ad.revenueCurrency ?? 'USD';

    // Validate revenue before sending
    if (revenueValue > 0) {
      final adData = {
        'adPlatform': 'AppLovin',
        'currency': currency,
        'revenue': revenueValue,
      };

      // Send ad revenue to Singular
      Singular.adRevenue(adData);

      // Log for debugging
      print('[$adType] Revenue: $revenueValue $currency');
    } else {
      print('[$adType] Invalid revenue: $revenueValue');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AppLovin MAX Revenue Tracking'),
      ),
      body: Center(
        child: Text('MAX revenue tracking active'),
      ),
    );
  }
}

Best Practice: Set up ad revenue listeners during app initialization before loading any ads to ensure no impressions are missed.


IronSource (Unity LevelPlay) Integration

Prerequisites

Enable Impression Level Revenue (ILR) in your IronSource dashboard and implement the IronSource Flutter plugin before integrating with Singular.

  • IronSource Setup: Enable ARM SDK Postbacks Flag in IronSource dashboard
  • Flutter Plugin: Install ironsource_mediation package. See Getting Started Guide
  • ILR API: Configure impression-level revenue tracking for your app

Implementation Example

Use the onImpressionDataSuccess callback to capture impression-level revenue data from IronSource mediation.

Dart
import 'package:ironsource_mediation/ironsource_mediation.dart';
import 'package:singular_flutter_sdk/singular.dart';

void setupIronSourceRevenueTracking() {
  // Set up impression data listener
  IronSource.setImpressionDataListener((ISImpressionData? impressionData) {
    onImpressionDataSuccess(impressionData);
  });
}

void onImpressionDataSuccess(ISImpressionData? impressionData) {
  // Ensure impression data is not null
  if (impressionData == null) {
    print('No impression data available');
    return;
  }

  // Extract and validate revenue
  final revenue = impressionData.revenue?.toDouble() ?? 0.0;
  
  if (revenue <= 0) {
    print('Invalid revenue value: $revenue');
    return;
  }

  // Create ad revenue data for Singular
  final adData = {
    'adPlatform': 'IronSource',
    'currency': 'USD',
    'revenue': revenue,
  };

  // Send to Singular
  Singular.adRevenue(adData);

  // Log for debugging
  print('IronSource Revenue: $revenue USD');
}

TradPlus Integration

Prerequisites

Set up the TradPlus impression delegate to capture eCPM data when ads generate revenue.

  • TradPlus Setup: Configure your app in the TradPlus dashboard
  • Impression Delegate: Enable impression-level tracking in TradPlus settings

eCPM Conversion: TradPlus typically reports eCPM in milli-units. Divide by 1000.0 to convert to dollars before sending to Singular.


Implementation Example

Use the global impression listener to capture revenue data from all TradPlus ad formats.

Dart
import 'package:singular_flutter_sdk/singular.dart';

void setupTradPlusImpressionListener() {
  // Set up global impression listener
  TradPlusSdk.setGlobalImpressionListener((tpAdInfo) {
    if (tpAdInfo == null) {
      print('AdInfo is null');
      return;
    }

    // Ensure eCPM is available
    if (tpAdInfo.ecpm == null) {
      print('eCPM value is null');
      return;
    }

    // Convert eCPM from milli-units to dollars
    double revenue = tpAdInfo.ecpm! / 1000.0;

    // Validate revenue
    if (revenue <= 0) {
      print('Invalid revenue: $revenue');
      return;
    }

    // Create ad revenue data
    final adData = {
      'adPlatform': 'TradPlus',
      'currency': 'USD',
      'revenue': revenue,
    };

    // Send to Singular
    Singular.adRevenue(adData);

    // Log for debugging
    print('TradPlus Revenue: $revenue USD');
  });
}

void main() {
  // Initialize TradPlus SDK
  setupTradPlusImpressionListener();
  
  runApp(MyApp());
}

Generic Integration (Other Platforms)

Custom Implementation

For mediation platforms not explicitly covered above, implement a custom revenue handler using the Singular ad revenue method.

Dart
import 'package:singular_flutter_sdk/singular.dart';

/// Generic function to report ad revenue to Singular
/// Use this for any mediation platform not explicitly supported above
void reportAdRevenue({
  required String adPlatform,
  required String currency,
  required double revenue,
}) {
  // Validate revenue value
  if (revenue <= 0) {
    print('Invalid revenue value: $revenue');
    return;
  }

  // Validate currency code
  if (currency.isEmpty) {
    print('Currency code is empty');
    return;
  }

  // Create ad revenue data
  final adData = {
    'adPlatform': adPlatform,
    'currency': currency,
    'revenue': revenue,
  };

  // Send to Singular
  Singular.adRevenue(adData);

  // Log for debugging
  print('Ad Revenue reported: $revenue $currency from $adPlatform');
}

// Example usage with a custom mediation platform
void onCustomAdImpression(Map<String, dynamic> impressionData) {
  reportAdRevenue(
    adPlatform: 'CustomNetwork',
    currency: impressionData['currency'] ?? 'USD',
    revenue: impressionData['revenue'] ?? 0.0,
  );
}

Best Practices and Troubleshooting

Data Validation

  • Always Validate: Check revenue > 0 and currency is non-empty before sending to Singular
  • Unit Conversion: Verify whether your platform reports in micros or dollars and convert accordingly
  • Currency Codes: Use ISO 4217 three-letter codes (USD, EUR, JPY) consistently
  • Test Thoroughly: Verify revenue data appears in Singular reporting before production release

Common Issues

  • Missing Revenue: Ensure impression-level revenue is enabled in your mediation platform dashboard
  • Incorrect Values: Check unit conversion (micros vs dollars) for your specific platform
  • No Data in Reports: Allow 24-48 hours for data to appear in Singular reporting after implementation
  • Null Revenue: Verify ad callbacks are properly set before ad load completes

Important: Ad revenue data cannot be corrected after transmission. Always validate data accuracy before calling Singular.adRevenue().