RORK LABJP
TEST — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer accountCLOUD — Code compiles on a cloud Mac, streaming a 60fps live simulator with real touch inputBROWSER — Design, code, and test entirely in Chrome or Safari — no Xcode requiredPUBLISH — Two-click App Store publishing keeps the submission process simpleMAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, and Vision ProRN — Standard Rork generates iOS and Android apps together with React Native (Expo)TEST — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer accountCLOUD — Code compiles on a cloud Mac, streaming a 60fps live simulator with real touch inputBROWSER — Design, code, and test entirely in Chrome or Safari — no Xcode requiredPUBLISH — Two-click App Store publishing keeps the submission process simpleMAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, and Vision ProRN — Standard Rork generates iOS and Android apps together with React Native (Expo)
Articles/Business
Business/2026-06-28Advanced

Don't Pay Out a Rewarded Ad on the Client's Word Alone — SSV Verification for a Rork (Expo) App on a Worker

Trusting the client-side 'reward earned' callback alone invites double-grants and spoofing. Here is how to wire AdMob server-side verification (SSV) into a Rork-generated Expo app, verify the signed callback on a Cloudflare Worker, and make payouts idempotent with transaction_id.

AdMob67Rewarded2SSVCloudflare Workers21Indie Dev34Rork470Monetization32

Premium Article

I run a handful of wallpaper and calming-themed apps as a solo developer, and rewarded ads are how I give back a little something extra to the people who choose to watch one. One day, scanning my analytics, I noticed that for a few users the number of reward grants was clearly higher than the number of completed views. I had been granting rewards straight off the device's onUserEarnedReward, so a modified client was replaying the same signal over and over.

You should never pay out a rewarded ad based only on the device saying "I finished watching." The thing that protects you here is AdMob's server-side verification (SSV). In this article I'll wire SSV into a Rork-generated Expo app and implement the verification endpoint as a Cloudflare Worker, in code close to what I actually run.

Where trusting the device breaks

The client-side callback is convenient, but it lives outside your trust boundary. Modified apps, proxy replays, hammering from an emulator — all of it happens inside the device, so you can't tell the genuine signal from a forged one. When the reward is something with value, like in-app currency or subscription days, this is a direct revenue leak.

SSV replaces that signal with one that travels through Google's servers. When a user finishes watching, AdMob's server sends a signed callback to a URL you specify. The signature is produced with Google's private key, so you only need the matching public key to confirm "this really came from AdMob and wasn't tampered with." You trust the device for nothing.

ConcernClient callback onlyWith SSV
SpoofingIndistinguishableRejected by signature check
Replay (hammering)Goes throughIdempotent via transaction_id
Where reward is finalizedDevice (outside boundary)Your server (inside boundary)
OfflineCan grant instantlyFinalized after server is reached

The key distinction: SSV doesn't replace the client callback, it just moves the finalization of the reward to the server. You can still show the instant "reward earned" feedback on the device, but you only actually increase the balance after the Worker has finished verifying the signature.

Client side — tell SSV who to reward

First, on the device, attach your user ID to the SSV callback. In react-native-google-mobile-ads you pass serverSideVerificationOptions when you build the ad request. userId and customData come back later as the callback's custom_data parameter.

import { RewardedAd, RewardedAdEventType, TestIds } from 'react-native-google-mobile-ads';
 
const adUnitId = __DEV__ ? TestIds.REWARDED : 'ca-app-pub-xxxxxxxx/yyyyyyyy';
 
export function loadRewardedAd(userId: string, purpose: string) {
  const rewarded = RewardedAd.createForAdRequest(adUnitId, {
    serverSideVerificationOptions: {
      // comes back in the SSV callback's custom_data
      userId,
      customData: JSON.stringify({ purpose, ts: Date.now() }),
    },
  });
 
  rewarded.addAdEventListener(RewardedAdEventType.LOADED, () => rewarded.show());
 
  // Use the device signal only for instant UI flair. Don't touch the balance.
  rewarded.addAdEventListener(RewardedAdEventType.EARNED_REWARD, () => {
    showOptimisticRewardUI(); // keep it to a "granting…" indicator
  });
 
  rewarded.load();
}

Putting a timestamp and purpose into customData helps later when you reconcile against your server logs. The important part is not writing a balance update into the device's EARNED_REWARD. If you do, the original hole stays open no matter how good your SSV is.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A complete Worker that fetches AdMob's public keys with a sub-24h cache and verifies the signature with ECDSA without slicing the signed query string wrong
An idempotency design that uses transaction_id as a KV key so a reward is granted exactly once even across Google's five retries
A payout flow that carries the user ID in custom_data and finalizes rewards independent of the device, resistant to spoofed views
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Business2026-05-03
Comparing Revenue Streams for Mobile Apps Built with Rork — A 2026 Honest Guide
An honest 2026 comparison of monetization channels for indie mobile apps built with Rork — ads, in-app purchases, subscriptions, paid downloads, sponsorship — written from the experience of running real apps to over ¥1.5M/month on AdMob.
Business2026-04-26
Building a Rork Max Top-100 App Store App That Pays — From Architecture to AdMob and Subscription
Going from 'Rork Max can generate a SwiftUI native app' to 'this app sits in the App Store category Top 100 and earns $1,500–4,000/month' is a real gap. Here's the complete monetization playbook — architecture, AdMob, IAP, subscription, ASO, and retention — with working SwiftUI code.
Business2026-04-26
Making a SwiftUI App on Rork Max Profitable: Three Decisions — Plan, Monetization, Timing
When you build a SwiftUI native app on Rork Max, profitability rarely hinges on the price tag — it hinges on which plan, which monetization model, and when you ship. Three decisions from indie experience that make the subscription pay back from month one.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →