# Integration

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Unity SDK</strong><br>Integrate with Metica Unity SDK</td><td></td><td><a href="/files/nJQBwL6cbxulnDLHQGdr">/files/nJQBwL6cbxulnDLHQGdr</a></td><td><a href="/pages/uEUG8A3I3obrj0INCtHa">/pages/uEUG8A3I3obrj0INCtHa</a></td></tr><tr><td><strong>Android SDK</strong><br>Integrate with Metica Android SDK</td><td></td><td><a href="/files/A4uwpIktL4Mk4KgHaEGQ">/files/A4uwpIktL4Mk4KgHaEGQ</a></td><td><a href="/pages/Vi3UTQARSYmWb4EPxUi5">/pages/Vi3UTQARSYmWb4EPxUi5</a></td></tr><tr><td><strong>iOS SDK</strong></td><td>Integrate with Metica iOS SDK</td><td><a href="/files/qbgROwoFNXCdQB06fQ9l">/files/qbgROwoFNXCdQB06fQ9l</a></td><td><a href="/pages/UeWMzRvQ8m4w8l32Th35">/pages/UeWMzRvQ8m4w8l32Th35</a></td></tr><tr><td><strong>REST API</strong><br>Send data to and query Metica</td><td></td><td><a href="/files/XcD9VVPjiAoszdRpKUDH">/files/XcD9VVPjiAoszdRpKUDH</a></td><td><a href="/pages/xlPRTcRMIqKRvMz4Nw6I">/pages/xlPRTcRMIqKRvMz4Nw6I</a></td></tr><tr><td><strong>SmartFloors Verification</strong><br>Verify your SmartFloors integration with Metica </td><td></td><td><a href="/files/CgCUpUbFeRJdJ9yhUy0d">/files/CgCUpUbFeRJdJ9yhUy0d</a></td><td><a href="/pages/2KAguk6QJAXRsH4XSRzb">/pages/2KAguk6QJAXRsH4XSRzb</a></td></tr></tbody></table>


# Get started with SmartFloors

Step-by-step guide for verifying SmartFloors integration with Metica SDK

## Purpose of this Guide

This page outlines the required steps for integrating with the Metica SDK and preparing your build for verification. It also clarifies what Metica will be testing for and how to ensure your integration is accepted quickly.

***

## Before You Begin Integration

Before you start working with the SDK, ensure the following:

* Your game has been registered in the Metica dashboard.
* Relevant ad units (e.g., interstitials, rewarded videos) are set up and configured.
* Metica has access to your MAX account.&#x20;

***

***

## Integration Requirements

Please follow the integration documentation provided by Metica. At a minimum, your app should:

* Call the Metica SDK initialization method early in the app’s lifecycle.
* Use the Metica SDK to load and show ads, not just native platform ad SDKs.
* Ensure your game generates ad impressions that can be reached without complex gating (e.g., ads shown after a short level or via a button).
* Log revenue-related events via the SDK.

***

## Build Requirements for Testing

You only need to prepare and submit **one standard build** of your APK for verification:

* The build should **not** have any hardcoded user IDs.
* It should reflect typical production behavior.

> The build should be stable and ready for testing. Avoid using early-stage development versions.

***

## SDK Integration Test Tool

Before sending your build to Metica, you should use the **SDK Integration Test** tool to validate your integration.

See: [How to Use the SDK Integration Test Tool](/uiguide/sdk-integration-test)

***

***

## What Metica Will Test

Metica will verify the following via log inspection and SDK-level analysis:

* The SDK was successfully initialized.
* Ads were properly requested and shown using the Metica integration.
* Expected SDK events (e.g., ad load, ad shown, ad revenue) were fired.
* Holdout behavior is correctly applied when using the test user ID.

> *Note: We use real devices to verify your builds and check behavior through logs. We are not testing for gameplay quality or user-facing QA.*

***

## Your Role in Testing

While Metica performs the core integration validation, we recommend that you:

* Conduct your own QA process in parallel to test ad placement and ensure no runtime errors.
* Confirm that ads appear in a predictable and testable flow (e.g., end-of-level, button-triggered).
* If possible, provide brief instructions on how to trigger ads in your game to help speed up our review process.

***

## Go-Live Approval

Once the integration has been verified:

* Metica will notify you with a go-ahead to release.
* You can proceed with your rollout according to your preferred deployment strategy (internal QA, staged rollout, etc).

***

## Support

If you have any questions or need help during integration:

* Refer to the official integration documentation provided by Metica.
* Reach out to your Metica point of contact for any clarification.
* Provide updated builds or debug logs if requested during the verification process.


# Android SDK

The Metica Ads SDK for Android enables developers to integrate advertising into Android applications with support for interstitial, rewarded, banner, and MREC ad formats.

## Requirements

* Android API 21 (Android 5.0) or higher
* Kotlin 1.8+
* AppLovinSDK dependency (version 13.0.0 or higher, managed by Metica SDK)
* Valid Metica API credentials (API key, App ID)
* AppLovin SDK Key from [AppLovin Dashboard](https://dash.applovin.com/o/account?r=3#keys) under "SDK Key"

{% hint style="warning" %}
Do not call, reference, or initialize the AppLovin SDK directly in your code. The Metica SDK manages all AppLovin integration internally. Direct usage may cause version conflicts and unexpected behavior.
{% endhint %}

## Download

The Metica Android SDK is distributed via Maven Central. You can find the latest version and version history at:

[**Maven Central - com.metica:metica-sdk**](https://central.sonatype.com/artifact/com.metica/metica-sdk/versions)

## Installation

Add the Metica SDK dependency to your app's `build.gradle.kts` or `build.gradle`

{% code title="build.gradle.kts" %}

```kotlin
dependencies {
    implementation("com.metica:metica-sdk:<version>")
}
```

{% endcode %}

Make sure you have Maven Central in your repositories:

{% code title="repositories" %}

```kotlin
repositories {
    mavenCentral()
}
```

{% endcode %}

## Callback Interfaces

The SDK uses callback interfaces to communicate events back to your application.

### MeticaInitCallback

Triggered when SDK initialization completes.

```kotlin
fun interface MeticaInitCallback {
    fun onInit(initResponse: MeticaInitResponse)
}
```

### MeticaAdsLoadCallback

Handles ad loading events.

```kotlin
interface MeticaAdsLoadCallback {
    fun onAdLoadSuccess(meticaAd: MeticaAd)
    fun onAdLoadFailed(meticaAdError: MeticaAdError)
}
```

### MeticaAdsShowCallback

Manages display lifecycle events for fullscreen ads.

```kotlin
interface MeticaAdsShowCallback {
    fun onAdShowSuccess(meticaAd: MeticaAd)
    fun onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError)
    fun onAdHidden(meticaAd: MeticaAd)
    fun onAdClicked(meticaAd: MeticaAd)
    fun onAdRevenuePaid(meticaAd: MeticaAd)
    fun onAdRewarded(meticaAd: MeticaAd)  // Only called for rewarded ads
}
```

### MeticaAdsAdViewCallback

Handles banner and MREC ad view events. Extends `MeticaAdsLoadCallback`.

```kotlin
interface MeticaAdsAdViewCallback : MeticaAdsLoadCallback {
    fun onAdClicked(meticaAd: MeticaAd)
    fun onAdRevenuePaid(meticaAd: MeticaAd)
}
```

## Privacy Configuration

Set privacy controls **before** initialization for GDPR and CCPA compliance:

{% code title="Privacy settings" %}

```kotlin
import com.metica.ads.MeticaAds

// GDPR - User consent for personalized ads
MeticaAds.PrivacySettings.setHasUserConsent(true, context)

// CCPA - Do Not Sell user data
MeticaAds.PrivacySettings.setDoNotSell(false, context)
```

{% endcode %}

## Supported Ad Formats

| Format       | Enum     | Dimensions        |
| ------------ | -------- | ----------------- |
| Interstitial | `INTER`  | Full-screen       |
| Rewarded     | `REWARD` | Full-screen video |
| Banner       | `BANNER` | 320x50 (adaptive) |
| MREC         | `MREC`   | 300x250           |

***

## SDK Initialization

Initialize the SDK once during application startup, before loading any ads.

{% code title="Initialize Metica SDK" %}

```kotlin
import android.app.Activity
import android.os.Bundle
import com.metica.MeticaInitCallback
import com.metica.MeticaInitConfig
import com.metica.MeticaInitResponse
import com.metica.MeticaSdk
import com.metica.ads.MeticaAds
import com.metica.ads.MeticaMediationInfo
import com.metica.ads.MeticaMediationInfo.MeticaMediationType

class MainActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initializeMeticaSdk()
    }

    private fun initializeMeticaSdk() {
        // Set privacy settings BEFORE initialization
        MeticaAds.PrivacySettings.setHasUserConsent(true, this)
        MeticaAds.PrivacySettings.setDoNotSell(false, this)

        val initConfig = MeticaInitConfig(
            apiKey = "YOUR_API_KEY",
            appId = "YOUR_APP_ID",
            userId = "unique-user-id"  // Optional - SDK generates one if empty
        )

        val mediationInfo = MeticaMediationInfo(
            mediationType = MeticaMediationType.MAX,
            key = "YOUR_APPLOVIN_SDK_KEY"
        )

        MeticaSdk.initialize(
            context = this,
            initConfig = initConfig,
            mediationInfo = mediationInfo,
            callback = object : MeticaInitCallback {
                override fun onInit(initResponse: MeticaInitResponse) {
                    println("Current user is part of ${initResponse.smartFloors?.userGroup}")
                    println("Metica initialization completed")

                    // SDK ready - start loading ads
                    loadAds()
                }
            }
        )
    }

    private fun loadAds() {
        // Load your ads here
    }
}
```

{% endcode %}

### Initialization Response

The `MeticaInitResponse` contains a `smartFloors` object with two properties:

* `userGroup` – The experiment group assigned to the current user: either `TRIAL` or `HOLDOUT`. This determines which cohort the user belongs to for A/B testing purposes.
* `isForcedHoldout` – A boolean indicating whether the current user has been forcefully assigned to the holdout group because of an issue, i.e. for a connection problem.

{% hint style="info" %}
These values are provided solely for analytics and logging. You do not need to implement any fallback logic, the SDK handles group assignment internally regardless of initialization outcome.
{% endhint %}

### Using Kotlin Lambda

```kotlin
MeticaSdk.initialize(
    context = this,
    initConfig = initConfig,
    mediationInfo = mediationInfo
) { initResponse ->
    println("Current user is part of ${initResponse.smartFloors?.userGroup}")
    println("Metica initialization completed")
}
```

***

## Interstitial Ads

Full-screen ads that cover the app interface.

{% code title="InterstitialAdManager.kt" %}

```kotlin
import android.app.Activity
import com.metica.MeticaSdk
import com.metica.ads.MeticaAd
import com.metica.ads.MeticaAdError
import com.metica.ads.MeticaAdsLoadCallback
import com.metica.ads.MeticaAdsShowCallback

class InterstitialAdManager(private val activity: Activity) :
    MeticaAdsLoadCallback, MeticaAdsShowCallback {

    private val adUnitId = "YOUR_INTERSTITIAL_AD_UNIT_ID"
    private var retryAttempt = 0
    private val maxRetryDelay = 64.0

    fun loadAd() {
        MeticaSdk.Ads.loadInterstitial(adUnitId, this)
    }

    fun showAd() {
        if (MeticaSdk.Ads.isInterstitialReady(adUnitId)) {
            MeticaSdk.Ads.showInterstitial(
                activity = activity,
                adUnitId = adUnitId,
                placementId = "main_menu",      // Optional - for analytics
                customData = "level_complete",  // Optional - for analytics
                callback = this
            )
        } else {
            println("Interstitial not ready")
        }
    }

    // Load callbacks
    override fun onAdLoadSuccess(meticaAd: MeticaAd) {
        retryAttempt = 0
        println("Interstitial loaded: ${meticaAd.adUnitId}")
    }

    override fun onAdLoadFailed(meticaAdError: MeticaAdError) {
        println("Interstitial load failed: ${meticaAdError.message}")

        // Exponential backoff retry
        retryAttempt++
        val delaySeconds = minOf(Math.pow(2.0, retryAttempt.toDouble()), maxRetryDelay)

        activity.window.decorView.postDelayed({
            println("Retrying interstitial load in ${delaySeconds}s")
            loadAd()
        }, (delaySeconds * 1000).toLong())
    }

    // Show callbacks
    override fun onAdShowSuccess(meticaAd: MeticaAd) {
        println("Interstitial shown")
    }

    override fun onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError) {
        println("Interstitial show failed: ${meticaAdError.message}")
    }

    override fun onAdHidden(meticaAd: MeticaAd) {
        println("Interstitial hidden")
        // Preload next ad
        loadAd()
    }

    override fun onAdClicked(meticaAd: MeticaAd) {
        println("Interstitial clicked")
    }

    override fun onAdRevenuePaid(meticaAd: MeticaAd) {
        println("Interstitial revenue: ${meticaAd.revenue} from ${meticaAd.networkName}")
    }

    override fun onAdRewarded(meticaAd: MeticaAd) {
        // Not called for interstitials
    }
}
```

{% endcode %}

***

## Rewarded Ads

Full-screen video ads that reward users upon completion.

{% code title="RewardedAdManager.kt" %}

```kotlin
import android.app.Activity
import com.metica.MeticaSdk
import com.metica.ads.MeticaAd
import com.metica.ads.MeticaAdError
import com.metica.ads.MeticaAdsLoadCallback
import com.metica.ads.MeticaAdsShowCallback

class RewardedAdManager(private val activity: Activity) :
    MeticaAdsLoadCallback, MeticaAdsShowCallback {

    private val adUnitId = "YOUR_REWARDED_AD_UNIT_ID"
    private var retryAttempt = 0
    private val maxRetryDelay = 64.0

    fun loadAd() {
        MeticaSdk.Ads.loadRewarded(adUnitId, this)
    }

    fun showAd() {
        if (MeticaSdk.Ads.isRewardedReady(adUnitId)) {
            MeticaSdk.Ads.showRewarded(
                activity = activity,
                adUnitId = adUnitId,
                placementId = "shop",         // Optional - for analytics
                customData = "extra_coins",   // Optional - for analytics
                callback = this
            )
        } else {
            println("Rewarded ad not ready")
        }
    }

    // Load callbacks
    override fun onAdLoadSuccess(meticaAd: MeticaAd) {
        retryAttempt = 0
        println("Rewarded ad loaded")
    }

    override fun onAdLoadFailed(meticaAdError: MeticaAdError) {
        println("Rewarded ad load failed: ${meticaAdError.message}")

        // Exponential backoff retry
        retryAttempt++
        val delaySeconds = minOf(Math.pow(2.0, retryAttempt.toDouble()), maxRetryDelay)

        activity.window.decorView.postDelayed({
            println("Retrying rewarded load in ${delaySeconds}s")
            loadAd()
        }, (delaySeconds * 1000).toLong())
    }

    // Show callbacks
    override fun onAdShowSuccess(meticaAd: MeticaAd) {
        println("Rewarded ad shown")
    }

    override fun onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError) {
        println("Rewarded ad show failed: ${meticaAdError.message}")
    }

    override fun onAdHidden(meticaAd: MeticaAd) {
        println("Rewarded ad hidden")
        // Preload next ad
        loadAd()
    }

    override fun onAdClicked(meticaAd: MeticaAd) {
        println("Rewarded ad clicked")
    }

    override fun onAdRevenuePaid(meticaAd: MeticaAd) {
        println("Rewarded ad revenue: ${meticaAd.revenue} from ${meticaAd.networkName}")
    }

    override fun onAdRewarded(meticaAd: MeticaAd) {
        println("User earned reward!")
        grantRewardToUser()
    }

    private fun grantRewardToUser() {
        // Grant in-game currency, extra lives, etc.
        println("Granting reward to user!")
    }
}
```

{% endcode %}

***

## Banner Ads

Small rectangular ads (320x50) that remain on screen.

{% code title="BannerActivity.kt" %}

```kotlin
import android.app.Activity
import android.os.Bundle
import android.view.Gravity
import android.widget.FrameLayout
import com.metica.MeticaSdk
import com.metica.ads.MeticaAd
import com.metica.ads.MeticaAdError
import com.metica.ads.MeticaAdView
import com.metica.ads.MeticaAdsAdViewCallback

class BannerActivity : Activity(), MeticaAdsAdViewCallback {

    private val adUnitId = "YOUR_BANNER_AD_UNIT_ID"
    private var bannerAdView: MeticaAdView? = null
    private lateinit var bannerContainer: FrameLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Assuming you have a FrameLayout in your layout with id "banner_container"
        bannerContainer = findViewById(R.id.banner_container)
        loadBanner()
    }

    private fun loadBanner() {
        // Clean up existing banner
        bannerAdView?.destroy()
        bannerAdView = null
        bannerContainer.removeAllViews()

        // Create banner ad view
        bannerAdView = MeticaSdk.Ads.createBannerAdView(adUnitId).apply {
            setListener(this@BannerActivity)
            setPlacement("home_screen")  // Optional - for analytics
        }

        // Add to container with layout params
        val layoutParams = FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.WRAP_CONTENT
        ).apply {
            gravity = Gravity.CENTER
        }

        bannerContainer.addView(bannerAdView, layoutParams)

        // Load the ad
        bannerAdView?.load()
    }

    override fun onAdLoadSuccess(meticaAd: MeticaAd) {
        println("Banner loaded: ${meticaAd.adUnitId}")
    }

    override fun onAdLoadFailed(meticaAdError: MeticaAdError) {
        println("Banner load failed: ${meticaAdError.message}")
    }

    override fun onAdClicked(meticaAd: MeticaAd) {
        println("Banner clicked")
    }

    override fun onAdRevenuePaid(meticaAd: MeticaAd) {
        println("Banner revenue: ${meticaAd.revenue}")
    }

    override fun onPause() {
        super.onPause()
        bannerAdView?.stopAutoRefresh()
    }

    override fun onResume() {
        super.onResume()
        bannerAdView?.startAutoRefresh()
    }

    override fun onDestroy() {
        super.onDestroy()
        bannerAdView?.destroy()
    }
}
```

{% endcode %}

### Banner Auto-Refresh Control

```kotlin
// Stop auto-refresh (e.g., when navigating away or during gameplay)
bannerAdView?.stopAutoRefresh()

// Resume auto-refresh
bannerAdView?.startAutoRefresh()

// Manual refresh (after stopping auto-refresh)
bannerAdView?.load()
```

***

## MREC Ads

Medium Rectangle ads (300x250).

{% code title="MrecActivity.kt" %}

```kotlin
import android.app.Activity
import android.os.Bundle
import android.view.Gravity
import android.widget.FrameLayout
import com.metica.MeticaSdk
import com.metica.ads.MeticaAd
import com.metica.ads.MeticaAdError
import com.metica.ads.MeticaAdView
import com.metica.ads.MeticaAdsAdViewCallback

class MrecActivity : Activity(), MeticaAdsAdViewCallback {

    private val adUnitId = "YOUR_MREC_AD_UNIT_ID"
    private var mrecAdView: MeticaAdView? = null
    private lateinit var mrecContainer: FrameLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mrecContainer = findViewById(R.id.mrec_container)
        loadMrec()
    }

    private fun loadMrec() {
        // Clean up existing MREC
        mrecAdView?.destroy()
        mrecAdView = null
        mrecContainer.removeAllViews()

        // Create MREC ad view
        mrecAdView = MeticaSdk.Ads.createMrecAdView(adUnitId).apply {
            setListener(this@MrecActivity)
            setPlacement("game_over_screen")  // Optional - for analytics
        }

        // Add to container - MREC is 300x250
        val layoutParams = FrameLayout.LayoutParams(
            dpToPx(300),
            dpToPx(250)
        ).apply {
            gravity = Gravity.CENTER
        }

        mrecContainer.addView(mrecAdView, layoutParams)

        // Load the ad
        mrecAdView?.load()
    }

    private fun dpToPx(dp: Int): Int {
        return (dp * resources.displayMetrics.density).toInt()
    }

    override fun onAdLoadSuccess(meticaAd: MeticaAd) {
        println("MREC loaded: ${meticaAd.adUnitId}")
    }

    override fun onAdLoadFailed(meticaAdError: MeticaAdError) {
        println("MREC load failed: ${meticaAdError.message}")
    }

    override fun onAdClicked(meticaAd: MeticaAd) {
        println("MREC clicked")
    }

    override fun onAdRevenuePaid(meticaAd: MeticaAd) {
        println("MREC revenue: ${meticaAd.revenue}")
    }

    override fun onPause() {
        super.onPause()
        mrecAdView?.stopAutoRefresh()
    }

    override fun onResume() {
        super.onResume()
        mrecAdView?.startAutoRefresh()
    }

    override fun onDestroy() {
        super.onDestroy()
        mrecAdView?.destroy()
    }
}
```

{% endcode %}

***

## Revenue Tracking

Track ad revenue for analytics and attribution.

{% code title="onAdRevenuePaid example" %}

```kotlin
override fun onAdRevenuePaid(meticaAd: MeticaAd) {
    // Revenue in USD
    val revenue = meticaAd.revenue ?: 0.0
    val networkName = meticaAd.networkName ?: "unknown"
    val adUnitId = meticaAd.adUnitId
    val adFormat = meticaAd.adFormat

    println("Ad revenue: $${"%.4f".format(revenue)}")
    println("Network: $networkName")
    println("Ad Unit: $adUnitId")
    println("Format: $adFormat")

    // Send to your analytics
    trackRevenue(revenue, networkName, adUnitId)
}
```

{% endcode %}

### MeticaAd Properties

| Property       | Type      | Description                               |
| -------------- | --------- | ----------------------------------------- |
| `adUnitId`     | `String`  | Unique identifier for the ad unit         |
| `revenue`      | `Double?` | Revenue in USD                            |
| `networkName`  | `String?` | Ad network that served the ad             |
| `placementTag` | `String?` | Placement identifier (if set)             |
| `adFormat`     | `String`  | Format type (INTER, REWARD, BANNER, MREC) |
| `creativeId`   | `String?` | Creative identifier                       |
| `latency`      | `Long?`   | Load latency in milliseconds              |

***

## AppLovin Consent Utilities

Access AppLovin-specific consent information via `MeticaSdk.Ads.Max`:

```kotlin
// Check if user has provided consent
val hasConsent = MeticaSdk.Ads.Max.hasUserConsent()

// Check if consent status has been set
val isConsentSet = MeticaSdk.Ads.Max.isUserConsentSet()

// Get user's geographical category for consent purposes
val geography = MeticaSdk.Ads.Max.getConsentFlowUserGeography()
```

***

## Debug Logging

Enable logging for development and debugging:

{% code title="Enable Metica logs" %}

```kotlin
import com.metica.internal.util.MeticaLogger

// Enable before initialization
MeticaLogger.enableLogs = true
```

{% endcode %}

Or via ADB:

{% code title="ADB command" %}

```bash
adb shell setprop debug.metica.internal_logging true
```

{% endcode %}

Look for "Metica" tag in Logcat.

***

## Best Practices

{% stepper %}
{% step %}

### Initialize Early

Call `MeticaSdk.initialize()` as early as possible, ideally in `Application.onCreate()` or `MainActivity.onCreate()`.
{% endstep %}

{% step %}

### Set Privacy Before Init

Always configure privacy settings before calling initialize:

```kotlin
MeticaAds.PrivacySettings.setHasUserConsent(hasConsent, context)
MeticaAds.PrivacySettings.setDoNotSell(doNotSell, context)
MeticaSdk.initialize(...)
```

{% endstep %}

{% step %}

### Preload Ads

Load the next ad immediately after showing:

```kotlin
override fun onAdHidden(meticaAd: MeticaAd) {
    loadAd()  // Preload next ad
}
```

{% endstep %}

{% step %}

### Check Readiness Before Showing

```kotlin
if (MeticaSdk.Ads.isInterstitialReady(adUnitId)) {
    MeticaSdk.Ads.showInterstitial(activity, adUnitId, null, null, callback)
}
```

{% endstep %}

{% step %}

### Pass Activity Context

Always pass the current Activity when showing fullscreen ads:

```kotlin
// Correct
MeticaSdk.Ads.showInterstitial(activity, adUnitId, null, null, callback)

// Wrong - don't use application context
MeticaSdk.Ads.showInterstitial(applicationContext, adUnitId, null, null, callback)  // Won't work
```

{% endstep %}

{% step %}

### Clean Up Banner/MREC Views

```kotlin
override fun onDestroy() {
    super.onDestroy()
    bannerAdView?.destroy()
}
```

{% endstep %}

{% step %}

### Handle Lifecycle for Ad Views

```kotlin
override fun onPause() {
    super.onPause()
    bannerAdView?.stopAutoRefresh()
}

override fun onResume() {
    super.onResume()
    bannerAdView?.startAutoRefresh()
}
```

{% endstep %}

{% step %}

### Implement Retry with Backoff

Use exponential backoff for failed loads to avoid overwhelming the network:

```kotlin
val delaySeconds = minOf(Math.pow(2.0, retryAttempt.toDouble()), 64.0)
```

{% endstep %}
{% endstepper %}

***

## Thread Safety

All callbacks are delivered on the same thread that made the original call. If you call from the main thread, callbacks arrive on the main thread - safe for UI updates:

{% code title="Thread safety example" %}

```kotlin
// Called from main thread
MeticaSdk.Ads.loadInterstitial(adUnitId, object : MeticaAdsLoadCallback {
    override fun onAdLoadSuccess(meticaAd: MeticaAd) {
        // Safe to update UI directly
        showButton.isEnabled = true
    }

    override fun onAdLoadFailed(meticaAdError: MeticaAdError) {
        // Safe to update UI directly
        statusText.text = "Load failed: ${meticaAdError.message}"
    }
})
```

{% endcode %}

***

## Complete Example

A complete activity demonstrating all features.

{% code title="CompleteExampleActivity.kt" %}

```kotlin
import android.app.Activity
import android.os.Bundle
import android.view.Gravity
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import com.metica.MeticaInitCallback
import com.metica.MeticaInitConfig
import com.metica.MeticaInitResponse
import com.metica.MeticaSdk
import com.metica.ads.*

class CompleteExampleActivity : Activity(),
    MeticaAdsLoadCallback, MeticaAdsShowCallback, MeticaAdsAdViewCallback {

    private val interstitialAdUnitId = "YOUR_INTERSTITIAL_AD_UNIT_ID"
    private val rewardedAdUnitId = "YOUR_REWARDED_AD_UNIT_ID"
    private val bannerAdUnitId = "YOUR_BANNER_AD_UNIT_ID"

    private var bannerAdView: MeticaAdView? = null
    private lateinit var statusText: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupUI()
        initializeSdk()
    }

    private fun setupUI() {
        val layout = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            gravity = Gravity.CENTER_HORIZONTAL
            setPadding(32, 32, 32, 32)
        }

        statusText = TextView(this).apply {
            text = "Initializing..."
            textSize = 16f
        }
        layout.addView(statusText)

        Button(this).apply {
            text = "Load Interstitial"
            setOnClickListener { loadInterstitial() }
        }.also { layout.addView(it) }

        Button(this).apply {
            text = "Show Interstitial"
            setOnClickListener { showInterstitial() }
        }.also { layout.addView(it) }

        Button(this).apply {
            text = "Load Rewarded"
            setOnClickListener { loadRewarded() }
        }.also { layout.addView(it) }

        Button(this).apply {
            text = "Show Rewarded"
            setOnClickListener { showRewarded() }
        }.also { layout.addView(it) }

        // Banner container at bottom
        val bannerContainer = FrameLayout(this).apply {
            id = android.R.id.content + 1000
            layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
            ).apply { weight = 1f }
        }
        layout.addView(bannerContainer)

        setContentView(layout)
    }

    private fun initializeSdk() {
        // Privacy settings first
        MeticaAds.PrivacySettings.setHasUserConsent(true, this)
        MeticaAds.PrivacySettings.setDoNotSell(false, this)

        MeticaSdk.initialize(
            context = this,
            initConfig = MeticaInitConfig(
                apiKey = "YOUR_API_KEY",
                appId = "YOUR_APP_ID",
                userId = ""
            ),
            mediationInfo = MeticaMediationInfo(
                mediationType = MeticaMediationInfo.MeticaMediationType.MAX,
                key = "YOUR_APPLOVIN_SDK_KEY"
            )
        ) { response ->
            // For analytics/logging only - no action needed
            println("Current user is part of ${response.smartFloors?.userGroup}")
            println("Metica initialization completed")

            statusText.text = "SDK Initialized"

            // Preload ads
            loadInterstitial()
            loadRewarded()
            loadBanner()
        }
    }

    private fun loadInterstitial() {
        MeticaSdk.Ads.loadInterstitial(interstitialAdUnitId, this)
    }

    private fun showInterstitial() {
        if (MeticaSdk.Ads.isInterstitialReady(interstitialAdUnitId)) {
            MeticaSdk.Ads.showInterstitial(this, interstitialAdUnitId, null, null, this)
        }
    }

    private fun loadRewarded() {
        MeticaSdk.Ads.loadRewarded(rewardedAdUnitId, this)
    }

    private fun showRewarded() {
        if (MeticaSdk.Ads.isRewardedReady(rewardedAdUnitId)) {
            MeticaSdk.Ads.showRewarded(this, rewardedAdUnitId, null, null, this)
        }
    }

    private fun loadBanner() {
        bannerAdView?.destroy()
        bannerAdView = MeticaSdk.Ads.createBannerAdView(bannerAdUnitId).apply {
            setListener(this@CompleteExampleActivity)
        }
        // Add to your banner container and call load()
        bannerAdView?.load()
    }

    // Callbacks
    override fun onAdLoadSuccess(meticaAd: MeticaAd) {
        statusText.text = "${meticaAd.adFormat} loaded"
    }

    override fun onAdLoadFailed(meticaAdError: MeticaAdError) {
        statusText.text = "Load failed: ${meticaAdError.message}"
    }

    override fun onAdShowSuccess(meticaAd: MeticaAd) {
        statusText.text = "${meticaAd.adFormat} shown"
    }

    override fun onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError) {
        statusText.text = "Show failed: ${meticaAdError.message}"
    }

    override fun onAdHidden(meticaAd: MeticaAd) {
        // Preload next ad
        when (meticaAd.adFormat) {
            "INTER" -> loadInterstitial()
            "REWARD" -> loadRewarded()
        }
    }

    override fun onAdClicked(meticaAd: MeticaAd) {}
    override fun onAdRevenuePaid(meticaAd: MeticaAd) {}
    override fun onAdRewarded(meticaAd: MeticaAd) {
        statusText.text = "User earned reward!"
    }

    override fun onDestroy() {
        super.onDestroy()
        bannerAdView?.destroy()
    }
}
```

{% endcode %}

***

## API Reference

### MeticaSdk

| Method                                                     | Description                |
| ---------------------------------------------------------- | -------------------------- |
| `initialize(context, initConfig, mediationInfo, callback)` | Initialize the SDK         |
| `Ads`                                                      | Access to ad functionality |

### MeticaSdk.Ads

| Method                                                                      | Description                    |
| --------------------------------------------------------------------------- | ------------------------------ |
| `loadInterstitial(adUnitId, callback)`                                      | Load an interstitial ad        |
| `showInterstitial(activity, adUnitId, placementId?, customData?, callback)` | Show an interstitial ad        |
| `isInterstitialReady(adUnitId): Boolean`                                    | Check if interstitial is ready |
| `loadRewarded(adUnitId, callback)`                                          | Load a rewarded ad             |
| `showRewarded(activity, adUnitId, placementId?, customData?, callback)`     | Show a rewarded ad             |
| `isRewardedReady(adUnitId): Boolean`                                        | Check if rewarded is ready     |
| `createBannerAdView(adUnitId): MeticaAdView`                                | Create a banner ad view        |
| `createMrecAdView(adUnitId): MeticaAdView`                                  | Create an MREC ad view         |
| `Max`                                                                       | AppLovin-specific functions    |

### MeticaAds.PrivacySettings

| Method                                  | Description          |
| --------------------------------------- | -------------------- |
| `setHasUserConsent(isConsent, context)` | Set GDPR consent     |
| `setDoNotSell(isDoNotSell, context)`    | Set CCPA do-not-sell |

### MeticaAdView

| Method                     | Description                    |
| -------------------------- | ------------------------------ |
| `setListener(callback)`    | Set the callback for ad events |
| `setPlacement(placement?)` | Set placement for analytics    |
| `load()`                   | Load the ad                    |
| `startAutoRefresh()`       | Start auto-refresh             |
| `stopAutoRefresh()`        | Stop auto-refresh              |
| `destroy()`                | Clean up resources             |


# iOS SDK

The Metica Ads SDK for iOS provides a simple, efficient way to integrate Metica's advertising functionality into your iOS applications. The SDK supports interstitial, rewarded, banner, and MREC ad formats with built-in A/B testing capabilities through Smart Floors.

## Requirements

* iOS 15.0 or higher
* Swift 5.0+
* Xcode 16.2+
* AppLovinSDK dependency (version 13.0.0 or higher)
* Valid Metica API credentials (API key, App ID)

{% hint style="warning" %}
**CRITICAL REQUIREMENT**

You must initialize the Metica SDK before invoking any ad methods. The Metica SDK handles AppLovin SDK initialization internally, so you should NOT initialize AppLovin SDK separately.
{% endhint %}

## Callback Interfaces

### MeticaInitCallback

Callback interface for SDK initialization events.

```swift
public protocol MeticaInitCallback {
    func onInit(initResponse: MeticaInitResponse)
}
```

| Method                  | Description                              |
| ----------------------- | ---------------------------------------- |
| `onInit(initResponse:)` | Called when the SDK has been initialized |

### MeticaAdsLoadCallback

Callback interface for ad loading events.

```swift
public protocol MeticaAdsLoadCallback {
    func onAdLoadSuccess(meticaAd: MeticaAd)
    func onAdLoadFailed(meticaAdError: MeticaAdError)
}
```

| Method                           | Description                                    |
| -------------------------------- | ---------------------------------------------- |
| `onAdLoadSuccess(meticaAd:)`     | Called when an ad has been successfully loaded |
| `onAdLoadFailed(meticaAdError:)` | Called when an ad load has failed              |

### MeticaAdsShowCallback

Callback interface for ad display events.

```swift
public protocol MeticaAdsShowCallback {
    func onAdShowSuccess(meticaAd: MeticaAd)
    func onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError)
    func onAdHidden(meticaAd: MeticaAd)
    func onAdClicked(meticaAd: MeticaAd)
    func onAdRevenuePaid(meticaAd: MeticaAd)
    func onAdRewarded(meticaAd: MeticaAd)
}
```

| Method                                    | Description                                        |
| ----------------------------------------- | -------------------------------------------------- |
| `onAdShowSuccess(meticaAd:)`              | Called when an ad has been shown successfully      |
| `onAdShowFailed(meticaAd:meticaAdError:)` | Called when an ad failed to show                   |
| `onAdHidden(meticaAd:)`                   | Called when an ad has been dismissed               |
| `onAdClicked(meticaAd:)`                  | Called when the user clicks an ad                  |
| `onAdRevenuePaid(meticaAd:)`              | Called when revenue has been attributed            |
| `onAdRewarded(meticaAd:)`                 | Called when a reward is earned (rewarded ads only) |

### MeticaAdsBannerCallback

Callback interface for banner/MREC ad events. Extends `MeticaAdsLoadCallback`.

```swift
public protocol MeticaAdsBannerCallback: MeticaAdsLoadCallback {
    func onAdClicked(meticaAd: MeticaAd)
    func onAdRevenuePaid(meticaAd: MeticaAd)
}
```

***

## Privacy Settings

Set privacy settings **before** calling `MeticaSdk.initialize()`:

```swift
// Set user consent for personalized ads
MeticaSdk.privacySettings.setHasUserConsent(isConsent: true)

// Set "Do Not Sell" flag for CCPA compliance
MeticaSdk.privacySettings.setDoNotSell(isDoNotSell: false)
```

***

## Ad Formats

| Format       | Enum Value  | Description                              |
| ------------ | ----------- | ---------------------------------------- |
| Interstitial | `.INTER`    | Full-screen ads that cover the interface |
| Rewarded     | `.REWARDED` | Video ads that reward users for watching |
| Banner       | `.BANNER`   | Adaptive banner ads (320x50)             |
| MREC         | `.MREC`     | Medium rectangle ads (300x250)           |

***

## Code Examples

### SDK Initialization

```swift
import MeticaSDK

class AppViewModel: ObservableObject, MeticaInitCallback {
    @Published var isInitialized = false
    @Published var userGroup = ""

    @MainActor
    func initializeSDK() {
        // Enable logging for development
        MeticaLogger.enableLogs = true

        // Set privacy settings BEFORE initialization
        MeticaSdk.privacySettings.setHasUserConsent(isConsent: true)
        MeticaSdk.privacySettings.setDoNotSell(isDoNotSell: false)

        // Create configuration
        let initConfig = MeticaInitConfig(
            apiKey: "YOUR_API_KEY",
            appId: "YOUR_APP_ID",
            userId: "" // Leave empty for auto-generated stable ID
        )

        let mediationInfo = MeticaMediationInfo(
            mediationType: .max,
            key: "YOUR_APPLOVIN_SDK_KEY"
        )

        // Initialize the SDK
        MeticaSdk.initialize(
            initConfig: initConfig,
            mediationInfo: mediationInfo,
            callback: self
        )
    }

    // MARK: - MeticaInitCallback

    func onInit(initResponse: MeticaInitResponse) {
        DispatchQueue.main.async {
            self.isInitialized = true
            self.userGroup = initResponse.smartFloors.userGroup == .trial ? "trial" : "holdout"

            if initResponse.smartFloors.isSuccess {
                print("Metica SDK initialized successfully")
            } else {
                print("Metica SDK initialized in fallback mode")
            }
        }
    }
}
```

### Interstitial Ads

```swift
class InterstitialAdManager: MeticaAdsLoadCallback, MeticaAdsShowCallback {
    let adUnitId = "YOUR_INTERSTITIAL_AD_UNIT_ID"
    private var retryAttempt: Int = 0
    private let maxRetryDelay: Double = 64.0

    func loadAd() {
        MeticaSdk.Ads.loadInterstitial(adUnitId: adUnitId, callback: self)
    }

    func showAd() {
        if MeticaSdk.Ads.isInterstitialReady(adUnitId: adUnitId) {
            MeticaSdk.Ads.showInterstitial(adUnitId: adUnitId, callback: self)
        } else {
            print("Interstitial not ready")
        }
    }

    // MARK: - MeticaAdsLoadCallback

    func onAdLoadSuccess(meticaAd: MeticaAd) {
        // Reset retry state on success
        retryAttempt = 0
        print("Interstitial loaded: \(meticaAd.adUnitId)")
    }

    func onAdLoadFailed(meticaAdError: MeticaAdError) {
        print("Interstitial load failed: \(meticaAdError.message)")
        
        // Exponential backoff retry (2s, 4s, 8s, ... capped at 64s)
        retryAttempt += 1
        let delaySeconds = min(pow(2.0, Double(retryAttempt)), maxRetryDelay)

        DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { [weak self] in
            guard let self = self else { return }
            print("Retrying interstitial load in \(delaySeconds)s")
            self.loadAd()
        }
    }

    // MARK: - MeticaAdsShowCallback

    func onAdShowSuccess(meticaAd: MeticaAd) {
        print("Interstitial shown")
    }

    func onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError) {
        print("Interstitial show failed: \(meticaAdError.message)")
    }

    func onAdHidden(meticaAd: MeticaAd) {
        print("Interstitial hidden")
        self.loadAd()
    }

    func onAdClicked(meticaAd: MeticaAd) {
        print("Interstitial clicked")
    }

    func onAdRevenuePaid(meticaAd: MeticaAd) {
        print("Interstitial revenue: \(meticaAd.revenue)")
    }

    func onAdRewarded(meticaAd: MeticaAd) {
        // Not called for interstitial ads
    }
}
```

### Rewarded Ads

```swift
class RewardedAdManager: MeticaAdsLoadCallback, MeticaAdsShowCallback {
    let adUnitId = "YOUR_REWARDED_AD_UNIT_ID"
    private var retryAttempt: Int = 0
    private let maxRetryDelay: Double = 64.0

    func loadAd() {
        MeticaSdk.Ads.loadRewarded(adUnitId: adUnitId, callback: self)
    }

    func showAd() {
        if MeticaSdk.Ads.isRewardedReady(adUnitId: adUnitId) {
            MeticaSdk.Ads.showRewarded(adUnitId: adUnitId, callback: self)
        } else {
            print("Rewarded ad not ready")
        }
    }

    // MARK: - MeticaAdsLoadCallback

    func onAdLoadSuccess(meticaAd: MeticaAd) {
        retryAttempt = 0
        print("Rewarded ad loaded")
    }

    func onAdLoadFailed(meticaAdError: MeticaAdError) {
        print("Rewarded ad load failed: \(meticaAdError.message)")
        
        // Exponential backoff retry (2s, 4s, 8s, ... capped at 64s)
        retryAttempt += 1
        let delaySeconds = min(pow(2.0, Double(retryAttempt)), maxRetryDelay)

        DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { [weak self] in
            guard let self = self else { return }
            print("Retrying rewarded load in \(delaySeconds)s")
            self.loadAd()
        }
    }

    // MARK: - MeticaAdsShowCallback

    func onAdShowSuccess(meticaAd: MeticaAd) {
        print("Rewarded ad shown")
    }

    func onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError) {
        print("Rewarded ad show failed: \(meticaAdError.message)")
    }

    func onAdHidden(meticaAd: MeticaAd) {
        print("Rewarded ad hidden")
        self.loadAd()
    }

    func onAdClicked(meticaAd: MeticaAd) {
        print("Rewarded ad clicked")
    }

    func onAdRevenuePaid(meticaAd: MeticaAd) {
        print("Rewarded ad revenue: \(meticaAd.revenue)")
    }

    func onAdRewarded(meticaAd: MeticaAd) {
        print("User earned reward!")
        // Grant reward to user here
    }
}
```

### Banner/MREC Ads

```swift
import SwiftUI
import UIKit
import MeticaSDK

// SwiftUI wrapper for banner ads
struct BannerAdView: UIViewRepresentable {
    let adUnitId: String
    let adFormat: MeticaAdFormatType
    let callback: MeticaAdsBannerCallback

    func makeUIView(context: Context) -> UIView {
        let container = BannerContainerView()
        container.loadAd(adUnitId: adUnitId, format: adFormat, callback: callback)
        return container
    }

    func updateUIView(_ uiView: UIView, context: Context) {}

    static func dismantleUIView(_ uiView: UIView, coordinator: ()) {
        (uiView as? BannerContainerView)?.cleanup()
    }
}

class BannerContainerView: UIView {
    private var currentAdView: MeticaAdView?

    func loadAd(adUnitId: String, format: MeticaAdFormatType, callback: MeticaAdsBannerCallback) {
        Task { @MainActor in
            let adView = await MeticaSdk.Ads.createBannerOrMrecAdView(
                adUnitId: adUnitId,
                adFormat: format
            )
            currentAdView = adView

            if let uiView = adView as? UIView {
                addSubview(uiView)
                uiView.translatesAutoresizingMaskIntoConstraints = false

                // Add constraints based on format
                if format == .BANNER {
                    NSLayoutConstraint.activate([
                        uiView.leadingAnchor.constraint(equalTo: leadingAnchor),
                        uiView.trailingAnchor.constraint(equalTo: trailingAnchor),
                        uiView.topAnchor.constraint(equalTo: topAnchor),
                        uiView.heightAnchor.constraint(equalToConstant: 50)
                    ])
                } else if format == .MREC {
                    NSLayoutConstraint.activate([
                        uiView.centerXAnchor.constraint(equalTo: centerXAnchor),
                        uiView.centerYAnchor.constraint(equalTo: centerYAnchor),
                        uiView.widthAnchor.constraint(equalToConstant: 300),
                        uiView.heightAnchor.constraint(equalToConstant: 250)
                    ])
                }
            }

            adView.setListener(callback: callback)
            adView.load()
        }
    }

    func cleanup() {
        currentAdView?.destroy()
        currentAdView = nil
    }
}

// Banner callback implementation
class BannerAdCallback: MeticaAdsBannerCallback {
    func onAdLoadSuccess(meticaAd: MeticaAd) {
        print("Banner loaded")
    }

    func onAdLoadFailed(meticaAdError: MeticaAdError) {
        print("Banner load failed: \(meticaAdError.message)")
    }

    func onAdClicked(meticaAd: MeticaAd) {
        print("Banner clicked")
    }

    func onAdRevenuePaid(meticaAd: MeticaAd) {
        print("Banner revenue: \(meticaAd.revenue)")
    }
}
```

***

## Best Practices

1. **Initialize on Main Thread**: Always call `MeticaSdk.initialize()` from the main thread using `@MainActor`.
2. **Set Privacy Settings First**: Configure privacy settings before calling `initialize()` to ensure proper propagation to ad networks.
3. **Enable Debug Logging**: During development, enable logging for easier debugging:

   ```swift
   MeticaLogger.enableLogs = true
   ```
4. **Check Ad Readiness**: Always verify ads are ready before showing:

   ```swift
   if MeticaSdk.Ads.isInterstitialReady(adUnitId: adUnitId) {
       MeticaSdk.Ads.showInterstitial(adUnitId: adUnitId, callback: self)
   }
   ```
5. **Clean Up Banner/MREC Views**: Call `destroy()` on banner and MREC ad views when they're no longer needed to free resources.
6. **Handle All Callbacks**: Implement both success and failure callbacks to handle all scenarios gracefully.
7. **Thread Safety**: All SDK methods are thread-safe. Callbacks are delivered on the main thread for UI updates.

***

## Smart Floors / User Groups

The SDK uses Smart Floors for A/B testing optimization. After initialization, users are assigned to one of two groups:

| User Group | isSuccess | Description                                                 |
| ---------- | --------- | ----------------------------------------------------------- |
| `trial`    | `true`    | Metica optimization is enabled for this user                |
| `holdout`  | `true`    | User is in the control group (no optimization)              |
| `holdout`  | `false`   | Initialization failed; user defaults to holdout as fallback |

Access the user's group after initialization:

```swift
func onInit(initResponse: MeticaInitResponse) {
    let userGroup = initResponse.smartFloors.userGroup // .trial or .holdout
    let isSuccess = initResponse.smartFloors.isSuccess

    if userGroup == .trial && isSuccess {
        print("User has Metica optimization enabled")
    } else if userGroup == .holdout && isSuccess {
        print("User is in control group")
    } else {
        print("Initialization failed, running in fallback mode")
    }
}
```


# Unity SDK


# Unity SDK 2

## What you need <a href="#what-you-need" id="what-you-need"></a>

To use the Metica Unity SDK you need:

* An `API key`, obtainable in the Metica platform
* An `appId`, obtainable in the Metica platform.
* A `Max SDK Key`, obtainable from Applovin platfom

## Dependencies

Import the AppLovin Unity Plugin into your project by following the guide below.

Only the Import the **AppLovin MAX Unity plugin** step is required, the Metica SDK handles everything else. The minimum version of MAX plugin that Metica requires is **8.2.0**.

{% embed url="<https://support.axon.ai/en/max/unity/overview/integration#import-the-applovin-max-unity-plugin>" %}

## Supported Ad Formats

The Metica SDK supports the following ad formats:

| Format           | Description                                                      |
| ---------------- | ---------------------------------------------------------------- |
| **Banner**       | Standard banner ads displayed at the top or bottom of the screen |
| **MREC**         | Medium Rectangle ads (300x250)                                   |
| **Interstitial** | Full-screen ads shown between content transitions                |
| **Rewarded**     | Video ads that reward users for watching                         |

{% hint style="info" %}
[#app-open-a-ds-not-natively-supported](#app-open-a-ds-not-natively-supported "mention"). See App Open Ads for a workaround using MAX SDK directly.
{% endhint %}

## Unity 2022 Android Build Configuration

Unity 2022 ships with an older Android Gradle Plugin (AGP) and Gradle version that are incompatible with the Metica SDK's dependencies. To build for Android, you must upgrade these tools manually.

{% hint style="info" %}
Unity 6 users: No additional configuration is needed — Unity 6 ships with AGP 8.4.0+, Gradle 8.6+, and JDK 17 by default.
{% endhint %}

### Prerequisites

* **JDK 17** or later installed on your machine. You can download [Aws Corretto](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/downloads-list.html) or [Adoptium](https://adoptium.net/).
* **Gradle 8.6** or later. Download it [here](https://services.gradle.org/distributions/gradle-8.6-bin.zip) and extract it to a known location.

{% stepper %}
{% step %}

### Step 1: Configure External Gradle

Unity 2022.3 bundles Gradle 7.5.1, which is too old. You need to point Unity to an external Gradle 8.6+ installation.

1. In Unity, go to **Preferences > External Tools** (macOS) or **Edit > Preferences > External Tools** (Windows)
2. Uncheck **"Gradle Installed with Unity (recommended)"**
3. Set the path to your Gradle 8.6 installation directory
   {% endstep %}

{% step %}

### Step 2: Update `baseProjectTemplate.gradle`

Enable the **Custom Base Gradle Template** in **Project Settings > Player > Android > Publishing Settings > Build**, then update `Assets/Plugins/Android/baseProjectTemplate.gradle`:

```javascript
buildscript {
    dependencies {
        classpath 'com.android.tools.build:gradle:8.4.0'
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22'
    }
}
plugins {
    id 'com.android.application' version '8.4.0' apply false
    id 'com.android.library' version '8.4.0' apply false
    **BUILD_SCRIPT_DEPS**
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
```

{% endstep %}

{% step %}

### Step 3: Configure JDK 17 for Gradle

Enable the **Custom Gradle Properties Template** in **Project Settings > Player > Android > Publishing Settings > Build**, then add the following line to `Assets/Plugins/Android/gradleTemplate.properties`:

```properties
org.gradle.java.home=<PATH_TO_JDK_17>
```

Replace `<PATH_TO_JDK_17>` with the absolute path to your JDK 17 installation

{% hint style="warning" %}
Do not use a relative path or environment variable, Gradle requires an absolute path to the JDK installation directory.
{% endhint %}
{% endstep %}
{% endstepper %}

## Installation <a href="#installation" id="installation"></a>

In Unity, you need to import a custom package stored here:

{% embed url="<https://github.com/meticalabs/metica-unity-package>" %}

{% hint style="info" %}
Before installing any new Metica `.unitypackage` , please remove any old Metica versions from your `Assets`
{% endhint %}

In Unity Editor, click on `Assets` -> `Import Package` -> `Custom Package` and load the `.unitypackage` downloaded from our repository

{% hint style="danger" %}
Do *not* call, reference, or initialize the Max SDK library (AppLovin MAX) directly in your code. Metica’s SDK already packages and manages the Max SDK integration internally. If you attempt to use MaxSDK directly, you may cause version conflicts, duplicate initialization, and unexpected behaviour in Metica’s ad logic. Only exception is related to [#app-open-a-ds-not-natively-supported](#app-open-a-ds-not-natively-supported "mention")
{% endhint %}

## Initialization

Initialize the SDK using your credentials and mediation configuration.

In `MeticaInitConfig`, `API_KEY` and `APP_ID` are mandatory. The `USER_ID` is optional, if you don't provide one (or pass an empty string), the Metica SDK will generate a unique UUID per player for you.

In `MeticaMediationInfo` provide the `Max SDK Key`.

The SDK provides two ways to initialize: a **callback-based** approach and an **async** approach. Both return a `MeticaInitResponse` object with the following properties:

* `SmartFloors.UserGroup` – the experiment group assigned to the current player (`holdout` or `trial`).
* `SmartFloors.isForcedHoldout` – a boolean indicating if the current player has been forcefully assigned to holdout, this can happen for several reasons, i.e. when there is a connection issue.
* `UserId` – the user ID for the current player. If you did not provide a `USER_ID` during initialization, this will contain the UUID that the SDK generated automatically.

{% hint style="info" %}
The initialization can take up to `30 seconds` to be completed.
{% endhint %}

{% hint style="danger" %}
`SmartFloors.UserGroup` and `SmartFloors.isForcedHoldout` are provided **for analytics and logging purposes only**. You do not need to branch your code or implement any fallback logic based on these values — the SDK handles all experiment assignment and holdout behavior internally.
{% endhint %}

{% hint style="danger" %}
**Call `Initialize` (or `InitializeAsync`) exactly once per app session.**

The SDK is not designed to be re-initialized. Calling it more than once, including as a retry if the callback is slow, may lead to undefined behavior, including missed or null callbacks. If you need to react to initialization completion in multiple places, cache the `MeticaInitResponse` from the single call and re-use it.
{% endhint %}

### Callback-based Initialization

Use `MeticaSdk.Initialize` with a callback to be notified when initialization completes. This approach works well in standard Unity `MonoBehaviour` lifecycles without requiring `async/await`.

```csharp
using Metica;
using Metica.Ads;
using UnityEngine;

public class MyGameManager : MonoBehaviour
{
    void Start()
    {
        // Set privacy preferences before initialization
        MeticaSdk.Ads.SetHasUserConsent(true);
        MeticaSdk.Ads.SetDoNotSell(false);

        MeticaSdk.SetLogEnabled(true);
        
        // Matches MAX's default: fullscreen revenue on native thread; banner/MREC on Unity main.
        MeticaAds.RevenueCallbackDelivery = CallbackDelivery.NativeThread;

        var config = new MeticaInitConfig("YOUR_API_KEY", "YOUR_APP_ID", "user-id");
        var mediationInfo = new MeticaMediationInfo(MeticaMediationType.MAX, "YOUR_MAX_SDK_KEY");

        // Initialize SDK with callback
        MeticaSdk.Initialize(config, mediationInfo, OnInitialized);
    }

    private void OnInitialized(MeticaInitResponse initResponse)
    {
        Debug.Log($"Current user is part of {initResponse.SmartFloors.UserGroup}");

        // This information is useful only for your analytics
        Debug.Log($"Metica forced holdout: {initResponse.SmartFloors.isForcedHoldout}");
        
        Debug.Log($"User ID: {initResponse.UserId}");

        Debug.Log("Metica SDK initialized successfully");

        // Now you can load ads
    }
}
```

### Async Initialization

Alternatively, you can use the async version if your codebase uses `async/await`:

```cs
using Metica;
using Metica.Ads;
using UnityEngine;

public class MyGameManager : MonoBehaviour
{
    void Start()
    {
        InitializeMetica();
    }

    async void InitializeMetica()
    {
        var config = new MeticaInitConfig("YOUR_API_KEY", "YOUR_APP_ID", "user-id");
        var mediationInfo = new MeticaMediationInfo(MeticaMediationType.MAX, "YOUR_MAX_SDK_KEY");

        // Set privacy preferences before initialization
        MeticaSdk.Ads.SetHasUserConsent(true);
        MeticaSdk.Ads.SetDoNotSell(false);

        // Matches MAX's default: fullscreen revenue on native thread; banner/MREC on Unity main.
        MeticaAds.RevenueCallbackDelivery = CallbackDelivery.NativeThread;
        
        // Initialize SDK
        var initResponse = await MeticaSdk.InitializeAsync(config, mediationInfo);

        Debug.Log($"Current user is part of {initResponse.SmartFloors.UserGroup}");

        // This information is useful only for your analytics
        Debug.Log($"Metica initialization completed: {initResponse.SmartFloors.isForcedHoldout}");
        
        Debug.Log($"User ID: {initResponse.UserId}");

        Debug.Log("Metica SDK initialized successfully");

        // Now you can load ads
    }
}
```

## **Ad implementation**

The SDK supports `Banner`, `MREC`, `Interstitial` and `Rewarded` ads. All ad operations are accessed via `MeticaSdk.Ads`.

### Banner Ads

Standard banner ads with auto-refresh functionality.

```csharp
using Metica;
using Metica.Ads;
using UnityEngine;

public class BannerExample : MonoBehaviour
{
    private const string BannerAdUnitId = "YOUR_BANNER_AD_UNIT_ID";

    void Start()
    {
        // Subscribe to callbacks
        MeticaAdsCallbacks.Banner.OnAdLoadSuccess += OnBannerLoaded;
        MeticaAdsCallbacks.Banner.OnAdLoadFailed += OnBannerLoadFailed;
        MeticaAdsCallbacks.Banner.OnAdClicked += OnBannerClicked;
        MeticaAdsCallbacks.Banner.OnAdRevenuePaid += OnBannerRevenuePaid;

        // Create and show banner
        var config = new MeticaAdViewConfiguration(MeticaAdViewPosition.BottomCenter);
        MeticaSdk.Ads.CreateBanner(BannerAdUnitId, config);
        MeticaSdk.Ads.ShowBanner(BannerAdUnitId);
    }

    void OnApplicationFocus(bool hasFocus)
    {
        if (hasFocus)
        {
            MeticaSdk.Ads.ShowBanner(BannerAdUnitId);
        }
        else
        {
            MeticaSdk.Ads.HideBanner(BannerAdUnitId);
        }
    }

    private void OnBannerLoaded(MeticaAd ad)
    {
        Debug.Log($"Banner loaded: {ad.adUnitId}");
    }

    private void OnBannerLoadFailed(MeticaAdError error)
    {
        Debug.LogError($"Banner failed to load: {error.message}");
    }

    private void OnBannerClicked(MeticaAd ad)
    {
        Debug.Log($"Banner clicked: {ad.adUnitId}");
    }

    private void OnBannerRevenuePaid(MeticaAd ad)
    {
        Debug.Log($"Banner revenue: ${ad.revenue} from {ad.networkName}");
    }
}
```

Banner ads auto-refresh by default. To control refresh manually these are the methods available:

```csharp
MeticaSdk.Ads.StopBannerAutoRefresh(BannerAdUnitId);  // Stop auto-refresh
MeticaSdk.Ads.LoadBanner(BannerAdUnitId);              // Manually load
MeticaSdk.Ads.StartBannerAutoRefresh(BannerAdUnitId); // Re-enable auto-refresh
```

### MREC Ads (300x250)

Medium Rectangle ads work the same way as banners but with a larger size.

```csharp
using Metica;
using Metica.Ads;
using UnityEngine;

public class MrecExample : MonoBehaviour
{
    private const string MrecAdUnitId = "YOUR_MREC_AD_UNIT_ID";

    void Start()
    {
        // Subscribe to callbacks
        MeticaAdsCallbacks.Mrec.OnAdLoadSuccess += OnMrecLoaded;
        MeticaAdsCallbacks.Mrec.OnAdLoadFailed += OnMrecLoadFailed;
        MeticaAdsCallbacks.Mrec.OnAdClicked += OnMrecClicked;
        MeticaAdsCallbacks.Mrec.OnAdRevenuePaid += OnMrecRevenuePaid;

        // Create and show MREC
        var config = new MeticaAdViewConfiguration(MeticaAdViewPosition.Centered);
        MeticaSdk.Ads.CreateMrec(MrecAdUnitId, config);
        MeticaSdk.Ads.ShowMrec(MrecAdUnitId);
    }

    void OnApplicationFocus(bool hasFocus)
    {
        if (hasFocus)
        {
            MeticaSdk.Ads.ShowMrec(MrecAdUnitId);
        }
        else
        {
            MeticaSdk.Ads.HideMrec(MrecAdUnitId);
        }
    }

    private void OnMrecLoaded(MeticaAd ad)
    {
        Debug.Log($"MREC loaded: {ad.adUnitId}");
    }

    private void OnMrecLoadFailed(MeticaAdError error)
    {
        Debug.LogError($"MREC failed to load: {error.message}");
    }

    private void OnMrecClicked(MeticaAd ad)
    {
        Debug.Log($"MREC clicked: {ad.adUnitId}");
    }

    private void OnMrecRevenuePaid(MeticaAd ad)
    {
        Debug.Log($"MREC revenue: ${ad.revenue} from {ad.networkName}");
    }
}
```

MREC ads also auto-refresh by default. Use the same refresh control methods as banners:

```csharp
MeticaSdk.Ads.StopMrecAutoRefresh(MrecAdUnitId);
MeticaSdk.Ads.LoadMrec(MrecAdUnitId);
MeticaSdk.Ads.StartMrecAutoRefresh(MrecAdUnitId);
```

### Interstitial Ads

Full-screen ads that are shown between content transitions.

{% hint style="info" %}
`ShowInterstitial` accepts optional `placementId` and `customData` parameters for analytics tracking.
{% endhint %}

```csharp
using Metica;
using Metica.Ads;
using UnityEngine;

public class InterstitialExample : MonoBehaviour
{
    private const string InterstitialAdUnitId = "YOUR_INTERSTITIAL_AD_UNIT_ID";
    private int interstitialRetryAttempt = 0;

    void Start()
    {
        // Subscribe to callbacks
        MeticaAdsCallbacks.Interstitial.OnAdLoadSuccess += OnInterstitialLoaded;
        MeticaAdsCallbacks.Interstitial.OnAdLoadFailed += OnInterstitialLoadFailed;
        MeticaAdsCallbacks.Interstitial.OnAdShowSuccess += OnInterstitialShown;
        MeticaAdsCallbacks.Interstitial.OnAdShowFailed += OnInterstitialShowFailed;
        MeticaAdsCallbacks.Interstitial.OnAdHidden += OnInterstitialHidden;
        MeticaAdsCallbacks.Interstitial.OnAdClicked += OnInterstitialClicked;
        MeticaAdsCallbacks.Interstitial.OnAdRevenuePaid += OnInterstitialRevenuePaid;

        // Load interstitial
        LoadInterstitial();
    }
    
    private void LoadInterstitial()
    {
        MeticaSdk.Ads.LoadInterstitial(InterstitialAdUnitId);
    }

    public void ShowInterstitial()
    {
        if (MeticaSdk.Ads.IsInterstitialReady(InterstitialAdUnitId))
        {
            MeticaSdk.Ads.ShowInterstitial(InterstitialAdUnitId, "main_menu", "level_complete");
        }
        else
        {
            Debug.LogWarning("Interstitial not ready yet");
        }
    }

    private void OnInterstitialLoaded(MeticaAd ad)
    {
        interstitialRetryAttempt = 0;
        Debug.Log($"Interstitial loaded: {ad.adUnitId}");
    }

    private void OnInterstitialLoadFailed(MeticaAdError error)
    {    
        interstitialRetryAttempt++;
        double delay = System.Math.Pow(2, System.Math.Min(6, interstitialRetryAttempt));
        Debug.LogWarning($"Load failed: {error.message}, retrying in {delay}s");
        Invoke(nameof(LoadInterstitial), (float)delay);
    }

    private void OnInterstitialShown(MeticaAd ad)
    {
        Debug.Log($"Interstitial shown: {ad.adUnitId}");
    }

    private void OnInterstitialShowFailed(MeticaAd ad, MeticaAdError error)
    {
        Debug.LogError($"Interstitial failed to show: {error.message}");
    }

    private void OnInterstitialHidden(MeticaAd ad)
    {
        Debug.Log($"Interstitial hidden: {ad.adUnitId}");
        // Reload for next time
        LoadInterstitial();
    }

    private void OnInterstitialClicked(MeticaAd ad)
    {
        Debug.Log($"Interstitial clicked: {ad.adUnitId}");
    }

    private void OnInterstitialRevenuePaid(MeticaAd ad)
    {
        Debug.Log($"Interstitial revenue: ${ad.revenue} from {ad.networkName}");
    }
}
```

### Rewarded Ads

Reward users for watching video ads.

{% hint style="info" %}
`ShowRewarded` accepts optional `placementId` and `customData` parameters for analytics tracking.
{% endhint %}

```csharp
using Metica; 
using Metica.Ads; 
using UnityEngine;

public class RewardedExample : MonoBehaviour { 
    private const string RewardedAdUnitId = "YOUR_REWARDED_AD_UNIT_ID"; 
    private int rewardedRetryAttempt = 0;

    void Start()
    {
        // Subscribe to callbacks
        MeticaAdsCallbacks.Rewarded.OnAdLoadSuccess += OnRewardedLoaded;
        MeticaAdsCallbacks.Rewarded.OnAdLoadFailed += OnRewardedLoadFailed;
        MeticaAdsCallbacks.Rewarded.OnAdShowSuccess += OnRewardedShown;
        MeticaAdsCallbacks.Rewarded.OnAdShowFailed += OnRewardedShowFailed;
        MeticaAdsCallbacks.Rewarded.OnAdHidden += OnRewardedHidden;
        MeticaAdsCallbacks.Rewarded.OnAdClicked += OnRewardedClicked;
        MeticaAdsCallbacks.Rewarded.OnAdRewarded += OnUserRewarded;
        MeticaAdsCallbacks.Rewarded.OnAdRevenuePaid += OnRewardedRevenuePaid;
    
        // Load rewarded ad
        LoadRewarded();
    }
    
    private void LoadRewarded()
    {
        MeticaSdk.Ads.LoadRewarded(RewardedAdUnitId);
    }
    
    public void ShowRewardedAd()
    {
        if (MeticaSdk.Ads.IsRewardedReady(RewardedAdUnitId))
        {
            MeticaSdk.Ads.ShowRewarded(RewardedAdUnitId, "shop", "extra_coins");
        }
        else
        {
            Debug.LogWarning("Rewarded ad not ready yet");
        }
    }
    
    private void OnRewardedLoaded(MeticaAd ad)
    {
        rewardedRetryAttempt = 0;
        Debug.Log($"Rewarded ad loaded: {ad.adUnitId}");
    }
    
    private void OnRewardedLoadFailed(MeticaAdError error)
    {
        rewardedRetryAttempt++;
        double delay = System.Math.Pow(2, System.Math.Min(6, rewardedRetryAttempt));
        Debug.LogWarning($"Rewarded failed: {error.message}, retrying in {delay}s");
        Invoke(nameof(LoadRewarded), (float)delay);
    }
    
    private void OnRewardedShown(MeticaAd ad)
    {
        Debug.Log($"Rewarded ad shown: {ad.adUnitId}");
    }
    
    private void OnRewardedShowFailed(MeticaAd ad, MeticaAdError error)
    {
        Debug.LogError($"Rewarded ad failed to show: {error.message}");
    }
    
    private void OnRewardedHidden(MeticaAd ad)
    {
        Debug.Log($"Rewarded ad hidden: {ad.adUnitId}");
        // Reload for next time
        LoadRewarded();
    }
    
    private void OnRewardedClicked(MeticaAd ad)
    {
        Debug.Log($"Rewarded ad clicked: {ad.adUnitId}");
    }
    
    private void OnUserRewarded(MeticaAd ad)
    {
        Debug.Log($"User earned reward from: {ad.adUnitId}");
        // Grant reward to user here
        GrantRewardToUser();
    }
    
    private void OnRewardedRevenuePaid(MeticaAd ad)
    {
        Debug.Log($"Rewarded ad revenue: ${ad.revenue} from {ad.networkName}");
    }
    
    private void GrantRewardToUser()
    {
        // Your reward logic here
        Debug.Log("Granting reward to user!");
    }
}
```

### App Open Ads (not natively supported)

The Metica SDK **does not currently support** the App Open ad format natively. If your application requires App Open ads, you can use the MAX SDK alongside the Metica SDK with the following considerations.

#### Integration Requirements

**Initialization Order**

```cs
// 1. Initialize Metica SDK first
await MeticaSdk.InitializeAsync(meticaInitConfig, meticaMediationInfo);

// 2. Initialize MAX SDK after Metica initialization completes
MaxSdkCallbacks.OnSdkInitializedEvent += OnMaxSdkInitialized;
MaxSdk.InitializeSdk();
```

**Callback Restrictions**

When using MAX SDK for App Open ads alongside Metica SDK, you must only register and use MAX SDK callbacks that are specifically related to App Open ads. Do not register MAX SDK callbacks for other ad types (banners, interstitials, rewarded) as these are managed by the Metica SDK.

```cs
private void OnMaxSdkInitialized(MaxSdkBase.SdkConfiguration sdkConfiguration)
{
    // ✅ Allowed: App Open specific callbacks
    MaxSdkCallbacks.AppOpen.OnAdLoadedEvent += OnAppOpenAdLoaded;
    MaxSdkCallbacks.AppOpen.OnAdLoadFailedEvent += OnAppOpenAdLoadFailed;
    MaxSdkCallbacks.AppOpen.OnAdDisplayedEvent += OnAppOpenAdDisplayed;
    MaxSdkCallbacks.AppOpen.OnAdHiddenEvent += OnAppOpenAdHidden;
    MaxSdkCallbacks.AppOpen.OnAdClickedEvent += OnAppOpenAdClicked;
    MaxSdkCallbacks.AppOpen.OnAdRevenuePaidEvent += OnAppOpenAdRevenuePaid;
    
    // ❌ Do not register: Other ad type callbacks (managed by Metica SDK)
    // MaxSdkCallbacks.Interstitial...
    // MaxSdkCallbacks.Rewarded...
    // MaxSdkCallbacks.Banner...
}
```

{% hint style="warning" %}
All other ad types (banners, interstitials, rewarded videos) should continue to be managed through the Metica SDK. Mixing callbacks for non-App Open ad types may cause unexpected behaviour or conflicts between the SDKs.
{% endhint %}

## Revenue Callback Delivery Mode

By default, `OnAdRevenuePaid` is delivered on Unity's main thread. During a fullscreen ad, Unity pauses the player loop. The callback is then deferred until the ad closes. If the process is killed before then, the event is lost.

This matters when you forward revenue events to analytics tools such as AppMetrica. Lost callbacks can mean lost revenue attribution.

**Note:** MaxSDK's  default for fullscreen revenue events is native-thread\
delivery, for the same reason.

For interstitial and rewarded ads, you can opt into synchronous delivery on the native callback thread:

```csharp
using Metica.Ads;

// This match default mode in MaxSdk
MeticaAds.RevenueCallbackDelivery = CallbackDelivery.NativeThread;
```

In `NativeThread` mode, `OnAdRevenuePaid` runs on a non-Unity thread as soon as the native SDK reports revenue. This bypasses the paused player loop and avoids losing the event if the process dies before the ad closes.

#### CallbackDelivery values

| Value             | Behavior                                                                                                                                                                                                                                            |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UnityMainThread` | <p>Default. Posts the callback to Unity's main thread. Delivery waits for the Unity player loop, which is paused while a fullscreen ad is displayed.<br><br>Use this if your game is currently enable `InvokeEventsOnUnityMainThread` in MaxSdk</p> |
| `NativeThread`    | <p>Runs the callback synchronously on a non-Unity thread as soon as the native SDK fires the event, including while the player loop is paused.<br><br>This match MaxSdk default behaviour</p>                                                       |

{% hint style="warning" %}
**In `NativeThread` mode:**

* Do **not** call Unity APIs directly from the handler.
* Marshal work to the main thread if needed.
* Exceptions thrown by the handler are caught and logged.
  {% endhint %}

#### Notes

* The setting is global and revenue-scoped.
* It affects only `OnAdRevenuePaid` for **interstitial** and **rewarded** ads.
* Banner and MREC revenue are unaffected.
* All other ad callbacks are unaffected.
* You can set it before or after `Initialize`.
* A value set before initialization is applied once the SDK is ready.

## API Reference

### MeticaSdk Methods

#### Initialization & Configuration

<table><thead><tr><th width="322.203125">Method</th><th>Return Type</th><th>Description</th></tr></thead><tbody><tr><td><code>InitializeAsync(MeticaInitConfig, MeticaMediationInfo)</code></td><td><code>Task&#x3C;MeticaInitResponse></code></td><td>Initializes the SDK.</td></tr><tr><td><code>SetLogEnabled(bool)</code></td><td><code>void</code></td><td>Enables/disables debug logging (default: <code>false</code>).</td></tr></tbody></table>

### MeticaSdk.Ads Methods

#### Privacy

<table><thead><tr><th width="251.796875">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>SetHasUserConsent(bool)</code></td><td>Sets GDPR consent status. Call before initialization.</td></tr><tr><td><code>SetDoNotSell(bool)</code></td><td>Sets CCPA do-not-sell flag. Call before initialization.</td></tr></tbody></table>

#### Banner Methods

<table><thead><tr><th width="508.125">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>CreateBanner(string adUnitId, MeticaAdViewConfiguration config)</code></td><td>Creates a banner ad view.</td></tr><tr><td><code>ShowBanner(string adUnitId)</code></td><td>Shows a banner ad.</td></tr><tr><td><code>HideBanner(string adUnitId)</code></td><td>Hides a banner ad.</td></tr><tr><td><code>LoadBanner(string adUnitId)</code></td><td>Manually loads a banner ad (only when auto-refresh is disabled).</td></tr><tr><td><code>StartBannerAutoRefresh(string adUnitId)</code></td><td>Starts automatic banner refresh.</td></tr><tr><td><code>StopBannerAutoRefresh(string adUnitId)</code></td><td>Stops automatic banner refresh.</td></tr><tr><td><code>DestroyBanner(string adUnitId)</code></td><td>Destroys a banner ad view.</td></tr><tr><td><code>SetBannerPlacement(string adUnitId, string? placement)</code></td><td>Sets the placement for a banner ad. Call before <code>LoadBanner</code>.</td></tr><tr><td><code>SetBannerCustomData(string adUnitId, string? customData)</code></td><td>Sets custom data for a banner ad.</td></tr><tr><td><code>SetBannerExtraParameter(string adUnitId, string key, string? value)</code></td><td>Sets an extra parameter for a banner ad.</td></tr><tr><td><code>SetBannerLocalExtraParameter(string adUnitId, string key, object? value)</code></td><td>Sets a local extra parameter for a banner ad.</td></tr><tr><td><code>SetBannerLocalExtraParameterJson(string adUnitId, string key, string? value)</code></td><td>Sets a local extra parameter for a banner ad using a JSON string value.</td></tr><tr><td><code>SetBannerWidth(string adUnitId, float widthDp)</code></td><td>Sets the width for a banner ad in density-independent pixels.</td></tr><tr><td><code>SetBannerBackgroundColor(string adUnitId, string hexColorCode)</code></td><td>Sets the background color for a banner ad using a hex color code.</td></tr></tbody></table>

#### MREC Methods

<table><thead><tr><th width="470.11328125">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>CreateMrec(string adUnitId, MeticaAdViewConfiguration config)</code></td><td>Creates an MREC ad view.</td></tr><tr><td><code>ShowMrec(string adUnitId)</code></td><td>Shows an MREC ad.</td></tr><tr><td><code>HideMrec(string adUnitId)</code></td><td>Hides an MREC ad.</td></tr><tr><td><code>LoadMrec(string adUnitId)</code></td><td>Manually loads an MREC ad (only when auto-refresh is disabled).</td></tr><tr><td><code>StartMrecAutoRefresh(string adUnitId)</code></td><td>Starts automatic MREC refresh.</td></tr><tr><td><code>StopMrecAutoRefresh(string adUnitId)</code></td><td>Stops automatic MREC refresh.</td></tr><tr><td><code>DestroyMrec(string adUnitId)</code></td><td>Destroys an MREC ad view.</td></tr><tr><td><code>SetMrecPlacement(string adUnitId, string? placement)</code></td><td>Sets the placement for an MREC ad. Call before <code>LoadMrec</code>.</td></tr><tr><td><code>SetMrecCustomData(string adUnitId, string? customData)</code></td><td>Sets custom data for an MREC ad.</td></tr><tr><td><code>SetMrecExtraParameter(string adUnitId, string key, string? value)</code></td><td>Sets an extra parameter for an MREC ad.</td></tr><tr><td><code>SetMrecLocalExtraParameter(string adUnitId, string key, object? value)</code></td><td>Sets a local extra parameter for an MREC ad.</td></tr><tr><td><code>SetMrecLocalExtraParameterJson(string adUnitId, string key, string? value)</code></td><td>Sets a local extra parameter for an MREC ad using a JSON string value.</td></tr></tbody></table>

#### Interstitial Methods

<table><thead><tr><th width="482.625">Method</th><th width="131.171875">Return Type</th><th>Description</th></tr></thead><tbody><tr><td><code>LoadInterstitial(string adUnitId)</code></td><td><code>void</code></td><td>Loads an interstitial ad.</td></tr><tr><td><code>ShowInterstitial(string adUnitId, string? placementId, string? customData)</code></td><td><code>void</code></td><td>Shows an interstitial ad with optional tracking parameters.</td></tr><tr><td><code>IsInterstitialReady(string adUnitId)</code></td><td><code>bool</code></td><td>Checks if interstitial is ready to show.</td></tr><tr><td><code>SetInterstitialExtraParameter(string adUnitId, string key, string? value)</code></td><td><code>void</code></td><td>Sets an extra parameter for an interstitial ad.</td></tr><tr><td><code>SetInterstitialLocalExtraParameter(string adUnitId, string key, object? value)</code></td><td><code>void</code></td><td>Sets a local extra parameter for an interstitial ad.</td></tr></tbody></table>

#### Rewarded Methods

<table><thead><tr><th width="409.45703125">Method</th><th width="126.3671875">Return Type</th><th>Description</th></tr></thead><tbody><tr><td><code>LoadRewarded(string adUnitId)</code></td><td><code>void</code></td><td>Loads a rewarded ad.</td></tr><tr><td><code>ShowRewarded(string adUnitId, string? placementId, string? customData)</code></td><td><code>void</code></td><td>Shows a rewarded ad with optional tracking parameters.</td></tr><tr><td><code>IsRewardedReady(string adUnitId)</code></td><td><code>bool</code></td><td>Checks if rewarded ad is ready to show.</td></tr><tr><td><code>SetRewardedAdExtraParameter(string adUnitId, string key, string? value)</code></td><td><code>void</code></td><td>Sets an extra parameter for a rewarded ad.</td></tr><tr><td><code>SetRewardedAdLocalExtraParameter(string adUnitId, string key, object? value)</code></td><td><code>void</code></td><td>Sets a local extra parameter for a rewarded ad.</td></tr></tbody></table>

### MeticaSdk.Ads.Max Methods

| Method                                         | Return Type                           | Description                                 |
| ---------------------------------------------- | ------------------------------------- | ------------------------------------------- |
| `SetExtraParameter(string key, string? value)` | `void`                                | Sets an extra parameter on the MAX SDK.     |
| `ShowMediationDebugger()`                      | `void`                                | Shows the AppLovin MAX mediation debugger.  |
| `HasUserConsent()`                             | `bool`                                | Returns whether user has consented.         |
| `IsUserConsentSet()`                           | `bool`                                | Returns whether consent was explicitly set. |
| `GetConsentFlowUserGeography()`                | `MaxSdkBase.ConsentFlowUserGeography` | Returns user's geography for consent flow.  |

### MeticaAd Properties

<table><thead><tr><th width="195.41796875">Property</th><th width="184.80859375">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>adUnitId</code></td><td><code>string</code></td><td>Ad unit identifier.</td></tr><tr><td><code>revenue</code></td><td><code>double?</code></td><td>Revenue generated by this ad impression (nullable).</td></tr><tr><td><code>networkName</code></td><td><code>string?</code></td><td>The ad network name (nullable).</td></tr><tr><td><code>placementTag</code></td><td><code>string?</code></td><td>Placement tag (nullable).</td></tr><tr><td><code>adFormat</code></td><td><code>string?</code></td><td>Ad format type (nullable).</td></tr><tr><td><code>creativeId</code></td><td><code>string?</code></td><td>Creative identifier (nullable).</td></tr><tr><td><code>latency</code></td><td><code>long?</code></td><td>Load latency in milliseconds (nullable).</td></tr></tbody></table>

### MeticaAdError Properties

<table><thead><tr><th width="203.9609375">Property</th><th width="179.703125">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>message</code></td><td><code>string</code></td><td>Error message.</td></tr><tr><td><code>adUnitId</code></td><td><code>string?</code></td><td>Associated ad unit identifier (nullable).</td></tr></tbody></table>

### MeticaAdViewPosition Enum

Position options for banner and MREC ads:

<table><thead><tr><th width="207.5625">Value</th><th>Description</th></tr></thead><tbody><tr><td><code>TopCenter</code></td><td>Top center of the screen.</td></tr><tr><td><code>Centered</code></td><td>Center of the screen.</td></tr><tr><td><code>BottomCenter</code></td><td>Bottom center of the screen.</td></tr></tbody></table>

### MeticaMediationType Enum

Supported mediation platforms:

<table><thead><tr><th width="214.5859375">Value</th><th>Description</th></tr></thead><tbody><tr><td><code>MAX</code></td><td>AppLovin MAX mediation platform.</td></tr></tbody></table>

### MeticaAdsCallbacks Events

#### Banner Callbacks

<table><thead><tr><th width="374.796875">Event</th><th width="206.80859375">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>MeticaAdsCallbacks.Banner.OnAdLoadSuccess</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Banner ad loaded successfully.</td></tr><tr><td><code>MeticaAdsCallbacks.Banner.OnAdLoadFailed</code></td><td><code>Action&#x3C;MeticaAdError></code></td><td>Banner ad failed to load.</td></tr><tr><td><code>MeticaAdsCallbacks.Banner.OnAdClicked</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Banner ad was clicked.</td></tr><tr><td><code>MeticaAdsCallbacks.Banner.OnAdRevenuePaid</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Banner ad revenue was recorded.</td></tr></tbody></table>

#### MREC Callbacks

<table><thead><tr><th width="358.73046875">Event</th><th width="220.046875">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>MeticaAdsCallbacks.Mrec.OnAdLoadSuccess</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>MREC ad loaded successfully.</td></tr><tr><td><code>MeticaAdsCallbacks.Mrec.OnAdLoadFailed</code></td><td><code>Action&#x3C;MeticaAdError></code></td><td>MREC ad failed to load.</td></tr><tr><td><code>MeticaAdsCallbacks.Mrec.OnAdClicked</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>MREC ad was clicked.</td></tr><tr><td><code>MeticaAdsCallbacks.Mrec.OnAdRevenuePaid</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>MREC ad revenue was recorded.</td></tr></tbody></table>

#### Interstitial Callbacks

<table><thead><tr><th width="426.97265625">Event</th><th width="298.9609375">Type</th><th width="211.8515625">Description</th></tr></thead><tbody><tr><td><code>MeticaAdsCallbacks.Interstitial.OnAdLoadSuccess</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Interstitial ad loaded successfully.</td></tr><tr><td><code>MeticaAdsCallbacks.Interstitial.OnAdLoadFailed</code></td><td><code>Action&#x3C;MeticaAdError></code></td><td>Interstitial ad failed to load.</td></tr><tr><td><code>MeticaAdsCallbacks.Interstitial.OnAdShowSuccess</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Interstitial ad displayed successfully.</td></tr><tr><td><code>MeticaAdsCallbacks.Interstitial.OnAdShowFailed</code></td><td><code>Action&#x3C;MeticaAd, MeticaAdError></code></td><td>Interstitial ad failed to display.</td></tr><tr><td><code>MeticaAdsCallbacks.Interstitial.OnAdHidden</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Interstitial ad was dismissed.</td></tr><tr><td><code>MeticaAdsCallbacks.Interstitial.OnAdClicked</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Interstitial ad was clicked.</td></tr><tr><td><code>MeticaAdsCallbacks.Interstitial.OnAdRevenuePaid</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Interstitial ad revenue was recorded.</td></tr></tbody></table>

#### Rewarded Callbacks

<table><thead><tr><th width="404.3125">Event</th><th width="293.640625">Type</th><th width="235.60546875">Description</th></tr></thead><tbody><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdLoadSuccess</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Rewarded ad loaded successfully.</td></tr><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdLoadFailed</code></td><td><code>Action&#x3C;MeticaAdError></code></td><td>Rewarded ad failed to load.</td></tr><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdShowSuccess</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Rewarded ad displayed successfully.</td></tr><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdShowFailed</code></td><td><code>Action&#x3C;MeticaAd, MeticaAdError></code></td><td>Rewarded ad failed to display.</td></tr><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdHidden</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Rewarded ad was dismissed.</td></tr><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdClicked</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Rewarded ad was clicked.</td></tr><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdRewarded</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>User earned the reward.</td></tr><tr><td><code>MeticaAdsCallbacks.Rewarded.OnAdRevenuePaid</code></td><td><code>Action&#x3C;MeticaAd></code></td><td>Rewarded ad revenue was recorded.</td></tr></tbody></table>

## Troubleshooting

### Important considerations while integrating

These are the patterns we see most often in real integrations. Most of them fail **silently** — no crash, just missing revenue or dropped callbacks — which is what makes them so easy to miss.

{% stepper %}
{% step %}

#### Wrapping revenue reporting in a main-thread post

{% hint style="info" %}
Native-thread delivery is opt-in. Enable it once, before you rely on it:

```csharp
// Default is CallbackDelivery.UnityMainThread.
// Opt into native-thread delivery for the revenue callback:
MeticaAds.RevenueCallbackDelivery = CallbackDelivery.NativeThread;
```

{% endhint %}

When using `CallbackDelivery.NativeThread`, do **not** wrap your whole `OnAdRevenuePaid` handler in a main-thread dispatcher (`UnityMainThreadDispatcher`, `SynchronizationContext.Post`, coroutine queue, etc.).

During fullscreen ad display the Unity player loop is **paused**, so anything posted to the main thread sits unpumped until the ad closes — and is lost entirely if the user kills the app mid-ad. That defeats the reason `NativeThread` delivery exists in the first place.

Third-party analytics SDKs (Adjust, Firebase, AppMetrica, …) have thread-safe native tracking calls — call them **directly** in the callback. Marshal only the code that genuinely touches Unity back to the main thread, and put it **after** the reporting calls:

```csharp
MeticaAdsCallbacks.Interstitial.OnAdRevenuePaid += (ad) =>
{
    // Thread-safe: report immediately, on the native thread.
    Adjust.TrackAdRevenue(BuildAdjustRevenue(ad));

    // Unity-dependent work goes to the main thread — after reporting.
    RunOnMainThread(() => UpdateRevenueUI(ad.revenue));
};
```

**Rule of thumb:** reporting first, directly on the native thread; Unity work last, marshalled to the main thread.
{% endstep %}

{% step %}

#### Calling Unity APIs from the native callback thread

The inverse mistake: in `NativeThread` mode, Unity APIs (`PlayerPrefs`, `GameObject`, `Instantiate`, coroutines, UI, most of `Application`) are main-thread-only and **throw** when called from the callback thread.

The SDK catches and logs handler exceptions, so the failure is silent: the exception aborts the handler, and every line after the throwing call — often the actual revenue reporting — **never runs**. The result is a systematic revenue undercount with nothing in your crash reporting.

Even an innocent-looking helper can be the culprit — e.g. a `GetLevelCount()` that reads `PlayerPrefs` internally. Audit the **full call chain** of your handler, not just the top-level code.
{% endstep %}

{% step %}

#### Comparing requested and returned ad unit IDs

Don't gate callback logic on the ad unit ID you requested:

{% hint style="danger" %}

```csharp
// DON'T do this
private void OnRevenuePaid(MeticaAd ad)
{
    if (ad.adUnitId != _loadedAdUnitId) return; // silently drops revenue
    ReportRevenue(ad);
}
```

{% endhint %}

Metica may load and show a **different ad unit** than the one you requested (parallel loading, failover units, SmartFloors routing), so requested and returned IDs are not guaranteed to match. An equality guard like this silently drops revenue and lifecycle events, or blocks the code path entirely.

This pattern usually comes from copying MAX integration code, where a per-ad-unit guard is idiomatic. With Metica it is a bug. If you need per-format routing, key on the ad format / callback source, not on ID equality with the requested unit.
{% endstep %}

{% step %}

#### Reporting revenue through both MAX and Metica

Metica mediates through MAX. If you keep your old `MaxSdkCallbacks.*.OnAdRevenuePaidEvent` analytics wiring alive alongside `MeticaAdsCallbacks.*.OnAdRevenuePaid`, the same impression is reported **twice** to your analytics (Adjust/Firebase/AppMetrica revenue inflated, impression counts doubled).

Route revenue through exactly **one** path per impression.
{% endstep %}

{% step %}

#### Re-initializing the SDK or re-subscribing handlers

`InitializeAsync` must be called exactly **once** per app launch. Re-running your init flow (scene reloads, retry logic, focus handlers) re-subscribes your event handlers, and every subsequent callback then fires once per subscription — duplicate revenue reports, duplicate reward grants.

Guard your init with a flag, or use `-=` before `+=` when subscribing.
{% endstep %}

{% step %}

#### Initializing off the Unity main thread

`MeticaSdk.InitializeAsync` must be invoked on the **Unity main thread**. The SDK captures the `SynchronizationContext` at the initialize call site and uses it for main-thread callback dispatch — if you initialize from a background thread (a `Task.Run`, a thread-pool continuation, a native callback), main-thread-delivered callbacks are dispatched to the wrong context and can misbehave in ways that are very hard to diagnose.

Watch out for `await`-heavy init flows: an `await` before the `InitializeAsync` call can silently move you off the main thread depending on the awaited task's context.
{% endstep %}

{% step %}

#### Initializing inside a CMP / UMP consent callback without marshalling

A common init pattern is: run the consent flow first, then initialize Metica in the consent-completed callback. The catch: CMP/UMP callbacks frequently fire **off the Unity main thread**.

Combined with #6, this means chaining `InitializeAsync` directly inside the consent callback can initialize the SDK on a background thread. Marshal back to the Unity main thread before initializing:

```csharp
ConsentFlow.OnCompleted += () =>
{
    RunOnMainThread(() => InitializeMetica());
};
```

{% endstep %}
{% endstepper %}

### Max SDK Conflict

The Metica Unity SDK already includes and manages the AppLovin MAX integration internally. Do not reference, call, or initialize MaxSdk or subscribe to MAX callbacks directly in your code, this includes any callbacks related to:

* SDK initialization
* Banner
* MREC
* Interstitial
* Rewarded

Direct usage of MAX APIs or subscribing to MAX events can cause version conflicts, duplicate initialization, callback collisions, or unexpected behavior. Instead, rely on the callbacks exposed by the Metica SDK for all ad lifecycle events.

### Moloco SDK Version

Moloco’s libraries (SDK and adapters) must be kept up to date to avoid dependency and build issues when used alongside Metica’s mediation setup.

* Ensure the Moloco SDK and its adapters are at least version 4.3.0 or above.

Older Moloco versions may lead to dependency resolution issues and incompatibilities with other mediation components.


# Unity Analytics SDK

## Analytics

Alongside SmartFloors ad optimization, the Metica SDK provides first-party event collection for player behaviour, monetization, and user state. Analytics shares the SDK's configuration and initialization — once the analytics package is installed and the SDK is initialized, log events through `MeticaSdk.Analytics`:

```csharp
MeticaSdk.Analytics.LogCustomEvent("levelComplete", new Dictionary<string, object>
{
    ["levelIndex"] = 12,
    ["stars"] = 3
});
```

### Prerequisites

Before using Metica Analytics, you need:

1. **API Key** — obtained from the Metica platform
2. **App ID** — obtained from the Metica platform
3. A **MAX SDK Key**, obtainable from the AppLovin platform (only required when initializing the SDK with ads)

### Installation

{% hint style="info" %}
If you haven't done so already, install the Metica SDK first by following the [Unity SDK integration guide](https://docs.metica.com/api/unity-sdk/unity-sdk-2). Analytics is part of the same SDK package — no separate SDK installation is required.
{% endhint %}

The Analytics feature requires one additional package on top of the Metica SDK:

1. Download [`com.metica.analytics.abstractions-1.0.0.tgz`](https://github.com/meticalabs/metica-unity-analytics/releases/tag/v1.0.0)
2. In Unity Editor, go to **Window → Package Manager**
3. Click the **+** button in the top-left corner and select **Add package from tarball...**
4. Select the downloaded `com.metica.analytics.abstractions-1.0.0.tgz`

The SDK detects the package automatically — once it is installed, the Analytics API is compiled in and `MeticaSdk.Analytics` becomes available. No scripting defines need to be set manually.

### Initialization

{% hint style="warning" %}
**Analytics does not need to be initialized separately.** The SDK is initialized once — when the analytics package is installed, the standard `MeticaSdk.Initialize` / `MeticaSdk.InitializeAsync` call also initializes Analytics, and `MeticaSdk.Analytics` is ready to use after it completes. The early-session flow below is only needed if you want Analytics available earlier in the app lifecycle than your standard SDK initialization.
{% endhint %}

```csharp
var config = new MeticaInitConfig("YOUR_API_KEY", "YOUR_APP_ID", "YOUR_USER_ID");
var mediationInfo = new MeticaMediationInfo(MeticaMediationType.MAX, "YOUR_MAX_SDK_KEY");
var initResponse = await MeticaSdk.InitializeAsync(config, mediationInfo);
```

#### Capturing early-session events

Some integrations defer `InitializeAsync` until later in the boot flow — waiting on user consent, a loading screen, or remote configuration. Events cannot be logged before the SDK is initialized, so early-session events such as `install` and `sessionStart` would be lost.

For this case only, initialize Analytics on its own as soon as the app starts. Analytics is fully active from this call — events are recorded and delivered normally. The later `InitializeAsync` call adds SmartFloors on top of the running SDK:

```csharp
// At app start: Analytics only
var config = new MeticaInitConfig("YOUR_API_KEY", "YOUR_APP_ID", "YOUR_USER_ID");
MeticaSdk.InitializeAnalytics(config);

// ... early-session events (install, sessionStart, ...) can be logged from here on ...

// Later, after consent / when ads are needed: add SmartFloors
var mediationInfo = new MeticaMediationInfo(MeticaMediationType.MAX, "YOUR_MAX_SDK_KEY");
var initResponse = await MeticaSdk.InitializeAsync(config, mediationInfo);
```

Pass the **same** `MeticaInitConfig` to both calls. If the second call supplies a different configuration, the SDK keeps the configuration from the first call and logs a warning.

If you initialize the SDK at app start (the standard flow), you do not need `InitializeAnalytics` — skip this and use the single `InitializeAsync` call above.

### User identity

Every event carries a `userId`. You can either:

* **Supply your own** stable user ID via `MeticaInitConfig`, or
* **Let the SDK generate one.** The SDK derives a deterministic UUID from a hash of a device identifier combined with your `appId`, so the generated ID remains consistent for a given device and app across launches. On Android the underlying identifier survives app reinstalls; on iOS it can be reset by the OS when all apps from the same vendor are uninstalled.

If you have your own account system, supplying your own ID is recommended so that events can be joined with your other data sources.

### Sessions

A **session** represents a continuous span of user activity within your game.

* **Session start** — the first activity after launch, or after at least 30 minutes of inactivity.
* **Session end** — the game stops producing events for 30 minutes.
* **Session length** — time of last recorded activity minus time of session start.

The SDK derives `sessionCount` and `lastSessionLength` from this definition and attaches them to every event automatically (see Environment attributes). Note that the `sessionStart` **event** is logged by your game (see sessionStart) — the SDK's session bookkeeping runs regardless, but the event itself is not emitted automatically.

### Event schema

Events fall into two categories:

* **Core events** — predefined events with a fixed schema, handled by the SDK with minimal developer input.
* **Custom events** — developer-defined events for use cases not covered by core events.

#### Base properties

Every event, core or custom, carries the following properties. All of them are populated automatically by the SDK — you never set them directly.

| Property        | Type   | Description                                                               |
| --------------- | ------ | ------------------------------------------------------------------------- |
| `eventType`     | string | Identifies the type of event (e.g. `purchase`, or your custom event name) |
| `eventId`       | string | Unique ID of this event, generated by the SDK                             |
| `appId`         | string | Your Metica app ID                                                        |
| `eventTime`     | number | Unix timestamp in milliseconds, set by the SDK at logging time            |
| `userId`        | string | The user ID (supplied or SDK-generated, see User identity)                |
| `customPayload` | object | Optional developer-supplied properties (see rules below)                  |

In addition, the SDK attaches the environment attributes to every event. In the delivered payload these appear as top-level fields on the event, alongside the base properties above.

**`customPayload` rules:**

* A single flat object — **nested objects are not permitted**.
* Values must be primitives (string, number, boolean) or **arrays of up to 100 elements** of the same primitive type.
* At most **100 properties** per payload. String values longer than **200 characters** are truncated. The same limits apply to `userStateAttributes`.
* Allowed on all core events **except** `fullStateUpdate` and `partialStateUpdate`.
* For custom events, `customPayload` carries the event's properties.

**Property type consistency:** once a property has been sent with a specific type, all future events — from your game or any other game in your organization — must use the same type for that property. Violating this causes errors and missing data in the ingestion pipeline. To change a property's type, stop sending it and introduce a new property name.

#### Environment attributes

The SDK automatically collects the following properties and attaches them to every event:

| Attribute            | Type    | Platform     | Description                                                          |
| -------------------- | ------- | ------------ | -------------------------------------------------------------------- |
| `appVersion`         | string  | All          | Host app's version (e.g. `1.4.2`)                                    |
| `meticaNativeSdk`    | string  | All          | Metica native SDK version                                            |
| `meticaUnitySdk`     | string  | All          | Metica Unity SDK version (when used from Unity)                      |
| `platform`           | string  | All          | `android` or `ios`                                                   |
| `osVersion`          | string  | All          | Operating system version (e.g. `14`, `17.4`)                         |
| `buildNumber`        | string  | All          | OS build identifier                                                  |
| `deviceModel`        | string  | All          | Device model identifier (e.g. `Pixel 7`, `iPhone15,2`)               |
| `deviceManufacturer` | string? | Android only | Hardware manufacturer (e.g. `Google`, `Samsung`)                     |
| `deviceType`         | string  | All          | `phone` or `tablet`                                                  |
| `deviceMemory`       | long    | All          | Total device RAM in megabytes                                        |
| `locale`             | string  | All          | BCP-47 language tag (e.g. `en-US`)                                   |
| `localTimezone`      | string  | All          | IANA time zone identifier (e.g. `Europe/London`)                     |
| `country`            | string? | All          | ISO 3166-1 alpha-2 country code, lowercased                          |
| `storeCountry`       | string? | iOS only     | App store storefront country (when available)                        |
| `gaid`               | string? | Android only | Google Advertising ID (lowercased); null when unavailable            |
| `idfa`               | string? | iOS only     | Apple Identifier For Advertisers; null when tracking is unauthorized |
| `idfv`               | string? | iOS only     | Apple Identifier For Vendor; null when tracking is unauthorized      |
| `sessionCount`       | long    | All          | Number of sessions started by this user on this device               |
| `lastSessionLength`  | long    | All          | Duration of the previous session, in seconds                         |

Attributes marked nullable (`?`) may be absent from the payload when the platform cannot resolve them — for example `gaid`/`idfa`/`idfv` when the user has not granted tracking consent.

#### Core events

Core events have a fixed schema. The base properties above are always included and are omitted from the field lists below.

**purchase**

Log a completed (or failed) in-app purchase.

| Field          | Type   | Required | Description                                   |
| -------------- | ------ | -------- | --------------------------------------------- |
| `productId`    | string | yes      | Store product identifier                      |
| `currencyCode` | string | yes      | ISO 4217 currency code (e.g. `USD`)           |
| `totalAmount`  | number | yes      | Purchase amount in the given currency         |
| `status`       | string | yes      | Purchase outcome (e.g. `completed`, `failed`) |
| `errorCode`    | string | no       | Error code when the purchase failed           |
| `referenceId`  | string | no       | Store transaction / receipt reference         |

```csharp
MeticaSdk.Analytics.LogPurchaseEvent(
    productId: "com.yourgame.gems_100",
    currency: "USD",
    amount: 4.99,
    status: "completed",
    errorCode: null,
    referenceId: "GPA.1234-5678",
    customPayload: null);
```

**sessionStart**

Marks the beginning of a session. No additional fields.

```csharp
MeticaSdk.Analytics.LogSessionStartEvent(customPayload: null);
```

**install**

Log this once, on the first launch after installation.

| Field        | Type   | Required | Description                                  |
| ------------ | ------ | -------- | -------------------------------------------- |
| `appVersion` | string | yes      | App version at install time (set by the SDK) |

```csharp
MeticaSdk.Analytics.LogInstallEvent(customPayload: null);
```

**impression**

Log an ad impression, including the revenue it generated. Use this only for ad sources that are **not** mediated through the Metica SDK — revenue for ads shown through the Metica SDK is reported automatically (as `estimatedAdRevenue` events).

| Field            | Type   | Required | Description                                       |
| ---------------- | ------ | -------- | ------------------------------------------------- |
| `value`          | number | yes      | Revenue of the impression in USD (e.g. `0.00555`) |
| `impressionType` | string | yes      | Ad format (e.g. `Rewarded`, `Interstitial`)       |
| `mediator`       | string | yes      | Mediation platform (e.g. `AppLovin`)              |
| `source`         | string | yes      | Ad network that served the ad (e.g. `ironSource`) |
| `placement`      | string | no       | Placement in your game (e.g. `LevelSuccess`)      |

```csharp
MeticaSdk.Analytics.LogImpressionEvent(
    value: 0.00555,
    type: "Rewarded",
    mediator: "AppLovin",
    source: "ironSource",
    placement: "LevelSuccess",
    customPayload: null);
```

**fullStateUpdate**

A snapshot of **all** attributes of the user state, typically sent once per session. It **replaces** all previously sent user state attributes. `customPayload` is not allowed on this event.

| Field                 | Type   | Required | Description                   |
| --------------------- | ------ | -------- | ----------------------------- |
| `userStateAttributes` | object | yes      | Map of attribute name → value |

```csharp
MeticaSdk.Analytics.LogFullStateUpdateEvent(new Dictionary<string, object>
{
    ["playerLevel"] = 42,
    ["softCurrencyBalance"] = 1200,
    ["vipTier"] = "gold"
});
```

**partialStateUpdate**

Updates a subset of user state attributes as the player progresses. `customPayload` is not allowed on this event.

| Field                 | Type   | Required | Description                        |
| --------------------- | ------ | -------- | ---------------------------------- |
| `userStateAttributes` | object | yes      | Map of the changed attributes only |

```csharp
MeticaSdk.Analytics.LogPartialStateUpdateEvent(new Dictionary<string, object>
{
    ["playerLevel"] = 43
});
```

#### Custom events

Custom events let you track game-specific behaviour not covered by core events. A custom event consists of the base properties plus your properties, carried in `customPayload`.

```csharp
MeticaSdk.Analytics.LogCustomEvent("earnAsset", new Dictionary<string, object>
{
    ["asset"] = "coin",
    ["source"] = "monthlyDrive",
    ["type"] = "rewarded",
    ["amount"] = 12
});
```

**Rules:**

* Event names must be **camelCase**, letters only, no digits and no special characters (e.g. `levelComplete`, `itemPurchased`). This is enforced — events with invalid names are dropped.
* The core event names are **reserved** and must not be used: `purchase`, `impression`, `sessionStart`, `install`, `fullStateUpdate`, `partialStateUpdate`.
* Properties follow the `customPayload` rules above: primitives or arrays of primitives, no nested objects, at most 100 properties, and stable types per property name.

#### Validation and delivery

* All events are schema-validated before transmission. Events that fail validation are **dropped** and not retried.
* Validated events are batched, persisted locally, and delivered with automatic retry — events are not lost to transient network failures or app restarts.

### Building a custom analytics wrapper

Studios that ship many titles often want a shared, internal analytics library that standardises how each game describes its world while still routing events through Metica. The SDK supports this pattern through the **`Metica.Analytics.Abstractions`** package installed above.

| Layer                           | Owner       | Changes often? | Purpose                                                                   |
| ------------------------------- | ----------- | -------------- | ------------------------------------------------------------------------- |
| `Metica.Analytics.Abstractions` | Metica      | Rarely         | Tiny, stable contract — the `IMeticaAnalytics` interface                  |
| Metica Unity SDK                | Metica      | Often          | Full implementation — transport, retries, device info, ad mediation       |
| Your wrapper library            | Your studio | As needed      | Game- or genre-specific helpers that map gameplay state to event payloads |

Your wrapper depends only on the abstractions package, not on the full SDK — SDK upgrades do not require rebuilding or changing your wrapper code. `MeticaSdk.Analytics` implements `IMeticaAnalytics`, so your wrapper accepts the interface in its constructor and never references the concrete SDK:

```csharp
using Metica.Analytics.Abstractions;

public class PuzzleAnalytics
{
    private readonly IMeticaAnalytics _analytics;

    public PuzzleAnalytics(IMeticaAnalytics analytics)
    {
        _analytics = analytics;
    }

    public void LogLevelEndOfferImpression(string offerId, int levelIndex, int stars)
    {
        _analytics.LogCustomEvent("puzzleLevelEndOfferImpression", new Dictionary<string, object>
        {
            ["offerId"] = offerId,
            ["levelIndex"] = levelIndex,
            ["stars"] = stars,
        });
    }
}

// Wiring it up after initialization:
var puzzle = new PuzzleAnalytics(MeticaSdk.Analytics);
```

### API reference

The full logging API, available via `MeticaSdk.Analytics` after initialization. Nullable parameters (`?`) accept `null` but must still be passed explicitly:

```csharp
public interface IMeticaAnalytics
{
    void LogPurchaseEvent(
        string productId,
        string currency,
        double amount,
        string status,
        string? errorCode,
        string? referenceId,
        Dictionary<string, object>? customPayload);

    void LogSessionStartEvent(Dictionary<string, object>? customPayload);

    void LogInstallEvent(Dictionary<string, object>? customPayload);

    void LogImpressionEvent(
        double value,
        string type,
        string mediator,
        string source,
        string? placement,
        Dictionary<string, object>? customPayload);

    void LogFullStateUpdateEvent(Dictionary<string, object> attributes);

    void LogPartialStateUpdateEvent(Dictionary<string, object> attributes);

    void LogCustomEvent(string eventName, Dictionary<string, object>? properties);
}
```

**Initialization method:**

* `InitializeAnalytics(MeticaInitConfig)` → `void` — analytics-only initialization for capturing early-session events; `InitializeAsync` later upgrades the same instance to full mode.


# Unity SDK 1

{% embed url="<https://github.com/meticalabs/metica-unity-sdk.git>" %}

{% hint style="warning" %}
v1.x is in maintenance mode: no new features. Only critical bug fixes, security patches, and carefully reviewed non-breaking stability/performance fixes are allowed. All new feature work goes into v2.x.
{% endhint %}

## Overview

The Metica Unity SDK offers a straightforward interface for interacting with the Metica backend API from your Unity game directly. The SDK provides simplified methods to fetch **offers** and **SmartConfigs** (abbreviated as **configs**), and to log user actions within your app or game as events. The Metica Unity SDK takes care of networking nuance, caching and much more so you don't have to.

<figure><img src="/files/WPM880qOjcGnbq0Jqxyz" alt=""><figcaption></figcaption></figure>

### Terminology

* *Application*\
  Your application or game that uses the Metica services.
* *User*\
  A user or player of the application. Player and user are used interchangeably.
* *Event*\
  An `event` refers to a user's action within the context of the application. For example, clicking a button, logging in, clicking an ad, etc.\
  The properties (or attributes) associated with these events vary based on the type of the event;\
  e.g., the property `totalAmount` must be set when logging an in-app offer `purchase` event.
* *User Properties*\
  Sometimes (and formerly) called *User Attributes*, user properties consist of general information about the user that best characterises them in your application. For example you could define your user properties to include game progression, demographics, user preferences, player's level, etc.

## What you need

To use the Metica Unity SDK you need:

* An `API key`, obtainable in the Metica platform
* An `appId`, obtainable in the Metica platform.

## Installation

In Unity, open the Package Manager (Window > Package Manager) and click on the '+' button in the top left corner. Select "Add package from git URL..." and enter the following:

`https://github.com/meticalabs/metica-unity-sdk.git`

## How to use the Metica SDK

Access to the SDK functionalities is obtained through an `IMeticaSdk` interface, an instance of which (its implementation) can be easily obtained with:

```csharp
private IMeticaSdk _sdk = MeticaSdk.SDK;
```

You can now use the `_sdk` to access all method calls, keeping in mind that most of them are **asynchronous**. Example:

<pre class="language-csharp"><code class="lang-csharp"><strong>configResult = await _sdk.GetConfigsAsync(new List&#x3C;string> { "dynamic_difficulty" });
</strong></code></pre>

## Setup

1. In Unity's Hierarchy View, right click (on an empty area) and select `Metica > Add SDK`. This will add a prefab\
   with the MeticaUnitySdk component attached (if not already in scene). Alternatively you can manually drag and drop the prefab from `Packages/Metica Unity Sdk/Runtime/Unity/Prefabs/`.
2. Select the prefab and click the `Create Configuration` button to create and save it in the folder you select.\*
3. Select the configuration file and fill the fields (see [SDK Configuration](#sdk-configuration))
4. If needed, add the file to the MeticaSdk prefab. When you use the *Create Configuration* button the configuration should be automatically linked.

\*: Alternatively, create a configuration asset by right-clicking a folder in the Project View and selecting `Create > Metica > SDK > New SDK Configuration`. This can also be found in the main menu under `Assets > Create > Metica` but it will create the asset in the Assets' root.

### SDK Configuration

The SDK configuration exposes the following parameters:

| Property        | Description                                                                                                                                                                                                                                                                                                                  |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey`        | Your API key                                                                                                                                                                                                                                                                                                                 |
| `appId`         | The application identifier accessible from Metica's dashboard.                                                                                                                                                                                                                                                               |
| `initialUserId` | A string that identifies a user. This can change during the lifetime of your app/game so, for example and depending on your needs, this could be a temporary id like "guest" that later becomes a specific userId, or it can be the current user's id if it's already identified.                                            |
| `Log Level`     | The level of the SDK's logs. Do not confuse this with the meaning *Log* in the context of event logging. The valid values are provided by the enumeration `Metica.SDK.LogLevel` and determine the verbosity of the logs with `Info` being the most verbose and `Debug` being the least verbose. `Off`suppresses all logging. |

## SmartConfigs

The `GetConfig` method can be used to retrieve the *SmartConfigs*.\
\
Because the result is specific to each application, the SDK represents it in a generic manner, through an `Dictionary<string, object>` instance.\
Each entry in the dictionary represents a configuration key and its value. The latter is expected to be valid json.

The operation can be passed a dictionary of user properties and the device details, as a `DeviceInfo` instance (see [Device Info](#device-info)), in order to personalise the returned configuration. The personalisation happens on the server side, based on the configured variations and experimentation setup.

**Signature**

```csharp
Task<ConfigResult> GetConfigsAsync(List<string> configKeys = null, Dictionary<string, object> userData = null, DeviceInfo deviceInfo = null);
```

**Example**

```csharp
var configResult = await _sdk.GetConfig(
    configKeys: new string[] { "key1", "key2" },
    responseCallback: result => { 
        if (result.Error == null) 
        {             
            Debug.Log(result.Result.Configs["key1"]);             
        } else 
        { 
            Debug.Log("Failed to get offers: " + result.Error); 
        } 
    },
    userProperties: {
        { "someAttribute", true },
        { "another", "someValue" }
    }, 
    deviceInfo: new DeviceInfo()
        {
            appVersion = "1.0.0",
            timezone = "UTC",
            locale = "en_US",
            store = "AppStore"
        });
        
Log.Debug(() => $"Configs: {configResult}");
```

If the `configKeys` argument is `null` or empty, then all the configured keys will be returned.

Note that the server side response for this call is going to be cached according to the cache control directives returned by the server.

### Device Info

The `DeviceInfo` stores information regarding the host device, and it can differ across different devices of the same user.

**Note that leaving this variable to `null` is perfectly fine as it will trigger a device detector that is internal to the SDK**

An overview of the role of each `DeviceInfo` property:

<table><thead><tr><th>Property</th><th width="187">Type</th><th>Description</th><th>Example</th></tr></thead><tbody><tr><td>store</td><td>string</td><td>Identifies the app store related to the in-game offers. Possible values:<br>- <code>GooglePlayStore</code>, the Google store<br>- <code>AppStore</code>, the Apple store</td><td>GooglePlayStore</td></tr><tr><td>timezone</td><td>string</td><td>Device timezone expressed with <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">IANA tz identifier format</a></td><td>+01:00</td></tr><tr><td>appVersion</td><td>string</td><td>The game/app version, in <a href="https://semver.org/">SemVer</a> format</td><td>0.1.5</td></tr><tr><td>locale</td><td>string</td><td>Locale expressed as a combination of language (<strong>ISO 639</strong>) and country (<strong>ISO 3166</strong>) <a href="https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html">JDK 8 standard reference</a>.</td><td>en-US</td></tr></tbody></table>

## Events Ingestion

This section explains how to ingest events from the game into Metica. Here is a list of methods that deliver these events to Metica, and following their signatures should be sufficient for an SDK developer. Additional details about them can also be found [here](/api/integration#event-schema).

### Offer Lifecycle Events

Logs offer related events like offer display, offer purchase, and offer interaction.&#x20;

All these events have two signatures: one with and one without `productId`. If the offer linked to the event was generated by Metica, the method without `ProductId` should be used. Conversely, if the offer is a game offer created internally within the game, the method with `ProductId` must be used, passing the relevant productId (any internal string representing it) in the method.

Examples:

```csharp
void LogOfferImpressionEvent(string placementId, string offerId, Dictionary<string, object> customPayload = null);
void LogOfferImpressionEventWithProductId(string productId, Dictionary<string, object> customPayload = null);
void LogOfferInteractionEvent(string placementId, string offerId, string interactionType, Dictionary<string, object> customPayload = null);
void LogOfferInteractionEventWithProductId(string productId, string interactionType, Dictionary<string, object> customPayload = null);
void LogOfferPurchaseEvent(string placementId, string offerId, string currencyCode, double totalAmount, Dictionary<string, object> customPayload = null);
void LogOfferPurchaseEventWithProductId(string productId, string currencyCode, double totalAmount, Dictionary<string, object> customPayload = null);
```

### Full State Update

`LogFullStateUpdate` sends a **complete snapshot** of the user's state to the server, replacing any previously stored data.\
Please note that **this method fully resets the user's state on the server and expects all relevant state information to be included** in the request.\
Any user attributes that are currently stored in the server with the given `userId` but are not sent with this update, will be erased.

```csharp
Dictionary<string, object> userAttributes = new Dictionary<string, object> { { "level", 25 }, { "favoriteItem", "shield" } };
_sdk.LogFullStateUserUpdateEvent(userAttributes); 
```

### Partial State Update

`LogPartialStateUpdate` sends a **partial update** of the user's state to the server,\
modifying or adding only the provided fields while preserving those that are currently stored on the server.\
This method cannot erase existing fields (like `LogFullStateUpdate` does); it can only overwrite values or introduce new ones.

```csharp
Dictionary<string, object> userAttributes = new Dictionary<string, object> { { "level", 26 } };
_sdk.LogPartialStateUserUpdateEvent(userAttributes)
```

### Custom Event Logging

`LogCustomEvent` logs custom application events. The only required field in this dictionary is `eventType` , which Metica uses to classify the different types of events your application sends.

<pre class="language-csharp"><code class="lang-csharp"><strong>Dictionary&#x3C;string, object> customUserEvent = new Dictionary&#x3C;string, object> { { "eventType", "completed_level" }, { "eventDetails", "level 5" } };
</strong>_sdk.LogCustomEvent(userEvent);
</code></pre>

### Custom Payloads

All events, except `fullStateUpdate`, `partialStateUpdate` and `CustomEvent`, support a custom payload that can include any information.\
It's passed as a `Dictionary<string, object>` to the logging methods. To avoid confusion with other fields, it is recommended to pass the parameter by name, i.g. `LogInstall(customPayload: playerExtraParams)` rather than just `LogInstall(playerExtraParams)`.

**Example**

<pre class="language-csharp"><code class="lang-csharp">Dictionary&#x3C;string, object> myCustomPayload {
    { "set", "Intense Stare Magic Mastery" },
<strong>    { "cosmetic", "flamingo shades" },
</strong>};
_sdk.LogOfferImpressionEvent(somePlacementId, someOfferId, myCustomPayload);
_sdk.LogOfferPurchaseEvent(somePlacementId, someOfferId, "USD", 2.49, customPayload: myCustomPayload);
</code></pre>

### Event Dispatch (Flushing)

When you log an event (both with static calling or using async), it isn't guaranteed to be sent immediately as events are sent in bulks.

If you need to make sure events are immediately sent to the ingestion endpoint, you can use the `RequestDispatchEvents` call but **don't overuse it as the default behaviour helps aggregating events for lower network load**.

## Ads Integration

This section explains how to integrate Metica's **interstitial ads** into your Unity project. Metica's streamlined API simplifies ad integration by automatically handling ad unit IDs and optimizing ad selection.

This guide assumes that the AppLovin SDK has already been configured. Otherwise please follow [this](https://developers.applovin.com/en/max/unity/overview/integration#initialize-the-sdk) guide. MeticaAds requires AppLovin SDK version ≥ 13.

<figure><img src="/files/aJPtWDRJdqRkfRvzQ9CM" alt=""><figcaption></figcaption></figure>

### Initialization

The `IsMeticaAdsEnabled` flag is a boolean variable that determines whether a specific user will receive ads optimized by Metica's AI or will be part of a holdout group receiving ads directly through the standard AppLovin MAX SDK. This flag is set during the SDK initialization and is used to A/B test the revenue performance of Metica's ad optimization against the baseline performance of the MAX SDK.

**Important: `MeticaAds.InitializeAsync()` must be called BEFORE initializing the AppLovin MAX SDK to ensure proper integration.**

```csharp
/// <summary>
/// Complete initialization flow for MeticaAds and MAX SDK integration
/// This method demonstrates the proper order and setup required for ad functionality
/// Call this method early in your app lifecycle, typically in Start() or Awake()
/// </summary>
public async void InitializeAds()
{
    // Step 1: Set User Identification (Critical Requirement)
    // Important: UserId must be set for the current user before any MeticaAds initialization
    // This unique identifier is used for:
    // - User analytics and behavior tracking
    // - A/B testing and holdout group management
    // - Revenue attribution and reporting
    // - Personalized ad targeting and optimization
    MeticaSdk.CurrentUserId = "your_unique_user_id";

    // Step 2: Configure MeticaAds initialization
    // Create the configuration object that will be passed to the MeticaAds SDK
    var meticaConfiguration = new MeticaConfiguration();
    
    // Step 3: Initialize MeticaAds SDK (Asynchronous Operation)
    // This call performs the core MeticaAds setup including:
    // - SDK authentication and configuration validation
    // - Network connectivity and endpoint verification  
    // - Platform-specific delegate initialization
    // - Ad callback registration and event system setup
    // Returns an object which:
    // - IsMeticaAdsEnabled = true if the current user is in trial
    // - IsMeticaAdsEnabled = false if the current user is in holdout
    // - Status: Normal (= trial), Holdout, HoldoutDueToError (= user is on holdout because the initialization has failed)
    var result = await MeticaAds.InitializeWithResultAsync(meticaConfiguration);
    IsMeticaAdsEnabled = result.IsMeticaAdsEnabled;
    if (result.getStatus == MeticaAdsAssignmentStatus.HoldoutDueToError) {
        Debug.Log("The current user is treated as holdout because the initialization has failed")
    }
    

    // Step 4: Initialize AppLovin MAX SDK (Traditional Synchronous Setup)
    // Set the SDK key that identifies your app in the AppLovin dashboard
    // This key is essential for ad serving, reporting, and revenue tracking
    MaxSdk.SetSdkKey("YOUR_MAX_SDK_KEY"); // Replace with your actual MAX SDK key
    
    // Set MAX initialization callback
    MaxSdkCallbacks.OnSdkInitializedEvent += sdkConfiguration =>
    {
        // AppLovin SDK is initialized, now start loading ads
        Debug.Log("MAX SDK Initialized");

        InitializeInterstitialAds();
        InitializeRewardedAds();
    };
    
    // Initialize the MAX SDK - this sets up the underlying ad infrastructure
    // Must be called after MeticaAds initialization to ensure proper integration
    MaxSdk.InitializeSdk();
}
```

### Holdout tracking

When making use of user holdout functionality it is important to implement following callbacks so comparison and analytics are possible. See the sample implementations further below for [interstitial](#loading-an-interstitial-a-d) and [rewarded](#loading-a-rewarded-a-d) ads.

```
MeticaAds.NotifyAdLoadAttempt(string adUnitId)
MeticaAds.NotifyAdLoadSuccess(MeticaAd meticaAd)
MeticaAds.NotifyAdLoadFailed(string adUnitId, string error)
MeticaAds.NotifyAdShowSuccess(MeticaAd meticaAd)
```

**Important: Only log ad events for types of interstitial/rewarded, others (banners) are not supported by Metica.**

### Interstitial ads

Interstitial ads are full-screen or full-page ads that temporarily cover an app’s interface. They’re typically shown at natural pauses or transition points, such as after completing a level in a game or when navigating between major views.

The following sections show you how to load and then show an interstitial ad.

### Loading an Interstitial Ad

The following code shows you how to attach listeners and load the first interstitial ad.

```csharp
private void InitializeInterstitialAds()
{
    // Attach callbacks based on whether MeticaAds is enabled
    if (IsMeticaAdsEnabled)
    {
        // Use MeticaAds callbacks with extension methods to convert to MAX callback signatures
        MeticaAdsCallbacks.Interstitial.OnAdLoadSuccess += (meticaAd) => OnInterstitialLoadedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Interstitial.OnAdLoadFailed += (error) => OnInterstitialFailedEvent("", error);
        MeticaAdsCallbacks.Interstitial.OnAdShowSuccess += (meticaAd) => OnInterstitialDisplayedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Interstitial.OnAdShowFailed += (meticaAd, error) => InterstitialFailedToDisplayEvent(meticaAd.adUnitId, error);
        MeticaAdsCallbacks.Interstitial.OnAdClicked += (meticaAd) => OnInterstitialClickedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Interstitial.OnAdHidden += (meticaAd) => OnInterstitialDismissedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Interstitial.OnAdRevenuePaid += (meticaAd) => OnInterstitialRevenuePaidEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
    }
    else
    {
        // Use standard MAX SDK callbacks
        MaxSdkCallbacks.Interstitial.OnAdLoadedEvent += OnInterstitialLoadedEvent;
        MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent += (adUnitId, adInfo) => OnInterstitialFailedEvent(adUnitId, adInfo.Message);
        MaxSdkCallbacks.Interstitial.OnAdDisplayedEvent += OnInterstitialDisplayedEvent;
        MaxSdkCallbacks.Interstitial.OnAdDisplayFailedEvent += (adUnitId, errorInfo, adInfo) => InterstitialFailedToDisplayEvent(adUnitId, errorInfo.Message);
        MaxSdkCallbacks.Interstitial.OnAdClickedEvent += OnInterstitialClickedEvent;
        MaxSdkCallbacks.Interstitial.OnAdHiddenEvent += OnInterstitialDismissedEvent;
        MaxSdkCallbacks.Interstitial.OnAdRevenuePaidEvent += OnInterstitialRevenuePaidEvent;
    }

    // Load the first interstitial
    LoadInterstitial();
}

void LoadInterstitial()
{
    interstitialStatusText.text = "Loading...";
    if (IsMeticaAdsEnabled)
    {
        MeticaAds.LoadInterstitial();
    }
    else
    {
        MeticaAds.NotifyAdLoadAttempt(InterstitialAdUnitId);
        MaxSdk.LoadInterstitial(InterstitialAdUnitId);
    }
}

private void OnInterstitialLoadedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    if (!IsMeticaAdsEnabled)
    {
        MeticaAds.NotifyAdLoadSuccess(adInfo.ToMeticaAd());
    }

    // Interstitial ad is ready to be shown
    interstitialStatusText.text = $"Loaded {adUnitId}";
    Debug.Log("Interstitial loaded");
    
    // Reset retry attempt
    interstitialRetryAttempt = 0;
}

private void OnInterstitialFailedEvent(string adUnitId, string error)
{
    if (!IsMeticaAdsEnabled)
    {
        MeticaAds.NotifyAdLoadFailed(adUnitId, error);
    }

    // Interstitial ad failed to load. Retry with exponentially higher delays up to a maximum delay
    interstitialRetryAttempt++;
    double retryDelay = Math.Pow(2, Math.Min(6, interstitialRetryAttempt));
    
    interstitialStatusText.text = $"Load failed {adUnitId}: " + error + "\nRetrying in " + retryDelay + "s...";
    Debug.Log("Interstitial failed to load with error: " + error);
    
    Invoke("LoadInterstitial", (float)retryDelay);
}

private void OnInterstitialDisplayedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    Debug.Log($"Interstitial shown {adUnitId}");
}

private void InterstitialFailedToDisplayEvent(string adUnitId, string error)
{
    // Interstitial ad failed to display. Load the next ad
    Debug.Log("Interstitial failed to display: " + error);
    LoadInterstitial();
}

private void OnInterstitialClickedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    Debug.Log($"Interstitial clicked {adUnitId}");
}

private void OnInterstitialDismissedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    // Interstitial ad is hidden. Pre-load the next ad
    Debug.Log("Interstitial dismissed");
    LoadInterstitial();
}

private void OnInterstitialRevenuePaidEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    if (!IsMeticaAdsEnabled)
    {
        MeticaAds.NotifyAdShowSuccess(adInfo.ToMeticaAd());
    }
    
    // Interstitial ad revenue paid. Use this callback to track user revenue
    double revenue = adInfo.Revenue;
    string countryCode = MaxSdk.GetSdkConfiguration().CountryCode;
    string networkName = adInfo.NetworkName;
    string adUnitIdentifier = adInfo.AdUnitIdentifier;
    string placement = adInfo.Placement;
    
    Debug.Log($"Interstitial revenue paid - AdUnit: {adUnitIdentifier}, Revenue: {revenue}, Network: {networkName}, Country: {countryCode}, Placement: {placement}");
}
```

### Showing an Interstitial Ad

To show an interstitial ad, call `ShowInterstitial()`:

<pre class="language-csharp"><code class="lang-csharp">void ShowInterstitial()
{
    if (IsMeticaAdsEnabled) {
<strong>        if (MeticaAds.IsInterstitialReady())
</strong>        {
            MeticaAds.ShowInterstitial();
        }
        else
        {
            interstitialStatusText.text = "Ad not ready";
        }
    }
    else
    {
        if (MaxSdk.IsInterstitialReady(InterstitialAdUnitId)) 
        {
            intersitialStatusText.text = "Showing";
            MaxSdk.ShowInterstitial(InterstitialAdUnitId);
        }
        else 
        {
            interstitialStatusText.text = "Ad not ready";
        }
    }
}

</code></pre>

### Rewarded Ads

Rewarded ads are video ads that users can choose to watch in exchange for in-app rewards.

### Loading a Rewarded Ad

The `InitializeRewardedAds` method attaches listeners for both Metica and MAX SDK rewarded ad callbacks and then loads the first rewarded ad.

```csharp
private void InitializeRewardedAds()
{
    // Attach callbacks based on whether MeticaAds is enabled
    if (IsMeticaAdsEnabled)
    {
        // Use MeticaAds callbacks with extension methods to convert to MAX callback signatures
        MeticaAdsCallbacks.Rewarded.OnAdLoadSuccess += (meticaAd) => OnRewardedAdLoadedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Rewarded.OnAdLoadFailed += (error) => OnRewardedAdFailedEvent("", error);
        MeticaAdsCallbacks.Rewarded.OnAdShowSuccess += (meticaAd) => OnRewardedAdDisplayedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Rewarded.OnAdShowFailed += (meticaAd, error) => OnRewardedAdFailedToDisplayEvent(meticaAd.adUnitId, error);
        MeticaAdsCallbacks.Rewarded.OnAdClicked += (meticaAd) => OnRewardedAdClickedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Rewarded.OnAdHidden += (meticaAd) => OnRewardedAdDismissedEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
        MeticaAdsCallbacks.Rewarded.OnAdRewarded += (meticaAd) => OnRewardedAdReceivedRewardEvent
        (
            meticaAd.adUnitId,
            new MaxSdkBase.Reward { Label = "Coin", Amount = 1 },
            meticaAd.ToAdInfo()
        );
        MeticaAdsCallbacks.Rewarded.OnAdRevenuePaid += (meticaAd) => OnRewardedAdRevenuePaidEvent(meticaAd.adUnitId, meticaAd.ToAdInfo());
    }
    else
    {
        // Use standard MAX SDK callbacks
        MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += OnRewardedAdLoadedEvent;
        MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += (adUnitId, errorInfo) => OnRewardedAdFailedEvent(adUnitId, errorInfo.Message);
        MaxSdkCallbacks.Rewarded.OnAdDisplayedEvent += OnRewardedAdDisplayedEvent;
        MaxSdkCallbacks.Rewarded.OnAdDisplayFailedEvent += (adUnitId, errorInfo, adInfo) => OnRewardedAdFailedToDisplayEvent(adUnitId, errorInfo.Message);
        MaxSdkCallbacks.Rewarded.OnAdClickedEvent += OnRewardedAdClickedEvent;
        MaxSdkCallbacks.Rewarded.OnAdHiddenEvent += OnRewardedAdDismissedEvent;
        MaxSdkCallbacks.Rewarded.OnAdReceivedRewardEvent += OnRewardedAdReceivedRewardEvent;
        MaxSdkCallbacks.Rewarded.OnAdRevenuePaidEvent += OnRewardedAdRevenuePaidEvent;
    }

    // Load the first rewarded ad
    LoadRewardedAd();
}

private void LoadRewardedAd()
{
    rewardedStatusText.text = "Loading...";
    if (IsMeticaAdsEnabled)
    {
        MeticaAds.LoadRewarded();
    }
    else
    {
        MeticaAds.NotifyAdLoadAttempt(RewardedAdUnitId);
        MaxSdk.LoadRewardedAd(RewardedAdUnitId);
    }
}

private void OnRewardedAdLoadedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{    
    if (!IsMeticaAdsEnabled)
    {
        MeticaAds.NotifyAdLoadSuccess(adInfo.ToMeticaAd());
    }

    // Rewarded ad is ready to be shown
    rewardedStatusText.text = $"Loaded {adUnitId}";
    Debug.Log("Rewarded ad loaded");

    // Reset retry attempt
    rewardedRetryAttempt = 0;
}

private void OnRewardedAdFailedEvent(string adUnitId, string error)
{
    if (!IsMeticaAdsEnabled)
    {
        MeticaAds.NotifyAdLoadFailed(adUnitId, error);
    }
        
    // Rewarded ad failed to load. Retry with exponentially higher delays up to a maximum delay
    rewardedRetryAttempt++;
    double retryDelay = Math.Pow(2, Math.Min(6, rewardedRetryAttempt));

    rewardedStatusText.text = $"Load failed {adUnitId}: " + error + "\nRetrying in " + retryDelay + "s...";
    Debug.Log("Rewarded ad failed to load with error: " + error);

    Invoke("LoadRewardedAd", (float)retryDelay);
}

private void OnRewardedAdDisplayedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    Debug.Log($"Rewarded ad displayed {adUnitId}");
}

private void OnRewardedAdFailedToDisplayEvent(string adUnitId, string error)
{
    // Rewarded ad failed to display. Load the next ad
    Debug.Log("Rewarded ad failed to display: " + error);
    LoadRewardedAd();
}

private void OnRewardedAdClickedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    Debug.Log($"Rewarded ad clicked {adUnitId}");
}

private void OnRewardedAdDismissedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    // Rewarded ad is hidden. Pre-load the next ad
    Debug.Log("Rewarded ad dismissed");
    LoadRewardedAd();
}

private void OnRewardedAdReceivedRewardEvent(string adUnitId, MaxSdk.Reward reward, MaxSdkBase.AdInfo adInfo)
{
    // Rewarded ad was displayed and user should receive the reward
    Debug.Log("Rewarded ad received reward");
    // Implement your reward logic here
}

private void OnRewardedAdRevenuePaidEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
    if (!IsMeticaAdsEnabled)
    {
        MeticaAds.NotifyAdShowSuccess(adInfo.ToMeticaAd());
    }
    
    // Rewarded ad revenue paid. Use this callback to track user revenue
    double revenue = adInfo.Revenue;
    string countryCode = MaxSdk.GetSdkConfiguration().CountryCode;
    string networkName = adInfo.NetworkName;
    string adUnitIdentifier = adInfo.AdUnitIdentifier;
    string placement = adInfo.Placement;

    Debug.Log($"Rewarded ad revenue paid - AdUnit: {adUnitIdentifier}, Revenue: {revenue}, Network: {networkName}, Country: {countryCode}, Placement: {placement}");
}
```

### Showing a Rewarded Ad

To show an interstitial ad, call `ShowRewarded()`:

```csharp
void ShowRewardedAd()
{
	if (IsMeticaAdsEnabled)
	{
		if (MeticaAds.IsRewardedReady())
		{
			rewardedStatusText.text = "Showing";
			MeticaAds.ShowRewarded();
		}
		else
		{
			rewardedStatusText.text = "Ad not ready";
		}
	}
	else 
	{
		if (MaxSdk.IsRewardedAdReady(RewardedAdUnitId))
		{
			rewardedStatusText.text = "Showing";
			MaxSdk.ShowRewardedAd(RewardedAdUnitId);
		}
		else
		{
			rewardedStatusText.text = "Ad not ready";
		}
	}
}

```

## Metica SDK vs AppLovin Interstitial API

### API Calls

Metica's API in contrast to AppLovin does not require an `adUnitId` to be passed. The reason is that Metica's AI technology automatically selects and manages the optimal ad unit IDs behind the scenes, eliminating the need for developers to manually specify these IDs in each function call and simplifying integration.

#### Metica Ads API

```csharp
MeticaAds.LoadInterstitial();        
MeticaAds.IsInterstitialReady();                
MeticaAds.ShowInterstitial();

MeticaAds.LoadRewarded();        
MeticaAds.IsRewardedAdReady();                
MeticaAds.ShowRewarded();
```

#### Max SDK API

```csharp
MaxSdk.LoadInterstitialAd(InterstitialAdUnitId);  
MaxSdk.IsInterstitialAdReady(InterstitialAdUnitId); 
MaxSdk.ShowInterstitialAd(InterstitialAdUnitId);

MaxSdk.LoadRewardedAd(RewardedAdUnitId);  
MaxSdk.IsRewardedAdReady(RewardedAdUnitId); 
MaxSdk.ShowRewardedAd(RewardedAdUnitId);
```

### Callbacks

#### Metica Ad Callbacks

```csharp
MeticaAdsCallbacks.Interstitial.OnAdLoadSuccess
MeticaAdsCallbacks.Interstitial.OnAdLoadFailed
MeticaAdsCallbacks.Interstitial.OnAdShowSuccess
MeticaAdsCallbacks.Interstitial.OnAdShowFailed
MeticaAdsCallbacks.Interstitial.OnAdClicked
MeticaAdsCallbacks.Interstitial.OnAdHidden

MeticaAdsCallbacks.Rewarded.OnAdLoadSuccess
MeticaAdsCallbacks.Rewarded.OnAdLoadFailed
MeticaAdsCallbacks.Rewarded.OnAdShowSuccess
MeticaAdsCallbacks.Rewarded.OnAdShowFailed
MeticaAdsCallbacks.Rewarded.OnAdClicked
MeticaAdsCallbacks.Rewarded.OnAdHidden
```

#### Max Ad Callback

```csharp
MaxSdkCallbacks.Interstitial.OnAdLoadedEvent
MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent
MaxSdkCallbacks.Interstitial.OnAdDisplayedEvent
MaxSdkCallbacks.Interstitial.OnAdDisplayFailedEvent
MaxSdkCallbacks.Interstitial.OnAdClickedEvent
MaxSdkCallbacks.Interstitial.OnAdHiddenEvent

MaxSdkCallbacks.Rewarded.OnAdLoadedEvent
MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent
MaxSdkCallbacks.Rewarded.OnAdDisplayedEvent
MaxSdkCallbacks.Rewarded.OnAdDisplayFailedEvent
MaxSdkCallbacks.Rewarded.OnAdClickedEvent
MaxSdkCallbacks.Rewarded.OnAdHiddenEvent
```

## Code Samples

The Metica Unity SDK package includes examples that can be easily imported into your Assets by selecting the package in the Package Manager and clicking the *Import* button next to one of the examples.

Currently available examples:

* **MeticaExample01** shows how to use the SDK via `MeticaAPI`(deprecated). This uses the static calls code style.
* **MeticaExample02** shows how to use the SDK by retrieving the instance and using asynchronous code style.

## Privacy Manifest

For **iOS**, **iPadOS**, **tvOS** and **watchOS** apps, we provide a [privacy manifest](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files) at [Assets/Plugins/PrivacyInfo.xcprivacy](https://github.com/meticalabs/dev-docs/blob/main/docs/Assets/Plugins/PrivacyInfo.xcprivacy) that describes the data collected by the Metica SDK.


# REST API

Send data to and query Metica

## Overview

This document provides a comprehensive guide for integrating with Metica's REST API. It covers prerequisites, endpoints, request headers, authentication, payload structure, and advanced usage examples for event ingestion, personalized offers, smart configurations, and useful event data.

***

## Prerequisites

Before initiating API requests, the Metica onboarding team will provide the following:

* **API Key**: Used to authenticate requests.
* **Application ID**: Used to identify the target application.

These credentials will be referred to as `API_KEY` and `APP_ID` throughout this document.

***

## API Conventions

### Base Endpoint

All API requests should be directed to the following base endpoint:

<https://api-gateway.prod-eu.metica.com>

***

### Request Headers

Metica's API uses JSON for both request and response payloads. Additionally, some `GET` endpoints may use parameters formatted as `application/x-www-form-urlencoded` query strings in the URL.

<table><thead><tr><th width="216">Header Name</th><th>Expected Value</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/json; charset=utf-8</code></td></tr></tbody></table>

***

## Authentication

Metica's API uses API keys for authentication and authorization. Include your API key in the `X-API-KEY` header for all API requests.

#### Example Using `curl`

```bash
curl --url https://api-gateway.prod-eu.metica.com/... \
  --header 'X-API-KEY: <API_KEY>'
```

***

## HTTP Status Codes

Metica's API utilises standard HTTP status codes to indicate the outcome of an operation.

<table><thead><tr><th width="100">Code</th><th>Description</th></tr></thead><tbody><tr><td>200</td><td><strong>Successful Request</strong> - The request was processed successfully.</td></tr><tr><td>202</td><td><strong>Accepted</strong> - The payload was accepted, and the request was completed successfully.</td></tr><tr><td>400</td><td><strong>Bad Request</strong> - The request is invalid (e.g., malformed syntax or missing parameters).</td></tr><tr><td>401</td><td><strong>Unauthorized</strong> - The request lacks valid authentication credentials.</td></tr><tr><td>403</td><td><strong>Forbidden</strong> - The API key does not have sufficient permissions for the requested operation.</td></tr><tr><td>404</td><td><strong>Not Found</strong> - The requested resource could not be located.</td></tr><tr><td>429</td><td><strong>Too Many Requests</strong> - The rate limit has been exceeded; retry after a delay.</td></tr><tr><td>5xx</td><td><strong>Internal Server Error</strong> - The server encountered an unexpected condition that prevented it from completing the request.</td></tr></tbody></table>

In cases of client errors (e.g., `400`), responses may include a payload describing the invalid part of the request to assist in troubleshooting.

***

## Event Ingestion

The event ingestion endpoint is the primary method for submitting user in-app behavior data to Metica.

### Endpoint

`/ingest/v1/events`

***

### Request Headers

<table><thead><tr><th width="220">Header Name</th><th>Expected Value</th></tr></thead><tbody><tr><td><code>X-API-KEY</code></td><td>Your unique API key.</td></tr><tr><td><code>Content-Type</code></td><td><code>application/json; charset=utf-8</code></td></tr></tbody></table>

***

### Request Payload

The payload should include a list of events, with each event containing key-value pairs representing individual user actions.

#### Example Payload

```json5
{
  "events": [
    {
      "userId": "u1",
      "appId": "<APP_ID>",
      "eventTime": "2024-11-28T15:19:07.458Z",
      "eventType": "purchase",
      "offerId": "a",
      "totalAmount": 1
    },
    {
      "userId": "u2",
      "appId": "<APP_ID>",
      "eventTime": "2024-11-28T15:24:07.000Z",
      "eventType": "login"
    }
  ]
}
```

***

### Example Request Using `curl`

```bash
curl --request POST \
  --url https://api-gateway.prod-eu.metica.com/ingest/v1/events \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: <API_KEY>' \
  --data '{
    "events": [
      {
        "userId": "u1",
        "appId": "<APP_ID>",
        "eventTime": "2024-11-28T15:19:07.458Z",
        "eventType": "purchase",
        "offerId": "a",
        "totalAmount": 1
      },
      {
        "userId": "u2",
        "appId": "<APP_ID>",
        "eventTime": "2024-11-28T15:24:07.000Z",
        "eventType": "login"
      }
    ]
  }'
```

***

### Response Codes

<table><thead><tr><th width="100">Code</th><th>Description</th></tr></thead><tbody><tr><td>202</td><td>The request was processed successfully. <strong>Note</strong> A 202 response indicates the request was accepted but does not guarantee the validity of the payload content.</td></tr><tr><td>4xx</td><td>Client error; refer to the HTTP Status Codes section for details.</td></tr><tr><td>5xx</td><td>Server error; refer to the HTTP Status Codes section for details.</td></tr></tbody></table>

***

## Event Schema

This section lists the schema for core events that Metica can ingest. Each core event requires specific properties to be included in the event payload, as described below.

### Base Fields

The base fields are **common** to **all core events** and must be included in every event payload.

<table><thead><tr><th width="172">Name</th><th width="151">Type</th><th width="100">Required</th><th>Description</th></tr></thead><tbody><tr><td>eventType</td><td>string</td><td>Yes</td><td>Enum that describes the type of the event (check <a href="#core-events">core events</a>).</td></tr><tr><td>eventId</td><td>string</td><td>No</td><td>Uniquely identifies the event; if undefined, it will be created at ingestion time by Metica.</td></tr><tr><td>appId</td><td>string</td><td>Yes</td><td>Refers to the external application ID defined during onboarding.</td></tr><tr><td>eventTime</td><td><p>bigint | </p><p><a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601 string</a></p></td><td>Yes</td><td>UTC timestamp of when the event occurred.</td></tr><tr><td>userId</td><td>string</td><td>Yes</td><td>Uniquely identifies the user within this application.</td></tr><tr><td>customPayload</td><td>object</td><td>No</td><td>Represents a custom object defined by each client. This property is valid for all events apart from <code>Full</code> and <code>Partial User State Update</code></td></tr></tbody></table>

***

### Core Events

#### Purchase

An event triggered every time an in-app purchase is made within the game.

<table><thead><tr><th width="188">Name</th><th width="146">Type</th><th width="100">Required</th><th>Description</th></tr></thead><tbody><tr><td>productId</td><td>string</td><td>No</td><td>Unique ID of an in-game product. Only one between <code>productId</code> and <code>meticaAttributes</code> should be populated.</td></tr><tr><td>meticaAttributes</td><td>object</td><td>No</td><td>Object provided by Metica, takes priority over <code>productId</code>. This object will be returned by the <a href="#example-response">Personalized Offer</a> endpoint</td></tr><tr><td>currencyCode</td><td>string | <a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 string</a></td><td>Yes</td><td>Currency of the purchase (<code>fiat</code> or <code>in-game</code> currency)</td></tr><tr><td>totalAmount</td><td>number</td><td>Yes</td><td>Total amount of the purchase expressed as double.</td></tr></tbody></table>

**Example Payload:**

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "purchase",
  "meticaAttributes": {
    "offer": {
      "offerId": "offer123",
      "variantId": "variant456",
      "bundleId": "bundle789"
    },
    "placementId": "placement001"
  },
  "currencyCode": "EUR",
  "totalAmount": 25.00
}
```

or

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "purchase",
  "productId": "12345",
  "currencyCode": "EUR",
  "totalAmount": 25.50
}
```

***

#### Offer Impression

This event triggers every time an offer is displayed.

<table><thead><tr><th width="228">Name</th><th width="100">Type</th><th width="100">Required</th><th>Description</th></tr></thead><tbody><tr><td>productId</td><td>string</td><td>No</td><td>Unique ID of an in-game product. Only one between <code>productId</code> and <code>meticaAttributes</code> should be populated.</td></tr><tr><td>meticaAttributes</td><td>object</td><td>No</td><td>This object will be returned by the <a href="#example-response">Personalized Offer</a> endpoint</td></tr></tbody></table>

**Example Payload:**

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "impression",
  "meticaAttributes": {
    "placementId": "placement001",
    "offer": {
      "offerId": "offer123",
      "variantId": "variant456",
      "bundleId": "bundle789"
    }
  }
}
```

or

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "impression",
  "productId": "12345"
}
```

***

#### Offer Interaction

Event triggers every time an interaction is made with an offer (e.g., click, dismiss).

<table><thead><tr><th width="229">Name</th><th width="100">Type</th><th width="100">Required</th><th>Description</th></tr></thead><tbody><tr><td>productId</td><td>string</td><td>No</td><td>Unique ID of an in-game product. Mandatory if <code>meticaAttributes</code> is missing.</td></tr><tr><td>interactionType</td><td>string</td><td>No</td><td>String that defines the type of interaction, i.e. <code>click</code>, <code>rejected</code>, etc...; mandatory if <code>meticaAttributes</code> is missing.</td></tr><tr><td>meticaAttributes</td><td>object</td><td>No</td><td>This object will be returned by the <a href="#example-response">Personalized Offer</a> endpoint</td></tr></tbody></table>

**Example Payload:**

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "interaction",
  "meticaAttributes": {
    "placementId": "placement001",
    "offer": {
      "offerId": "offer123",
      "variantId": "variant456",
      "bundleId": "bundle789"
    },
    "interactionType": "click"
  }
}
```

or

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "interaction",
  "productId": "12345",
  "interactionType": "click"
}
```

***

#### AdRevenue

An event triggered every time revenue is generated by an ad within the game.

<table><thead><tr><th width="188">Name</th><th width="146">Type</th><th width="100">Required</th><th>Description</th></tr></thead><tbody><tr><td>currencyCode</td><td><a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 string</a></td><td>Yes</td><td>Currency of the total amount</td></tr><tr><td>totalAmount</td><td>number</td><td>Yes</td><td>Total amount generated by the ad expressed as double.</td></tr><tr><td>placement</td><td>string</td><td>No</td><td>String that represents the place where the ad has been displayed inside the game, i.e. <code>shop_daily_reward</code></td></tr><tr><td>type</td><td>string</td><td>No</td><td>Type of ad that has been displayed, i.e. <code>INTER</code>, <code>REWARDED</code>, etc...</td></tr><tr><td>source</td><td>string</td><td>No</td><td>Source of the ad, i.e. <code>Unity Ads</code>, <code>Liftoff Monetize</code>, etc... </td></tr></tbody></table>

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "adRevenue",
  "currencyCode": "EUR",
  "totalAmount": 0.00123373762,
  "placement": "shop_daily_reward_double",
  "type": "video",
  "source": "unity"
}
```

***

#### Full User State Update

Event triggers at regular intervals, indicating the user’s current state.

{% hint style="info" %}
Be aware this event doesn't allow the `customPayload` property
{% endhint %}

<table><thead><tr><th>Name</th><th width="92">Type</th><th width="120">Required</th><th>Description</th></tr></thead><tbody><tr><td>userStateAttributes</td><td>object</td><td>Yes</td><td>Attributes that represent the user's current state.</td></tr></tbody></table>

**Example Payload:**

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "fullStateUpdate",
  "userStateAttributes": {
    "level": 5,
    "inventoryBalance": 100,
    "isActive": true
  }
}
```

***

#### Partial User State Update

Triggers when the user state changes, updating only the affected properties.

<table><thead><tr><th>Name</th><th width="96">Type</th><th width="115">Required</th><th>Description</th></tr></thead><tbody><tr><td>userStateAttributes</td><td>object</td><td>Yes</td><td>Attributes that have changed values in the user's state.</td></tr></tbody></table>

**Example Payload:**

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "partialStateUpdate",
  "userStateAttributes": {
    "level": 6
  }
}
```

***

#### Login

Event triggers when the user enters the app, one event per session.

<table><thead><tr><th>Name</th><th width="100">Type</th><th width="124">Required</th><th>Description</th></tr></thead><tbody><tr><td>(No Fields)</td><td></td><td>Yes</td><td>No additional fields required.</td></tr></tbody></table>

**Example Payload:**

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "login"
}
```

***

#### Install

Event triggers when the user installs the app.

<table><thead><tr><th>Name</th><th width="100">Type</th><th width="129">Required</th><th>Description</th></tr></thead><tbody><tr><td>(No Fields)</td><td></td><td>Yes</td><td>No additional fields required.</td></tr></tbody></table>

**Example Payload:**

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "<APP_ID>",
  "eventTime": "2024-11-01T10:00:00Z",
  "userId": "d8973ecd-c2b5-4efb-8b91-935ae4c5b201",
  "eventType": "install"
}
```

***

## Personalized Offers

The personalized offers endpoint enables you to fetch offers tailored to specific users based on their [attributes](#full-user-state-update) or [device](#request-body-schema) information. These offers are returned grouped by placements. Each placement can contain one or more offers.

***

### Endpoint

`POST /offers/v1/apps/<APP_ID>`

Use this endpoint to retrieve personalized offers for specific placements within your app.

***

### Request Headers

<table><thead><tr><th width="206">Header Name</th><th>Expected Value</th></tr></thead><tbody><tr><td><code>X-API-KEY</code></td><td>Your unique API key.</td></tr><tr><td><code>Content-Type</code></td><td><code>application/json; charset=utf-8</code></td></tr></tbody></table>

***

### Request Parameters

<table><thead><tr><th width="155">Name</th><th width="80">Type</th><th width="99">Required</th><th width="242">Description</th><th>Example</th></tr></thead><tbody><tr><td><code>placements</code></td><td>string</td><td>No</td><td>A comma-separated list of placements for which offers should be returned. If not provided, offers for all placements will be returned.</td><td><code>?placements=main,shop</code></td></tr></tbody></table>

***

### Request Body Schema

A JSON-encoded body must be included in the request. The request body should adhere to the following schema:

<table><thead><tr><th width="216">Field Name</th><th width="90">Type</th><th width="101">Required</th><th width="202">Description</th><th>Example</th></tr></thead><tbody><tr><td><code>userId</code></td><td>string</td><td>Yes</td><td>Unique identifier for the user.</td><td><code>"abc"</code></td></tr><tr><td><code>deviceInfo</code></td><td>object</td><td>No</td><td>Information about the user's device. Can be partially filled.</td><td>See fields below.</td></tr><tr><td><code>deviceInfo.store</code></td><td>string</td><td>No</td><td>The platform's app store (<code>AppStore</code> or <code>GooglePlayStore</code>)</td><td><code>"AppStore"</code></td></tr><tr><td><code>deviceInfo.timezone</code></td><td>string</td><td>No</td><td>Device timezone as an IANA identifier or UTC offset.</td><td><code>"Europe/London"</code> or <code>"+05:00"</code></td></tr><tr><td><code>deviceInfo.appVersion</code></td><td>string</td><td>No</td><td>The app version, following the Semantic Versioning format.</td><td><code>"1.4.5"</code></td></tr><tr><td><code>deviceInfo.locale</code></td><td>string</td><td>No</td><td>The user's locale, formatted according to IETF BCP 47.</td><td><code>"en-US"</code></td></tr><tr><td><code>userData</code></td><td>object</td><td>No</td><td>Real-time user state data to override pre-ingested <a href="#Full-User-State-Update">user state attributes</a>, conforms to its <code>userStateAttributes</code> property.</td><td>See example below.</td></tr></tbody></table>

**Example Request Body:**

```json
{
  "userId": "abc",
  "deviceInfo": {
    "store": "AppStore",
    "timezone": "+05:00",
    "appVersion": "1.4.5",
    "locale": "en-US"
  },
  "userData": {
    "level": 10,
    "inventory": {
      "potion": {
        "quantity": 30
      }
    },
    "gemBalance": 51
  }
}
```

***

### Response Schema

The response includes a list of offers for each placement, sorted from the most important to the least important. Each offer contains details about the offer's ID, items, pricing, and additional metadata.

<table><thead><tr><th width="199">Field Name</th><th width="128">Type</th><th width="287">Description</th><th>Example</th></tr></thead><tbody><tr><td><code>placements</code></td><td>object</td><td>A mapping of placement IDs to their corresponding offers.</td><td>See example below.</td></tr><tr><td><code>offerId</code></td><td>string</td><td>Unique identifier for the offer.</td><td><code>"offer_abc"</code></td></tr><tr><td><code>creativeId</code></td><td>string | undefined</td><td>Reference to the creative ID associated with the offer.</td><td><code>"creative_large"</code></td></tr><tr><td><code>creativeOverride</code></td><td>string | undefined</td><td>Override for the creative associated with the offer.</td><td><code>"override_123"</code></td></tr><tr><td><code>items</code></td><td>array</td><td>List of items included in the offer, each with an ID and quantity.</td><td>See below.</td></tr><tr><td><code>items[].id</code></td><td>string</td><td>The external ID of the item.</td><td><code>"potion"</code></td></tr><tr><td><code>items[].quantity</code></td><td>number</td><td>The quantity of the item included in the offer.</td><td><code>1</code></td></tr><tr><td><code>expirationTime</code></td><td>string | undefined</td><td>Expiration time of the offer in ISO 8601 format.</td><td><code>"2025-01-16T12:00:00Z"</code></td></tr><tr><td><code>iap</code></td><td>string | undefined</td><td>IAP product ID associated with the offer.</td><td><code>"iap_product_45"</code></td></tr><tr><td><code>price</code></td><td>number | undefined</td><td>Price of the offer.</td><td><code>50</code></td></tr><tr><td><code>discount</code></td><td>number | undefined</td><td>Discount applied to the offer, expressed as a value between 0 and 1 (e.g., 0.23 = 23%).</td><td><code>0.23</code></td></tr><tr><td><code>currencyId</code></td><td>string | undefined</td><td>Currency used for the offer price (e.g., fiat or in-game currency).</td><td><code>"gems"</code></td></tr><tr><td><code>customPayload</code></td><td>object | undefined</td><td>Custom object defined by the client.</td><td><code>{...}</code></td></tr><tr><td><code>metrics</code></td><td>object</td><td>Metrics to track the offer lifecycle events. This object should be returned inside the <a href="#purchase">purchase</a> event as <code>meticaAttributes</code>.</td><td><code>{...}</code></td></tr></tbody></table>

***

### Example Response

```json
{
  "placements": {
    "p1": [
      {
        "offerId": "offer_abc",
        "creativeId": "creative_large_bundle_94",
        "items": [
          {
            "id": "potion",
            "quantity": 1
          }
        ],
        "expirationTime": "2025-01-16T12:00:00Z",
        "iap": "iap_potion_bundle",
        "price": 50,
        "discount": 0.23,
        "currencyId": "gems",
        "customPayload": {
          "additionalInfo": "special offer"
        },
        "metrics": {
          "purchase": {
            "userId": "abc",
            "appId": "<APP_ID>",
            "meticaAttributes": {
              "offer": {
                "offerId": "11655",
                "variantId": "14554",
                "bundleId": "14904"
              },
              "placementId": "6200"
            },
            "eventType": "meticaOfferInAppPurchase"
          },
          "display": {
            "userId": "abc",
            "appId": "<APP_ID>",
            "meticaAttributes": {
              "offer": {
                "offerId": "11655",
                "variantId": "14554",
                "bundleId": "14904"
              },
              "placementId": "6200"
            },
            "eventType": "meticaOfferImpression"
          },
          "interaction": {
            "userId": "abc",
            "appId": "<APP_ID>",
            "meticaAttributes": {
              "offer": {
                "offerId": "11655",
                "variantId": "14554",
                "bundleId": "14904"
              },
              "placementId": "6200"
            },
            "eventType": "meticaOfferInteraction"
          }
        },
      }
    ],
    "p2": []
  }
}
```

***

### Example Request Using `curl`

```bash
curl -X POST 'https://api-gateway.prod-eu.metica.com/offers/v1/apps/<APP_ID>?placements=p1,p2' \
--header 'Content-Type: application/json; charset=utf-8' \
--header 'X-API-KEY: <API_KEY>' \
--data '{
  "userId": "abc",
  "deviceInfo": {
    "store": "AppStore",
    "timezone": "+05:00",
    "appVersion": "1.4.5",
    "locale": "en-US"
  },
  "userData": {
    "level": 10,
    "inventory": {
      "potion": { "quantity": 30 }
    },
    "gemBalance": 51
  }
}'
```

***

### Notes

* The `placements` parameter is optional. If omitted, the response will include offers for all available placements.
* Only `userId` is mandatory in the request body; other fields are optional and can be partially filled.
* The `metrics` field in the response payload contains lifecycle event tracking data that should be reported back to Metica when corresponding events occur.
* Replace `<APP_ID>` and `<API_KEY>` with your application's unique identifiers.

***

## Smart Config

The Smart Config endpoint allows you to fetch dynamic and personalized configurations tailored to specific users based on their [attributes](#full-user-state-update) or [device](#request-body-schema-1) information. The set of configuration keys can be restricted using the `keys` parameter. If no keys are specified, all the configurations will be returned.

***

### Endpoint

`POST /configs/v1/apps/<APP_ID>`

Use this endpoint to retrieve personalized configurations for a specific application.

***

### Request Headers

<table><thead><tr><th width="213">Header Name</th><th>Expected Value</th></tr></thead><tbody><tr><td><code>X-API-KEY</code></td><td>Your unique API key.</td></tr><tr><td><code>Content-Type</code></td><td><code>application/json; charset=utf-8</code></td></tr></tbody></table>

***

### Request Parameters

<table><thead><tr><th width="100">Name</th><th width="85">Type</th><th width="100">Required</th><th width="274">Description</th><th>Example</th></tr></thead><tbody><tr><td><code>keys</code></td><td>string</td><td>No</td><td>A comma-separated list of configuration keys to fetch. If not provided, all configuration keys will be returned.</td><td><code>?keys=difficulty,suggested_prices</code></td></tr></tbody></table>

***

### Request Body Schema

<table><thead><tr><th width="235">Field Name</th><th width="81">Type</th><th width="101">Required</th><th width="213">Description</th><th>Example</th></tr></thead><tbody><tr><td><code>userId</code></td><td>string</td><td>Yes</td><td>Unique identifier for the user.</td><td><code>"abc"</code></td></tr><tr><td><code>deviceInfo</code></td><td>object</td><td>No</td><td>Information about the user's device. Can be partially filled.</td><td>See fields below.</td></tr><tr><td><code>deviceInfo.store</code></td><td>string</td><td>No</td><td>The platform's app store (<code>AppStore</code> or <code>GooglePlayStore</code>).</td><td><code>"AppStore"</code></td></tr><tr><td><code>deviceInfo.timezone</code></td><td>string</td><td>No</td><td>Device timezone as an IANA identifier or UTC offset.</td><td><code>"Europe/London"</code> or <code>+05:00</code></td></tr><tr><td><code>deviceInfo.appVersion</code></td><td>string</td><td>No</td><td>The app version, following the Semantic Versioning format.</td><td><code>"1.4.5"</code></td></tr><tr><td><code>deviceInfo.locale</code></td><td>string</td><td>No</td><td>The user's locale, formatted according to IETF BCP 47.</td><td><code>"en-US"</code></td></tr><tr><td><code>userData</code></td><td>object</td><td>No</td><td>Real-time user state data to override pre-ingested <a href="#Full-User-State-Update">user state attributes</a>, conforms to <code>userStateAttributes</code>.</td><td>See example below.</td></tr></tbody></table>

**Example Request Body:**

```json
{
  "userId": "abc",
  "deviceInfo": {
    "store": "AppStore",
    "timezone": "+05:00",
    "appVersion": "1.4.5",
    "locale": "en-US"
  },
  "userData": {
    "level": 10,
    "inventory": {
      "potion": {
        "quantity": 30
      }
    },
    "gemBalance": 51
  }
}
```

***

### Example Request Using `curl`

```bash
curl -X POST 'https://api-gateway.prod-eu.metica.com/configs/v1/apps/<APP_ID>?keys=difficulty,suggested_prices' \
--header 'Content-Type: application/json; charset=utf-8' \
--header 'X-API-KEY: <API_KEY>' \
--data '{
  "userId": "abc",
  "deviceInfo": {
    "store": "AppStore",
    "timezone": "+05:00",
    "appVersion": "1.4.5",
    "locale": "en-US"
  },
  "userData": {
    "level": 10,
    "inventory": {
      "potion": { "quantity": 30 }
    },
    "gemBalance": 51
  }
}'
```

***

### Response Schema

The response includes a mapping of configuration keys to their respective values defined inside the Metica platform. Each configuration key can return various types of data, such as numbers, strings, arrays, or objects.

**Example response body of two configured keys:**

```json
{
  "difficulty": "hard",
  "suggested_prices": [
    4.99,
    9.99,
    12.99
  ]
}
```

### Notes

* The `keys` parameter is optional. If omitted, the response will include all available configuration keys.
* Only `userId` is mandatory in the request body; other fields are optional and can be partially filled.
* The returned configurations are dynamically generated based on the provided user attributes.
* Replace `<APP_ID>` and `<API_KEY>` with your application's unique identifiers.


# UI Guides

Get an overview of Metica's features and how to use them.

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-type="content-ref"></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>SmartConfigs</strong></td><td>Learn how to use Metica’s SmartConfigs UI</td><td><a href="/pages/FlycTjO5FwDWaAlbUIzM">/pages/FlycTjO5FwDWaAlbUIzM</a></td><td><a href="/files/kinNgFaE44ZkuNwWvFen">/files/kinNgFaE44ZkuNwWvFen</a></td><td><a href="/pages/FlycTjO5FwDWaAlbUIzM">/pages/FlycTjO5FwDWaAlbUIzM</a></td></tr><tr><td><strong>SDK Integration Test UI</strong></td><td>How to use the SDK Integration Test tool for SmartFloor</td><td><a href="/pages/L7C67Urp83HqCss6zgzO">/pages/L7C67Urp83HqCss6zgzO</a></td><td><a href="/files/1oChCbrNj1MZqKmkhshy">/files/1oChCbrNj1MZqKmkhshy</a></td><td><a href="/pages/L7C67Urp83HqCss6zgzO">/pages/L7C67Urp83HqCss6zgzO</a></td></tr><tr><td><strong>Technical FAQ</strong></td><td>Get quick answers to common technical questions</td><td><a href="/pages/K0Qt7yf5gV6ABJt6iggD">/pages/K0Qt7yf5gV6ABJt6iggD</a></td><td><a href="/files/2dQl54P7iHajboZJHC4I">/files/2dQl54P7iHajboZJHC4I</a></td><td><a href="/pages/K0Qt7yf5gV6ABJt6iggD">/pages/K0Qt7yf5gV6ABJt6iggD</a></td></tr></tbody></table>


# SmartFloors Integration Test UI

Placeholder for instructions on using the SDK Integration Test tool

## Purpose of this Guide

This page provides a step-by-step guide for using the **SDK Integration Test** tool to validate your integration with the Metica mobile SDK for SmartFloor before submitting your build for verification.

***

## Overview

The SDK Integration Test tool helps you test your client integration with the Metica SDK for SmartFloor. You must test both normal and holdout behaviors by selecting the appropriate test user ID for each scenario. Instructions and expected results may differ depending on which test user ID you select.

**Available test user IDs:**

* `metica-force-test` — for normal behavior
* `metica-force-holdout` — for holdout behavior

You should perform the test twice:

1. First, select `metica-force-test` and complete the test for normal behavior.
2. Then, restart the test, select `metica-force-holdout`, and complete the test for holdout behavior.

Be sure to restart the test each time you change the test user ID.

***

## Steps to Use the SDK Integration Test Tool

1. **Log in to the Metica Platform**
   * Go to <https://platform.metica.com>
2. **Select the Correct Game**
   * Ensure you are in the game you want to test (use the game selector at the top left).
3. **Navigate to the Integration Test Page**

   In the left navigation, select **SmartFloor → Integration Test**.

   <figure><img src="/files/IJh5WyllhpoFhAvZDE92" alt="Integration Test navigation"><figcaption></figcaption></figure>
4. **Start the Test**
   1. Select one of the test user IDs (`metica-force-test` for normal behavior, or `metica-force-holdout` for holdout behavior).

      <figure><img src="/files/9y8ymo9iqWAIPChU4xnN" alt="Select test user"><figcaption></figcaption></figure>
   2. Follow the on-screen instructions to set the test user ID in your game. The instructions may change depending on which test user ID you select.

      <figure><img src="/files/FsO2wdngzNh7ave8NcVj" alt="Debugging/set user ID instruction"><figcaption></figcaption></figure>
   3. Click **Start**.

      <figure><img src="/files/gy9yK4RSPUOkp4VzmTr9" alt="Start button"><figcaption></figcaption></figure>
   4. Completely close (if already open) the game you want to test, then reopen it.
   5. Play the game toward the ad showing path (e.g., finish a level, trigger an ad button).
   6. The testing page will display events sent by your game. **Key events:** - SDK Initialization - Ad Load - Ad Shows

      <figure><img src="/files/gGulnEg5iKS8Sh0QN9Jw" alt="Events list"><figcaption></figcaption></figure>
   7. Click on any event card to view detailed information sent by your game.

      <figure><img src="/files/0jaiTOgDoyIucncuQk69" alt="Event detail"><figcaption></figcaption></figure>

> **Important:** If you change the test user ID, you must restart the test from this step to ensure accurate results for each behavior.

***

## Troubleshooting

If you see an error in any event, fix the issue in your integration and restart the test from step 4.

Example screenshot of a failing event:

<figure><img src="/files/3ywJch5gZljefA5EAnJO" alt="Failing event"><figcaption></figcaption></figure>

***

## Support

If you have questions or need help, contact your Metica point of contact or refer to the official documentation.


# SmartConfigs UI

Step-by-Step Configuration Guide

## Purpose of this Guide

This document provides a comprehensive, general explanation of how to use Metica’s Smart Config UI. It is intended for anyone responsible for configuring personalization use cases or experiments in Metica.

The guide walks through each part of the UI, what it’s for, what inputs are expected, and how to make good configuration decisions — regardless of the specific product use case.

***

## What Are Smart Configs?

Smart Configs are the central mechanism in Metica for configuring and delivering dynamic content or experiments. They define:

* What value(s) to return to the game or app (payload JSONs)
* Who should receive those values (context fields - basically “segmentation” criteria like platform, country, behavioral metrics)
* How the system should optimize outcomes (success metric, e.g., maximize revenue)
* How personalization is driven (via contextual bandits or A/B logic)\
  \
  Smart Configs can be used for use cases like offer timing, ad frequency, reward size, pricing experiments, and more.

***

## Glossary of Key Terms

**Smart Config:** A customizable, versioned configuration that determines what value to return to a user request.

**Variant:** A specific payload or experience option that a user might receive.

Contextual Bandit: A machine learning model that uses user properties (contexts) to personalize which variant is shown.

**Success Metric:** A key performance indicator Metica should optimize for (e.g., revenue, engagement).

Attribution Window: The time range after a user is assigned to a variant during which Metica tracks the success metric as a “reward” for the assignment decision.

**Assignment Duration:** The minimum length of time a user is locked into a variant before reassignment is allowed.

**Attribution vs Assignment Duration**

* These control the timing of optimization and reassignment.
* Example: if you want to see a 3-day revenue impact, attribution should be 3 days.
* Assignment duration (stickiness) should typically match unless you want dynamic reassignment.

**User Properties:** Fields that describe user traits or behavior (e.g., platform, session count).

**Calculated Attributes:** Derived user properties that are generated within Metica (e.g., average session length over 3 days).

**Holdout Group:** A percentage of users who are not exposed to personalization or experimentation, used as a baseline for comparison.

**Eligibility Conditions:** Filters used to include or exclude users from a config.

***

## How to Set Up Your First Smart Config

Welcome to the Metica Platform! This guide walks you through setting up your first Use Case within the Smart Config feature.&#x20;

This guide will help you to:

1. Check you are sending events
2. Configure all success metrics required for your use case
3. Configure all user properties required for your use case
4. Create a smart config with a Bandit
5. Testing

Follow these simple steps to get started:

### Step 1: Select Your Game

After logging into the Metica Platform:

1. Navigate to the top-left corner of the dashboard.
2. Click the dropdown to select your game from the list.
3. Once selected, the platform will automatically load the menu and data relevant to your chosen game.

***

### Step 2: Access Event Monitoring

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXeiKuyolhTLMYz1z5gHTuqWhKVhf-aLYDidhAIx-753h4wLtJsCAG-esq7bTnue_dhLk1t0D_mvXmsdGYm-aFd-5Xj_9KUvdRgMKdbKvfeyWOh66kGhFXB_Q9D_opTKFXX2yFgmQw?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

To begin monitoring events in your application:

1. Look at the left-hand navigation menu.
2. Go to Integration > Monitoring.

* This section is where incoming event data is monitored.
* Check whether all events you are sending are being received, and whether there are any errors.
* If required fields are missing, coordinate with developers to ensure proper integration.

Example: You might confirm that an ad\_impression or purchase\_event is being received before using it in a metric.

***

### Step 3: Data Dictionary Set Up

**Here we want to configure the data so that it matches the requirements of your use cases.**

1. Look at the left-hand navigation menu.
2. Go to Integration > Data Dictionary.<br>

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXdH-vgUIkQPcLzYk6lhiqWvfhtGVhgyBrV9346PvFXnYk3nkjwW0vCB_hl4phngIR2OZpw2X3rnpE5UE1MJaq0ByHiza8TuxbNlJY4s5ZH6HuREibZjz6OkhFZrUkkekfVMalxnmg?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

**User Properties**

These are used as contexts for personalization and decision-making.

* Must be defined here before they can be referenced in a Smart Config.
* Set the correct data type (string, number, boolean, timestamp).
* Optional fields help avoid failures when properties are missing.
* Recommended: platform, acquisition channel, device memory, early user behavior (e.g., impressions, spend)

Set-Up

* Select ‘+New Property’
* Choose the Display Name & Property Name
  * \*\*Ensure the Property name has the same format as the user state event property name.\*\*
* Choose the Type of data e.g string, number or Boolean
* Confirm

**Calculated Attributes**

Create dynamic user properties to use in context fields or conditions based on a live calculation applied to incoming player events.

This allows you to define metrics like:

* The player’s average session time (over N days)
* The player’s total purchases (last N days)
* The player’s ads watched count (last N hours)

Often used when values must be calculated dynamically over a window of time.

**Set-up**

* Select ‘+New Attribute’
* Complete the Attribute Name and Reference fields&#x20;
  * \*\*Ensure the Reference name has the same format as the event.\*\*
* Choose your Source event
* Confirm
* Check the following are correct:
  * Reference Name & Source event are correct
  * Event Filter
  * Aggregation Function
  * Calculated attributes target window
* Activate (Top right hand corner purple button)

Tip: These are useful when a player’s state can be derived from other existing events without having to duplicate that information.

#### Success Metrics

What is the Primary Success Metric for?\
The primary success metric is the real time success criteria used by a contextual bandit to inform the quality of its past decisions and influence its next decisions. A great success metric strikes the right balance between being immediately measurable (e.g. next 3 days) and aligned on a more holistic business goal (e.g. increasing revenue net of cannibalization effects).

How to decide which success metric is mandatory?

Revenue is recommended (90%+ of cases) as it's closest to what needs optimization.

How does the Secondary Metric work?

It’s used for monitoring and analysis but doesn’t influence the machine learning system. Only the primary success metric drives variant allocation decisions.

Guidance to Choosing Success Metrics

* Pick metrics that reflect actual value: revenue, retention, engagement.
* Avoid vanity metrics (e.g., number of clicks without downstream value).
* Ensure the event is being sent and correctly structured.
* Recommended: platform, acquisition channel, device memory, early user behavior (e.g., impressions, spend)

Set-Up

* Go to the Success Metric Tab
* Select the Purple button in the top right hand corner ‘+New metric’
* Add the Success Metric Name
* Choose the source event e.g adRevenue
* Confirm
* Add the Event filter  and Property or Type
* Confirm
* Check the data entered - Source event
* Add Event filter
* Select the aggregation function (sum, count, average), and optionally a filter.
* Activate (Top right hand corner purple button)

Examples:

* Session starts = count of session\_start
* RV impression count = count of ad\_impression where ad\_type = rewarded

Can’t add what you need to the Success Metrics? Let your Customer Success Manager know and we can add it for you e.g

* Total revenue = sum of iap\_event + ad\_revenue

***

### Step 4: Creating a Smart Config

Navigate to the "Smart Configs" tab in the left hand Menu

![](https://lh7-qw.googleusercontent.com/docsz/AD_4nXcRTdQWjkqidyLyXCDaZa4FiY7MjI4_0IEbNsvfgjjLamsmuSltQkYsR35mMyHuxCmeeIqaKlHafqPm222FjyKeX7uhj1ezCv_vVoLLU37sk6RMhYKTjRZn7bm0kNg7qxJY4NzBzg?key=FoO_b0XcNgL3co0B-gVtvnAm)\ <br>

In the Top right hand corner there will be a Purple button Click "+ New Config".

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXe_spsilizqC9E1FvWsmPQBXjggimldS8iGwhDhZtRiqGYVxRB8q0t1DoaKMAu8Olc7GJdmyD5F1HPmrnlEbqtdfz5JfBESaEYPUF91pNr7D7gtGrB-UdWKRx6F6WZVbwarSVH1Mw?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

Choose your Test Name e.g Interstitial Frequency

Here you will find a blank canvas waiting for you to build your Smart Config logic.

A Smart Config requires:

* A Condition
* A Holdout
* Variants&#x20;
* Default Payload

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXeFatQF4EbUTVF_tszezbBZOvCEHnmEy3LXxXFv8yOyDazqmumZnhaeqi7QbjI60ISoc4CWqqDSawAneTxw7mIidkgEOiAoooLzWOeO7k56FkcAdNPirpQYxzcZy79HyxxX-4aH-w?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

Here is an EXAMPLE of an Interstitial Frequency Smart Config Logic:

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXde78LnhKgDE5vPRHXC0lukWAMLwhCvKr8Nu2CuSD7QKzs0TAeyvmxJzTkeD0pUNH5VIPfrEvrfTtw2ReMvvm9x8avdIJ91FVeLEIovHOl4LX75BwBxvuzGVPMbAkp-eTyXJt0w?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

<br>

#### Eligibility Conditions

* Conditions define a given player should qualify for the config.

Add a Condition:&#x20;

In this example we are asking the Player age (account tenure in daysp) and have created two bandits based on the answer.<br>

* Click to Add your condition
* Name your Condition
* Add your Condition details
* Go back to the Canvas page

***EXAMPLE***&#x20;

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXfTwX2VOUWrWzaCPgyjubCNQ-yeIEMD1LXlxslCWdlZos23SAUm9jAtXPdTHVq3Zv0quhfDyBjl4PHWP48NTikrXZzlwgt1YC3tnC8lvFzNonH85GlNku-H_m42WVuKbNiV29FWHQ?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

#### Add a Bandit & Variant

After adding a condition you will see you have more logic now on the canvas.

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXcjB2V9l599rQ2qK1PEXsDZ_nAV2XTlOUk20IMjFuj1TMNem9sYPzKIumcOIBv74zSLw0E84X6Ebv4BMspADoVNWlo_yWI7LiwNjsQdYnsa2pT3Eu6R8PpFjLnLqVWu6GqUTSjWOA?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

#### Designing Variants

* Make sure variants differ in ways that are expected to change behavior.
* Use real parameters (e.g., wait times, reward multipliers) rather than abstract tags.

You can use:

* Simple values (like variant = A)
* Parameter sets (multiple fields per variant)

Metica does not auto-generate variants; you must define them.

Add your first variant option into the Payload.

* Click on the Payload
* Name your Payload
* Add your first Variant  in the form of a JSON

***EXAMPLE***

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXfONjB6mp3t5KCqOze-UuLiWju7CqOK574J_lvY8uOprEPrQ7La5Sg2C5Szwyha82eRP_tOozczu6XM0iD7ILgofLZaBa6xoQPgILlk3MhjpOSYv-i_6fazYV7GiSI61W18_oqv6A?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

**Now you need to Add your second Variant:**

Here you will be asked what type of Experiment or Test you would like to run:

* A/B Test
* Contextual bandit (This Guide is based on selecting the Contextual bandit)
* Confirm

**Configure your Contextual Bandit**

The information you add here refers back to the information you have added into your Data Dictionary.

* Primary success metric (what to optimize for)
* Attribution window (how long to track the success metric as a reward condition)
* Assignment duration (minimum period for which a user stays in a variant)
* User contexts (inputs to the model

**Guidelines:**

* Attribution and assignment duration should usually match.
* Use only user properties that are meaningful and not overly fragmented.
* Avoid overfitting with too many or high-cardinality fields (e.g., hundreds of UA campaigns).

Here is an ***EXAMPLE*** of configuration for the Interstitial Frequency Experiment:

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXdHj9-S9uuluJxyC-GQ9wV46gHF7H458uZu5FC9cHlNpxFLMxw6JKy41yE6fKQxl7mteu_T0LNpHvp-uUsVK-Mfl86E1_fcueSYg04QF7OrPbEdR4IfAKymjg24X4VpcNJixmh36Q?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

Continue adding your logic and Variants for each Bandit.

***

\
**Add a Default Payload**&#x20;

Defaults are also returned to users who don’t meet eligibility conditions.

* You can define what that default is:
  * An empty payload
  * One of the test variants

#### You can use:

* Simple values (like variant = A)
* Parameter sets (multiple fields per variant)

***

\
**Add a Holdout**

Select the Holdout button at the top of the page

<figure><img src="https://lh7-qw.googleusercontent.com/docsz/AD_4nXddlYRIFCJZQ9vgdWiuy1kA8HfnfbllNSSW9XkLutMV_vWTXaNKCHPMay0G2yX492dRzZjzjXdOLv1ea7Fsqt8r4Kj03JJv-SsCqnvEH7q7byp5SPqfY55uYbZuiUvxjhhIU42fDg?key=FoO_b0XcNgL3co0B-gVtvnAm" alt=""><figcaption></figcaption></figure>

#### Holdout Groups&#x20;

Holdouts allow you to measure uplift from personalization.

* Define a holdout group (e.g., 20% of users) who receive a default experience.
* The UI will generate a SEED for you:
* You can define what that default is:
  * An empty payload
  * One of the test variants

***

#### Activation & Versioning

* All configs begin in "Draft".
* You can edit drafts freely.
* Once a config is activated, editing it creates a new version.
* Versions are tracked independently to allow rollback or experimentation changes.

Once a config is live, the SDK can begin calling it and receiving variant assignments.


# Technical FAQ

## Where to Get Started?

#### There are a lot of possibilities with your tool, but considering the current situation, what would you recommend we do first? &#x20;

We strongly recommend beginning your journey into game personalization with a use case that delivers high impact while carrying minimal risk. This approach enables fast wins, builds internal confidence, and establishes the technical and organizational foundation needed to scale personalization across the game.

Ideal starting points include:

**Interstitial Frequency Optimization**

Dynamically adjusting interstitial ad frequency based on player context such as session length, engagement level, or churn risk can significantly enhance user experience while protecting monetization. For example, reducing ad exposure for highly engaged players or increasing it for at-risk users allows you to balance retention with revenue. This is a technically simple and low-risk use case that offers immediate insight into the value of personalization.

**Bid Floor Personalization**

Personalizing bid floors based on signals like predicted lifetime value, engagement behavior, or monetization potential lets you unlock more revenue from ad impressions without sacrificing player experience. By intelligently matching the bid floor to the value of each impression, you can drive measurable gains in ARPDAU with minimal implementation overhead.

**When should we use Bandits vs A/B testing**

* A/B Testing: Suitable for situations where learning is the goal e.g. new features
* Bandits: Best for real-time optimization of established features, and yield improvement where success metrics are well-defined.

## Core Proposition & Methodology

<details>

<summary>How does your Machine Learning determine what &#x26; when to offer each variant? </summary>

The model continuously learns from user behavior. If a player doesn’t respond to Variant A, the system will learn to try Variant B more often when it next sees another player with similar behavioral traits. With every new interaction the model’s decisioning quality improves as it seeks to maximise its success metric.

</details>

<details>

<summary>How does it determine what a segment is?</summary>

Segments are not predefined. Instead, they’re dynamically formed based on observed behavioral patterns and user attributes (e.g., geography, UA source) that are selected as its context user properties. Two users are deemed similar on the fly if they share similar traits. In rare cases, some user properties like country will be pre-grouped by Metica using embeddings as a way to speed up the system’s learning. For example, Chile and Bolivia can be passed as inputs to the model as a single geo rather than distinct ones.&#x20;

</details>

<details>

<summary>How does your agent differentiate randomness from causality?  </summary>

Using statistical methods like Bayesian inference and significance testing, the system continuously updates its understanding of what causes changes in outcomes, as opposed to random fluctuations.

</details>

<details>

<summary>Are models based on uncertainty?</summary>

Yes. Uncertainty is a core feature. The model balances exploration (trying unknowns) with exploitation (using what’s known to work), adapting its strategy based on confidence levels.

</details>

<details>

<summary>Do we actually have uncertainty in those models, or is this more random? </summary>

The model actively manages uncertainty. For example, during early exploration, it purposefully tests different options to learn, then gradually shifts toward exploiting known best-performing variants.

</details>

<details>

<summary>For training, will you use our available data?  </summary>

Yes. Both historical and real-time data from your game are used to refine learning. While the model can learn from scratch, historical data helps guide initial segmentation and feature development.

</details>

<details>

<summary>Can historical data be used for the AI learning?</summary>

Yes. Although the model has a 7-day exploration period to learn from scratch, historical data can accelerate learning and improve early performance.

</details>

<details>

<summary>Is it possible to isolate the uplift from a specific feature like the IAP offer?</summary>

We do report per-variant performance against holdout, but: Users may be in multiple test groups, so it's difficult to directly attribute results to one feature due to overlapping influences.

</details>

<details>

<summary>When a user is first assigned during the initial exploration period, will they be re-assigned at the end of their AssignmentDuration or directly after the exploration period?</summary>

After their Assignment Duration.

</details>

<details>

<summary>When a user installs for the first time after the initial global exploration period, will there be a “personal” exploration period where they are randomly assigned?</summary>

No, outside of the global exploration period users are assigned based on their context and the expected most valuable variant - in a small % of cases where the uncertainty for a context is large, the model might decide to deliver the second best variant in order to learn.

</details>

## Data Integration

<details>

<summary>How long is the technical integration usually? What team size is required?  </summary>

The data integration using the SDK takes most teams 1-2 days. The starter use cases Bid Floors and Interstitial Frequency take 1 day to integrate. More bespoke use cases can be more involved and the effort depends on your design choices.

</details>

<details>

<summary>Should we send events for all users or just those on the SDK? </summary>

For all users. This enables better historical analysis and more robust comparisons.

</details>

<details>

<summary>Does the letter case of the country code matter for registerCountry?  </summary>

No. Case-insensitive.

</details>

<details>

<summary>For clientIpAddress, which IP should be used? </summary>

The IP address at the time of the visit, not the installation.

</details>

<details>

<summary>Are platform names fixed (e.g., ios, android)?  </summary>

You may use your internal labels like ios, gp (Google Play), or az (Amazon).

</details>

<details>

<summary>Is there a fixed set of values for deviceType?  </summary>

Yes. Use standard categories: tablet, phone, desktop.

</details>

<details>

<summary>Are the uaNetwork, uaCampaign, and uaCreative parameters mandatory? </summary>

No. Partial info is acceptable. Send what’s available; updates can be appended later.

</details>

<details>

<summary>Is there a specific event template required for real-time learning?</summary>

Yes, we provide a standardized format which is customizable to suit your game’s schema.

</details>

## Platform Information

<details>

<summary>What is the Assignment Duration and Attribution Window? </summary>

* Assignment Duration\*\*: Time a user remains in their assigned group even if their context changes. &#x20;
* Attribution Window\*\*: Duration post-assignment during which revenue is considered for performance measurement.

</details>

<details>

<summary>What is Context?</summary>

You can imagine Metica's system to be "segmentation on steroids"

Users are grouped by what we call "contexts" - these are basically segmentation criteria like platform, country, but also behavioural criteria including the player progression, as well as their recent ad and iap revenues

The platform then uses a reinforcement learning model that learns for each individual player based on their current context, which variant has the highest expected success metric.

The machine optimises directly for the success metric (mostly revenue); for monitoring we use additional metrics like games played, impressions, CPMs, retention

How much does each context contributes to the assignment position?\
We have a set of internal reports that track the performance of each experiment. However, this functionality is not currently available on the platform.

</details>

<details>

<summary>What is the Primary Success Metric for?</summary>

The primary success metric is the main outcome used to judge if an experiment achieved its goal, chosen before the test and aligned with its objective.

</details>

<details>

<summary>How to decide which success metric is mandatory?</summary>

Revenue is recommended (90%+ of cases) as it's closest to what needs optimization.

</details>

<details>

<summary>How does the Secondary Metric work?</summary>

It’s used for monitoring and analysis but doesn’t influence the machine learning system. Only the primary success metric drives variant allocation decisions.

</details>

<details>

<summary>Does having both a primary and a secondary success metric affect performance?</summary>

Secondary metrics are used strictly for monitoring purposes and do not influence model learning or delivery. To incorporate multiple metrics into learning, a hybrid primary metric would need to be designed.

</details>

<details>

<summary>What should be the baseline variant? </summary>

Always use your current default experience as the baseline to properly compare uplift.

</details>

<details>

<summary>Can test parameters or variants be changed after launch? </summary>

Yes. Arms can be added/removed, and filters updated without affecting the model, unless bandit parameters themselves are altered. Contexts should remain the same.

</details>

<details>

<summary>What are the requirements for experimentation?</summary>

* At least 100 responses per variant per week &#x20;
* 7-day learning phase &#x20;
* 3% statistically significant improvement &#x20;

</details>

<details>

<summary>Can users participate in multiple use cases?  </summary>

Yes, with appropriate configuration to avoid interference.

</details>

<details>

<summary>How is the holdout group assigned?</summary>

Randomized via a seed/user ID pair. Sample size and diversity are ensured via statistical checks like winsorisation and regression balancing.

</details>

<details>

<summary>When generating the holdout seed, should it be the same or different across experiments?</summary>

There are pros and cons to configuring a different holdout seed in each new experiment. When two experiments have a different holdout seed they become completely independent of one another. The benefit is that we believe it to be valuable to understand the isolated effect of any given use case on revenue.

</details>

<details>

<summary>Is there a way to view holdouts within the platform?</summary>

Not at the moment, but the team is actively working on adding this feature. In the meantime, we can provide Google Sheet outputs and periodic snapshots.

</details>

<details>

<summary>How do you handle outliers?  </summary>

Using Winsorization techniques to limit skew from extreme data points.

</details>

<details>

<summary>How is uplift measured? </summary>

Comparing variant performance vs. control group using primary success metric, often revenue.

</details>

<details>

<summary>What is z_stat? </summary>

Statistical test value indicating confidence intervals: 1.65 = 90%, 1.95 = 95%.

</details>

<details>

<summary>How many variants should be tested? </summary>

As many as required however we recommend 3-5 is optimal to balance learning with exploration depth. The variants should be diverse for optimal performance.

</details>

<details>

<summary>How critical is device memory as a context?</summary>

Very important. It’s one of the most powerful predictors in performance analysis.

</details>

<details>

<summary>Can the holdout evolve over time?  </summary>

Yes, with caution. Decreasing holdout size is safe. Increasing its risks including previously exposed users, potentially biasing results.

</details>

<details>

<summary>How does the allocation system work during the exploration phase?</summary>

During the exploration phase (the first 7 days), equally allocate users for the first 7 days of exploration and only then start adjusting allocation/move users around. Afterwards the bandits have a dynamic rate of exploitation (going for highest revenue), and exploration (learning more), which depends on the level of confidence the model has for specific contexts.

</details>

## Bid Floor Personalization

<details>

<summary>Why do publishers need Metica for bid floor optimization? </summary>

Static, rule-based approaches are blunt tools that leave revenue on the table. Metica uses real-time user behavior to dynamically optimize bid floors at the individual level, unlocking significantly higher revenue and performance that manual segmentation simply can’t match.

</details>

<details>

<summary>How quickly does Metica return bid floor recommendations? </summary>

Within 200ms of receiving user data.

</details>

<details>

<summary>How often are bid floors updated?</summary>

Currently once per session. We’re developing more frequent update capabilities.

</details>

<details>

<summary>How does Metica refine bid floors over time?  </summary>

By analyzing user behavior, engagement, fill rates, CPM trends, and more learning from each session.

</details>

<br>


# Developer guides

<table data-view="cards"><thead><tr><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Getting Started</strong><br>The basics of the Metica platform</td><td><a href="/files/2FoC4P9k2TcU57DEA5FD">/files/2FoC4P9k2TcU57DEA5FD</a></td><td><a href="/pages/7aobIuh1M0JH816rx71L">/pages/7aobIuh1M0JH816rx71L</a></td></tr><tr><td><strong>MMP integration</strong><br>How to give Metica access to your MMP data</td><td><a href="/files/NOtYnn27FmznRNBmJGVk">/files/NOtYnn27FmznRNBmJGVk</a></td><td><a href="/pages/wmmOanhZruG0MNW6isif">/pages/wmmOanhZruG0MNW6isif</a></td></tr><tr><td></td><td></td><td></td></tr></tbody></table>


# Getting Started


# What is Metica?

***

Metica is an advanced platform engineered to significantly boost in-app purchase (IAP) revenue for games by integrating personalization and optimization technologies with a unique approach to making in-game shops more dynamic. Leveraging a near real-time data infrastructure and state-of-the-art machine learning, Metica delivers personalized interventions for every player, optimizing engagement and monetization strategies.

Key to Metica's innovation is its comprehensive experimentation framework that transcends traditional A/B testing. It supports continuous testing, multi-armed bandits, and contextual multi-armed bandits through reinforcement learning, enabling precise and effective optimization of player experiences based on evolving behaviors and preferences.

Furthermore, Metica enhances the dynamic nature of game shops by offering:

* Greater flexibility in bundling items together, allowing developers to create tailored offer bundles either as IAP or purchasable with virtual currencies.
* The ability to hyper-personalize each offer, ensuring that every player receives the most relevant and enticing deals.
* The capacity to experiment with and continuously optimize what’s presented in the shop, ensuring that offerings remain fresh and effective in driving revenue.

Metica also provides a Predicted Life Time Value (pLTV) model for players to help understand and maximize the long-term revenue potential from each player. The platform is expanding its predictive modeling capabilities to include other player states such as churn and spend propensity, further empowering developers to proactively engage and retain players.

By enabling the operationalization of both first-party and third-party machine learning models, Metica allows for unparalleled customization and optimization of the gaming experience. This comprehensive approach ensures that game developers and publishers can leverage cutting-edge data science and machine learning technologies not only to increase IAP revenue but also to significantly enhance player satisfaction and retention through a more dynamic and personalized in-game shop experience.


# SmartFloors

SmartFloors is Metica’s real-time bid floor optimization system for mobile games. It integrates directly with AppLovin’s MAX mediation platform and uses machine learning to set dynamic bid floors at the user level. The goal is to maximize ad revenue from each impression by tailoring floor prices based on the predicted value of individual players.

### What it does

SmartFloors replaces static or manually configured bid floors with a fully automated system that responds to player behavior and market conditions. It analyzes signals such as in-app purchases, churn likelihood, engagement patterns, country, device type, and available memory to estimate a player’s value to demand-side platforms. Based on this, it selects the optimal floor pricing strategy for each ad request.

#### How It Works

* Player Value Estimation: Uses predictive models trained on in-game and monetization data to estimate each player’s value.
* Floor Strategy Selection: Chooses from a predefined grid of bid floor strategies using AppLovin MAX’s Management API, factoring in device, geo, and more.
* Real-Time Execution: The Metica SDK intercepts ad requests and applies the selected strategy dynamically.
* Continuous Learning: Ad outcomes (e.g., fill rate, CPM, engagement) are logged and fed back into the model to improve predictions over time.

#### Key Benefits

* Automatically increases monetization performance, typically delivering a 25 to 30 percent uplift in ad revenue
* Adapts in real time to live events, promotions, and changes in game performance
* Works with your existing MAX setup and requires minimal integration effort
* Learns and improves without requiring manual tuning or segmentation work

SmartFloors brings advanced intelligence and automation to ad monetization, enabling studios to capture more value from each user while maintaining control and visibility into performance.


# Unity SDK

## What you need

To use the Metica Unity SDK you need:

* An `API key`, obtainable in the Metica platform
* An `appId`, obtainable in the Metica platform.

## Installation

In Unity, open the Package Manager (Window > Package Manager) and click on the '+' button in the top left corner. Select "Add package from git URL..." and enter the following:

`https://github.com/meticalabs/metica-unity-sdk.git`

## How to use the Metica SDK

The recommended way to use the SDK is through `MeticaSdk` which offers `async`/`await` methods alongside `coroutine` ones.

For backwards compatibility the old **deprecated** mechanism is still available which has only the coroutine approach: `MeticaAPI`

You can retrieve an instance of `IMeticaSdk` with

```csharp
private IMeticaSdk _sdk = MeticaSdk.SDK;
```

## Setup

The SDK configuration exposes the following parameters:

| Property | Description                                                                                                                                                                                                                                                          |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey` | Your API key                                                                                                                                                                                                                                                         |
| `appId`  | The application identifier accessible from Metica's dashboard.                                                                                                                                                                                                       |
| `userId` | A string that identifies a user. This can change during the lifetime of your app/game so, for example and depending on your needs, this could be a temporary id that later becomes a specific userId, or it can be the current user's id if it's already identified. |

## Interstitial Ads

Interstitial ads are full-screen or full-page ads that temporarily cover an app’s interface. They’re typically shown at natural pauses or transition points, such as after completing a level in a game or when navigating between major views.

The following sections show you how to load and then show an interstitial ad.

### Loading an Interstitial ad

The following code shows you how to attach listeners and load the first interstitial ad:

```csharp
private void InitializeInterstitialAds() {
    // Attach callbacks
    MeticaAdsCallbacks.Interstitial.OnAdLoadSuccess += OnInterstitialLoadedEvent;
    MeticaAdsCallbacks.Interstitial.OnAdLoadFailed += OnInterstitialFailedEvent;
    MeticaAdsCallbacks.Interstitial.OnAdShowSuccess += OnInterstitialDisplayedEvent;
    MeticaAdsCallbacks.Interstitial.OnAdShowFailed += InterstitialFailedToDisplayEvent;
    MeticaAdsCallbacks.Interstitial.OnAdClicked += OnInterstitialClickedEvent;
    MeticaAdsCallbacks.Interstitial.OnAdHidden += OnInterstitialDismissedEvent;
    // Load the first interstitial
    LoadInterstitial();
}
void LoadInterstitial() {
    if (IsMeticaAdsEnabled) 
    {
        MeticaAds.LoadInterstitial(); 
    } 
    else
    { 
        MaxSdk.LoadInterstitial(InterstitialAdUnitId); 
    }
}
private void OnInterstitialLoadedEvent(string adUnitId) {
    // Interstitial ad is ready to be shown. MeticaAds.IsInterstitialReady() will now return 'true'
}
private void OnInterstitialFailedEvent(string adUnitId, string error) {
    // Interstitial ad failed to load. We recommend retrying with exponentially higher delay.
}
private void OnInterstitialDisplayedEvent(string adUnitId) {
    Debug.Log($"Interstitial shown {adUnitId}");
}
private void InterstitialFailedToDisplayEvent(string adUnitId, string error) {
    // Interstitial ad failed to display. We recommend loading the next ad
    Debug.Log("Interstitial failed to display: " + error);
    LoadInterstitial();
}
private void OnInterstitialClickedEvent(string adUnitId) {
    Debug.Log($"Interstitial clicked {adUnitId}");
}
private void OnInterstitialDismissedEvent(string adUnitId) {
    // Interstitial ad is hidden. Pre-load the next ad
    Debug.Log("Interstitial dismissed");
    LoadInterstitial();
}
```

## Showing an Interstitial Ad

To show an interstitial ad, call `ShowInterstitial()`:

<pre class="language-csharp"><code class="lang-csharp">void ShowInterstitial()
{
    if (IsMeticaAdsEnabled) {
<strong>        if (MeticaAds.IsInterstitialReady())
</strong>        {
            MeticaAds.ShowInterstitial();
        }
        else
        {
            interstitialStatusText.text = "Ad not ready";
        }
    }
    else
    {
        if (MaxSdk.IsInterstitialReady(InterstitialAdUnitId)) 
        {
            intersitialStatusText.text = "Showing";
            MaxSdk.ShowInterstitial(InterstitiaAdUnitId);
        }
        else 
        {
            interstitialStatusText.text = "Ad not ready";
        }
    }
}

</code></pre>

## Rewarded Ads

Rewarded ads are video ads that users can choose to watch in exchange for in-app rewards.

### Loading a Rewarded Ad

The `InitializeRewardedAds` method attaches listeners for both Metica and MAX SDK rewarded ad callbacks and then loads the first rewarded ad.

```csharp
private void InitializeRewardedAds() 
{
	if (IsMeticaAdsEnabled)
	{
		MeticaAdsCallbacks.Rewarded.OnAdLoadSuccess += OnRewarderAdLoadedEvent;
		MeticaAdsCallbacks.Rewarded.OnAdLoadFailed += OnRewardedAdFailedEvent;
		MeticaAdsCallbacks.Rewarded.OnAdShowSuccess += OnRewardedAdDisplayedEvent;
		MeticaAdsCallbacks.Rewarded.OnAdShowFailed += OnRewardedAdFailedToDisplayEvent;
		MeticaAdsCallbacks.Rewarded.OnAdClicked += OnRewardedAdClickedEvent;
		MeticaAdsCallbacks.Rewarded.OnAdHidden += OnRewardedAdDismissedEvent;
		MeticaAdsCallbacks.Rewarded.OnAdRewarded += OnRewardedAdReceivedRewardEvent;
	}
	else
	{
       	MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += (adUnitId, adInfo) => OnRewardedAdLoadedEvent(adUnitId); 
       	MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += (adUnitId, errorInfo) => OnRewardedAdFailedEvent(adUnitId, errorInfo.Message); 
		MaxSdkCallbacks.Rewarded.OnAdDisplayFailedEvent += (adUnitId, errorInfo, adInfo) => OnRewardedAdFailedToDisplayEvent(adUnitId, errorInfo.Message); 
		MaxSdkCallbacks.Rewarded.OnAdDisplayedEvent += (adUnitId, adInfo) => OnRewardedAdDisplayedEvent(adUnitId);
		MaxSdkCallbacks.Rewarded.OnAdClickedEvent += (adUnitId, adInfo) => OnRewardedAdClickedEvent(adUnitId);
		MaxSdkCallbacks.Rewarded.OnAdHiddenEvent += (adUnitId, adInfo) => OnRewardedAdDismissedEvent(adUnitId);
		MaxSdkCallbacks.Rewarded.OnAdReceivedRewardEvent += (adUnitId, reward, adInfo) => OnRewardedAdReceivedRewardEvent(adUnitId);
		MaxSdkCallbacks.Rewarded.OnAdRevenuePaidEvent += OnRewardedAdRevenuePaidEvent;
	}

	// Load the first RewardedAd
	LoadRewardedAd();
}

private void LoadRewardedAd()
{
	rewardedStatusText.text = "Loading..."; 
	if (IsMeticaAdsEnabled)
	{
		MeticaAds.LoadRewarded(); 
	}
	else
	{
		MaxSdk.LoadRewardedAd(RewardedAdUnitId);
	}
}
private void OnRewardedAdLoadedEvent(string adUnitId)
{
	Debug.Log("Rewarded ad loaded");
}

private void OnRewardedAdDisplayedEvent(string adUnitId)
{
	Debug.Log($"Rewarded ad displayed {adUnitId}"); 
}
private void OnRewardedAdFailedToDisplayEvent(string adUnitId, string error)
{
	// Rewarded ad failed to display 
	Debug.Log("Rewarded ad failed to display: " + error);
	LoadRewardedAd();
}

private void OnRewardedAdClickedEvent(string adUnitId)
{
	Debug.Log($"Rewarded ad clicked {adUnitId}");
}

private void OnRewardedAdDismissedEvent(string adUnitId)
{
	// Rewarded ad is hidden Debug.Log("Rewarded ad dismissed"); 
	LoadRewardedAd();
}

private void OnRewardedAdReceivedRewardEvent(string adUnitId)
{
	// Rewarded ad was displayed and user should receive the reward Debug.Log("Rewarded ad received reward"); 
	// Implement your reward logic here
}
```

### Showing a Rewarded Ad

To show an interstitial ad, call `ShowRewarded()`:

```csharp
void ShowRewardedAd()
{
	if (IsMeticaAdsEnabled)
	{
		if (MeticaAds.IsRewardedReady())
		{
			rewardedStatusText.text = "Showing";
			MeticaAds.ShowRewarded();
		}
		else
		{
			rewardedStatusText.text = "Ad not ready";
		}
	}
	else 
	{
		if (MaxSdk.IsRewardedAdReady(RewardedAdUnitId))
		{
			rewardedStatusText.text = "Showing";
			MaxSdk.ShowRewardedAd(RewardedAdUnitId);
		}
		else
		{
			rewardedSWtatusText.text = "Ad not ready";
		}
	}
}

```

## Purchase Event

To enhance the integration experience, it's crucial to share additional information with Metica, such as in-app purchase details. The Metica SDK offers a method to log a `purchase` event. This event includes the product's unique ID, the currency code, the total amount, and an optional custom payload for developers to add personalized data.

```csharp
void LogOfferPurchaseEventWithProductId(string productId, string currencyCode, double totalAmount, Dictionary<string, object> customPayload = null);
```


# Managing Holdout Users

This section explains how Metica’s integration works alongside your game’s ad logic, and how to handle holdout users correctly.

### Understanding Holdout Users

A holdout is a user who does not receive Metica-optimized ads. Instead, they continue to see ads through your game’s default AppLovin MAX setup. Holdout users are selected randomly and represent a small percentage of your audience.

### Why Holdouts Are Important

Holdout users play a critical role in both measuring performance and improving system accuracy:

#### **Measure Performance Uplift**

Holdout users act as a control group. By comparing their results to those receiving Metica’s optimizations, you can clearly see the added value Metica brings to your ad monetization.

#### Improve Learning and Optimization

By analyzing data from both holdout and optimized users, Metica is able to refine its models more quickly. This leads to better predictions, stronger default strategies, and improved results, especially during early rollout phases or when onboarding new games.

#### What You Need to Do

During initialization, your game will receive a flag:

IsMeticaAdsEnabled = true means use Metica’s logic.

IsMeticaAdsEnabled = false means the user is in holdout and should get your standard MAX ad flow.

Handling this correctly ensures accurate testing, better results, and faster learning.

<figure><img src="https://docs.metica.com/~gitbook/image?url=https%3A%2F%2F2594492910-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252FHimctsSfODP3WqmgRoOH%252Fuploads%252FZQkOZiDkNIXDQEaDHZi3%252FAB_testing_flowchart.png%3Falt%3Dmedia%26token%3D4a1dd033-0510-478d-ba1a-ef728fb4d5e8&#x26;width=768&#x26;dpr=4&#x26;quality=100&#x26;sign=78cc9dea&#x26;sv=2" alt=""><figcaption></figcaption></figure>

## Code Sample <a href="#initialization" id="initialization"></a>

The `IsMeticaAdsEnabled` flag is a boolean variable that determines whether a specific user will receive ads optimized by Metica's AI or will be part of a holdout group receiving ads directly through the standard AppLovin MAX SDK. This flag is set during the SDK initialization and is used to A/B test the revenue performance of Metica's ad optimization against the baseline performance of the MAX SDK.

**Important: `MeticaAds.InitializeAsync()` must be called BEFORE initializing the AppLovin MAX SDK to ensure proper integration.**

Copy

```csharp
/// <summary>
/// Complete initialization flow for MeticaAds and MAX SDK integration
/// This method demonstrates the proper order and setup required for ad functionality
/// Call this method early in your app lifecycle, typically in Start() or Awake()
/// </summary>
public async void InitializeAds()
{
    // Step 1: Set User Identification (Critical Requirement)
    // Important: UserId must be set for the current user before any MeticaAds initialization
    // This unique identifier is used for:
    // - User analytics and behavior tracking
    // - A/B testing and holdout group management
    // - Revenue attribution and reporting
    // - Personalized ad targeting and optimization
    MeticaSdk.CurrentUserId = "your_unique_user_id";

    // Step 2: Configure MeticaAds initialization
    // Create the configuration object that will be passed to the MeticaAds SDK
    var meticaConfiguration = new MeticaConfiguration();
    
    // Step 3: Initialize MeticaAds SDK (Asynchronous Operation)
    // This call performs the core MeticaAds setup including:
    // - SDK authentication and configuration validation
    // - Network connectivity and endpoint verification  
    // - Platform-specific delegate initialization
    // - Ad callback registration and event system setup
    // Returns true if Metica should be used, false otherwise
    IsMeticaAdsEnabled = await MeticaAds.InitializeAsync(meticaConfiguration);

    // Step 4: Initialize AppLovin MAX SDK (Traditional Synchronous Setup)
    // Set the SDK key that identifies your app in the AppLovin dashboard
    // This key is essential for ad serving, reporting, and revenue tracking
    MaxSdk.SetSdkKey("YOUR_MAX_SDK_KEY"); // Replace with your actual MAX SDK key
    
    // Set MAX initialization callback
    MaxSdkCallbacks.OnSdkInitializedEvent += sdkConfiguration =>
    {
        // AppLovin SDK is initialized, now start loading ads
        Debug.Log("MAX SDK Initialized");

        InitializeInterstitialAds();
        InitializeRewardedAds();
    };
    
    // Initialize the MAX SDK - this sets up the underlying ad infrastructure
    // Must be called after MeticaAds initialization to ensure proper integration
    MaxSdk.InitializeSdk();
}
```


# MMP integration

Grant Metica access to your MMP for a free analysis

To provide you with a free game analysis report, Metica requires access to your MMP platform and data. By granting us access, we will be able to thoroughly analyze your game’s performance and provide valuable insights tailored to your needs. Follow these simple steps to give Metica access:

***

### Granting Adjust access

#### Step 1: Receive Your Bespoke Email Address

Metica will provide you with a unique, bespoke email address for access purposes. Make sure you have received this email from us before continuing.

#### Step 2: Grant Access to Metica

1. Once logged in, go to the **User Management** section from your dashboard.
2. Click **Invite User** or **Add New User**.
3. Enter the bespoke email address provided by Metica in the **Email Address** field.
4. Choose the appropriate role and permissions to allow Metica access to the data relevant for the analysis.
   * We recommend granting **View and Edit** permissions for optimal collaboration.
5. Click **Send Invitation**.

#### Step 3: Confirmation

Once access is granted, Metica will receive the invitation and confirm access. After this, we will begin analyzing your data to produce your **free game analysis report**.

#### Privacy and Data Use

At Metica, we take your privacy seriously. We do not share, sell, or distribute your data to third parties. Your data is used solely for analysis purposes and is kept private throughout the process. We value our customers' privacy and are committed to protecting it. For more details, please refer to our Privacy Policy.

If you have any questions or run into issues, feel free to reach out to us at <hello@metica.com>.

***

### Granting AppsFlyer access

#### Step 1: Invite Metica as a user

1. Log into to Appsflyer
2. Create a new user for the unique Metica email address retrieved earlier. Detailed AppsFlyer instructions here.
3. Select an appropriate role. Read-only is acceptable.

#### Step 2: Receive Your Bespoke Email Address

Metica will provide you with a unique, bespoke email address for access purposes. Make sure you have received this email from us before continuing.

#### Step 3: Share your API key with Metica

1. Create a new API key, or "token": [how to do it instructions](https://support.appsflyer.com/hc/en-us/articles/360004562377-Managing-AppsFlyer-tokens#retrieving-api-tokens)
2. Share it with Metica either by pasting it into the form of the "[Get started page](https://www.metica.com/get-started)" or by sending us an [email](mailto:hello@metica.com)

#### Step 4: Confirmation

Once access is granted, Metica will receive the invitation and confirm access. Afterward, we will begin analyzing your data to generate your **free game analysis report**.

***

### Granting Singular access

#### Step 1: Receive Your Bespoke Email Address

Metica will provide you with a unique, bespoke email address for access purposes. Make sure you have received this email from us before continuing.

#### Step 2: Grant Access to Metica

1. Once logged in, navigate to the **Team Members** or **User Management** section.
2. Click **Add Team Member** or **Invite User**.
3. Enter the bespoke email address provided by Metica in the **Email Address** field.
4. Assign the appropriate role and permissions to allow Metica access to the necessary data for analysis.
   * We recommend granting **View and Edit** permissions for seamless collaboration.
5. Click **Send Invitation** to complete the process.

#### Step 3: Confirmation

After access is granted, Metica will receive the invitation and confirm access. We will then begin analyzing your data to produce your **free game analysis report**.

***

### **Granting Tenjin access**

#### **Step 1: Receive Your Bespoke Email Address**

Metica will provide you with a unique, bespoke email address for access purposes. Make sure you have received this email from us before continuing.

#### **Step 2: Invite Metica as a user in Tenjin**

* Log into your Tenjin dashboard.
* From the main menu, go to **Manage Organization**.
* Click **Invite user** / **Add user**.
* Enter the bespoke email address provided by Metica in the **Email** field.
* Select an appropriate permission type that allows creating API access tokens (avoid *Reports only*, as it cannot generate tokens).
* Send the invitation to complete user setup.

#### **Step 3: Generate a Tenjin API access token**

* Ask <support@tenjin.com> to enable API Access token access.
* Once granted, in Tenjin, navigate to **AUTOMATE → API Access Tokens**.
* Click **New Access Token** and give it a recognizable name (e.g. “Metica Integration”).
* Click **Generate Token**.
* Copy the generated token and keep it somewhere secure until you share it with Metica.

#### **Step 4: Share your Tenjin token with Metica**

* Paste the Tenjin API access token into the form on Metica’s **Get Started** page, **or**
* Send it to us via email using your Metica contact channel.

#### **Step 5: Confirmation**

Once access is granted and the token has been shared, Metica will confirm connectivity to Tenjin. Afterward, we will begin analyzing your data to generate your free game analysis report.

***

#### Privacy and Data Use

At Metica, we take your privacy seriously. We do not share, sell, or distribute your data to third parties. Your data is strictly used for analysis purposes and is kept confidential. We are committed to protecting your privacy. For more information, please review our Privacy Policy.

If you have any questions or encounter any issues, feel free to reach out to us at <hello@metica.com>.


# FAQs


