← Back to projects

Matching Engine

A domain-agnostic AI matching engine - one scoring core, any industry, from housing to healthcare.

PythonFastAPIClaude APIReactEvals
The Matching Engine interface showing a ranked list of roommate matches with explainable scores.
Year
2026
Stack
Python · FastAPI · Anthropic Claude API · React · Tailwind · Vite · pytest
01

The Problem

Most matching products are built for a single vertical - a roommate app, a dating app, a hiring tool - with the matching logic fused to that one use case. Change the industry and you rewrite the engine.

I wanted to separate the two: a generic scoring engine that knows only entities, attributes, constraints, and weights, and per-domain config files describing what those mean for a given industry. The engine is the product; each domain is a consumer.

Housing is the first reference implementation. Healthcare - patient to therapist - is the second, added as a config file and seed data with the engine running unchanged.

The same engine scoring the healthcare domain, matching patients to therapists.
The same engine, a different config file: healthcare added without touching the scoring core.
02

How It Works

Matching runs as a three-tier pipeline. Tier 0 filters on hard gates - location, licensure, gender requirements - so incompatible pairs are removed before scoring rather than ranked low. Tier 1 computes a deterministic 0-90 base score from structured attributes, combining hard constraints, similarity scoring where alike is better, and complementary scoring where one side’s strength fills the other’s need.

Tier 2 is a bounded LLM nuance layer. It reads free-text bios for signal the structured fields cannot capture and applies a bonus of at most ±10 - hard-capped in code, not by prompt - to top candidates only, with graceful fallback to deterministic-only scoring if the model call fails.

Scores are computed in both directions and the lower one is displayed. A match is only as strong as its least-enthusiastic side.

03

Key Decisions

A deterministic core with a bounded AI bonus, not an LLM verdict. The tempting build is to hand both profiles to a model and let it return a score. That is unexplainable and unstable across runs. Constraining the model to a capped adjustment on top of a deterministic base meant every score could be decomposed and shown to the user - base, AI delta, final - and a bad model day could never invert a ranking.

Tiering the pipeline for cost, not just correctness. Running the LLM layer only on candidates that survive filtering and rank near the top cut LLM calls by 86% against a naive score-everything approach, with no measurable loss in match quality.

Evals before features. The scoring logic is validated by an eval harness with authored golden pairs plus an LLM-judged groundedness check on the AI bonus. Every significant decision is recorded as an ADR in the repo, which is what made it safe to add a second domain without fear of silently regressing the first.

04

Security & Scope

I triaged security by risk rather than treating it as all-or-nothing. Handled: the API key lives in a gitignored environment file and is never committed, and the AI bonus is contained in code so it can never override a hard constraint.

Planned before any public deployment: rate limiting on the LLM-backed endpoint so a public link cannot run up API cost, and deliberate input separation for prompt-injection handling on user-authored bios - the ±10 cap already limits the blast radius.

Out of scope by design: authentication and PII. This is a portfolio demo on synthetic seed data. The healthcare domain demonstrates domain-agnosticism only - it is not a clinical product and makes no medical claims.