◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
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.

AdMob71RewardedSSVCloudflare Workers25Indie Dev39Rork577Monetization38

✦ 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 $15 for lifetime access
View Membership →

Related Articles

⟐ Business2026-04-26
Taking a Rork Max Native App Up the App Store Charts — The Order I Build Ads, StoreKit 2, and Subscriptions
The order I follow to grow a Rork Max SwiftUI app into one that stays near the top of its App Store category: a single AdGate that decides when ads may show, StoreKit 2 Transaction.updates monitoring, and where I draw the lines on ASO and retention — with working code.
⟐ Business2026-04-04
Rork App Revenue Design: Where Ads and Subscriptions Eat Each Other
How to layer AdMob, subscriptions, and one-time purchases in a Rork app without letting them cannibalize each other. Covers value-density placement, entitlement-first RevenueCat architecture, server-side receipt verification, and behavior-triggered paywalls.
⟐ Business2026-09-22
A successful payment never tells you what to unlock. Routing four products through one Checkout
Tips, single articles, monthly plans and lifetime access all came back from Stripe looking identical. Here is how I moved entitlement off the amount and onto metadata, and why I now fail closed when the entitlement store goes quiet.
📚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