My AI Portfolio Reviewer, Part 1: The Monthly GCP + Claude Pipeline That Reviews My Portfolio So I Don't Have To

✍️ Writing · August 2026

gcpbigquerydata-engineeringinvestingclaude-api

A GCP + Claude pipeline that reviews a real portfolio every month and emails the verdict automatically — no manual review, no dashboard to check. Part 1 of a 3-part series.

Simple flow diagram: Zerodha holdings get screened for free, Claude judges the ones that qualify — the only paid step — Python allocates the budget for free, and an email goes out automatically every month.

📖 Preface

This isn’t a typical GCP how-to — it’s the real story behind a personal portfolio I’ve been building for years, and the automation I eventually needed to keep it honest. Three parts, in order: this one is the system itself — the problem, the architecture, why it’s built this way. Part 2 gets into what it actually costs to run Claude inside it, and where that cost came from. Part 3 — still pending real hardware — takes it off my inbox and onto a screen.

Real data throughout, every step numbered so I can point back to exactly what I mean. Let’s get into it. 🚀

🎯 Intro

It’s been close to four years of actively investing across asset classes — equity stocks, REITs, InvITs, gold and silver ETFs — on one simple strategy: pick quality, buy it cheap, and hold it unless the quality itself starts to deteriorate.

That strategy worked. The portfolio was beating its benchmarks. But somewhere around a hundred stocks and tickers in, a real problem came into focus: how do you actually keep tabs on that many positions, every month, to catch the ones quietly losing quality before it’s too late? By hand, that’s not a monthly review anymore — that’s a job I don’t have room for.

So I built a pipeline to do it instead.

Concretely, the gap: a portfolio with real shape now — a locked split across equity, banking, gold, silver, REITs, and InvITs — needs the same question asked, every month, about every holding that’s dropped meaningfully below cost: is the damage to the price, or to the business? Doing that by hand, consistently, across a hundred positions, isn’t a realistic ask of anyone.

🛠️ The approach

A monthly, unattended GCP pipeline: it screens my holdings — currently around 80, and growing every month as I keep adding — applies Buffett’s temporary-vs-permanent-impairment framework to whichever ones dropped far enough to matter, and emails the answer — not a dashboard to remember to open, an email that lands whether or not I go looking for it. The email carries a money-flow ledger (what’s coming in, what’s going out, what stays uninvested), an asset-allocation snapshot against the locked targets, and a check on how evenly the equity picks are actually weighted against each other.

That “locked targets” phrase does real work every cycle. Every new rupee gets allocated against a fixed split across six asset classes — equity, banking, gold, silver, REITs, InvITs — decided once, ahead of time, precisely so a good or bad month never becomes an excuse to freelance the allocation on the fly. The pipeline’s allocation stage checks incoming capital against this split every single run.

Donut chart of the locked target asset allocation: 66% equity, 10% banking, 7% REITs, 7% InvITs, 6.2% gold, 3.8% silver.

Gold and silver share a combined 10% slot, but that doesn’t mean a rigid 50/50 split every month. A gold-silver-ratio (GSR) rule tilts each month’s new gold/silver purchase toward whichever metal is trading cheap relative to the other — a modest lean, not a bet — while keeping the combined 10% target itself locked. Same idea as the impairment check on equities: the target doesn’t move, only the path to it adapts to what the market’s actually offering that month.

I read it. I can override any of it. Nothing executes on its own — this is a recommendation engine, not an autopilot.

🏗️ Architecture

Architecture diagram of My AI Portfolio Reviewer, numbered 1 through 10: a Claude routine pulls Zerodha holdings into Google Drive, Cloud Scheduler triggers a Cloud Function that lands the holdings into BigQuery, runs a three-stage screen-judge-allocate pipeline where only the Claude judgment stage costs money, then sends an MJML-built email via Gmail, with Secret Manager supplying credentials throughout.

Every box above is a real, deployed piece — not a conceptual sketch. Numbered so the steps below can be referenced directly.

Step 1 — a Claude scheduled routine, not this pipeline itself. Zerodha requires a live login every trading day; that can’t be scripted away without storing a broker password and TOTP secret, which wasn’t a trade-off worth making for a once-a-month convenience.

Real config for the Claude routine behind Step 1: active, firing at 09:00 AM on day 25 of the month, running with the Zerodha Kite MCP connector and Google Drive on Haiku 4.5, 4 of 4 recent runs succeeded.

The routine’s own instructions aren’t shown here on purpose — the config above is real evidence enough without publishing the internal prompt for a personal financial tool.

Step 2 — Google Drive holds that Sheet, named with the pull date. Step 4 checks this filename against the current month before doing anything — if the latest file isn’t from this month, the whole cycle waits rather than processing stale data.

Real Google Drive folder listing for “Zerodha Monthly Holdings,” showing two dated Sheets from actual runs — thumbnails too small to read any individual figures.

For scale: here’s what that Sheet is actually built from — my real Zerodha holdings screen, 83 positions and counting, every rupee figure masked. This is the ~80 holdings Step 6 screens every month.

Real Zerodha holdings screen, 83 equity positions, every quantity/cost/value/P&L figure masked — only instrument symbols and the LTP (a public market price) are visible.

Step 3 — Cloud Scheduler fires the next day, the 26th — deliberately one day after Step 1, buffer time for the manual login click to have actually happened. It sends an authenticated HTTP POST to Step 4’s endpoint with an empty body (a request body is only used to override the budget for a manual test trigger).

Real Cloud Scheduler job “zerodha-holdings-pipeline-monthly”: region asia-south1, cron 30 9 26 * * (Asia/Calcutta), state Enabled, last execution Success.

Step 4 — Cloud Function (Gen2), HTTP-triggered, --no-allow-unauthenticated (invoked via an identity token, not open to the public internet).

Real Cloud Function (Gen2) service “zerodha-holdings-pipeline”: region asia-south1, Python 3.12, scaling Auto (Min 0, Max 1), source view showing the actual entry-point docstring.

Step 5 — BigQuery, dataset sudhirkenguva.zerodha_pipeline, four tables:

Real BigQuery dataset “zerodha_pipeline” — the four tables above, with actual create timestamps.

📊 Real schema, pulled straight from INFORMATION_SCHEMA — claude_usage:

run_id (STRING)       call_type (STRING)     tradingsymbol (STRING)
model (STRING)        input_tokens (INT64)   output_tokens (INT64)
cost_usd (FLOAT64)    called_at_ist (DATETIME)

💰 And real rows from an actual production run (run_id=2026-08-20) — API cost data only, nothing about the underlying portfolio:

Symbol Call Model In tokens Out tokens Cost
IRCTC stage_b sonnet-5 78,981 1,998 $0.178
ITC stage_b sonnet-5 79,968 2,861 $0.189
JYOTIRES stage_b sonnet-5 75,273 2,466 $0.175
KSOLVES stage_b sonnet-5 51,484 1,509 $0.118
PIIND stage_b sonnet-5 44,177 1,161 $0.100
— gsr_lookup haiku-4.5 9,867 138 $0.011
— notes haiku-4.5 410 108 $0.001

(A few stocks show a second, more expensive row — a retry after an earlier partial failure that same day. Real systems retry; the ledger shows it honestly rather than only keeping the clean run.)

From there, three stages, read straight off Step 5’s data:

Step 8 writes the verdict and cost back to Step 5, then hands off to Step 9 (MJML → HTML, built in Python — MJML specifically because it sidesteps a confirmed Gmail-Android rendering bug, wrapping each stacked line in its own table row) and Step 10 (Gmail delivery) — the entire interface is one email.

📧 Rebuilt from the real email’s actual sections and real counts — every rupee figure (budget, cost, in/out/remainder, current/post-recommendation weights) masked, since none of that has appeared anywhere in this article and it isn’t starting here:

Mockup of the real monthly email, rebuilt with actual section structure and real stock/count data, all rupee figures masked: header with the Claude API cost pill, the temporary-vs-permanent framework callout, a 5-evaluated/2-add/3-hold/0-exit summary, an asset-allocation table with target weights real and current/post-recommendation weights masked, a monthly-budget box fully masked, and one real held-stock card with Claude’s actual reasoning.

⚖️ Key decisions and tradeoffs

Three stages (6/7/8), not one big AI call on everything. The judgment call is the only part that actually needs a model — screening by price and sizing by a fixed formula are both things plain code does exactly as well, for free. Steps 6 and 8 do as much of the work as code possibly can, so Step 7 — the expensive part — only runs on the handful of stocks that genuinely need judgment. (What that discipline actually saved, and what it took to tune Step 7 itself, is Part 2.)

Buffett’s framework specifically, not a generic sentiment check. Averaging into weakness only works when the business is still sound — averaging into something genuinely deteriorating is just throwing good money after bad. “Is the damage to the price, or the business?” is the exact question that tells the two apart.

A locked allocation split, not in-the-moment sizing. In-the-moment sizing is exactly the kind of decision market noise is good at corrupting. Fixing the split in advance, and capping any class at its target weight per cycle, takes that decision out of the moment entirely.

A separate signal for gold and silver. Buffett’s framework is a business-quality question; gold and silver aren’t businesses, so it doesn’t apply to them. They get a Gold-Silver Ratio valuation check instead — a deliberately different signal type, not one framework stretched to cover two different kinds of assets.

A parity check on the equity book. Nothing above protects against slow, unintentional concentration — a few winners quietly becoming a much larger share of the book than any single decision ever intended. That’s arithmetic, not judgment, so it’s not a Claude call — it runs over data that’s already landed, for free, every cycle.

Unattended, not autopilot. The goal was never to remove the decision from me — it was to remove the chance that the review just doesn’t happen. Steps 9 and 10 are the entire interface: an email I read and can override beats a system I’d have to fully trust to act without me.

🧰 Tech stack

🚧 Current scope and boundaries

In scope today: averaging into existing equity/ETF/REIT/InvIT holdings that have dropped meaningfully below cost, plus a separate gold/silver valuation signal. Out of scope, on purpose, not by oversight: it doesn’t discover new stocks to buy, it doesn’t run a full portfolio-wide exit review independent of a price drop, and it never places a trade. Each of those is a deliberate “not yet” with its own roadmap, not a gap nobody noticed.


Part 2 covers what happened when a paid LLM call went into that unattended, scheduled Stage B — the real cost data, the tuning that worked, the tuning that made things worse, and where a retrieval-augmented approach might go next. Part 3 (on hold until there’s something real to show) covers putting the results on a physical screen.