Ads and revenue
Rewarded ads
people choose to watch.
A short ad for a reward stated up front: a life, coins, a premium recipe or story. Zurla, a frontier model built for advertising, offers it at the right moment, and the reward lands when the ad ends, confirmed to your server.
Level 13 is our toughest level. Can ads help there?
Yes, it’s a good spot for a rewarded ad: players can choose to watch one for an extra life. Your app already asks me each time a player runs out of moves, so it needs no release.
- Offered on a player’s second try, reward stated up front
- Plays when a player chooses to watch
- One more life when the ad ends
- Each life confirmed to your server
Extra life offer liveLevel 13
Illustrative data · fictional app and brands
In three apps
An offer, never an interruption.
People choose to watch.
A rewarded ad waits for a moment people want more, says what they get before anyone taps, and plays when someone chooses to watch. Ask Zurla for an offer in each app, and watch it play out on the phone.
Brindle Blocks: give players who run out of lives a way back in.
A rewarded offer fits that moment. Players who choose to watch a short ad get one more life, and anyone can say no:
- Out of lives: one more life, stated on the offer
- No thanks, one tap away
- The life lands when the ad ends
Extra life offer liveAlways opt-in
Illustration: one phone runs three fictional apps, Brindle Blocks (a puzzle game by Sorrelby Games), Fernhop Recipes and Trailmint (a fitness app). In each, a rewarded offer appears at a natural moment and states its reward; a short ad plays, with an Ad label and a reward countdown, when the person chooses to watch; and the reward lands when the ad ends.
Out of lives
Watch a short ad for one more life?
Watch ad No thanksFernwickTravel
Slow roads, long views
Why this ad
Report this ad
+1 life
Saffron tomato risotto
Premium recipe
Watch a short ad to unlock it free today
Watch ad Go PremiumOrvelle Coffee
Cold brew, slow-steeped overnight
Why this ad
Report this ad
Unlocked for today
Plans
Bonus plan: 10-minute core
Watch a short ad to unlock it today
Watch ad Not nowQuoll Quest
Explore a hand-drawn island
Why this ad
Report this ad
Plan unlocked
let rewardAd = ZurlaRewarded(placement: "extra_life")
val rewardAd = ZurlaRewarded("extra_life")
let rewardAd = ZurlaRewarded(placement: "premium_unlock")
val rewardAd = ZurlaRewarded("premium_unlock")
let rewardAd = ZurlaRewarded(placement: "bonus_plan")
val rewardAd = ZurlaRewarded("bonus_plan")
Same SDK, three apps. People choose to watch, for a reward you state up front: a life, coins, a hint, an unlocked recipe. The reward lands when the ad finishes, and anything of value can be confirmed to your server. Ads are chosen from the app and the screen, never from workout or health data.
Illustrative data · fictional apps and brands
Design a fair offer
A fair trade,
every time.
A rewarded ad is a trade people agree to. Offer it when they want more, say exactly what they get, and keep it special. You choose the moments, the rewards and the caps; Zurla paces the offers inside them, toward your goals.
When to offer
At a moment people want more: out of lives, a locked recipe, a premium story in a news app. Never in the middle of a level, a workout or cooking mode, and never as a gate people must pass.
What to give
One clear reward, written on the offer before anyone taps: one more life, 50 coins, a recipe for today. Then grant exactly what the offer says, every time.
How often
Daily caps keep rewards special and your store worth visiting. You set the caps, and Zurla paces the offers inside them while holding the retention you asked for.
Two rules for Brindle Blocks: players who bought Remove ads keep rewarded, and coin rewards stop at 3 a day.
Both set. They reach players the next time the app opens, with no new build:
- Remove ads buyers: rewarded stays, because they choose it
- Rewarded coins: up to 3 a day, so the store keeps its value
2 rules keptLive on every session
Offer rules
Brindle Blocks, a puzzle game
-
Players who bought Remove ads: no banners, native ads or interstitials; rewarded stays, because they choose it
Your rule -
Rewarded coins: capped at 3 a day, so the store keeps its value
Your rule -
Offline: play carries on, and the offer stays hidden until an ad is ready
Zurla -
Every offer: the reward written on it, and No thanks one tap away
Always
Caps and pacing are Zurla settings: a change reaches players the next time the app opens, with no new build or app review.
For your developers
Granted, then
confirmed.
When the ad ends, the SDK hands your app the reward, which is enough for a life or a hint. For coins or anything of value, Zurla also sends a signed callback to your server, so you grant it there, once. Zurla writes these calls for your engine.
Load ahead
Load the rewarded ad early, and show your offer while
isReadyis true.Show on a tap
Call
showwith your own player ID when someone taps Watch ad.Grant at the end
Your reward handler runs when the ad ends: fine for a life or a hint.
Confirm on your server
Zurla’s signed callback brings the player ID, the reward and a transaction ID. Check the signature, then grant once per transaction.
// Illustrative: ZurlaSDK, its API and the placement are
// placeholders until engineering confirms them.
import UIKit
import ZurlaSDK
final class LevelViewController: UIViewController {
private let extraLife = ZurlaRewarded(placement: "extra_life")
private let player = "player-1234" // your own player ID
private var lives = 0
override func viewDidLoad() {
super.viewDidLoad()
extraLife.load() // load ahead, ready for the offer
}
// Show your offer while an ad is ready (offline, it stays hidden).
var canOfferLife: Bool { extraLife.isReady }
// The player tapped "Watch ad" on your offer.
func playerChoseToWatch() {
guard extraLife.isReady else { return }
// playerID comes back in Zurla's signed callback to your server.
extraLife.show(from: self, playerID: player) { [weak self] reward in
// Runs when the ad ends: fine for a life or a hint.
// Grant coins from the server-side callback instead.
self?.lives += reward.amount
}
}
}
// Illustrative: com.zurla.sdk, its API and the placement are
// placeholders until engineering confirms them.
package com.sorrelby.brindleblocks
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.zurla.sdk.ZurlaRewarded
class LevelActivity : AppCompatActivity() {
private val extraLife = ZurlaRewarded("extra_life")
private val playerId = "player-1234" // your own player ID
private var lives = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
extraLife.load() // load ahead, ready for the offer
}
// Show your offer while an ad is ready (offline, it stays hidden).
fun canOfferLife() = extraLife.isReady
// The player tapped "Watch ad" on your offer.
fun playerChoseToWatch() {
if (!extraLife.isReady) return
// playerId comes back in Zurla's signed callback to your server.
extraLife.show(this, playerId) { reward ->
// Runs when the ad ends: fine for a life or a hint.
// Grant coins from the server-side callback instead.
lives += reward.amount
}
}
}
// Illustrative: the Zurla Unity package, its API and the placement
// are placeholders until engineering confirms them.
using UnityEngine;
using Zurla;
public class LevelAds : MonoBehaviour
{
private ZurlaRewarded extraLife;
private const string PlayerId = "player-1234"; // your own player ID
private int lives;
private void Start()
{
extraLife = new ZurlaRewarded("extra_life");
extraLife.Opened += PauseGame; // the game holds still
extraLife.Closed += ResumeGame; // while the ad is open
extraLife.Load(); // load ahead, ready for the offer
}
// Show your offer while an ad is ready (offline, it stays hidden).
public bool CanOfferLife => extraLife.IsReady;
// The player tapped "Watch ad" on your offer.
public void OnPlayerChoseToWatch()
{
if (!extraLife.IsReady) return;
// PlayerId comes back in Zurla's signed callback to your server.
extraLife.Show(PlayerId, reward =>
{
// Runs when the ad ends: fine for a life or a hint.
// Grant coins from the server-side callback instead.
lives += reward.Amount;
});
}
private void PauseGame() { Time.timeScale = 0f; }
private void ResumeGame() { Time.timeScale = 1f; }
}
// Illustrative: the zurla_ads plugin, its API and the placement
// are placeholders until engineering confirms them.
import 'package:flutter/material.dart';
import 'package:zurla_ads/zurla_ads.dart';
class LevelScreen extends StatefulWidget {
const LevelScreen({super.key});
@override
State<LevelScreen> createState() => _LevelScreenState();
}
class _LevelScreenState extends State<LevelScreen> {
final extraLife = ZurlaRewarded('extra_life');
final playerId = 'player-1234'; // your own player ID
int lives = 0;
@override
void initState() {
super.initState();
extraLife.load(); // load ahead, ready for the offer
}
// Show your offer while an ad is ready (offline, it stays hidden).
bool get canOfferLife => extraLife.isReady;
// The player tapped "Watch ad" on your offer.
void onPlayerChoseToWatch() {
if (!extraLife.isReady) return;
// playerId comes back in Zurla's signed callback to your server.
extraLife.show(playerId: playerId, onReward: (reward) {
// Runs when the ad ends: fine for a life or a hint.
// Grant coins from the server-side callback instead.
if (mounted) setState(() => lives += reward.amount);
});
}
@override
Widget build(BuildContext context) => Text('Lives: $lives');
}
// Illustrative: @zurla/react-native-ads, its API and the placement
// are placeholders until engineering confirms them.
import React, { useEffect, useState } from 'react';
import { Button, Text, View } from 'react-native';
import { ZurlaRewarded } from '@zurla/react-native-ads';
const extraLife = new ZurlaRewarded('extra_life');
const playerId = 'player-1234'; // your own player ID
export default function LevelScreen() {
const [lives, setLives] = useState(0);
useEffect(() => {
extraLife.load(); // load ahead, ready for the offer
}, []);
// The player tapped "Watch ad" on your offer.
const onPlayerChoseToWatch = () => {
if (!extraLife.isReady) return;
// playerId comes back in Zurla's signed callback to your server.
extraLife.show({
playerId,
// Runs when the ad ends: fine for a life or a hint.
// Grant coins from the server-side callback instead.
onReward: (reward) => setLives((n) => n + reward.amount),
});
};
return (
<View>
<Text>Lives: {lives}</Text>
{/* Show your offer while an ad is ready (offline, it stays hidden). */}
{extraLife.isReady && (
<Button title="Watch ad for one more life" onPress={onPlayerChoseToWatch} />
)}
</View>
);
}
// Illustrative: the callback's fields, its signature and the key are
// placeholders until engineering confirms them. Zurla calls the URL
// you set in your account each time a rewarded ad ends.
import crypto from 'node:crypto';
import express from 'express';
import { grantOnce } from './rewards.js'; // your own store
const app = express();
const KEY = process.env.ZURLA_CALLBACK_KEY; // from your Zurla account
app.get('/zurla/reward', async (req, res) => {
const { player_id, reward, amount, transaction_id, signature } = req.query;
// 1. Check that the callback comes from Zurla.
const payload = [transaction_id, player_id, reward, amount].join(':');
const expected = crypto.createHmac('sha256', KEY).update(payload).digest('hex');
const signed = typeof signature === 'string' && signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!signed) return res.sendStatus(403);
// 2. Grant each reward once, even when a callback arrives twice.
await grantOnce(transaction_id, player_id, reward, Number(amount));
res.sendStatus(200);
});
app.listen(8080);
{
"app": {
"bundle": "<App Store ID>",
"publisher": { "id": "ZRL-000000" }
},
"imp": [{
"id": "1",
"tagid": "extra_life",
"instl": 1,
"rwdd": 1,
"secure": 1,
"video": {
"mimes": ["video/mp4"],
"w": 390,
"h": 844,
"maxduration": 30
}
}],
"source": {
"schain": {
"ver": "1.0",
"complete": 1,
"nodes": [{ "asi": "zurla.com", "sid": "ZRL-000000", "hp": 1 }]
}
}
}
Snippets are illustrative. Package names, APIs and callback fields are placeholders until the SDK ships; your real keys come with your account.
- Marked in the bid requestOpenRTB 2.6: rwdd
- Each rewarded request carries
rwdd: 1, so buyers know the person chose to watch for a reward. Your app never writes or sends the request. - Ready before the offer
- The ad loads ahead and waits on the device, so your offer appears once there is an ad to play. Zurla’s auctions finish in under 100 ms.
- Reward check in your reports
- Every reward confirmed by a signed callback shows in your reports, next to what the ad earned.
Questions
Good to know.
Rewarded ads are always opt-in, in every app and on every offer.
Is a rewarded ad always opt-in?
Yes. A rewarded ad plays after someone taps Watch ad on your offer, and No thanks is always one tap away. Nothing plays on its own, and saying no never costs anyone a thing.
What happens when someone is offline?
Play carries on as usual. The offer stays hidden until an ad is ready, so nobody taps Watch ad and waits. Zurla checks again later, spacing out each check, and the offer comes back once an ad has loaded.
How do I grant coins safely?
Grant them from Zurla’s signed server-side callback. Check the signature with the key from your account, and record each transaction ID, so every reward is granted once, even if a callback arrives twice. The SDK’s own reward handler is fine for things like a life or a hint.
Can players who bought Remove ads still earn rewards?
Yes, if you want them to. A common rule: no banners, native ads or interstitials for those players, while rewarded offers stay, because they choose them. Tell Zurla once, and the rule holds on every session.
Which apps suit rewarded ads?
Games most of all: an extra life, coins, a hint. They also work well in news apps that unlock a premium story for today, recipe apps that unlock a recipe and fitness apps that unlock a bonus plan. Wherever people want a little more, an offer they can choose fits.
Do changes to caps or offers need an app release?
No. Caps, pacing and which buyers are asked are Zurla settings, so changes reach people the next time the app opens. A new offer on a new screen takes one call in your code and ships with your next store release.
Does Zurla read consent before showing ads?
Yes. Ads start after your consent tool and Apple’s tracking prompt (ATT) have finished, and Zurla reads both answers, including IAB TCF v2.3 consent strings. More on consent and privacy.
Which engines does the SDK support?
Native iOS (Swift Package Manager), Android (Gradle), Unity, Flutter and React Native, with the same rewarded calls in each. The same SDK serves native, banner and interstitial ads too, and Zurla runs the mediation itself. Using another engine? Ask us.
Rewards people come back for.
Add one call for each offer. Zurla runs the auction, paces the offers toward your goals and confirms every reward to your server.