2014年から個人で続けているアプリ開発のなかで、いちばん長く向き合ってきたジャンルが壁紙アプリです。累計で 5,000 万ダウンロードを超えたあたりから、機能追加よりも「広告と利用体験のバランスをどう保つか」のほうが、毎月の集中時間を圧倒的に消費するようになりました。AdMob の管理画面と App Open / インタースティシャル / リワードの設定を、リリースのたびに少しずつ動かしては、Day 1 のリテンションが落ちる、eCPM が下がる、IAP のコンバージョンが下がる、と毎回どこかでつまずきます。
ここ数か月、Antigravity の Agent と Firebase Remote Config を組み合わせて、この調整作業を「リリースを伴わない、小さく安全な微調整ループ」に置き換える試みを続けてきました。動くようになった部分は仕組みとしてかなり手応えがあり、同時に「ここは Agent には任せない」と決めた判断もはっきりしてきました。ここからは、その全体像と運用上のディテールを順を追ってまとめていきます。新規開発というより、既に AdMob で実収益が出ているアプリの保守・最適化の話に寄っています。
起点:Day 1 が 2% 落ちたとき、原因が分からなかった
きっかけは去年の秋のリリースでした。App Open 広告の表示頻度を「セッションあたり1回」から「Day 0 のみ抑制、Day 1 以降は無制限」に切り替えたところ、翌週から Firebase Analytics の Day 1 リテンションが 33% から 31% に落ちました。アプリ全体の DAU は数十万のスケールなので、2 ポイントの差は無視できません。
指標 変更前(1週間平均) 変更後(1週間平均) 差分
Day 0 → Day 1 リテンション 33.4% 31.2% -2.2pt
インタースティシャル eCPM $4.12 $4.05 -1.7%
App Open eCPM $2.85 $3.91 +37.2%
1日あたり広告売上 $1,820 $1,910 +4.9%
短期の売上は伸びました。問題は、リテンション低下が長期的に LTV を削り、3 か月後には広告売上が逆転して落ち始めたことです。当時はリリースのなかで他にも3つ変更を加えていたので、どれが効いたのかすら切り分けられず、結局アプリ全体を 1 つ前のバージョンに戻して、AdMob の管理画面で広告ユニットを手動でロールバックしました。アプリストア審査を待っていた 3 日間で、推定 $5,000 ほど機会損失が出た計算です。
このとき個人開発者として痛感したのは、広告関連の判断は「アプリのバージョンと完全に切り離せたほうがいい」ということでした。バージョンに紐付いたまま広告ロジックを変えると、いざ戻したくなったときに App Store / Google Play の審査時間ぶんだけ判断が遅れます。Firebase Remote Config に出会ったのはこの直後でした。
なぜ Remote Config だけでは足りなかったか
Remote Config 自体は導入してすぐに馴染みました。広告ユニット ID、表示頻度、Day0 抑制フラグ、リワードの倍率といった「動かしたい変数」を Remote Config に逃がし、アプリ側は起動時に fetchAndActivate() で受け取って AdMob ラッパーに渡すだけです。リリースを挟まずに値を変えられるようになり、ロールバックも「前回値に戻す」だけで済みます。
問題は、「どの値に動かすべきか」を決める頭脳の部分が、引き続き私の手作業として残ったことでした。具体的には、
AdMob 管理画面で eCPM 推移を見る
Firebase Console で Day 1 リテンションを見る
Google Play Console / App Store Connect のクラッシュとレビューを確認する
それらを総合して「今日は App Open の頻度を 1.0 → 0.7 倍に絞る」のような判断を下す
Remote Config を更新する
この一連の動きを毎朝 30〜45 分続けるのは、個人開発の他の作業時間を確実に侵食します。複数アプリを並行運用していると、休日が広告監視で潰れていく感覚すらありました。Antigravity の Agent をここに差し込めないか、というのが本稿のテーマです。
設計の前提:Agent に任せる範囲を狭く切る
最初に明確にしたのは、Agent には判断の全権を渡さない、という原則です。Remote Config は本番ユーザに直接届く設定なので、Agent が暴走すれば全ユーザの体験が一度に壊れます。私が決めた境界はこうです。
Agent が触ってよいパラメータは Remote Config の「動的調整可」マークが付いた変数だけ
値の変動幅は元値の ±15% 以内、かつ事前に決めた絶対上限・下限を超えない
適用範囲は「最大 10% のユーザセグメント」を上限とする(Remote Config の条件で制御)
Agent が判断材料にできるのは Firebase Analytics・AdMob・Crashlytics のメトリクスのみ。レビュー本文の感情判定で値を動かすのは禁止
この境界を「Antigravity Agent への運用ガードレール」として AGENTS.md にも書き、Agent が読み込めるようにしました。AGENTS.md の該当セクションを抜き出すとこうなります。
## Remote Config 自動調整の制約
このリポジトリの Remote Config キーのうち、 `tunable.` プレフィックスを持つキー
だけが Agent による自動調整の対象です。 `fixed.` プレフィックスのキーは絶対に
書き換えてはいけません。
- 1 回の調整で変更してよいキーは最大 2 個
- 値は元値からの相対変動率 ±15% 以内
- 適用ユーザは「audience: opt_in_experiment」(全体の 10%)に限定
- 1 日に同一キーへの変更は最大 1 回
- 直近 24 時間で Crashlytics の crash-free users が 99.0% を下回っている場合、
自動調整は一切実行しない(人間の確認が必要)
ここまで書いてようやく、Agent に「データを見て判断する」役割を渡せる土台ができました。次にやったのは、判断ループを Antigravity Workflow に落とすことです。
オンデマンド最適化ループの全体像
Antigravity の Agent を組み込んだ最適化ループは、次の 6 つのステップを毎日 1 回、深夜 2 時に走らせる構成にしています。深夜帯にしたのは、Remote Config の変更が一晩寝かせた後に確認できるよう、私の起床時刻にちょうど指標が出揃うようにするためです。
Firebase Analytics と AdMob から直近 7 日のメトリクスを取得(BigQuery エクスポート + AdMob API)
Crashlytics の crash-free users を取得し、99.0% を下回っていたらここで終了
直近 24 時間の Remote Config 変更履歴を取得(Firebase Remote Config REST API)
Antigravity Agent に上記データを渡し、「次に動かすべきキーと値」を最大 2 件提案させる
提案を AGENTS.md のガードレールで検証し、違反していたら破棄
残った提案を Remote Config に書き込み、変更内容を Slack に通知
最初は Step 4 を Agent に任せた直後にすぐ Step 6 まで行く構成にしていたのですが、明らかに筋の悪い提案が紛れることがあり、Step 5 を独立した検証ステップに分離しました。Agent の提案を別の小さなコードで機械的に検証する、という構成は今のところ非常に有効です。
// scripts/admob-tuning/validate-proposal.ts
import { AGENTS_GUARDRAIL } from "./guardrail" ;
type Proposal = {
key : string ;
current_value : number ;
proposed_value : number ;
reason : string ;
};
export function validateProposal ( p : Proposal ) : { ok : boolean ; reason ?: string } {
if ( ! p.key. startsWith ( "tunable." )) {
return { ok: false , reason: `non-tunable key: ${ p . key }` };
}
if ( ! AGENTS_GUARDRAIL .allowedKeys. includes (p.key)) {
return { ok: false , reason: `key not in allowedKeys: ${ p . key }` };
}
const delta = Math. abs (p.proposed_value - p.current_value) / p.current_value;
if (delta > 0.15 ) {
return { ok: false , reason: `delta ${ ( delta * 100 ). toFixed ( 1 ) }% > 15%` };
}
const bounds = AGENTS_GUARDRAIL .bounds[p.key];
if (p.proposed_value < bounds.min || p.proposed_value > bounds.max) {
return { ok: false , reason: `out of bounds: ${ bounds . min }〜${ bounds . max }` };
}
return { ok: true };
}
このコードは Agent には書かせず、私が手で書いています。判断は Agent に任せても、判断の検証は人が書いた決定論的なコードに通す。この役割分担をはっきりさせると、本番運用での安心感が一段違いました。
メトリクス取得の落とし穴
Firebase Analytics を BigQuery にエクスポートしている方は分かると思うのですが、デフォルトでは前日のデータが朝の数時間で揃います。最適化ループを深夜 2 時に走らせる場合、参照できるのは「昨日のデータ」ではなく「一昨日のデータ」になります。これは認識しておかないと、Agent が「直近の変更の影響が見えていない状態で次の提案を出す」事故が起きます。
私は Agent に渡すコンテキストの先頭に、必ず次のような注記を入れています。
データの新鮮さに関する注意:
- Firebase Analytics: 直近24時間のデータは含まれない(BigQuery export の仕様)
- AdMob API: 当日のデータは午後に確定するため、直近12時間は含まれない
- Crashlytics: 概ねリアルタイム(直近1時間程度の遅延)
したがって、本日 02:00 時点で参照できる Analytics データは 5/19 までです。
昨日(5/20)行った Remote Config 変更の効果は、この時点ではまだ判定できません。
昨日の変更がある場合は「効果未測定」として、新たな提案を保留してください。
この注記を入れてから、Agent が「変更直後に再度変更する」という最も危ない行動を取らなくなりました。プロンプトの設計で防げる事故と、コードで防ぐべき事故を切り分けることは、Agent を本番に組み込むうえでとても重要です。
Antigravity Workflow への落とし込み
ここまでのループを、Antigravity の Workflow 機能で 1 ファイルに束ねます。私は .antigravity/workflows/admob-nightly.yaml に次のように書いています。
name : "AdMob nightly tuning"
schedule :
cron : "0 2 * * *"
timezone : "Asia/Tokyo"
variables :
app_id : "com.dolice.wallpaper.zen"
guardrail_path : "scripts/admob-tuning/guardrail.ts"
steps :
- id : fetch_metrics
run :
command : "pnpm exec tsx scripts/admob-tuning/fetch.ts"
env :
TARGET_APP_ID : "${{ variables.app_id }}"
timeout : 120
- id : crash_gate
run :
command : "pnpm exec tsx scripts/admob-tuning/crash-gate.ts"
on_failure : abort_workflow
- id : agent_proposal
agent :
profile : "tuning-strategist"
input_from : "fetch_metrics"
max_iterations : 4
timeout : 300
- id : validate
run :
command : "pnpm exec tsx scripts/admob-tuning/validate.ts --proposal=${{ steps.agent_proposal.output_path }}"
on_failure : skip_remaining
- id : apply_remote_config
run :
command : "pnpm exec tsx scripts/admob-tuning/apply.ts --proposal=${{ steps.validate.output_path }}"
- id : notify
run :
command : "pnpm exec tsx scripts/admob-tuning/notify-slack.ts --result=${{ steps.apply_remote_config.output_path }}"
agent.profile で参照している tuning-strategist は、Antigravity 側に登録した Agent プロファイルです。プロファイル定義の側で「使ってよいツール」「読んでよい AGENTS.md のセクション」「期待する出力フォーマット」を制約します。
# .antigravity/agents/tuning-strategist.yaml
name : "tuning-strategist"
description : "AdMob と Firebase メトリクスを読んで Remote Config の調整案を出す"
allowed_tools :
- read_file
- run_shell : [ "pnpm exec tsx scripts/admob-tuning/fetch.ts --help" ]
context_files :
- "AGENTS.md#remote-config-自動調整の制約"
- "scripts/admob-tuning/playbook.md"
output_schema : "scripts/admob-tuning/proposal.schema.json"
max_proposals : 2
output_schema を強制することで、Agent の自然言語の説明と一緒に必ず構造化 JSON が返ってきます。この JSON が次の validate ステップに渡され、機械的に検証されます。Agent の出力を「自由形式の文章」のまま下流に流すと、必ずどこかで形式の崩れが起きます。Antigravity の Workflow で完結させるなら、各ステップの境界で構造化フォーマットを強制するのが、半年運用して得た一番の知見でした。
「触ってよい比率」をどう決めるか
ここまで仕組みの話を書いてきましたが、運用上もっとも難しいのは「ユーザの何 % まで自動調整の対象にしてよいか」という比率の設計です。私の現在の設計は次のとおりです。
アプリ全体のうち 90% は手動で固定した値(fixed バケット)
残り 10% を Agent による自動調整の対象(experiment バケット)
experiment バケットの中で、さらに condition で 2 つのサブバケットに分け、片方は「ベースライン保持」(直近 7 日の平均値を維持)、もう片方は「Agent 提案を適用」
experiment バケット 10% という比率は、当初 30% にしていたのを段階的に下げた結果です。30% で運用すると、Agent が立て続けに値を動かしたタイミングで、全体の eCPM が 5〜8% 単位で揺らぐことがありました。10% に下げてからは、揺らぎは全体の 1% 以内に収まり、もし提案が悪手だった場合の影響範囲も読める規模に収まっています。
個人開発で AdMob を回している場合、収益は予測可能性が極めて重要です。eCPM の月間ボラティリティが大きいと、月次の積立や次の制作費の計画が立てにくくなります。私は「収益の月間ボラティリティを 5% 以内」という制約を運用上の北極星にしていて、experiment バケットの比率はこれを超えない範囲で決めています。
eCPM と Day 1 リテンションを同時に守る判断軸
Agent に渡すプレイブック(playbook.md)には、次のような判断軸を文章で書いています。
## 判断軸の優先順位
1. crash-free users が 99.0% を下回っているなら、いかなる調整も提案しない
2. Day 1 リテンションが直近 7 日平均より -1.5pt を超えて低下しているなら、
広告表示頻度を減らす方向(マイナス側の調整)のみ提案する
3. Day 1 リテンションが安定(直近 7 日平均の ±0.5pt 以内)かつ eCPM が直近 14 日
最低値を下回っているなら、フォーマット切り替え系のキーのみ提案する
4. 上記いずれにも該当しない場合は「提案なし」を返す
## 禁止されている提案
- リワード広告の倍率を 1.5 倍より高くする
- App Open の頻度をセッションあたり 1.5 回より高くする
- Day 0 抑制フラグを false に切り替える(Day 0 はリテンション最重要日)
判断軸の優先順位を明示すると、Agent の提案がぶれにくくなります。注意したいのは、判断軸は AGENTS.md ではなくプレイブックに書き、頻繁に更新する想定にしておくことです。AGENTS.md は「絶対に守るべきガードレール」、playbook.md は「現時点の戦略」と役割を分けると、改善のサイクルが回しやすくなります。
Slack 通知の設計:判断と意思を分離する
最後に、Slack 通知の設計だけ補足します。Agent が提案して Remote Config に書き込んだ内容は、必ず Slack に流して、私の朝の最初の 5 分で目を通せるようにしています。通知本文の構成はこうです。
[AdMob nightly tuning] 2026-05-21 02:14 JST
📊 評価したメトリクス(5/13〜5/19)
- Day1 retention: 32.8% (-0.3pt vs 7d avg)
- Interstitial eCPM: $4.07 (-2.4% vs 14d min)
- App Open eCPM: $3.21 (-5.6% vs 14d min)
- Crash-free users: 99.62%
🤖 Agent 提案(2 件)
1. tunable.interstitial_frequency_cap: 4 → 3 (-25.0%)
理由: eCPM が 14d 最低値を下回り、頻度を下げて単価を回復させたい
→ 却下: 変動幅 25% が 15% 上限を超過
2. tunable.app_open_min_interval_sec: 30 → 35 (+16.7%)
理由: App Open eCPM が下落傾向。表示間隔を広げて単価を維持
→ 却下: 変動幅 16.7% が 15% 上限を超過
✋ 適用された変更: 0 件
💡 メモ: 今回は両提案ともガードレール超過で却下されました。
連続却下が 3 日続いたら、playbook.md の判断軸を見直してください。
私はこの通知を見て、必要に応じて手動で Remote Config を動かすか、playbook.md の判断軸を緩めるかを決めます。Agent の提案を機械的に却下した記録もすべて Slack に残るので、後から「なぜ Agent はあのとき何もしなかったのか」が追えるようになっています。Agent と人間の意思決定の境界をログとして可視化することが、Agent を信頼するための最低条件だと感じます。
6 か月運用しての所感
この仕組みを 6 か月運用した結果、手応えのある変化と、手応えのない変化の両方がありました。手応えのあった変化を先に挙げます。
広告監視に費やす時間が 1 日 30〜45 分 → 1 日 5〜10 分 に減った
過去 6 か月で広告関連の事故(Day 1 リテンション 1.5pt 超下落)は 0 件
experiment バケットの eCPM が、fixed バケットに対して平均 +3.2% で推移
手応えがなかったほうの話もしておきます。「Agent に任せたら eCPM がぐっと伸びる」という期待は、現時点では裏切られています。experiment バケットの +3.2% は、fixed バケットを保守的に運用しているぶんが大きく、Agent 単独の貢献は誤差の範囲です。Agent の現在の役割は「私の判断を肩代わりする」よりも「私が手を動かさなくても収益のボラティリティを抑える」ことのほうが近いと感じています。
それでも、毎日の広告監視時間が 1 日 30 分減ったぶんで、アート活動のための時間や、子どもたちと話す時間を取り戻せたことは、個人開発を続けるうえで決定的に大きい変化でした。両家の祖父が宮大工で、丁寧に組み上げたものは何十年も持つ、という感覚を幼少から見て育ったので、収益の仕組みもできる限り長く、無理なく保てる形にしておきたいと考えています。Agent を組み込んだ Remote Config の最適化ループは、いまのところその方向に動いてくれている実感があります。
次に試したいこと
最後に、これから試したい改善を 2 つだけ書いておきます。
1 つ目は、experiment バケットを「ユーザのアプリ内行動セグメント」で切り直すことです。現在は単純なランダム 10% ですが、Firebase Analytics のオーディエンスを使えば、「過去 30 日でリワード広告を 3 回以上見たユーザ」「IAP 未購入かつ Day 7 まで継続したユーザ」のような行動セグメントに対して、別々のチューニング戦略を当てられます。すでに 5 つほどのオーディエンスを定義済みで、来月以降に Agent プロファイルを分けて当て込む予定です。
2 つ目は、Antigravity の Background Agent を使って、Agent の判断ログ自体を別の Agent にレビューさせる仕組みです。週次で「先週 Agent が出した提案 7 件のうち、却下された 5 件は妥当だったか」「適用された 2 件は事後的に見ても正解だったか」を別の Agent に評価させ、結果を playbook.md の改訂材料にする、という二段構えです。Agent の判断品質を、別の Agent と過去データで継続的に評価する仕組みは、本番投入の前提条件として、今後どの個人開発者にも必要になっていくと感じています。
仕組みのコード一式とプレイブックの実例は、私が運営するサイトの dolice.design 側で別途まとめる予定です。同じように個人で複数アプリを運用しながら AdMob と Remote Config を回している方の参考になれば幸いです。お読みいただきありがとうございました。