At Doriot, I built a recommender that matches startups with investors, both VCs and angels. The dataset came from Crunchbase and funding news. It had more than a million startup-investor relationships and over a billion underlying data points. But the interesting design decisions weren't about scale. They were about the shape of the problem.
Matching startups to investors is a two-sided marketplace, and a two-sided marketplace is naturally a graph. Startups and investors connect through investment relationships. Investors connect to each other through co-investment patterns. Flat feature vectors and a similarity score throw away exactly the structure that makes matching meaningful. That's what pushed the architecture toward graph neural networks instead of simple embeddings and cosine similarity.
Candidate Generation With GraphSAGE
Running a heavy attention model over every startup-investor pair doesn't scale. The pipeline starts with GraphSAGE for candidate generation. GraphSAGE's inductive sampling plus k-nearest-neighbor clustering narrows the graph to a manageable pool before anything expensive runs. Recommendation systems everywhere use a cheap retrieval stage before a heavier ranking stage. This is the graph version of the same idea.
Two GATv2 Models Instead of One
The actual matching runs on GATv2 (Graph Attention Networks v2) with multi-head attention over the bipartite investor-startup graph. The decision that took the most convincing was running two separate GATv2 instances: one trained for VCs, one for angels. Not a single shared model covering both.
VC and angel investment dynamics genuinely differ. Check sizes, stage focus, decision-making speed, and portfolio construction aren't just different in magnitude. They're different in kind. One shared model averages across two distinct behavior patterns and dilutes the signal each needs. Two dedicated models let each specialize instead of learning a compromise that fits neither well.
Re-ranking and Rivalry Signals
Raw GATv2 output isn't the final ranking. Bayesian likelihood estimation re-ranks the candidate list. Jaccard similarity separately detects investor rivalry: investors who compete for the same deals and tend not to co-invest, or investors who cluster together. That rivalry signal feeds into ordering. A good match for a startup also depends on which other investors are already in or near the deal.
Making the Feature Space Workable
With this much data, the raw feature space is large. PCA reduces it to 512 dimensions before it reaches the graph models. That keeps training and inference tractable without hand-picking features. Hyperparameter tuning for both GATv2 models runs through Optuna. Tuning two models by hand isn't worth it once you have automated search that does it better.
An Honest Note on Results
I'm not attaching a precision or accuracy number. The project doesn't have one published, and I'd rather say that plainly than invent one. The point is the architecture reasoning: why this is a graph problem, why candidate generation and ranking are separate stages, and why two investor classes with different dynamics earned two models instead of one that tries to do both.
That last point is the generalizable lesson. If two sub-populations in your graph behave differently enough that averaging across them hurts both, you usually need two models. Not one model plus more features.
Learn More
Related: Why candidate generation comes before ranking in this investor graph.
The full project is open source: Investor Recommender on GitHub.