Wingman
Inspiration
Dating apps ask you to describe yourself, and almost nobody is good at that. You get three photos and a prompt that says "I guess math?"
Meanwhile you have spent two years telling ChatGPT what you are working on, stuck on, and excited about. That is a running self portrait nobody writes on purpose, and it is far more honest than anything you would type into a profile field. Wingman starts there: your agent learns you from conversations you were already having, meets other people's agents, and brings back people worth your time. You still swipe. You still decide.
What it does
Connect ChatGPT, watch it build your agent card, then swipe through people with a plain reason attached to each. A mutual like opens chat, with a helper that drafts messages in your voice.
One rule shapes everything: nothing leaves your profile until you approve it, field by field. Your agent never sends a message, contacts anyone, or changes a match state.
How we built it
SwiftUI on iOS, with consent and matching logic in a Swift package so it runs from the command line without a simulator. The privacy boundary is a type, not a convention: publicSnapshot emits only approved fields, and a self test asserts agent proposed traits cannot reach a sync payload without approval.
Reply drafting fans out three tones in parallel through Render Workflows to NVIDIA NIM, keys off the phone, deterministic fallback when the network dies.
Ranking is reciprocal on purpose. A one directional score lets a small popular group absorb every impression, so the ordering weights both directions:
$$ s(u, v) = \alpha \, f(u \to v) + \beta \, f(v \to u) + \gamma \, \mathrm{sim}(c_u, c_v) $$
with a maximal marginal relevance pass so the next ten cards are not ten versions of the same person. Similarity stays internal. We never render it as a compatibility percentage, because that is a claim we cannot support.
What we learned
There is no API for this. Nothing reads a consumer ChatGPT account's history. The only supported route is the user's own data export, which turned out better anyway: explicit, revocable, local first.
The export is a tree, not a list. In conversations.json, mapping forks a branch on every edit and regeneration, and all of them persist. Iterating the values, the obvious implementation, pulls in abandoned drafts and skews every frequency downstream. The correct walk starts at current_node and follows parents to the root. That one detail separates real signal from confident noise.
Challenges
Selecting ChatGPT did nothing for a while. Sign in was pre-selecting the provider, so the row arrived ticked and the first tap read as a deselect. The import was never skipped, it was never triggered.
Presenting that import as a modal was unreliable, since it was set in the same update as the mutation re-rendering its parent. Making it a route instead removed the race and let it survive a relaunch.
Bumping the persisted schema briefly wiped older saves the decoder could already read. It now migrates forward instead of rejecting.
Log in or sign up for Devpost to join the conversation.