AdMob メディエーションを設定した翌朝、ダッシュボードを開いて思わず固まりました。
eCPM が30%下がっていたのです。「複数の広告ネットワークを競わせれば収益が上がる」という理屈で導入したのに、逆の結果が出ました。胸の奥がすっと冷えるような感覚を、今でも覚えています。2014年から個人でアプリ開発を続けてきた中で、収益周りでこれほど「やってしまった」と思った瞬間はほとんどありませんでした。
個人開発で長くAdMobと付き合っていると、収益最大化のための試行錯誤はどうしても量が増えていきます。その経験からすると、メディエーション導入直後の失敗は「あるある」です。特にRorkで作ったアプリは、設定の落とし穴にはまりやすいポイントが独自にあると気づきました。
以下では、私が実際に踏んだ失敗と回復の過程を追います。eCPMが下がる仕組み、Rork特有の落とし穴、アダプターの生存確認、フロアを実測から決める手順、効果を数値で確かめる計測方法、そして「そもそも導入すべきか」の判断基準まで、順を追って共有していきます。
AdMob メディエーションで何が起きていたか
メディエーションとは、複数の広告ネットワーク(AppLovin、Unity Ads、Meta Audience Network など)に入札させて、最高額を提示したネットワークの広告を表示する仕組みです。うまく機能すれば eCPM は上がります。
ただし「うまく機能すれば」という条件付きです。
私が経験した eCPM 下落の原因は、後から振り返ると明確でした。ウォーターフォール(各ネットワークへの入札順序)の設定が間違っていたのです。AdMob が最初に呼ばれるべき場面で、単価の低いネットワークが先に応答してしまっていました。
Rorkで広告実装をするとき、AIが生成するコードはAdMobの基本設定としては問題ありません。ただしメディエーションアダプターの追加設定は、コード外のコンソール操作が多く、AIの出力だけでは完結しない部分が残ります。そこが詰まりやすいポイントです。
メディエーションが eCPM を下げてしまう理屈
「競わせれば上がるはず」なのに下がる。この矛盾を理解するには、ウォーターフォール方式の内部動作を押さえる必要があります。
ウォーターフォール方式では、各ネットワークに優先順位(eCPMフロア)を割り当て、上から順に「この単価で広告を出せますか」と問い合わせます。あるネットワークが応答すれば、そこで確定して下位には問い合わせません。
ここに罠があります。フロアを低く設定したネットワークが上位に来ていると、本来もっと高く売れたはずの表示枠を、安値で即決してしまうのです。オークションで言えば、最高額の入札を待たずに最初に手を挙げた人へ売ってしまう状態です。
私のケースでは、初期値の $0.50 フロアのネットワークが実質的な「即決係」になっていました。本来 $1.80〜$2.20 で売れる枠が、$0.50 前後で次々と処理されていた。これが30%下落の正体でした。
つまりメディエーションは「導入すれば上がる」ものではなく、「各ネットワークのフロアを実態に合わせて初めて機能する」仕組みだということです。ここを飛ばすと、ネットワークを増やしたことがそのまま収益の穴になります。
Rork アプリ特有の3つの落とし穴
落とし穴1: ウォーターフォールの順序をデフォルトのまま放置する
AdMob コンソールでメディエーションを設定するとき、各ネットワークの eCPM フロアをデフォルト値のままにしていませんか。
Googleが推奨する「Bidding(入札型)」方式を使えば自動最適化されますが、一部のネットワークは現時点でもウォーターフォール方式にしか対応していません。この場合、eCPM フロアを実際の表示単価から±10〜15%の範囲で手動設定する必要があります。
私の失敗は、初期設定の $0.50 というフロアをそのまま使ったことでした。実際の eCPM が $1.80 〜 $2.20 のレンジで推移していたため、$0.50 フロアの低品質な広告が大量に表示されてしまいました。フロアを $1.50 に設定し直したところ、3日で元の水準に戻りました。
落とし穴2: iOS の ATT と広告 ID の連携が切れる
Rork のプロジェクトで ATT(App Tracking Transparency)の実装を後から追加したとき、メディエーション側のネットワークが広告IDを受け取れていない状態になることがあります。
ATT の許可を取得した後、メディエーションアダプターを初期化するタイミングが重要です。ATT の許可を取得する前にアダプターを初期化してしまうと、iOS 14以降では広告IDなしで動作するため、ターゲティングなし広告が表示され続けます。
さらに EU 圏へ配信しているなら、その前段に UMP(同意管理プラットフォーム)が入ります。正しい順序は UMP の同意取得 → ATT のプロンプト → SDK 初期化 です。この3段を1つの非同期関数にまとめてしまうのが、私の落ち着いた形でした。
import { Platform } from 'react-native' ;
import { requestTrackingPermission } from 'react-native-tracking-transparency' ;
import MobileAds, { AdsConsent } from 'react-native-google-mobile-ads' ;
// ✅ UMP同意 → ATT許可 → SDK初期化 の順序を1本の関数で保証する
export const initializeAdsPipeline = async () => {
// 1. UMP: EEA/UK 対象ユーザーにのみフォームが出る。対象外なら即座に解決する
const consentInfo = await AdsConsent. requestInfoUpdate ();
if (consentInfo.isConsentFormAvailable) {
await AdsConsent. showForm ();
}
// 2. ATT: iOS のみ。UMP のあとに出すことでフォームの二重表示を避ける
if (Platform. OS === 'ios' ) {
const status = await requestTrackingPermission ();
console. log ( 'ATT status:' , status);
}
// 3. SDK 初期化: ここで初めて各アダプターが同意状態を読み込む
const adapterStatuses = await MobileAds (). initialize ();
return adapterStatuses;
};
Rork のプロンプトで「UMP同意 → ATT許可 → AdMob SDK初期化の順で実装する」と明示すると、AIもこの順番でコードを生成します。ただし既存プロジェクトに後からATTやUMPを追加するときは、生成コードをそのまま使わず、初期化タイミングを必ず確認してください。同意まわりの設計そのものはUMP × ATT 同意管理の本番実装パターン に詳しくまとめています。
落とし穴3: Android と iOS でアダプター設定を分けていない
Rork は iOS/Android 両対応のコードを一括で生成しますが、AdMob メディエーションのアダプターは OS ごとに別のパッケージIDが必要です。
app.json のプラグイン設定で iOS 用と Android 用を分けて記述していないと、どちらかのプラットフォームでアダプターが認識されずに広告が表示されないことがあります。
// app.json のプラグイン設定例(iOS/Android 分離)
{
"plugins" : [
[
"react-native-google-mobile-ads" ,
{
"androidAppId" : "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX" ,
"iosAppId" : "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX" ,
"userTrackingUsageDescription" : "This identifier will be used to deliver personalized ads to you." ,
"skAdNetworkItems" : [
{ "SKAdNetworkIdentifier" : "cstr6suwn9.skadnetwork" },
{ "SKAdNetworkIdentifier" : "4fzdc2evr5.skadnetwork" }
]
}
]
]
}
SKAdNetworkItems の一覧は各ネットワークが公式ドキュメントで提供していますが、バージョン更新のたびに内容が変わります。月に1回は確認する習慣をつけておくと、iOS 広告の表示率が落ちるトラブルを防げます。
なお iOS 側でアダプターを足した直後にビルドが通らなくなるケースは、この設定ではなくネイティブのリンク段階に原因があることが多いです。その切り分けはアダプター追加後のリンカエラーと広告非表示の切り分け にまとめました。
アダプターが本当に生きているかを、起動時に自分で確かめる
私が30%下落から学んだ最大の教訓は、「コンソールで設定した=動いている」ではないということでした。コンソール側で有効になっていても、アプリ側でアダプターが初期化に失敗していれば、そのネットワークはオークションに参加しません。参加しないネットワークが多いほど、残った安値ネットワークが落札しやすくなります。
MobileAds().initialize() はアダプターごとの状態を返します。ここを読み捨てているプロジェクトが本当に多い。私も長らく読み捨てていました。
import MobileAds, { AdapterStatus } from 'react-native-google-mobile-ads' ;
// 期待するアダプター名(AdMob コンソールで有効化したものと突き合わせる)
const EXPECTED_ADAPTERS = [
'com.google.android.gms.ads.MobileAds' ,
'com.applovin.mediation.adapters.AppLovinMediationAdapter' ,
'com.google.ads.mediation.unity.UnityAdapter' ,
];
export const verifyAdapters = ( statuses : AdapterStatus []) => {
// state: 0 = NOT_READY, 1 = READY
const notReady = statuses. filter (( s ) => s.state !== 1 );
const missing = EXPECTED_ADAPTERS . filter (
( name ) => ! statuses. some (( s ) => s.name === name),
);
if (notReady. length || missing. length ) {
// 本番では console ではなく Crashlytics の非致命ログへ送る
console. warn ( '[ads] adapter check failed' , {
notReady: notReady. map (( s ) => `${ s . name }: ${ s . description }` ),
missing,
});
}
// 参加ネットワーク数を返しておくと、後で eCPM と突き合わせられる
return { ready: statuses. length - notReady. length , total: statuses. length };
};
ポイントは description を捨てないことです。ここに「SDK version mismatch」「missing app id」といった具体的な理由が入ります。私の場合、片方の OS だけアダプターの App ID を書き忘れていたことが、このログでようやく判明しました。コンソールの画面をいくら眺めても気づけない類の不具合です。
本番では警告を Crashlytics の非致命イベントとして送り、ready / total の比率をリリースごとに眺めるようにしています。この比率が下がったリリースは、まず間違いなく eCPM も下がります。
フロアを実測から決める3ステップ
「フロアを実態に合わせる」と言葉で書くのは簡単ですが、では何ドルにするのか。私が今使っている決め方は3ステップです。
直近14日分の表示データを、ネットワーク別・国別に分解して取り出す
そのネットワークが実際に払った eCPM の分布を作り、下から40%の位置(40パーセンタイル)を読む
その値を初期フロアに置き、7日ごとに充填率(Fill Rate)を見ながら上下に5%ずつ動かす
平均ではなく分位点を使うのが肝心です。平均は一部の高単価インプレッションに引きずられて実態より高く出るため、平均をフロアにすると充填率が落ちて在庫が余ります。逆に最低値をフロアにすると、私が踏んだ「安値の即決係」を自分で作ってしまいます。40パーセンタイルは、その両方を避けるために試行錯誤の末に落ち着いた位置でした。
// ネットワーク別 eCPM の分位点からフロア候補を出す
type Row = { adSource : string ; earnings : number ; impressions : number };
const percentile = ( sorted : number [], p : number ) => {
const idx = Math. floor ((sorted. length - 1 ) * p);
return sorted[idx];
};
export function suggestFloors ( rows : Row [], p = 0.4 ) {
const byNetwork = new Map < string , number []>();
for ( const r of rows) {
if (r.impressions < 100 ) continue ; // 母数が小さい日は分布を歪めるので除外
const ecpm = (r.earnings / r.impressions) * 1000 ;
byNetwork. set (r.adSource, [ ... (byNetwork. get (r.adSource) ?? []), ecpm]);
}
return [ ... byNetwork. entries ()]. map (([ adSource , values ]) => {
const sorted = values. sort (( a , b ) => a - b);
return {
adSource,
floor: Number ( percentile (sorted, p). toFixed ( 2 )),
samples: sorted. length ,
};
});
}
impressions < 100 の行を捨てている点が地味に効きます。表示が数件しかない日の eCPM は極端な値になりやすく、そのままだと分布全体が歪みます。私は最初これを入れていなくて、実態より1ドル近く高いフロアを提案されて充填率を落としました。
そして忘れがちなのが、フロアは国別に分けるべきだということです。同じネットワークでも、日本と東南アジアでは実勢が数倍違います。全世界一律のフロアを置くと、単価の高い国では取りこぼし、低い国では在庫が埋まりません。フロアを下げる判断そのものの危うさについてはフロアを下げたら収益が崩れた話 にも書きました。
ウォーターフォールと入札型(Bidding)をどう併用するか
「じゃあ全部 Bidding にすればいい」と思われるかもしれません。実際、対応しているネットワークは Bidding に寄せるのが正解です。ただ現実には、ウォーターフォールでしか繋げられないネットワークが残るため、両者の併用になります。
私が最終的に落ち着いた棲み分けを表にまとめます。
方式 向くネットワーク 設定の手間 フロア調整
Bidding(入札型) AdMob本体・Meta・AppLovin など主要どころ 低い(自動最適化) 不要
ウォーターフォール Bidding未対応の中小ネットワーク 高い(手動) 実測の40パーセンタイルから手動設定
考え方はシンプルです。Bidding 対応ネットワークはすべて Bidding に入れて自動オークションに任せ、その上でウォーターフォール側は「Biddingの落札額を下回らないフロア」を設定します。こうするとウォーターフォールが安値で即決する事故が起きません。
Rorkで作ったアプリでも、この構成の判断はコンソール側の作業なので、生成コードには影響しません。むしろコードを気にせず収益設計に集中できる点は、AIビルダーを使う利点だと感じています。Bidding 側を実際に5ネットワークまで広げたときの推移はAdMob Bidding を本番投入した実装メモ にまとめてあります。
広告が出ない・単価が落ちたときの切り分け表
メディエーションのトラブルは、原因が「コード」「コンソール」「審査状態」の3層に散らばるため、闇雲に触ると余計に壊れます。私が手元に置いている切り分け表を共有します。
症状 まず疑うところ 確認方法
特定OSだけ広告が出ない app.json のプラットフォーム別 App ID adapterStatuses に該当アダプターが存在するか
広告は出るが単価だけ低い ウォーターフォールのフロア AD_SOURCE 別に表示回数を分解し、偏りを見る
iOSのみ単価が低い ATT/UMP と初期化順序 ATT許可率と、非パーソナライズ広告の比率
導入直後だけ低い 学習期間(数日) 7日待ってから判断。触りすぎない
特定ネットワークだけ充填ゼロ そのネットワーク側の審査・支払い設定 各ネットワーク管理画面のアプリ承認状態
一番下の行を最後に置いたのには理由があります。私は一度、コードとフロアを丸2日いじり倒した末に、原因が「ネットワーク側でアプリが審査中のままだった」と判明したことがありました。コードの外を先に疑うほうが早いケースは、思っているより多いです。
そして4行目。導入直後の数日は各ネットワークが配信を学習する期間で、数字が沈みます。ここで慌てて設定を戻すと、学習がリセットされてまた沈みます。最低7日は触らないと決めておくのが、結果的に近道でした。
導入前後の eCPM を自分で計測する仕組み
メディエーションの効果は「なんとなく上がった気がする」で判断してはいけません。私が30%下落に気づけたのも、日次で eCPM を記録していたからでした。
AdMobのレポートは管理画面でも見られますが、導入前後を並べて比較するなら、AdMob API から日次で取得して手元に残す仕組みを一度作っておくと後が楽です。React Native アプリ側ではなく、収益データを引くだけの軽いスクリプトで十分です。
// AdMob Reporting API から日次 eCPM を取得する最小構成の例
// 認証済みの google-auth ライブラリで accessToken を取得済みとする
async function fetchDailyEcpm ( accountId : string , accessToken : string ) {
const endpoint =
`https://admob.googleapis.com/v1/accounts/${ accountId }/mediationReport:generate` ;
const body = {
reportSpec: {
dateRange: { startDate: last7Days ().start, endDate: last7Days ().end },
dimensions: [ 'DATE' , 'AD_SOURCE' , 'COUNTRY' ],
metrics: [ 'ESTIMATED_EARNINGS' , 'IMPRESSIONS' ],
},
};
const res = await fetch (endpoint, {
method: 'POST' ,
headers: {
Authorization: `Bearer ${ accessToken }` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify (body),
});
// 収益 ÷ 表示回数 × 1000 で eCPM を自前計算し、ネットワーク別に並べる
return res. json ();
}
大切なのは AD_SOURCE(広告ソース)ディメンションでネットワーク別に分解することです。全体の eCPM が下がったとき、どのネットワークが足を引っ張っているかが一目でわかります。私の場合、この分解でようやく「$0.50フロアのネットワークだけ表示回数が異常に多い」という事実に気づけました。COUNTRY を足しておくと、前節の国別フロアをそのまま計算できます。
もう一段深く、実際に払われた1インプレッションごとの単価まで取りにいく方法はImpression-Level Ad Revenue を計測基盤に流し込む実装記録 に分けて書きました。レポートAPIの集計値では見えない偏りが見えるようになります。
下落を「翌朝」気づくためのしきい値
数値を貯めるだけでは、私のように翌朝ダッシュボードを開いて凍りつくことになります。私が今かけている条件は、実運用の中でだいぶ単純化されました。
前週同曜日比で eCPM が15%以上下がったら通知する(曜日の周期性を除くため、前日比では見ない)
充填率が10ポイント以上落ちたら通知する(フロアを上げすぎたサイン)
特定ネットワークの表示シェアが一晩で20ポイント以上動いたら通知する
3つ目が、まさに私の30%下落を検知できたはずの条件です。当時これがあれば、翌朝ではなく設定した日の夜に気づけていました。通知そのものの組み方はAdMob のダッシュボードを毎日見ない運用 に書いた4階層の仕組みを流用しています。
数値を毎日残しておくと、感覚ではなく事実で判断できます。収益周りの意思決定において、この差はとても大きいと実感しています。
メディエーションを入れるべきか、見送るべきか
ここまで書いておいてなんですが、すべてのアプリにメディエーションが必要なわけではありません。導入と運用にはそれなりの手間がかかるため、規模が見合わなければ AdMob 単体のほうが健全です。
私なりの判断の目安を挙げておきます。
日次のインプレッションがまだ数千に届かないうちは、メディエーションより先にeCPMフロアや広告フォーマットの見直しで伸びしろが大きいです
主要国からのアクセスが中心なら、まず Bidding 対応ネットワークだけで組み、ウォーターフォールは後回しでも構いません
手動フロア調整を月1回は見直せる運用体制があるか。これがないとウォーターフォールは放置されて劣化します
逆に言えば、ある程度のインプレッションがあり、運用に月数十分を割ける個人開発者であれば、メディエーションは十分に投資対効果が見合います。導入それ自体より、導入後に手を入れ続けられるかどうかが分かれ目です。
回復してわかったこと
設定を修正してから2週間後、eCPM は導入前の水準に戻り、さらに8%ほど上回るようになりました。メディエーションが本来の機能を発揮し始めたのです。
振り返ると、効いたのは派手な施策ではありませんでした。フロアを分位点から決め直したこと、アダプターの状態を起動時に自分で検査するようにしたこと、そして下落を翌朝ではなく当日に気づける条件を置いたこと。この3つだけです。どれも一度作れば、あとは静かに働き続けてくれます。
個人開発でメディエーションを扱うとき、最初の設定に時間をかけることが後の収益に大きく効きます。Rorkでアプリを素早く作れるようになったぶん、広告設定のような「コード外の作業」に時間を割く余裕も生まれました。それは良い変化だと思っています。
ATTの承認率そのものを上げる実装についてはATT承認率を最大化する方法 を、広告と課金の初期化順序で有料ユーザーに広告を見せてしまった失敗については起動順序設計の記録 も参照してみてください。
まず AdMob コンソールでウォーターフォールのeCPMフロアを実態に近い値に設定し直すことが、今日からできる最初のアクションです。過去14日分のネットワーク別 eCPM を並べて、40パーセンタイルの位置を読むところから始めてみてください。
同じ失敗をしている方の、遠回りを少しでも短くできれば幸いです。お読みいただきありがとうございました。