Mate
An AI matchmaker for romance, friendship and small groups. No swiping: a scheduled job reads what you told it about yourself, scores candidates on the dimensions the research says actually predict compatibility, and hands you a match with a written reason. One production incident, and a market validation pass that killed the framing the whole product was named after.
- Role
- Solo: product, research, backend, front-end, design system
- Build window
- 2025-10-28 → 2026-08-15, 291 commits
- Runs on
- Vercel, Supabase Postgres, Inngest, Ably
- Models
- Gemini Flash for chat, Llama 3.3 70B on Groq for analysis
Repository private. Happy to walk through it on a call.



$ cat README.md
The swipe optimizes for photographs and volume. Users are leaving it.
Tinder shed paying subscribers for eleven consecutive quarters, Bumble's payers fell year on year, and burnout surveys keep landing in the same place. The exodus runs toward products that produce an outcome rather than a better card stack.
My thesis in October 2025: if a model can learn who you are from an ordinary conversation, it can evaluate compatibility on values, life goals and communication style, and do it continuously in the background, without you rating strangers by photograph.
So onboarding is a conversation with a schema behind it. Ten topics, each carrying guide questions, the exact database fields it feeds, and extraction hints, all in a JSON config an admin can edit without a deploy. Topics branch on what you say: tell it you want something casual and it stops asking about marriage timelines. When the conversation ends, a second model pass reads the transcript and writes roughly thirty structured profile fields, turning "I am useless before 10am" into morningNight: 3.
You can also skip it. I hand you a prompt, you paste it into whichever assistant you already talk to every day, it summarizes what it has learned about you across months of real conversation, and you paste the answer back. The profile gets built from evidence you generated without performing for a dating app, which is exactly the evidence a dating app can never collect about you itself.
$ ./bin/report --totals
Counted on 15 August 2026, users and matches from the deployed database and the rest from the repository. Read the user figure carefully: it is registered accounts, and the schema carries no flag separating the synthetic users the seeder creates from anyone else, which is exactly the lesson the egress section below teaches. There is a deployed environment with a database and running cron jobs, and there is no public launch, so nobody arrived at this app from an advert. The match count is what the hourly job produced on its own; none of it came from a swipe.
$ ls -la ./engineering
The drivers of a good romance are not the drivers of a good friendship, and the literature says so plainly. The weights are split by connection type, and each carries its source in the comment above it: a PNAS meta-analysis of 11,000 couples, Campbell's friendship-chemistry work, the 2025 community-formation research. One community weight is multiplied by zero, with a comment explaining that the 2025 evidence stopped supporting it. I left the function in and zeroed the weight rather than deleting the reasoning.
Comparing two people means putting both of them in front of a model. The obvious way is to send everything they have ever told the assistant, about 10,000 tokens for the pair. Pairs grow quadratically, so that build is insolvent somewhere around a hundred users. So the conversations never reach the matching model at all. A nightly job reads each user's recent messages and writes a small structured summary, kept in three buckets that age out. The matcher only ever sees the summaries. Same decision on a fiftieth of the input, 10,000 tokens down to about 200, because the summarisation runs once per user per day rather than once per pair per analysis, and users grow linearly where pairs do not.
// what the matching model reads, per pair naive ████████████████████████████████████████ ~10,000 tokens every message both people ever sent, never built after █ ~200 tokens last week, rewritten nightly at 03:00 ~150 last month, folded monthly at 04:00 computed, not yet read ~100 long-term, folded quarterly at 05:00 computed, not yet read
Two of the three buckets are write-only today. The monthly and quarterly folds run on schedule and store their output, but the hourly matcher still injects only the weekly bucket. The compression is real and so is the saving; the long-memory half is built and not yet wired to the thing that would use it.
The AI layer is abstracted by purpose, not by vendor: a cheap fast model for conversation, a strong one for analysis, each chosen by environment variable. Today that is Gemini Flash and Llama 3.3 70B on Groq, picked because Groq's free tier allows 30 requests a minute against Gemini's two. The retry handler reads the provider's own hint back out of the error string, and it exists because I hit both 429 and 503 repeatedly in production.
Every verification vendor charges per check, so verification runs entirely in the browser. face-api.js loads its models as WebAssembly, pulls a 128-dimension descriptor from the live camera, does the same for the profile photo, and compares them by Euclidean distance. Liveness is a gesture prompt: you smile. The video never leaves the device, and nothing but a boolean and a timestamp is stored.
The language model handles nuance. It does not handle the parts where the research already gives an answer: those are code, and each carries the study it came from in the comment above it. Values and interests by Jaccard similarity, boosted when both sides mark the same item important. Life goals on a penalty model, where a children mismatch costs 60 points when either side calls it a dealbreaker. Emotional stability asymmetrically, where a pair both above 7 scores 97 to 100 and either below 3 is capped at 30. Communication through a love-language complementarity graph. Availability as overlap across six named time slots, with a floor so zero overlap is not fatal.

$ crontab -l
Most of this product runs while nobody is looking at it.
None of the work below has a screen. Eight scheduled jobs and one on-demand repair tool read conversations, compress them, find candidates, score pairs, write the artifacts a match needs before either person opens it, and clean up after themselves. One of them, conversation-analysis, serves the retired agent-to-agent subsystem and now runs against an empty queue. It should have been deleted when the architecture changed.
For each active user the job pulls 20 candidates with a SQL filter (age range, gender, connection type, and an existing-match exclusion) then applies the rules that cannot be expressed as a preference. Mutual interest is enforced both ways, so a match can never be one-directional. Verification acts as a privacy filter: ask for verified matches only and you see only verified people, and if you are unverified yourself you are shown only to people who opted in. Then a hard ceiling of three model calls per user per hour, with a second of spacing between them. That number is a budget.
$ cat docs/EGRESS_ANALYSIS_2025-11-14.md
The database hit its egress limit, and three quarters of the users driving it were fixtures.
The hourly matchmaking job loaded every active agent with a full relational include, then queried twenty candidates each, with full includes again. Eight hundred profile loads an hour. Nineteen thousand a day. Roughly 94 MB of egress daily from this job alone, about 2.8 GB a month against a plan allowing 5.5 GB. The job was not the whole bill, it was the half of it I could delete.
I wrote diagnostic scripts rather than guessing, and the numbers said something I did not expect: 44 of 59 users were test users. That is an architecture problem, and it presented as a billing alert.
The remediation had four parts, ordered by impact: delete the test users, replace every include with an explicit select naming only the fields the analysis reads, cut the candidate batch, and reduce the cron frequency.
$ npx tsx scripts/check-db-stats.ts total users .......... 59 test users ........... 44 ← 75% of the database active agents ........ 40 all matched hourly matches .............. 35 est. daily egress .... 94 MB → ~2.8 GB/month from this job alone
Only the query-shaping half is in the code today. The job still runs hourly and still takes twenty candidates. I fixed the expensive part, left the cheap part, and the analysis document still sits in docs/ describing work I did not finish. The second lesson was about rate limits: a free tier removes the cost and none of the constraints. A ceiling of two requests per minute shaped the whole background architecture, the three-analyses-per-hour cap, the deliberate sleep between calls, the exponential backoff. The architecture followed from a quota.
$ cat docs/MARKET_VALIDATION_2026-06.md
I ran the research, and it killed my framing.
By June 2026 the product worked, so I stopped building and ran a structured validation instead: three parallel deep-research passes across competitive landscape, pricing economics and low-budget go-to-market, with adversarial verification of anything load-bearing.
It confirmed the mechanic and killed the framing. Conversational onboarding into an AI matchmaker with no swiping is now the industry's consensus bet: Bumble announced it would remove the swipe entirely, Hinge's founder left to build an AI-first dating app, and four funded startups converged on the same shape. But the research pass found that "AI agents represent users" is the most backlash-prone framing in the category, and the two figures below come from that pass rather than from primary surveys I read myself. The agent-as-proxy products I could find had all folded or pivoted. 58% of daters call AI-written messages catfishing, and 68% of Americans will not let AI act unreviewed on their behalf.
The uncomfortable part: my architecture was already on the right side of that line and my copy was not. There is no agent-to-agent conversation anywhere in the product. The matching model reads two profiles and two insight summaries and scores them. The AI never speaks as the user, to anyone. I had built a matchmaker and marketed a proxy.
| Assumption | My model | Benchmark |
|---|---|---|
| Download to paid | 5.0% | 2.0% |
| Annual renewal | assumed strong | 25% |
| ARR at 10k users | $162,000 | ~$4,000 |
Two more findings. My revenue projections were about 2.5 times industry medians: I had modelled 5% premium conversion against a 2.0% benchmark for the worst-retaining category, and a $162K ARR projection was closer to $4K. And putting three connection types in one app runs against the best evidence available, because Bumble ran that experiment for eight years and unwound it. The rename to Mate came out of this. The product now says the AI filters the noise and sends you to high-affinity connections.


$ ls ./built
What is built, and what is not
- email and Google auth, 18+ gated
- conversational onboarding and AI-history import
- three connection types: romance, friendship, communities
- scheduled matchmaking with written explanations
- real-time chat, typing indicators, read receipts
- per-match AI assistant and ice-breakers
- browser-side identity verification with an appeals queue
- block, report, moderation queue
- GDPR export and account deletion
- admin dashboard with impersonation and seeding
- push notifications, EN/ES localization, native iOS and Android
# Not built
- Payments. The pricing model is researched, not implemented
- Transactional email. Password reset writes the token to the console
- Geolocation distance matching. The fields exist, unpopulated
- The community approval flow. A TODO sits where the record should be created
- Stage 1 of the matching pipeline, wired in
- A production deployment currently serving traffic
$ cat method.md
Of the first 282 commits I attributed, 155 were written by a coding agent and 127 were mine.
The interesting question is what you did with the parts it cannot do.
What the coding agent did well was mechanical breadth. In January 2026 I audited the whole feature surface against the codebase, wrote the gaps into a prioritised task file, and ran an autonomous loop against it. In two iterations it closed block and report, password reset, GDPR deletion and export, typing indicators, read receipts, unmatch, community creation, a moderation queue, verification appeals and profile preview, each with tests and both translation files updated.
What it could not do is where my time went. Deciding what the weights should be: no coding agent reads a meta-analysis of 11,000 couples and concludes that emotional stability deserves 5% in romance and zero in communities. Root-causing the egress incident, which took the instinct to measure before optimising and to suspect the fixtures. Running the validation that invalidated my own premise, then acting on it. And going looking for what was missing, because absences never show up in a diff.
A coding agent will happily write a correct module and never wire it in. This codebase contains an example. lib/matching/quick-filter.ts is a complete, tested, zero-cost pre-filter, documented as Stage 1 of a two-stage engine, and the production job does not import it. One unit test, one barrel export, one admin action. The documented two-stage pipeline is one and a half stages in production. I found it while writing this page.
$ cat POSTMORTEM.md
The mechanic held up and the scope did not.
What works: the matching produces real, explained matches on a schedule, at a cost that survives a free tier, in two languages, on web and native. What the research says will not work is the shape I gave it, three connection types, no city, no launch gate, and a name that described the one framing the category rejects.
The evidence on cold start is unambiguous: one city, one community, roughly 150 people, open only when the threshold is met. That is the next build, and it is mostly deletion again.
# What I would do differently
- Validate the framing before writing 68,000 lines. Three deep-research passes cost me one day in June. They would have cost that same one day in October and changed the product's name, its copy, and probably its scope.
- Never let test fixtures share a code path with real users. That 44 of 59 users were fixtures is something I learned from a billing alert.
- Ship narrow. One city, one community, a launch gate. I built for three connection types and two languages and zero cities.
- Wire the composition root first. Build the pipeline end to end with stub stages, then fill them in, and a correct-but-orphaned module becomes impossible by construction.