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,recordandgetProof: 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
appIdandappKey. See the Quick Start. WithoutappKey,sendProofandrecordsign and send nothing. registryAppIdis set to yourappId. It is theappIdwritten inside each proof; leftnull, proofs carry a development id that DEA refuses. See Accounts and keys.- The device key is registered: call
attest()and getPASSEDbefore 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 tierC. It claimsBonly after aPASSEDregistration 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:
- The impression, with an empty
sessionId. DEA opens the ad session and answers with its id, which arrives asRecordOutcome.sessionId. Do not leavesessionIdout: the default is the capture session's id (sessionInfo.session), which is not an ad session's id. - The click, if there is one, with the id DEA answered.
- 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.