●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 Daily Revenue Pipeline for 4 Wallpaper Apps: 8 Weeks Running App Store Connect API + AdMob With Claude Code
An 8-week record of running App Store Connect API and AdMob Reporting API through a Python pipeline for 4 iOS wallpaper apps, with Claude Code helping absorb JWT auth quirks, gzip TSV edge cases, timezone misalignment, and Slack alerting thresholds — written from an indie developer's perspective.
A Slack message arrives at 7:00 JST every morning with yesterday's revenue, AdMob earnings, and Day 7 subscription retention for all four of my wallpaper apps, on a single line. It took about eight weeks to reach that state.
I have been building iOS apps as an indie developer since 2014, and even after my apps crossed 50 million cumulative downloads, my morning routine was still to open App Store Connect, then AdMob, then Firebase one by one and copy numbers into a Google Sheet. It is the kind of work that is dull and easy to get wrong. Forgetting to do it on a weekend means stacking three days of work on Monday morning.
To hand off this routine for good, I asked Claude Code to build me a Python pipeline that runs at 7:00 JST every morning, calls App Store Connect API and AdMob Reporting API for all four apps, joins them on a JST date axis, and writes back to a dashboard. Writing the first draft took a day. Making it boring and reliable in production took eight weeks. Below are the specific traps I hit along the way and where Claude Code actually mattered.
Why "just look at the dashboards" did not work
App Store Connect, AdMob, and Firebase all offer fine dashboards. I still built my own pipeline for three reasons.
The first reason is timezone mismatch. App Store Connect's daily reports use US Pacific time (UTC-8). My AdMob account also runs in Pacific. Firebase Analytics is set to JST on some projects and Pacific on others. Comparing "revenue for May 22, 2026" across three dashboards requires constant mental correction across slightly different windows. Running four apps in parallel as an indie, doing that adjustment in your head all day is not sustainable.
The second reason is the lookback window. AdMob's UI gives you two years, but if you want the same weekly aggregate every Friday, accumulating it yourself is dramatically faster. After eight weeks of operation, I have 180 days of daily data sitting in a local SQLite database. When I run a new experiment I can ask "how does this compare to the same weekday over the past six months" on the spot.
The third reason is cross-app signal latency. When you look at four apps' revenue, eCPM, and retention separately, small signals like "wallpaper app A's AdMob CPM has been dropping since Friday" disappear into the noise. A single dashboard surfaces those. In the past eight weeks, my morning Slack digest caught a 38% Wednesday-to-Thursday eCPM drop on one rewarded ad unit, and I flipped the mediation order over the weekend instead of finding out the following Tuesday.
Overall structure: what to fetch, where to put it
The first decision I made with Claude Code was about storage and granularity. For a single indie developer with four apps, jumping straight to Cloud SQL or BigQuery felt like overkill.
I put S3 in the middle on purpose so that API fetching and aggregation are fully decoupled. If an API call fails due to rate limits or an auth error, the raw CSV still exists, and the aggregation layer can be re-run as many times as needed. Telling Claude Code from the start "I want fetch, transform, and deliver split into three phases so I can isolate failures later" paid off later many times over.
SQLite was enough. Eight weeks of data sit in about 200 KB. Being able to drop into sqlite3 from the terminal whenever I want to reshape the schema is genuinely useful for an evolving setup.
✦
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 working ES256 JWT auth flow for App Store Connect API that avoids the DER vs raw signature trap, plus retry strategy for empty Sales Reports (200 status with header-only TSV)
✦Concrete AdMob Reporting API v1beta report spec with currency JPY, language ja, and timeZone Asia/Tokyo, plus a SQLite schema that joins ASC and AdMob on a single JST date axis
✦Six battle-tested alerting rules from 8 weeks of operation, including a 14.3% AdMob eCPM recovery I caught only because the morning Slack digest surfaced a Wednesday-to-Thursday drop
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 first wall was JWT authentication for App Store Connect API. You generate a .p8 API key under Users and Access, then build a JWT using the Key ID and Issuer ID.
The first code Claude Code wrote for me looked like this:
This returned 401 Unauthorized. Working through it with Claude Code, three traps surfaced.
First, iat and exp need to share the same time snapshot. Writing exp = int(time.time()) + 1200 and iat = int(time.time()) separately means the call to time.time() could differ by microseconds on fast machines, which sometimes shows up as boundary expiry errors during debugging. Capture now once and reuse it.
Second, the scope field can break some endpoints. Apple's sample code sometimes includes scope, but the Sales/Trends Reports endpoint was more stable without it.
Third, and the decisive one, handcrafted ES256 implementations can confuse DER and raw signature formats. PyJWT handled this correctly out of the box, but the early manual cryptography implementation Claude Code drafted returned a raw 64-byte signature (r + s concatenated). Apple's API expected DER. This is not stated explicitly in the docs — you only discover it by reading the library source. The final working version is below.
I recommend PyJWT 2.8 or newer. After three days of debugging, I left a comment at the top of the script that reads: "Generate JWT with PyJWT 2.8+. If you must implement ES256 by hand, sign in DER format." Having Claude Code write down lessons learned right next to the code is one of the things I value most about working with it on solo projects.
Sales Report retrieval: gzip and empty-report days
Once JWT works, fetching Sales Reports is straightforward. The endpoint is https://api.appstoreconnect.apple.com/v1/salesReports with filter[frequency]=DAILY and filter[reportDate]=YYYY-MM-DD. The response is gzip-compressed TSV with Content-Type: application/a-gzip.
Two traps live here.
# Trap 1: Forget to decompress gzip and your CSV parser eats garbled text# Trap 2: Zero-sales day returns 200 (not 404) with a header-only bodydef fetch_daily_sales(token: str, vendor_id: str, report_date: str) -> bytes | None: url = "https://api.appstoreconnect.apple.com/v1/salesReports" params = { "filter[frequency]": "DAILY", "filter[reportDate]": report_date, "filter[reportType]": "SALES", "filter[reportSubType]": "SUMMARY", "filter[vendorNumber]": vendor_id, "filter[version]": "1_0", } headers = {"Authorization": f"Bearer {token}"} r = requests.get(url, params=params, headers=headers, timeout=30) if r.status_code == 404: # Apple has not finalized aggregation yet (can happen on early morning UTC-8 calls) return None r.raise_for_status() return gzip.decompress(r.content)
The truly nasty case is "a 200 response with only the TSV header line on a zero-sales day". I missed this at first and was happily logging "revenue 0 yen" until a Slack alert showed wallpaper app B sitting at zero for two days. The real cause was a holiday delaying Apple's aggregation, leaving a header-only TSV behind. After Claude Code added a check that treats header-only TSVs as None and re-queues them, the false alerts stopped.
A simple exponential backoff was enough for retry:
def fetch_with_retry(fn, *args, max_retry: int = 4) -> bytes | None: for attempt in range(max_retry): try: result = fn(*args) if result is not None: lines = result.decode("utf-8").splitlines() if len(lines) <= 1: return None # header-only TSV return result except requests.HTTPError as e: if e.response.status_code in (429, 500, 502, 503): time.sleep(2 ** attempt) continue raise return None
My ASC key happily handled around 30 req/min with no issue. Four apps × one fetch per day never came close to hitting it.
AdMob Reporting API: localization settings and date filters
AdMob is a Google API, so OAuth2 service account auth is straightforward. google-api-python-client makes the rest easy.
The trap is localizationSettings. I left it out initially and AD_REQUEST figures occasionally came back inconsistent. Setting currencyCode: "JPY" and languageCode: "ja" made earnings come back in JPY, which removed the need to convert USD inside my aggregation layer.
The timeZone: "Asia/Tokyo" line is the key bit. AdMob defaults to the account's timezone, but you can override it per report. This way, the timezone correction for ASC (Pacific to JST) and the AdMob timezone setting are decoupled and clearly visible at the call site.
If you join "Pacific 5/22 revenue" with "JST 5/22 AdMob earnings" naively, you end up off by 17 hours. My pipeline pre-shifts ASC reports forward 17 hours so they sit on JST day boundaries.
-- JST-aligned daily summaryCREATE TABLE daily_summary ( jst_date TEXT NOT NULL, app_id TEXT NOT NULL, asc_revenue_jpy INTEGER, -- App Store Connect (PT -> JST adjusted) admob_earnings_jpy INTEGER, -- AdMob (already JST via report spec) admob_impressions INTEGER, admob_ecpm_jpy REAL, subscriber_count INTEGER, PRIMARY KEY (jst_date, app_id));
This is an operational approximation, not an accounting-grade reconciliation. For interpreting daily variation through a human lens it works. A 1-2% gap between monthly sums and the ASC dashboard does not matter for this purpose.
Alerting: "25% day-over-day" and "three straight days at -15% same-weekday"
After eight weeks of operation, I learned that being too conservative with alert thresholds trains me to ignore Slack messages, while being too loose buries the signal that actually matters. The rules I settled on are these.
25% or more day-over-day variation on per-app ASC revenue or AdMob earnings triggers an alert.
Three consecutive days of "same-weekday down 15%" triggers a structural eCPM warning.
Day 7 subscription retention moves ±3 points from the trailing 14-day median, alert.
AdMob mediation CPI on a single ad unit drops 30% or more, per-unit alert.
App Store Connect API 401/403 in a row, immediate auth alert.
Two straight days of empty Sales Reports for the same app, treat as aggregation delay.
Rule 6 is what handles the header-only TSV case I described above. With it in place I can distinguish Apple-side aggregation delay from genuine zero-revenue days.
A sample alert SQL looks like this:
WITH yesterday AS ( SELECT app_id, asc_revenue_jpy, admob_earnings_jpy FROM daily_summary WHERE jst_date = date('now', '-1 day', 'localtime')),day_before AS ( SELECT app_id, asc_revenue_jpy AS prev_asc, admob_earnings_jpy AS prev_admob FROM daily_summary WHERE jst_date = date('now', '-2 day', 'localtime'))SELECT y.app_id, y.asc_revenue_jpy, d.prev_asc, ROUND( CAST(y.asc_revenue_jpy AS REAL) / NULLIF(d.prev_asc, 0) - 1.0 , 3) AS asc_pct, y.admob_earnings_jpy, d.prev_admob, ROUND( CAST(y.admob_earnings_jpy AS REAL) / NULLIF(d.prev_admob, 0) - 1.0 , 3) AS admob_pctFROM yesterday y JOIN day_before d ON y.app_id = d.app_idWHERE ABS(CAST(y.asc_revenue_jpy AS REAL) / NULLIF(d.prev_asc, 0) - 1.0) >= 0.25 OR ABS(CAST(y.admob_earnings_jpy AS REAL) / NULLIF(d.prev_admob, 0) - 1.0) >= 0.25;
The NULLIF(prev, 0) is a small but important detail. Without it, a freshly launched app with zero revenue the day before causes the Monday morning alert job to crash.
Three numbers from eight weeks: AdMob +14.3%, retention split, and a habit change
Three concrete numbers from eight weeks of operation drove real operational decisions. App names are anonymized; these are averages across the four apps.
First, AdMob rewarded eCPM improved by +14.3% on average. The daily summary surfaced "this one ad unit's eCPM has been dropping since Friday" during the second week of May. I rebalanced the mediation waterfall over that weekend and CPM recovered the following week. Without the morning digest I would not have noticed until the next Tuesday at the earliest. The biggest single app gained +21.8%.
Second, Day 7 subscription retention has a structural weekday/weekend gap. People who sign up on weekends had Day 7 retention 4.2 points higher on average than weekday sign-ups. My working hypothesis is that weekend sign-ups are "give it a try" while weekday sign-ups skew toward "I clicked the rewarded ad and forgot." This split led me to test different subscription creative on weekdays versus weekends.
Third, the habit of glancing at four apps' revenue every morning is the change that matters most. Under the manual routine, weekends would stack and I would put off Monday's check until Tuesday. With Slack pushing the digest at 7:00 JST, it now takes 30 seconds while my coffee brews. I only dig deeper when one number looks off. The psychological cost of operating four apps in parallel dropped significantly.
Where Claude Code actually mattered: log triage, not raw coding
Looking back over the eight weeks, Claude Code's biggest contribution was not the speed of writing code. It was working through stuck logs with me and leaving a note about the cause next to the code.
The DER-vs-raw JWT bug, the missing localizationSettings behavior, the header-only Sales Report pattern — none of these are stated in the public docs. Asking Claude Code "what's likely causing this 401" and getting three candidates, then verifying with a minimal reproduction script, let me close each one in half a day to a day.
What I appreciated most is that Claude Code tends to factor a minimal reproduction case out into its own file and compress logs to a single JSON line per event. When I write reproductions on my own I scatter print calls across the source. Claude Code's reproductions are structured logs that jq filters cleanly, which makes incident triage faster than I expected.
Both of my grandfathers were temple carpenters, and the joints they assembled hold up for decades. I have always felt the same about code. Code that just "ran" tends to break a few weeks later. After eight weeks of running this pipeline, what I can say is that the gap between "it ran" and "it does not break" is wider than most write-ups admit. Claude Code helps narrow that gap, and it fits the operational scale of indie development very well.
A minimum setup you can ship over a weekend
Reproducing eight weeks of operation from scratch is daunting, so here is the smallest version you can ship in a weekend.
# 1) Virtualenvpython3 -m venv .venv && source .venv/bin/activatepip install pyjwt cryptography requests google-api-python-client \ google-auth google-auth-httplib2# 2) Layoutmkdir -p revenue_pipeline/{secrets,raw,scripts,db}# 3) Drop your ASC .p8 and AdMob service account JSON into secrets/# 4) A prompt to hand to Claude Code:# "Write revenue_pipeline/scripts/asc_fetcher.py:# - JWT auth via PyJWT 2.8# - Pull 7 days of Sales Report (DAILY/SUMMARY)# - Decompress gzip, save to raw/asc_YYYY-MM-DD.tsv# - 200 with header-only body returns None and re-queues# - Exponential backoff up to 4 retries"
That prompt alone gets you about 80% of the way to a working script. The remaining 20% — adapting to your environment, paths, and secrets — takes one or two more hours. Expanding to multiple apps and rolling into SQLite is comfortably a next-weekend task.
For developers who run multiple apps as a one-person operation, the morning revenue check is the kind of task that becomes a quiet weight you don't notice until it's gone. Handing it to Claude Code frees up time for the work you actually want to be doing — new features, customer support, the next product. Personally, the 15 minutes I used to spend every morning has fully dissolved into the pipeline, and that time now goes into customer responses and planning the next app.
The pipeline code itself remains private, but if you are operating in a similar setup, the failure stories and the six alerting rules should be the most directly reusable parts.
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.