Selected work · 2026
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.
Job-search CRM with Claude-powered agents that find and score high-fit roles.
02 · FintechMicro-lending for migrants, with KYC and a custom fine-tuned credit model.
03 · AnalyticsAAARRR funnel metrics and lead intent scoring on Supabase.
04 · CommunityLive communities in the browser — polls, quizzes and Q&A over the slide.
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.
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.
profile.yml, with read-only share links.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.
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.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.careerops-evaluator:<tag>.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)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.
terepay-analystNew 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.
TERE001) created by the lender and later claimed through 3-way verification (email + DOB + name).withAuth() → Upstash rate limit → reCAPTCHA → business logic → auditLog(). Clients never write to Firestore directly.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.
parser_mode: fallback blocks automatic approval and caps confidence at 40. The lender still chooses Proceed / Decline and owns the reasons (CCCFA s.9CA).train.jsonl.terepay-analyst; Exams score each candidate on held-out cases before an admin selects it in /admin/credit-assessment.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)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.
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.
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.lead_people view) for outreach. Behaviour never feeds fit; quiz never feeds intent.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 readLive 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.
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.
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)