DEV Community

Richard Fu
Richard Fu

Posted on • Originally published at richardfu.net on

Richfolio, three months in: AI architecture in production

From v1.0 to v1.6 — what I rebuilt, what I borrowed, and what’s running in production for $0/month

Three months ago I shipped v1.0 of Richfolio — a zero-maintenance side project that emails me a daily AI-powered portfolio digest. I wrote about the original motivation and v1 architecture in this earlier post if you want the background.

Since then it’s gone through six minor releases. The code roughly tripled in size, the AI architecture got rebuilt from the ground up to handle production use, and I’ve been using it on my own portfolio daily. This post is a project-level update: where Richfolio is, what stack it runs on, the architectural patterns I borrowed (mostly from OpenAlice), and one concrete case study showing how those patterns paid off in v1.6.

At the end I’m looking for alpha testers, so if any of this is interesting and you’ve got a portfolio that doesn’t look like mine, keep reading.

What Richfolio is now

A single Node.js + TypeScript pipeline, no API server, no dashboard. Runs as a GitHub Actions cron job (8am AEST). Four modes:

  • Daily — full analysis + email + Telegram
  • Intraday — periodic STRONG BUY change alerts (only fires on real signal changes)
  • Weekly — rebalancing drift report
  • Refresh — re-analyze a single ticker with after-hours prices

Output looks like this: a dark-themed HTML email per morning, condensed Telegram message in parallel, and for every STRONG BUY ticker, a “More Details” link to a dedicated analysis page on GitHub Pages with an interactive TradingView chart, AI-generated buy thesis, and risk analysis.

The stack — still $0/month

Everything Richfolio runs on is free-tier:

Component Service Free tier
Prices, fundamentals, technicals Yahoo Finance via yahoo-finance2 v3 unlimited
News NewsAPI.org 100 req/day
AI analysis Google Gemini 2.5 Flash 250 req/day
Email delivery Resend.com 3,000/month
Push notification Telegram Bot API unlimited
Scheduler GitHub Actions cron 2,000 min/month
Static hosting GitHub Pages unlimited
State persistence actions/cache unlimited

No database. No queue. No server. State is a JSON file in the repo that survives cron runs via actions/cache. The full cost ceiling for a single user is the Resend tier — and Richfolio sends maybe 60 emails a month, so there’s headroom for ~50 users on a single account.

The interesting constraint isn’t the budget — it’s the request budget. Gemini’s 250/day cap means every “let me ask the AI” feature has to be designed around batch calls, not per-ticker calls. That shaped a lot of the architecture below.

Architecture: patterns I borrowed from OpenAlice

The biggest single influence on Richfolio’s post-v1 evolution was OpenAlice — an autonomous AI trading agent from TraderAlice. OpenAlice is much bigger than Richfolio (multi-broker, multi-asset, with a UI) but its cognitive architecture transferred cleanly. Four patterns in particular.

1. Two-stage Think/Plan AI prompting

The v1 pipeline used a single Gemini call: shove everything into one prompt, parse one JSON response. The problem: when the AI conflates data parsing with decision-making, it hallucinates support for whatever conclusion it’s already started writing. STRONG BUY false positives crept in monthly.

The fix was to split the call into two stages, mirroring OpenAlice’s think and plan tools:

  • Stage 1 (Observe) — Gemini gets raw data and produces structured observations only: which price-level signals are present, which momentum signals, which risk flags, a one-line valuation summary, a one-line technical summary, news sentiment. No actions, no recommendations.
  • Stage 2 (Decide) — Gemini gets the observations and applies strict decision rules to produce ranked recommendations.

Two API calls instead of one — wasteful in theory, but still 2/250 of the daily quota. The practical win is significant: forcing the AI to commit to what it sees before deciding what to do dropped false-positive STRONG BUYs to near zero.

2. Post-AI guard pipeline

The other half of trustworthy AI output is not trusting the AI. OpenAlice’s guard-pipeline.ts runs sequential validation between the AI’s proposed trades and the broker. Richfolio adapts the same pattern in guards.ts:


export function validateRecommendations(
  recs: AIBuyRecommendation[],
  priceData: Record<string, QuoteData>,
  technicals: Record<string, TechnicalData>,
  report: AllocationReport,
): void {
  guardBondETFCap(recs, report);
  guardEarningsProximity(recs, priceData);
  guardStrongBuyCriteria(recs, report, technicals);
  guardMaxStrongBuy(recs);
  guardConfidenceSanity(recs);
  guardBuyValueSanity(recs, report);
}

Enter fullscreen mode Exit fullscreen mode

Six sequential checks that programmatically enforce the rules I told the AI to follow. Each guard logs when it fires, so I can see which AI mistakes were caught downstream — useful signal when tuning prompts.

The architectural pattern — let the AI propose freely, validate deterministically afterward — is the single most useful idea I borrowed from OpenAlice. It makes prompt experiments low-risk.

3. Seven-day reasoning persistence

OpenAlice has a Brain module that tracks cognitive state across sessions via Git-like commits. Richfolio doesn’t need that level of machinery, but the principle — let the AI see its own past conclusions — adapts cleanly to a 7-day rolling history of AI snapshots.

Every decision prompt now receives a “HISTORICAL CONTEXT” block like:


AAPL: BUY 72% → BUY 68% → HOLD 55% — weakening
NVDA: STRONG BUY 88% → STRONG BUY 91% → STRONG BUY 89% — stable
BSV: BUY 75% → BUY 75% → BUY 75% — flat

Enter fullscreen mode Exit fullscreen mode

(More on that last line in a minute.)

The history forces the AI to acknowledge trend changes. Without it, the model would sometimes flip a ticker from BUY 70% to HOLD 50% on consecutive days with no acknowledgement — which feels wrong even if individually justified.

4. Macro environment context

Yahoo Finance is free, and yahooFinance.quoteSummary('^VIX') works the same as it does for any equity. So every Richfolio run now fetches VIX, the 10-year Treasury yield, S&P 500, oil (WTI), and the USD index (DXY) — five extra calls — and pipes them as a MACRO ENVIRONMENT: preamble into both the observation and decision prompts.

This is OpenAlice’s “equity research” influence translated to a portfolio context. The macro block lets Gemini write things like “elevated VIX + high yields suggest defensive positioning” instead of generic boilerplate, and gates STRONG BUYs more conservatively in high-volatility environments.

Case study: fixing the BSV “75% BUY every day” problem

The patterns above sound abstract. v1.6 was a concrete test of whether they paid off.

Since v1.4 (which introduced the bond ETF framework back in early April), my morning email had this same line every single day:

BSV — 75% BUY — Systematic accumulation to fill 5% allocation gap.

The reason was an over-rigid prompt rule. BSV is a short-duration bond ETF — it has a ~2% annual price range, and equity-style signals like RSI and MACD are noise on a security that barely moves. So I’d hard-coded a confidence formula by allocation gap:


Gap ≥ 5%: confidence 70-75%
Gap 3-5%: confidence 60-70%
Gap 1-3%: confidence 45-55%
Gap &lt; 1%: HOLD

Enter fullscreen mode Exit fullscreen mode

That removed false momentum signals but flatlined the output. With a steady ~5% gap, the answer was 75% every day forever.

The fix needed some timing signal. Just not equity timing signals. After thinking about what actually matters when buying bond ETFs, three orthogonal signals fell out.

1. Price percentile in a rolling 90-day window. Where today’s price sits in BSV’s recent range. Computed from existing chart data:


let pricePercentile90d: number | null = null;
if (closes.length >= 90) {
  const last90 = closes.slice(-90);
  const min90 = Math.min(...last90);
  const max90 = Math.max(...last90);
  if (max90 > min90) {
    pricePercentile90d =
      Math.round(((currentPrice - min90) / (max90 - min90)) * 1000) / 10;
  }
}

Enter fullscreen mode Exit fullscreen mode

2. 10-year Treasury yield direction. Bonds get cheaper when rates rise. I added a 20-trading-day delta on the existing macro fetch:


case "treasury10y": {
  indicators.treasury10y = Math.round(price * 100) / 100;
  const period1 = new Date();
  period1.setDate(period1.getDate() - 35);
  const chart = await yahooFinance.chart(ticker, {
    period1, period2: new Date(), interval: "1d",
  });
  const closes = (chart.quotes ?? [])
    .map(q => q.close)
    .filter((c): c is number => c != null);
  if (closes.length >= 21) {
    indicators.treasury10yChange20d =
      Math.round((closes.at(-1)! - closes.at(-21)!) * 1000) / 1000;
  }
  break;
}

Enter fullscreen mode Exit fullscreen mode

3. Distribution yield level. Yahoo’s summaryDetail.yield returns the SEC / 12-month yield for funds. High yield = high income premium.

Then a new prompt framework that scores via base + timing modifiers :


12a. SHORT-DURATION BOND ETFs:
  BASE (from gap): 5%→55, 3-5%→45, 1-3%→35, &lt;1%→HOLD
  + 90d percentile: ≤20% +12, ≥80% -15
  + 10Y 20d change: >+0.15% +6, &lt;-0.15% -12
  + Yield: >4.5% +3, &lt;3% -2

Enter fullscreen mode Exit fullscreen mode

On a great day for BSV — near 90-day low, rates spiking, high yield, big gap — confidence reaches ~90. On a bad day — top of range, rates falling — it drops to ~25. Real day-to-day variance.

This is where the architectural patterns paid off. The bond ETF rewrite is exactly the kind of change I’d previously been nervous to ship — the AI could ignore the new modifiers and hallucinate a STRONG BUY on BSV, which I never want. But the guard pipeline catches it:


for (const rec of recs) {
  if (!SHORT_DURATION_BOND_ETFS.has(rec.ticker.toUpperCase())) continue;

  if (rec.action === "STRONG BUY") rec.action = "BUY";
  if (rec.suggestedLimitPrice && rec.suggestedLimitPrice > 0) {
    rec.suggestedLimitPrice = 0;
    rec.limitPriceReason = "";
  }
  if (rec.confidence > 95) rec.confidence = 95;
}

Enter fullscreen mode Exit fullscreen mode

And the action-tier sort (new in v1.6) means even if BSV’s confidence reaches 90 on a great day, equity STRONG BUYs still rank above it visually:


const ACTION_PRIORITY: Record<string, number> = {
  "STRONG BUY": 0, BUY: 1, HOLD: 2, WAIT: 3,
};
recommendations.sort((a, b) => {
  const pa = ACTION_PRIORITY[a.action] ?? 99;
  const pb = ACTION_PRIORITY[b.action] ?? 99;
  if (pa !== pb) return pa - pb;
  return b.confidence - a.confidence;
});

Enter fullscreen mode Exit fullscreen mode

The whole BSV fix was about 200 lines of code across four files. The reason it was easy: the guards layer meant I could trust prompt changes to fail safe.

Other things shipped since v1.0

Beyond the architecture and the bond ETF case study, a lot of smaller features landed across v1.1 through v1.6:

  • Earnings calendar guard — Yahoo’s calendarEvents module ships next-earnings date inside the existing quoteSummary call. Hard HOLD ≤3 days to earnings, no STRONG BUY ≤7.
  • News sentiment scoring — every headline tagged bullish/bearish/neutral with high/medium/low impact, in the same Gemini call that was already filtering relevance.
  • Technical indicators expanded — beyond SMA/RSI from v1: MACD with crossover detection, Bollinger Bands with squeeze detection, ATR, Stochastic, OBV trend. All from existing chart data.
  • Value investing framework — A–D fundamental ratings for individual stocks based on ROE, debt/equity, FCF, growth, analyst target.
  • Crypto bottom-fishing model — RSI + volume contraction + 200MA + death cross confluence detection for BTC/ETH.
  • STRONG BUY analysis pages — interactive TradingView chart + buy thesis + risk analysis per STRONG BUY ticker, all encoded in the URL hash (no server).
  • Refresh mode — re-analyze a single ticker with after-hours / pre-market price for late-night decisions.
  • Intraday alerts — only fire on STRONG BUY changes (upgrade, downgrade, or ≥10pt confidence shift). No noise.
  • International currency support — non-USD portfolios fully supported with live FX from Yahoo. Sub-unit handling for exchanges quoting in pence/agorot/cents (LSE GBp → GBP ÷100).
  • CI + tests — 63 unit tests via node:test, zero dependencies, runs on every PR.
  • i18n docs — full English / Simplified Chinese / Traditional Chinese site via Jekyll polyglot.

Full release notes are on GitHub Releases.

The honest results so far

I’ve been running Richfolio against my own portfolio for the past few months. Across that window I’m up roughly 6% from acting on the AI’s STRONG BUY suggestions.

The caveat I have to put in writing: US markets have been strong over the same window. A meaningful chunk of that 6% is probably tailwind. I can’t say “the AI added 6%” — I can only say “following the AI’s suggestions during a strong market gave me 6% on those entries.”

To know whether Richfolio actually adds value vs riding the tape, I need to see it run against portfolios that look nothing like mine.

Alpha testers wanted

If you’d like to point Richfolio at your portfolio for a few weeks and tell me where it’s wrong, I’d love to hear from you.

What I’d love testers to bring:

  • A portfolio that looks different from mine — heavy crypto, sector-concentrated, dividend-focused, international, REIT-heavy, small-cap heavy
  • Honesty about whether the STRONG BUY signal actually matches what you’d buy yourself
  • Bug reports — weird tickers, exchanges I don’t trade on, edge cases in the FX layer

What you get:

  • A working free-tier portfolio monitor
  • A 1-on-1 setup walkthrough
  • Direct line to me for feedback and feature requests
  • First look at upcoming versions

Setup is a GitHub fork plus a few environment variables — full docs at furic.github.io/richfolio.

DM me or open an issue on the repo if you’re interested.

The post Richfolio, three months in: AI architecture in production appeared first on Richard Fu.

Top comments (0)