Skip to content

Ads

Prove to your advertising partners that an impression or a click came from a real person. On Android, your app tells the SDK when an ad is on screen, clicked and gone; for each, the SDK signs the session's current score with the device key and sends the proof to DEA's recording service.

The SDK does not request, render or wrap ads. Your ad stack keeps doing that.

Note: Android has no ad session object. It has the lower-level proof calls sendProof, record and getProof: your app keeps the ad session's id and declares each event itself. See Compared with the Web SDK.

When to use it

  • You sell ad inventory and want to show buyers verified human traffic
  • You want each impression, click and end of an ad signed with the score of the session it happened in

Compared with the Web SDK

The Web SDK's Ads module does more of the work. On Android:

Web Android
ads.session_opened(surface?, adUnit?) sends the impression and resolves with an AdSession sendProof(ProofEvent.AD_IMPRESSION, sessionId = "") sends the impression; keep the sessionId of the RecordOutcome yourself
ad.click() sendProof(ProofEvent.AD_CLICK, sessionId = id)
ad.end() sendProof(ProofEvent.AD_ENDED, sessionId = id, update = true)
A click on the watched element that nothing touched is refused Not checked. A click is signed whenever your app declares one
One click per ad session, and none after end() Not enforced. Your app decides
ads.counts The RecordOutcome of each call
Proofs are sent in the background; click() and end() return at once Each call blocks until DEA answers or delivery gives up: call it off the main thread

Prerequisites

  • The SDK is installed and initialized with your appId and appKey. See the Quick Start. Without appKey, sendProof and record sign and send nothing.
  • registryAppId is set to your appId. It is the appId written inside each proof; left null, proofs carry a development id that DEA refuses. See Accounts and keys.
  • The device key is registered: call attest() and get PASSED before the first ad. The SDK does not register the key by itself, and DEA accepts a proof only when the key that signed it is registered under the same app. Without a registration the SDK still signs; the proof then claims tier C. It claims B only after a PASSED registration that carried a Play Integrity token and has not expired.
DeaSdk.init(
    this,
    DeaConfig(appId = "YOUR_APP_ID", appKey = "YOUR_APP_KEY", registryAppId = "YOUR_APP_ID"),
)

Usage

Send one ad's events

An ad session takes three proofs, in this order:

  1. The impression, with an empty sessionId. DEA opens the ad session and answers with its id, which arrives as RecordOutcome.sessionId. Do not leave sessionId out: the default is the capture session's id (sessionInfo.session), which is not an ad session's id.
  2. The click, if there is one, with the id DEA answered.
  3. The end, with the same id and update = true, which sends it as an update of that ad session (PUT /api/sessions/{id}) rather than as a new one.

Pass the same adUnit to all three: it is signed into each proof.

Each call blocks: it fetches a challenge from DEA, signs, and sends. When DEA cannot be reached or answers with a server error, the SDK sends the proof again — at most five sends in all, and none after the proof expires, 90 seconds after it was signed — so a call can block for about that long. This class runs each call in a coroutine:

import ai.dea.sdk.DeaSdk
import ai.dea.sdk.attest.Verdict
import ai.dea.sdk.proof.ProofEvent
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/** DEA's proofs for one ad. Create one for each ad you show. */
class AdProofs(private val scope: CoroutineScope, private val adUnit: String) {
    @Volatile private var sessionId: String? = null
    @Volatile private var clickSent = false

    /** The ad is on screen. */
    fun shown() {
        scope.launch(Dispatchers.IO) {
            val dea = DeaSdk.instance ?: return@launch
            if (dea.attest().verdict != Verdict.PASSED) return@launch

            // An empty session id: DEA opens the ad session and answers with its id.
            val impression = dea.sendProof(ProofEvent.AD_IMPRESSION, sessionId = "", adUnit = adUnit)
            sessionId = impression.sessionId
            if (impression.sessionId == null) Log.w("MyApp", "No ad session: ${impression.said}")
        }
    }

    /** The user clicked the ad. */
    fun clicked() {
        val id = sessionId ?: return // no ad session, or the ad has ended
        if (clickSent) return // one click per ad session
        clickSent = true

        scope.launch(Dispatchers.IO) {
            val click = DeaSdk.instance?.sendProof(ProofEvent.AD_CLICK, sessionId = id, adUnit = adUnit)
            if (click?.accepted != true) Log.w("MyApp", "Click not recorded: ${click?.said}")
        }
    }

    /** The ad is gone: removed or replaced. */
    fun ended() {
        val id = sessionId ?: return
        sessionId = null

        scope.launch(Dispatchers.IO) {
            DeaSdk.instance?.record(ProofEvent.AD_ENDED, sessionId = id, adUnit = adUnit, update = true)
        }
    }
}

Call shown() when the ad is on screen, clicked() when it is clicked, and ended() when it is removed or replaced — from your ad SDK's callbacks, for example. Pass a scope that outlives the screen, such as one your Application holds: an ad often ends as its screen closes, and a scope cancelled with the screen would never send the end. The next ad is a new AdProofs.

A click or an end before DEA has answered the impression has no ad session id to be sent with, so this class sends nothing for it, as the Web SDK does.

Note: On Android the SDK does not check a click against the user's touches. Your app declares the click, and the SDK signs it whether or not anything touched the ad.

Nothing is signed until the session is sufficient: until it has as many actions as the model asks for, 8 with the built-in model (see Evidence). Before that, sendProof answers nothing signed: core refused -33, so an impression declared then is not recorded, and the SDK does not keep it to send later. When an impression has to be recorded, check that getClassification().result is not INSUFFICIENT first.

Find out why a proof was not accepted

sendProof answers with a RecordOutcome. accepted is true when DEA took the proof. Otherwise, isSigned says whether anything was signed, and said says why:

import ai.dea.sdk.DeaSdk
import ai.dea.sdk.proof.ProofEvent
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

/** Sends the impression; returns the ad session's id, or null. */
suspend fun openAdSession(adUnit: String): String? = withContext(Dispatchers.IO) {
    val dea = DeaSdk.instance ?: return@withContext null
    val outcome = dea.sendProof(ProofEvent.AD_IMPRESSION, sessionId = "", adUnit = adUnit)

    when {
        outcome.accepted -> Log.d("MyApp", "ad session ${outcome.sessionId}")
        !outcome.isSigned -> Log.w("MyApp", "Nothing signed: ${outcome.said}")
        else -> Log.w("MyApp", "Signed, not accepted: ${outcome.said}")
    }
    outcome.sessionId
}
said starts with Why
no appKey No appKey was given. Nothing is signed or sent
no device key on this handset The device cannot keep a key: Android below 6.0, or the Keystore refused
nothing signed: core refused -33 The session is not sufficient yet: fewer actions than the model asks for, 8 with the built-in model. Nothing was sent. Send the impression again once getClassification().result is no longer INSUFFICIENT, if the ad is still on screen
nothing signed: no core loaded The scoring engine is not running. Check coreFault
nothing signed: the device key did not sign The Keystore refused to sign
/api/sessions answered, or /api/sessions/{id} answered for an end DEA answered with the error status that follows. A 4xx is final — for example the device key is not registered (call attest() first), or appKey and registryAppId do not name the same app. A 5xx is retried, and this is the last answer
unreachable DEA could not be reached in five sends
proof expired before it was delivered The retries ran past the proof's 90 seconds
failed The request failed in another way — for example it was made on the main thread. Not retried

After an impression, accepted with a null sessionId means DEA took the proof but named no ad session; there is then no id for the click and the end.

Sign without sending

getProof signs one event and returns the proof instead of sending it, for an app that delivers proofs itself. It takes the same event, sessionId, attestation, adUnit and appScopedUid as sendProof, and blocks while it fetches the challenge. It returns null, with no reason, in the cases where sendProof says nothing signed or no device key on this handset. Unlike sendProof, it is not refused without appKey.

Read the classification before you request an ad

The proof calls do not read the classification: once the session is sufficient, every ad your app declares is signed, whatever the score, and the score inside the proof is what tells DEA how human the session was. To decide whether to request an ad at all, read the classification first and act on it in your own ad stack — see Human Scoring.

API

Member Returns Description
sendProof(event, sessionId, attestation, adUnit, appScopedUid, extra, update) RecordOutcome Signs one ad event and sends it to DEA. Blocking
record(event, sessionId, attestation, adUnit, appScopedUid, extra, update) Boolean The same send; true when DEA accepted the proof. Blocking
getProof(event, sessionId, attestation, adUnit, appScopedUid) SignedProof? Signs one ad event without sending it. Blocking

ProofEvent: AD_IMPRESSION, AD_CLICK, AD_ENDED.

RecordOutcome:

Field Type Description
accepted Boolean Whether DEA accepted the proof
isSigned Boolean Whether a proof was signed
signed SignedProof? The proof, or null when nothing was signed
said String? DEA's answer as it came, or why DEA was never reached
sessionId String? The session_id in DEA's answer: after an impression, the ad session's id

See the API Reference for every parameter and for SignedProof.