Rafael Castillo · Case studies

Selected work · 2026

Four products, shipped end to end.

I design, build and operate the whole thing — data model, API, UI, deployment, the admin tooling that keeps it running, and increasingly the AI layer on top: Claude-powered agents, fine-tuned local models, and the training loops that make them better. Below are the four I'm proudest of: a job-search CRM for a global community, a regulated micro-lending platform, a growth-analytics dashboard, and a live-events platform for communities.

RoleFounder-engineer · Product
StackNext.js · Firebase · Supabase · Claude API · Ollama · Vercel
Commits across these repos640+
01 · Product

JobHacker Compass

Job-search CRM with Claude-powered agents that find and score high-fit roles.

02 · Fintech

TerePay

Micro-lending for migrants, with KYC and a custom fine-tuned credit model.

03 · Analytics

JHG Dashboard

AAARRR funnel metrics and lead intent scoring on Supabase.

04 · Community

Tertulia

Live communities in the browser — polls, quizzes and Q&A over the slide.

01 · JobHackers Global

JobHacker Compass

A mobile-first tracker, CRM and dashboard that turns a job search into a measurable weekly system — with a team of AI agents, powered by Claude, that scan the market, score every posting against the member's profile and only surface the roles worth applying to.

RoleDesign, build, deploy
TimelineJun 2026 → ongoing
PlatformPWA + responsive web
UsersMembers + coaches + admins
5AI agents: Scout · Evaluator · Tailor · Writer · Researcher
A–Hstructured fit evaluation per posting, 1–5 score
≥ 4.0score threshold before a role reaches the member
220 / 38 / 81commits · API routes · test files

The problem

JobHackers teaches that most offers come from the hidden job market — conversations, not applications. But members tracked effort in spreadsheets, coaches had no visibility, and access to the programme was handled by hand.

What I built

  • Tracker — weekly actual / target / variance per activity category, with the 80/20 hidden-vs-visible split front and centre.
  • CRM — contacts, interactions (info / job / pleasure interviews, thank-yous) and an opportunities board; CSV import with duplicate detection and two-way company linking.
  • Compass — a single "True North" goal statement, plus a reading rail pulled from Ghost CMS.
  • JobHacking portal — Narrative, True North, Target companies and Artefacts rendered from one profile.yml, with read-only share links.

The hard parts

  • Time-boxed access — payment webhook → Firebase Admin creates the user + a 3-month grant with a 24–48h redeem window; expiry drives a paywall with admin-editable copy.
  • Admin portal — four tabs (TOFU funnel, lead-magnet CMS, user interactions, analytics) where every editor sits beside a true-size preview of the real screen. Content saves deep-merge so one tab can never clobber another.
  • Two databases on purpose — Firestore for app data, Supabase for an append-only event log tagged by AAARRR stage.
AI at the core

Finding high-fit jobs with Claude

Most job tools match keywords. Compass reads the whole posting the way a senior recruiter would — against the member's CV, goal statement and target-company list — and writes a structured verdict. Claude does the reasoning; the member only sees roles that clear the bar.

How a posting becomes a verdict

  • Scout scans portals, ATS boards and company career pages; Extractor pulls the JD and classifies whether the posting is even live.
  • Evaluator builds a prompt from the member's CV + profile.yml + JD and asks Claude for an A–H report: role summary, per-requirement CV match with importance weights, level strategy, comp research, personalisation angles, STAR+R interview prep, and a Block G legitimacy check that flags scams and ghost jobs. One holistic 1–5 score, not an arithmetic formula.
  • Pipeline mode: report → pending card on the Progress board → if the score clears 4.0, Tailor drafts a role-specific CV and Writer the cover note. Below 4.0 the recommendation is "don't apply", with reasons.
  • Researcher uses Claude + web search for company deep-dives and contact discovery for the hidden-market outreach.
  • Guardrails baked into every prompt: the agents never submit an application, and JD text is treated as untrusted input — injected instructions are quoted in Block G, not followed.

Claude as teacher, local models as students

  • Each agent's model is admin-selectable: claude-api (Anthropic API), claude-cli (Claude Code on the worker), or an Ollama model on a Raspberry Pi 5 for members whose CVs must never leave the device.
  • Admin → Agents → Training runs a teacher–student loop: Claude writes gold A–H evaluations for synthetic and approved live cases → dataset → LoRA fine-tune on a GPU host → GGUF into Ollama as careerops-evaluator:<tag>.
  • Every candidate model sits an exam against held-out cases — score MAE, within-0.5 rate, archetype and legitimacy agreement — before an admin promotes it.
  • Prompts are versioned per agent with diff and rollback; member data only enters a dataset with explicit opt-in.
  • Vercel never runs a model. Jobs are queued on Upstash Redis; the worker pulls over HTTPS, so nothing is exposed and results stream back into Firestore for a live UI.
Payment page ──POST /api/webhooks/access──▶ Firebase Admin: create user + accessGrant (90d, redeem ≤48h)
                                           └─▶ Resend: set-password email (copy editable in /admin)
Member: "Scan" ──▶ careerops:queue (Upstash) ──▶ Pi worker ──▶ Scout → Extractor → Evaluator (Claude) ──▶ A–H report + score
                                                                        score ≥ 4.0 ──▶ Tailor (CV) + Writer (cover) ──▶ Progress board card
                                                                        results mirrored to users/{uid}/careerOps/* ──▶ live UI (onSnapshot)
Member action ──▶ Firestore write ──▶ server-side event ──▶ Supabase compass_events ──▶ /admin#analytics (AAARRR · ICP × intent)
Anthropic Claude APIClaude CodeOllama (Llama 3.2 · Qwen 2.5)Unsloth LoRAn8nRaspberry Pi 5Next.js 14TypeScriptTailwindFirebase Auth + FirestoreSupabaseUpstash RedisResendKitGhost Content APIGA4 Data APIVitestVercel
02 · Fintech · New Zealand

TerePay

A micro-lending platform for migrants: applicants self-onboard through KYC, and the lender reviews affordability with a custom credit-assessment model I trained on the lender's own cases — reading bank statements, applying a behaviour scorecard and writing an evidenced analyst note, with every decision logged for CCCFA compliance.

RoleArchitecture, build, security
TimelineMar 2026 → ongoing
RolesApplicant · Lender · Admin
Loan range$200 – $2,000
LoRAcustom fine-tuned model, terepay-analyst
7training job types: import · backtest · dataset · fine-tune · exam …
AES-256GCM field encryption, key-versioned
379 / 82commits · API routes

The problem

New migrants have thin or no local credit history, so mainstream lenders say no. A community lender wanted to say yes responsibly — but needed identity verification, affordability evidence and an audit trail that stands up to the CCCFA.

What I built

  • Two customer models — online applicants via Firebase Auth, and offline customers (friendly IDs like TERE001) created by the lender and later claimed through 3-way verification (email + DOB + name).
  • KYC onboarding — intro → email → mobile OTP (Twilio) → profile → identity documents, gating every loan application.
  • Lender workspace — applications, customers, portfolio, benchmarks, repayment schedules with early-repayment interest rebate, and a PDF affordability report.
  • Admin + content editor — feature flags, integrations, email templates, users.

The hard parts

  • Security by default — every state-changing route goes Zod → withAuth() → Upstash rate limit → reCAPTCHA → business logic → auditLog(). Clients never write to Firestore directly.
  • Thin-file applicants — no bureau score to lean on, so affordability has to be evidenced from statements and behaviour. See the credit model below.
  • PII encryption — application-layer AES-256-GCM with key versioning so keys can rotate without re-encrypting the world.
AI at the core

A custom credit model, trained on the lender's own book

Off-the-shelf credit scores don't exist for people with no local history. So the model learns from what this lender actually saw: real applications, real bank statements, real outcomes. It runs on-prem, is advisory by design, and leaves an audit trail a regulator can follow.

What happens when the lender presses "Run AI assessment"

  • The site assembles every input — declared income and expenses (fortnightly → monthly), existing debt, loan purpose, the applicant's repayment history on prior TerePay loans — or returns exactly what's missing. Nothing is guessed.
  • A worker downloads only the bank statements and payslips (never identity documents), parses the transactions, computes observed income and spend, and reconciles declared vs observed.
  • A rules engine applies the Expense Red Flags checklist, Client Behaviour thresholds and the Borrower Behaviour Scorecard; behaviour ticks like paid previous loan early or requests bigger loan too fast come from TerePay's own ledger.
  • The fine-tuned model writes the analyst note and answers a few gated judgement calls. The result: risk rating, recommendation, confidence, factors, verbatim evidence lines, data gaps and affordability holds.
  • If statements can't be parsed, parser_mode: fallback blocks automatic approval and caps confidence at 40. The lender still chooses Proceed / Decline and owns the reasons (CCCFA s.9CA).

The training loop (Model Training console)

  • Cases — import historical application folders from Google Drive, attach declared figures and real outcomes, label them with the officer's decision.
  • Backtest — run the rules engine over every labelled case and compare tiers to what actually happened.
  • Gold review — synthetic gold cases plus officer-reviewed real ones become train.jsonl.
  • Fine-tune — LoRA on a GPU host, exported to GGUF and imported into Ollama as terepay-analyst; Exams score each candidate on held-out cases before an admin selects it in /admin/credit-assessment.
  • The requested model is written into the assessment record and compared with the model the worker actually ran — a mismatch shows in the lender's panel. Nothing heavy runs on Vercel; the Pi pulls jobs over Upstash Redis from behind NAT.
  • Fine-tuning never changes a decision: the rules engine decides, the model explains.
Lender: "Run AI assessment" ──POST /api/applications/[id]/credit-assessment──▶ assemble inputs (missing → 422, nothing queued)
        ├─ Firestore creditAssessments/<jobId>          ├─ Upstash LPUSH assessment:queue
        ▼                                                 ▼
  panel polls every 4s ◀── worker: download docs → parse → metrics → rules → terepay-analyst (fine-tuned) note → gated merge ──┘
  Lender still chooses Proceed / Decline and owns the reasons (CCCFA s.9CA)
Ollama · custom LoRA modelUpstash Redis job queueRaspberry Pi workerGoogle Drive APINext.js 16React 19 + React CompilerTypeScript strictTailwind 4Firebase 12 / Admin 13Zod 4 + React Hook FormTwilioResendUpstash Ratelimit@react-pdf/rendererVercel Flags + Edge ConfigFirebase Emulators
03 · Growth analytics

JHG Dashboard

Pirate metrics for a creator business: one event log in Supabase, scored into funnel stages, intent and ICP fit at read time — so changing a weight in the UI re-scores every lead instantly.

RoleData model, SQL, UI
TimelineJun 2026 → ongoing
Data~1,700 events · ~1,500 people
HostingVite SPA + Vercel functions
20SQL migrations
6AAARRR stages tracked
6chart views on one dataset
0stored scores — all computed on read

The problem

Leads arrived from webinars, newsletters, LinkedIn, a quiz and a product — each in its own tool. Nobody could answer "where do we leak?" or "who should I contact today?" without a spreadsheet afternoon.

What I built

  • AAARRR dashboard — Awareness → Revenue KPI cards with per-stage CSV export; funnel, bar, trend, mix, source-matrix and sub-step views over the same filters.
  • People page — one row per email, combining every interaction into a single intent score and an intent × fit quadrant with a recommended action per segment.
  • Experiments & targets — weekly targets, cohorts and experiment-impact windows.
  • Integrations — LinkedIn OAuth import, GA4 for awareness, cron-driven sync.

The hard parts

  • Scoring in Postgres — intent = stage_weight(furthest stage) + tag_points − decay; churn always ≤ −1, everyone else ≥ 0. Weights and tag rules are tables, editable from the admin UI.
  • Two grains, deliberately — row grain for the event log, person grain (lead_people view) for outreach. Behaviour never feeds fit; quiz never feeds intent.
  • Stages go backwards in real data (an old email click logs activation after retention), so "effective stage" is the furthest ever reached — unless the latest event is churn.
jobhackers_leads (event log) ──▶ compute_lead_intent(stage, tag)      ── row grain ──▶ Leads table
                              └─▶ lead_people view (group by email)  ── person grain ─▶ People · intent × fit quadrant
lead_intent_stage_weights · lead_intent_tag_rules · decay config  ──▶ edited in /intent, applied on next read
React 18ViteReact Router 7Supabase (Postgres + RLS)PL/pgSQLRechartsVercel ServerlessLinkedIn OAuthGA4 APIVercel Cron
04 · Live communities · tertulia.cloud

Tertulia

Live communities in the browser. Organisers run talks, workshops and classes with polls, quizzes, word clouds and Q&A layered over the slide everyone is already looking at — and participants join with a name and an email, nothing to install.

RoleDesign, build, launch
TimelineSep 2026 → live
StatusIn production, taking registrations
PricingFree to start · Starter $29/mo
~1sraise hand → on stage with camera
$3–10streaming cost per 100-person hour (HLS)
8live activity types over the slide
0accounts needed to attend

The problem

Webinar incumbents charge ~$100/month, cap attendees, and make you bolt on Slido-style tools. For a community that runs a weekly session, the engagement is the product — so I built the engagement layer first and treated video as a commodity.

What I built

  • Communities — a room an organiser owns: colour, logo, public page, upcoming and past events; every event is also a registration page with seats and a waiting list.
  • Frictionless attendance — register with name + email, get a personal link and join code; reminders on paid plans; registered vs turned-up reporting.
  • The room — PDF deck presented live, presenter video in the corner, waiting room with image / video / audio, six floating reactions, chat, Q&A with upvotes.
  • Activities — presenter-authored polls, scored quizzes, word clouds and CTAs with timers; results reveal as an overlay (tally, cloud or leaderboard) when the organiser says so.

The hard parts

  • Cost-shaped architecture — 2–3 presenters on WebRTC, everyone else on HLS with no socket. Promote-to-stage pre-warms the publish token and camera prompt so the hop is only the connect.
  • Latency-aware sync — HLS viewers sit 2–5s behind. Clients measure their live-edge latency and delay slide changes and activity openings so they land with the audio.
  • Realtime without postgres_changes — 100 clients subscribing with RLS is slow, so a trigger broadcasts every write to one session:{id} channel and the client runs a reducer. Answers are rejected and quizzes scored in the database, not the browser.
Presenter (WebRTC) ──▶ LiveKit room ──room-composite egress──▶ HLS playlist ──▶ participants (hls.js, no socket)
Host panel: next slide / open poll ──▶ Supabase row ──trigger: realtime.broadcast_changes──▶ session:{id} channel
                                                                              └─▶ client reducer, scheduled at server_ts + measured HLS latency
Raise hand ──▶ stage_roster: viewer → invited (pre-warm token + camera) → live (publish) → revoked (back to HLS)
Next.js 16React 19TypeScriptTailwind 4Supabase (Postgres · RLS · Realtime broadcast · Storage · anonymous auth)LiveKit Cloudhls.jspdf.jsStripeResend-style transactional emailGA4Vercel