Rewarded ads are familiar from mobile games: watch a 30-second video, get an extra life. Google Ad Manager supports the same format on the web through Google Publisher Tag (GPT). We built it into one of our tools, where users can watch an optional ad in exchange for points.
The interesting engineering problem is not showing the ad. It is deciding when to believe that someone actually watched it.
How web rewarded ads work
You define an out-of-page slot with the rewarded format, then listen for three events:
slot = googletag.defineOutOfPageSlot(adUnit, googletag.enums.OutOfPageFormat.REWARDED);
slot.addService(googletag.pubads());
pubads.addEventListener('rewardedSlotReady', e => {
// an ad is loaded; show your "Watch ad" button
// and call e.makeRewardedVisible() when the user clicks it
});
pubads.addEventListener('rewardedSlotGranted', () => {
// the user earned the reward
});
pubads.addEventListener('rewardedSlotClosed', () => {
googletag.destroySlots([slot]); // clean up, then request the next one
});Two details matter for the user experience. First, a rewarded ad must only appear when the user asks for it, so the ad is loaded in the background and the button only becomes active on rewardedSlotReady. Second, there is not always an ad to show. If nothing is ready after about twenty seconds, we say so plainly ("No ad available right now") and try again a minute later, rather than leaving a spinner running forever.
The trust problem
rewardedSlotGranted fires in the browser. Our server only hears about it because our own JavaScript tells it. Anyone who opens the developer console can call that endpoint directly, as often as they like.
Mobile ad SDKs solve this with server-side verification: the ad network calls your server directly with a signed message when a reward is earned. Web rewarded ads through GPT do not give you that callback. So the server can never be certain an ad was watched, and the design has to accept that.
Our answer is to make cheating bounded rather than impossible.
Four limits
- A nonce per ad. Before showing an ad, the page asks the server to start one. The server stores a random nonce and a timestamp on the user's record and returns the nonce. A grant is only accepted if it carries that exact nonce, and the nonce is deleted on use, so one ad cannot be claimed twice.
- A minimum duration. If a grant arrives less than five seconds after its ad started, it is rejected. A real rewarded ad cannot finish faster than that, so this stops simple scripts.
- A gap between ads. Each user can start a new ad at most once every 30 seconds, enforced on the server.
- A daily cap. Thirty ads per user per day, counted on the server and reset at midnight UTC.
if ($U['adCount'] >= $C['ads_per_day']) out(429, ['error' => 'Daily limit reached']);
$wait = ($U['lastAdAt'] ?? 0) + $C['ad_gap'] - time();
if ($wait > 0) out(429, ['error' => "Next ad in {$wait}s"]);
$U['pendingAd'] = ['nonce' => rand_hex(12), 'at' => microtime(true)];Put together, the worst a determined cheater can do is collect the same 30 rewards a day that an honest user can. That is an acceptable worst case only because of what the reward is.
Why the reward is points, not money
It would be easy to turn points into cash or gift cards. We decided not to, for three reasons.
It would break the economics of cheating. The limits above make cheating pointless when the prize is points inside our tool. If points became money, a few cents a day per account would be worth automating across thousands of fake accounts, and no client-side limit could stop that.
It would break ad-network rules. Ad networks are paying for real human attention. Paying people to view or click ads is the textbook definition of invalid traffic, and Google's publisher policies prohibit compensating users for viewing ads outside of approved rewarded formats with in-app rewards. Getting this wrong does not just cost the cheated revenue; it can close the publisher account.
It would change who uses the product. A tool that pays cash attracts people who want cash. A tool with points attracts people who like the tool. We wanted the second group.
Keeping ads honest in the interface
A few smaller decisions follow from the same thinking:
- Ordinary banner ads in the same app are never tied to points, and they sit apart from the rewarded button. Nothing in the interface suggests that viewing or clicking a banner earns anything.
- The button text says exactly what happens: watch a full ad, earn a fixed number of points, one every 30 seconds.
- Limits are shown as clear messages ("Next ad in 12s", "Daily limit of 30 ads reached"), not silent failures.
Summary
On the web you cannot prove that a rewarded ad was watched, so design for that instead of pretending otherwise. Issue a nonce per ad, enforce a minimum duration, a gap and a daily cap on the server, and keep the reward something with no value outside your product. Then the worst-case cheater is no better off than an honest user, and you never have to win an arms race you cannot win.
