●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
A Morning Digest Agent across App Store Connect, Play Console, Crashlytics, and AdMob — 30 days of running it on Claude Agent SDK
Opening four dashboards each morning across six apps used to eat 30 to 50 minutes of my day. Here is the Claude Agent SDK recipe that compressed it into one email, with the measured numbers from a full month.
For the last few years my mornings followed the same pattern. Open App Store Connect for yesterday's sales, then Play Console for ratings and crash-free rate, then Crashlytics for new issues, then AdMob for eCPM anomalies, then Gmail for user mail. Across six apps in active operation this took 30 to 50 minutes, and by the time my coffee was gone the entire morning had been spent on confirmation work instead of building anything.
In April 2026 I handed that ritual to a Claude Agent SDK session and compressed it into a single morning email. What follows is the design, the failure patterns that surfaced in the first 30 days, and the practical fixes for things like AdMob's reporting lag and Crashlytics payload bloat, all with measured numbers. I write as Masaki Hirokawa (Dolice), an indie developer working since 2014 across a catalog that has now passed 50 million cumulative downloads. The conclusion that shaped this work is simple: the most valuable time an indie developer can reclaim is not judgment time but confirmation time.
What the morning digest does — and what it deliberately does not do
I want to draw the line up front. The morning digest is responsible for compressing the time it takes to confirm five dashboards across six apps. Judgment stays with me. Earlier experiments where I let an LLM auto-reply to App Store reviews led to one warning about policy violations, and that experience pushed me toward a stricter scope. The agent's job is just three things:
Aggregate 6 apps × 5 sources = 30 cells per day into one email body
Highlight only the values that diverge from baseline (deltas)
Surface a short list of "TODO candidates" that need my judgment
Review reply drafting, automated Crashlytics dismissals, and refund decisions are all intentionally left out. My family roots are in carpentry — both of my grandfathers were temple carpenters — and the sense that "working with one's hands is a form of faith" runs underneath this catalog of apps. Delegating the judgment itself would betray that.
A Cloudflare Workers cron fires at 6:50 JST and starts a Claude Agent SDK session. The agent calls five custom MCP tools in turn, accumulates their results, and finally formats a Markdown body that ships to me via SendGrid. The reason the digest goes to email and not Slack is just that I open Gmail first thing in the morning by habit, and habits matter more than tools when it comes to whether you actually read the digest.
// worker.ts — Cloudflare Workers cron triggerimport { ClaudeAgent } from "@anthropic-ai/agent-sdk";import { tools } from "./digest-tools";export default { async scheduled(event: ScheduledEvent, env: Env) { const agent = new ClaudeAgent({ apiKey: env.ANTHROPIC_API_KEY, model: "claude-sonnet-4-6", systemPrompt: PERSONA_PROMPT, // static, eligible for prompt caching tools, maxIterations: 12, }); const result = await agent.run({ task: "Generate today's morning digest and ship it via send_email_digest.", metadata: { userId: "masaki", date: today() }, }); console.log("digest sent:", result.summary); },};
The maxIterations: 12 cap is a safety valve. Early on I left it uncapped, and a day when the AdMob report was late produced an agent loop that retried 30 times before giving up. A fixed ceiling is cheaper than a fancy backoff strategy for a once-a-day cron.
✦
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
✦Concrete architecture (with code) that compresses App Store Connect / Play Console / Crashlytics / AdMob / Gmail into one email
✦Measured $14.20 monthly API spend with 92.3% prompt-cache hit rate on the static persona portion
✦Three real failure patterns from 30 days of operation — partial-failure swallow, AdMob 24h lag, and three false alerts — and the guardrails that fixed them
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.
The five custom tools — specs and the traps inside each
Each source is a single MCP tool. Tool names, input schemas, and output schemas need to line up exactly or Claude will call them in surprising ways, so I define everything in Zod and validate both directions.
1. fetch_app_store_connect_sales
This calls the App Store Connect API's salesReports/CONSOLIDATED endpoint for one day. The XML response is normalized through a parseAscSalesReport helper that I built up over a decade of running this catalog, and the tool returns units, proceeds_usd, and top_3_countries per app.
export const fetch_app_store_connect_sales = tool({ name: "fetch_app_store_connect_sales", description: "Aggregate yesterday's App Store Connect sales across all apps. proceeds are in USD.", input: z.object({ date: z.string().describe("YYYY-MM-DD") }), handler: async ({ date }) => { const jwt = await mintAppStoreJwt(); const xml = await fetch(`https://api.appstoreconnect.apple.com/v1/salesReports?...&filter[reportDate]=${date}`, { headers: { Authorization: `Bearer ${jwt}` }, }).then((r) => r.text()); return parseAscSalesReport(xml); },});
There are three traps. First, the JWT only lives for 20 minutes, and Cloudflare Workers CPU timing means a token minted before the fetch can already be invalid when the request lands, returning a 401. I now mint a fresh JWT inside every call. Second, App Store Connect's salesReports does not have yesterday's data ready until roughly noon UTC, which is well after JST 6:50; I deliberately pull "the day before yesterday" instead, and I accept that the morning summary is one day delayed. Third, converting proceeds from USD to JPY needs a TTM rate pulled by a separate fetch_fx_rate tool once per morning, otherwise daily aggregates wobble by 1 to 2 percent depending on which app was fetched at which moment.
2. fetch_play_console_reviews
This tool pulls the last 24 hours of reviews.list from the Google Play Developer API. Review bodies are heavy on agent context, so the tool returns the top 10 most recent reviews plus every 1- or 2-star review, and only counts for the rest.
redactPII redacts email addresses, phone numbers, and competitor app names from review bodies. I skipped it on the first version, then one morning the agent quoted a user's real email back to me in the digest. That was enough to make PII redaction non-negotiable.
3. fetch_crashlytics_top_issues
Firebase Crashlytics' REST API has been in beta on the surfaces I need, so my tool routes through the BigQuery export instead. It returns the top 5 issues by occurrence count from the past 24 hours, plus every issue flagged as new regardless of count.
SELECT issue_id, issue_title, ANY_VALUE(version) AS version, COUNT(*) AS occurrences, MIN(event_timestamp) AS first_seenFROM `crashlytics_pkg_name.crashes_*`WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE('Asia/Tokyo'), INTERVAL 1 DAY)) AND fatal = TRUEGROUP BY issue_id, issue_titleORDER BY occurrences DESCLIMIT 5;
The tool's return shape includes a last_known_action field that comes from a Firestore document I maintain — "have I already touched this crash?" The agent uses it to avoid resurfacing the same crash every morning, which was a real failure mode in week one.
4. fetch_admob_yesterday_revenue
The AdMob Network Report API gives me eCPM, fill rate, and estimated_earnings. The tool's return always includes a report_lag_hours field, which is my defense against AdMob's published-but-not-final 24-hour window. The agent is instructed via prompt to write "data unavailable" instead of any numbers when report_lag_hours > 6.
5. fetch_inbox_developer_mail
This tool pulls Gmail using the filter from:noreply@apple.com OR from:googleplay-developer-noreply@google.com OR label:app-support for the last 24 hours, and returns only subject lines plus the first 240 characters of the body. Returning full bodies once caused a 60k-token spike when a long support email arrived. Truncation is the cheap fix and it has held.
System prompt design — a 5-minute TTL that actually pays off
To get value from Claude Sonnet 4.6's prompt caching, the system prompt is split into two strict layers.
STATIC_PERSONA holds the six product names, their categories, the baseline eCPM range per app, the review reply policy, and the strict output template. dynamicPart only carries today's date and FX rate. My initial assumption was that a 5-minute TTL would never help a once-a-day cron, but in practice the agent re-issues the same prompt within 2 to 4 minutes during delta detection and retries, so over 30 days the hit rate landed at 92.3% against 4,710,000 cumulative cache_read_input_tokens, billed at one-tenth of standard input pricing. Total monthly API spend came out to $14.20, or roughly $0.47 per cron run.
Formatting — write only what is different
The body of the digest is produced by a final tool format_morning_digest rather than by Claude directly. The reason is the three false alerts from the first month: in one of them the agent wrote "eCPM dropped 50%" when AdMob had simply not reported yet. That mistake is what convinced me to keep the threshold logic on the deterministic side.
function formatMorningDigest(data: AggregatedData): string { const sections: string[] = []; sections.push(headerLine(data)); sections.push(salesSection(data, baselineSalesLast14d)); // only deltas vs. 14d baseline sections.push(reviewsSection(data)); // only 1-2 star and new low reviews sections.push(crashlyticsSection(data, dismissedIssues)); // exclude already-dismissed sections.push(adMobSection(data, baselineEcpmByApp)); // only beyond normal range sections.push(inboxSection(data)); // subjects + leading text for urgent sections.push(todoCandidates(data)); // items that need my judgment return sections.join("\n\n");}
What I do let Claude do is rank the todoCandidates items by urgency. The thresholds — crash-free rate below 99.8%, three or more 1-star reviews, sales down more than 30% vs. 14-day baseline — are hard-coded so the boundary does not drift with model temperament.
Three real failures in 30 days, and how each was fixed
The system was not a finished product on day one. Three operational failures shaped the current version.
The first was partial-failure swallow. On a day when AdMob returned 503, the agent dropped the AdMob section entirely instead of writing "AdMob: fetch failed." I read the empty space as "zero revenue" for a panicked second. Fix: the agent prompt now explicitly says "each source must be acknowledged, failures included," and format_morning_digest injects (no data) whenever a section comes back empty. Two-layer defense for a class of bug that only shows up under partial failure.
The second was Crashlytics payload bloat. The morning after a new release pushed 187 new crashes through Crashlytics, and the tool returned all 187. The agent's context filled to ~180k tokens, the digest landed but the cron run took 45 seconds and was one notch shy of the Workers subrequest timeout. Fix: the tool now groups by issue_id, returns the top N grouped entries, and adds a remainder_count field so the agent can mention the volume without ingesting every entry. Worst-case context consumption is now under 12k tokens.
The third was three false alerts: the eCPM drop already mentioned, a day when sudden FX volatility made the proceeds field look double-counted, and a morning when re-labelling Gmail support threads showed up as "unread = 0, possible outage." Each one taught the same lesson: thresholds belong in code, not in an AI's interpretation. The fixes I make to AI judgment cost me more in re-runs than the same fix to a if (...) return WARN line. That has been true since I first integrated machine-decided logic into my apps in 2014, and it remains true with Claude in 2026.
A redacted sample of the actual digest
Here is what an actual morning email looked like a few days ago, with identifiers stripped.
Morning Digest — 2026-05-XX
Cron run #29 / API cost: $0.46 / latency: 11.3s
Sales (day-before-yesterday, USD baseline)
Wallpaper Aurora : 312 units / $94.20 / -8% vs 14d avg (mildly low)
Healing Sounds Pro : 41 units / $40.59 / +12%
Manifestation Diary : 88 units / $79.20 / +3%
(3 other apps within normal range — omitted)
Reviews
Wallpaper Aurora: 2 one-star reviews
"wallpaper freezes" (iOS 18.4, iPhone 14 Pro) — new
"too many ads" — known pattern
Others within normal range
Crashlytics
New issues: 1
[Aurora 2.4.1] NSInvalidArgumentException -[NSNull length] (12 occurrences)
Top recurring: none beyond already-dismissed
AdMob
eCPM within normal range, nothing to note
Inbox (4)
- "Refund question" from xxxx@xxx.com (urgency: medium)
- "Music won't stop" from xxxx@xxx.com (urgency: high / Healing Sounds)
- (2 more, subject only)
Today's TODO candidates (suggested by Claude)
1. The Aurora NSNull length crash is new. Confirm the reproduction path.
2. The "music won't stop" thread in Healing Sounds likely points at the BGM persistence bug.
3. Aurora's mild sales dip is plausibly post-holiday rebound. Recommend waiting one more day.
It took about three weeks for the format to settle. Early versions used no emoji and listed every value for every app, which produced a wall of text I started skimming past — exactly the symptom the whole project was meant to cure. Switching to "omit normal range" plus headline emoji dropped my read time from 4 minutes to about 50 seconds.
Real numbers over 30 days
Here are the actual costs from a month of operation, in case it helps you decide whether to build your own.
Item
30-day total
Per day
Claude API (Sonnet 4.6, prompt cache included)
$14.20
$0.47
Cloudflare Workers Cron invocations
$0.00 (free tier)
$0.00
BigQuery Crashlytics query
$0.31
$0.01
SendGrid send
$0.00 (free tier)
$0.00
Total
$14.51
$0.48
Where it left me, in practical terms: roughly 15 hours of recovered focus time per month, in exchange for $14.51. That trade did not require much deliberation for me as an indie developer. The 15 hours quietly go into calls with my children who live some distance away, or into time spent in the Kichijoji studio working on art that has no business value, and those hours have grown to matter more than the spreadsheet would suggest.
What I want to build next
The next step I am actively designing is a return loop: the agent reads my replies to the digest email, parses TODO statuses (dismissed / started / later), and writes them back to Firestore. That feedback would let the next morning's digest automatically suppress items I have already acted on. The Claude Agent SDK side will need an inbound mail hook, and the design is still in flight.
If you want to try this in your own catalog, the most reliable starting point is one source — for example, AdMob alone — wired up the same day so you get an email at the end of it. Aiming for the finished version up front is what kills these projects. I started with a 30-line script that piped App Store Connect sales into Slack, and I built from there. Thank you for reading. If you are another indie developer who walks through the same dashboards before dawn, I hope this proves useful.
Share
Thank You for Reading
Claude 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.