App Store Connect の数字を、毎朝Slackで「使える短い文章」として読みたい。2014年から続けている個人開発の現場で長らく感じていた小さな願いを、Gemini 2.5 Flash でようやく形にした2ヶ月のメモです。
毎朝のSlackに数字ではなく所感を流したかった理由
iOS と Android で累計5,000万ダウンロードに育った壁紙系と癒し系のアプリ群を、ひとりで運営しています。AdMob 収益が柱ですが、有料アプリと App内課金の売上も無視できない規模で動いており、地域別の崩れ方や OS バージョン別の傾向は毎日眺めたい類の情報です。
それまでは App Store Connect の Sales/Trends API から落とした CSV を Numbers で開いて、自分の頭の中で「昨日と比べてどう動いたか」を整理していました。数字を読むのは嫌いではないのですが、寝起きの15分をそこに溶かしてしまうのが惜しく、もう少し短い時間で全体像を掴みたかったのです。
そこで Gemini 2.5 Flash を要約レイヤーに挟み、Slack の朝チャンネルに「数字+一段落の所感」が並ぶ形にしました。本記事はその仕組みを2ヶ月運用したあとの率直な振り返りです。
パイプラインの全体像
ジョブは Cloudflare Workers Cron で毎日 JST 7:30 に起動し、以下の順に処理しています。
# scripts/daily_revenue_digest.py(抜粋)
import os, json, datetime
from google import genai
from appstoreconnect_client import fetch_sales_report
from slack_sdk import WebClient
YESTERDAY = datetime.date.today() - datetime.timedelta(days=1)
WINDOW = 14 # 14日分の傾向を一緒に渡す
def build_prompt(today_rows, trend_rows):
return f"""
あなたは個人開発者の運用パートナーです。
以下は iOS / Android アプリ群の昨日({YESTERDAY})の売上ログと、直近{WINDOW}日のトレンドです。
3段落以内、各150字以内で、日本語の所感を書いてください。
ルール:
- 数値は最大3つまで本文に入れる
- 異常検知は「平常」「やや異常」「要確認」の3段階で1行ラベルを付ける
- 翌朝に読む人が安心できるトーンで、扇情語は使わない
[昨日の生データ]
{json.dumps(today_rows, ensure_ascii=False)}
[直近{WINDOW}日のトレンド]
{json.dumps(trend_rows, ensure_ascii=False)}
"""
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
resp = client.models.generate_content(
model="gemini-2.5-flash",
contents=build_prompt(today_rows, trend_rows),
config={"temperature": 0.3, "max_output_tokens": 600},
)
WebClient(token=os.environ["SLACK_BOT_TOKEN"]).chat_postMessage(
channel="#dolice-morning",
text=resp.text,
)要点はプロンプトに2つだけルールを書いたことです。1つは「数値は本文に最大3つまで」、もう1つは「扇情語を使わない」です。前者は要約が数字の羅列に逆戻りするのを防ぎ、後者は2.5 Flash が時折出す「劇的に伸びました」「驚異の上昇」といった、個人開発者の朝に欲しくないテンションを抑えるためでした。
Pro ではなく Flash を選んだ理由
最初のひと月は Gemini 2.5 Pro で試しました。文章は自然でしたが、150字 × 3段落というサイズに対しては明らかに過剰で、月の API コストが概算で4倍弱になりました。
App Store Connect の日次売上は構造化されたタブlular データで、しかも前日比という比較対象が明確です。Flash でも「14日のトレンドのうち、今日が下振れたら指摘して」程度の判断は十分に下せました。temperature: 0.3 を維持する限り、暴走するような所感は2ヶ月で1度も出ていません。
Pro を残しているのは週次のロールアップだけです。月曜の朝に1回、過去7日の Sales/Trends と AdMob レポートを並べ、「週として何が伸びたか」「来週注視するべき地域はどこか」を200字程度の所感にまとめてもらっています。深い因果関係を考えてほしいときだけ Pro、淡々と読みたいときは Flash、という切り分けがいまのところ落ち着いています。
14日のトレンドを「一緒に渡す」だけで読み違いが減った
最初の数週間は、Flash に「昨日の売上だけ」を渡していました。要約自体は数行できれいに上がってくるのですが、「平常」「やや異常」の判断が日によって揺れる弱点がありました。Flash の側で何が平常なのかを知らないので、当たり前と言えば当たり前です。
そこで、Sales/Trends API から取得した直近14日分のレコードを、軽く集約した状態で一緒に渡すように変えました。地域別の DL 数だけ、課金タイプ別の売上だけ、というふうにスリムに切ってから JSON で添える形です。
def slim_trend(rows, days=14):
by_country = {}
for r in rows:
key = r["country"]
by_country.setdefault(key, []).append(r["amount_jpy"])
return [
{"country": k, "avg": round(sum(v)/len(v)), "n": len(v)}
for k, v in by_country.items()
]平均値とサンプル数だけ添えるだけで、Flash の所感は明らかに落ち着きました。「直近14日でドイツが平均1,820円のところ、今日は2,140円」という比較が自然に書かれるようになり、私が頭の中でやっていた前日比の暗算がほぼ要らなくなりました。トークンコストは2割増えましたが、要約の信頼性が上がった対価としては十分に安いものでした。
AdMob と一緒に読むとき詰まったこと
App Store Connect 単体での要約は早い段階で安定したのですが、AdMob レポートと並べた瞬間に Flash が混乱した時期がありました。原因はシンプルで、AdMob の eCPM は USD 建て、App Store Connect の売上はストアごとに通貨が違うため、同じ表の中で「+18%」と「+22円」が混在し、Flash の所感が単位の混同を起こしていました。
対処は、プロンプトに与える前に Python 側で全額を JPY 換算に揃え、必要なら currency_original という別フィールドで原通貨を残す、という前処理を入れただけです。
def normalize_revenue(rows, fx_table):
out = []
for r in rows:
jpy = r["amount"] * fx_table.get(r["currency"], 1.0)
out.append({
"country": r["country"],
"amount_jpy": round(jpy),
"currency_original": r["currency"],
})
return outLLM 側の限界を技術で取りに行くより、データ側で素直に整える方が運用ログ的にも安全でした。LLM に通貨換算の判断まで任せると、為替が動いた日に「異常に伸びた」と誤認識される事故が起こりやすかったのです。
滑った日の対処と「平常ラベル」の効用
2ヶ月のうち、Flash の要約が滑った日は3回ありました。いずれも App Store Connect 側の遅延配信で「昨日のデータが半日分しか入っていない」状況で、それを Flash が「売上が急減した」と読み違えてしまった日でした。
直したのは2点です。
1つ目は、Sales/Trends API から取得したレコードに「対象期間の終了タイムスタンプ」を必ず添え、Flash 側にも「データの確報性: 速報 / 確報」というメタ情報を渡すようにしたこと。2つ目は、所感の冒頭に「平常 / やや異常 / 要確認」の1行ラベルを必ず付ける運用にしたことです。
def annotate_completeness(rows):
return [
{**r, "data_state": "preliminary" if r["row_count"] < r["expected"] * 0.9 else "final"}
for r in rows
]ラベルが付くことで、寝起きの15秒で「これは普通の日」と判断できるようになり、3段落の所感を読むかどうかも自分で選べるようになりました。要約があることよりも、要約に手を出さないでよい朝を増やすことが、運用品質の体感を変えました。
2ヶ月運用してわかった、所感の限界と相棒としての距離感
Flash の要約は、個人開発の朝チャンネルにちょうど良い相棒になりました。一方で、Flash の所感はあくまで「数字をなだらかに読ませる補助線」であって、戦略を立てる材料には足りません。たとえば「ドイツ語圏で壁紙アプリの新規DLが2週間連続で増えている」という事実は拾えても、「そこに合わせて新作カラーパレットを足すべきか」までは判断できません。
その境界を引いたあとは、毎朝のSlackは「読んで安心するための場所」、週次の Pro レポートは「考えるための場所」、と役割を分けるようになりました。1997年に16歳でインターネットに触れて以来、独学のプログラミングが個人運営の支えになってきましたが、その手触りに「数字をやわらかく読む層」が一つ加わったような感覚です。
明日の朝、もし同じパイプラインを試してみたい方がいたら、まずは Cloudflare Workers Cron と Gemini 2.5 Flash の組み合わせで「Slack に150字の所感を流す」だけのミニマム構成から始めてみるのが良いと思います。プロンプトに「扇情語を使わない」の一文を必ず添えること、それだけで朝の質感がだいぶ変わるはずです。
お読みいただきありがとうございました。