Northside Ventures

Demir with the Northside Ventures banner
Timeline
10 Months, July 2025 – April 2026
Role
Engineer, now Scout (pre-seed/seed)
Overview
Northside Ventures is a solo GP venture fund in Toronto focused on supporting strong Canadian founding teams at the earliest stages. The partner, Alex McIsaac, previously worked at Global Founders Capital and BDC, bringing over 15 years of experience in the Canadian ecosystem as both an operator and early stage investor.
Tools
Python, FastAPI, PostgreSQL, Fine-Tuning

Background

Founder screening was the biggest bottleneck in deal flow. Analysts spent hours each week manually reviewing LinkedIn profiles, and scores varied depending on who was doing the review.

I built an end-to-end system to automate that workflow: fine-tuned models score founders against a proprietary investment rubric, sync with the CRM and deal pipeline, and give the team a conversational interface to query founders and deals. What used to take hours per candidate now runs in seconds.

That meant scoring profiles in seconds instead of hours, feeding analyst corrections back into training, keeping contacts deduped with live pipeline status, and giving the team a plain-language way to query across founders and deals.

Architecture

Analyst Dashboard

Search founders, score profiles, override scores, push to CRM

FastAPI Backend

15+ endpointsPydantic validationWebhook handlers

OpenAI

Scoring + fine-tuning

PostgreSQL

Profiles, scores

CRM API

Synced records

Scraping API

LinkedIn profiles

It's a Python monolith deployed on a PaaS. The backend is built with FastAPI and exposes both synchronous scoring endpoints and async webhook-based flows. The database is PostgreSQL via Supabase. External integrations handle LinkedIn scraping, CRM sync, and LLM inference + fine-tuning.

Scoring Engine

At the center is the scoring engine: given a founder's profile, it returns a scored evaluation with reasoning.

Rubric Engineering

The rubric went through 4 iterations to calibrate the model's judgment against the investment team's. We landed on a structured format with score bands and guardrails that kept evaluations consistent across analysts.

Hard floors and caps were the most important piece: they stop the model from under-scoring strong profiles or over-scoring weak ones, regardless of how polished the LinkedIn copy looks. That turned out to be the single most impactful design decision in the system.

Prompt Design

The scoring prompt follows a system + user pattern. The system prompt sets the investor persona and output format. The user prompt injects the rubric and a canonicalized profile:

python
1def score_profile(profile, experiences):
2 canon = canonicalize(profile, experiences)
3
4 messages = [
5 {
6 "role": "system",
7 "content": "You are an investor. Score 1-9 using the rubric. "
8 "Return JSON only: {\"score\": x, \"why\": \"<=60 words\"}."
9 },
10 {
11 "role": "user",
12 "content": f"Rubric ->\n{RUBRIC}\n\nProfile ->\n{canon}\n\n"
13 "Return JSON only.",
14 },
15 ]
16
17 resp = client.chat.completions.create(
18 model=MODEL, # base or fine-tuned
19 temperature=0,
20 messages=messages,
21 )
22 return json.loads(resp.choices[0].message.content)

Canonicalization is important because it reduces messy scraper JSON into a consistent text format (name, headline, about, top skills, 5 most recent experiences sorted by recency) so the model always sees the same structure regardless of input source.

Autonomous Pipeline

The flagship endpoint handles the full lifecycle in a single API call:

  1. Cache check: if the profile exists and has a score, return instantly
  2. Scrape: trigger the scraping service to pull the LinkedIn profile
  3. Normalize: parse raw JSON, generate a canonical profile ID (MD5 of URL), deduplicate via SHA256
  4. Score: run the profile through the rubric with the current model
  5. Persist: store profile, experiences, and score in the database

For production reliability, there's also a webhook-based flow that decouples scraping from scoring. The scraping service calls back to a webhook endpoint when complete, triggering normalization and scoring asynchronously. This prevents timeout issues and allows retry logic.

Fine-Tuning

The human-in-the-loop training loop is what makes the system improve over time. Analysts review AI scores in the dashboard and can override them with their own judgment and reasoning.

Conversational Assistant

Beyond scoring, the system includes a conversational AI layer that gives the investment team a natural language interface across all data sources. It works across email, messaging, API, and the dashboard.

Intent Classification

Inbound messages are classified into intents using a structured JSON-mode call:

python
1INTENTS = {
2 "lookup": "Wants to know if we've seen a founder or company",
3 "reminder": "Wants to set a reminder or schedule a task",
4 "memo": "Wants to generate a deal memo",
5 "draft": "Wants to draft an outbound email or reply",
6 "unknown": "Cannot determine; escalate to human",
7}

The classifier also extracts entities (company name, founder name, LinkedIn URL, domain) in the same call using JSON mode, so we can immediately route to the correct lookup strategy without a second LLM call.

CRM Lookup

The lookup system searches across multiple data sources in priority order:

  1. LinkedIn URL (highest confidence): exact match in CRM or profiles table
  2. Company name: fuzzy search across CRM organizations
  3. Founder name: search across scored profiles, then cross-reference with CRM
  4. Domain: fallback to domain-based lookup

Each result includes a confidence level (high, medium, low) so the assistant can flag ambiguous matches instead of guessing. This was important because in a VC context, confidently returning the wrong company is worse than admitting uncertainty.

Reply Generation

The reply generator uses few-shot examples to maintain a consistent voice: concise, factual, never speculative. It injects live CRM and database context before generating:

python
1def generate_reply(user_message, lookup_result, intent, channel):
2 context = build_context_string(lookup_result)
3
4 messages = [
5 {"role": "system", "content": SYSTEM_PROMPT + channel_note},
6 *FEW_SHOT_EXAMPLES,
7 {"role": "system",
8 "content": f"Relevant data:\n\n{context}"},
9 {"role": "user", "content": user_message},
10 ]

The system adapts tone by channel: shorter for messaging, more structured for email. The assistant never fabricates information; if no match is found, it says so clearly and offers to add the entry.

Deployment

The backend is deployed on a PaaS as a single FastAPI service. This simplicity was intentional. Scoring requests are short-lived (under 15 seconds), and the webhook flow handles longer operations, so there's no need for complex orchestration, queues, or workers.

Environment configuration uses the platform's secrets management for all API keys. The service auto-restarts on deploys with zero downtime via health checks against /healthz.

Results

The system went from concept to production in about 3 months, built and maintained as a solo engineer.

Hours → seconds

Screening time per candidate

Consistent

Every founder scored against the same rubric

Self-improving

Analyst corrections feed back into training

Next Steps

These are the natural extensions that would deepen coverage without changing what already works.

01

Agentic Screening

Move from single-shot scoring to multi-step research, letting the system autonomously gather funding history, co-founder backgrounds, and company traction before producing a score.

02

RAG over Deal History

Build a retrieval layer over historical deal memos and IC notes to give the assistant deeper context about why the firm passed or invested in similar companies.

03

Email Automation

Extend the assistant to fully handle inbound deal flow emails: classify, lookup, draft responses, and route to the right team member automatically.

04

Multi-Firm Generalization

Abstract the rubric and scoring system to support multiple firms with different investment theses, turning it into a platform.