Inspiration
We both run events and recruiting pipelines, so we meet thirty-plus new people a month. We remember maybe a third of them, and our contacts apps have no idea what we're actually trying to do — land an internship, find a hardware mentor, get a first check. LinkedIn shows connections with no context, and the person across the table who happens to know exactly who you need usually never mentions it.
The question that started Cirql: what if the graph did that work? Not by scraping anyone's profile, but by letting two people's agents talk to each other, each on its own graph, and reveal a person only after everyone involved says yes.
What it does
Cirql is a personal CRM where AI agents walk your relationship graph. The loop is four steps:
- Capture — a 30-second voice memo after you meet someone becomes people, facts, intents, and promises in your graph, every one pointing back at the memo that produced it.
- Aim — set a goal ("summer 2027 VC internship") and your network re-ranks around it, with a reason and evidence per person. Change the goal, the ranking rebuilds. Close it, the ranking disappears.
- Find the gap — the agent notices the goal needs a kind of person you have no path to: "No warm path to climate-tech VC."
- Ask the network — your agent posts a redacted request. Your friends' agents each search their own graph and answer with a count and a strength, no names. The friend approves, the contact opts in, and only then is a card exchanged and a three-way intro drafted.
Rules we enforce in code, not in a slide: an agent never reads another user's graph; every step that moves someone toward a new person needs a human click; the only object that ever crosses accounts is a card its owner wrote; relationships never decay and nobody gets a worth score; every suggestion has a receipt.
How we built it
Everything is Jac. The data is a graph — Person, Note, Fact, Intent, Goal, Card, IntroRequest are nodes; Knows, Reported, RelevantTo, Asserts are typed edges. Every feature is a walker: Capture, GoalRank, GapFinder, ScoreAgainstNeed, and the Exchange protocol (PostRequest → ReplyToRequest → ApproveReply → OptIn → Reveal), each a state transition on the graph.
by llm replaced all our prompt glue: extraction, relevance scoring, and drafting are typed functions returning objs, and the prompt is built from the signature and sem strings. Gemini is the model behind them.
Per-user roots gave us the privacy model for free: every account's graph hangs off its own root and walkers run there. For the one thing that legitimately crosses accounts — the reveal — we use Jac.allow_root to grant a single Card node, read-only, to a single user. The Exchange itself lives under root.shared.
The UI is Jac too: cl blocks compile to React, the same bundle serves the website and wraps into a phone app through the Capacitor target. [Voice capture goes through ElevenLabs speech-to-text from the phone's mic.] [~XX% of the codebase is Jac.]
Two people, ~22 hours. We split by file ownership rather than by feature — one of us owned every *.sv.jac, the other everything client-side — with a written contract of walker report shapes in between, so both halves could build in parallel against mocks. Each of us ran a Claude Code orchestrator that delegated to subagents; the compiler's built-in jac guide references were the thing that kept three models from inventing three syntaxes.
Challenges we ran into
- Jac's toolchain moves fast. The client plugin needs Python 3.12; on 3.11 pip silently resolves a two-year-old
jaclang. And putting server modules in aserver/directory madejac starttreat everysv importtarget as a separate microservice that never became healthy. Flattening to*.sv.jacat the repo root and one line injac.tomlfixed it — after an hour we didn't have. - Cross-user access is the whole product, and it was our biggest risk. We set a 9 PM gate: prove
grant/allow_root/root.sharedwork across two real accounts before writing a line of the Exchange, with the intra-graph Matchmaker (same scorer, one graph) as the fallback. [It passed / We took the fallback and say so in the demo.] - Saying only true things about privacy. Per-user roots isolate by construction, but they don't encrypt anything or protect from the operator. We kept "private" and "secure" out of our vocabulary and wrote the sentence we can defend instead.
- Scope. We had three PRDs' worth of ideas. The ranked feature list was the contract; anything not in Tier 0 by dinner went to the roadmap.
What we learned
Graph-native languages change what's easy. "Which of my contacts is relevant to this goal" is a walker and an edge, not a join. So is "answer a stranger's request from my own graph without leaking it." We also learned that the fastest way to make an LLM agent write correct code in an unfamiliar language is to make it read the compiler's own reference before every file.
And a product lesson: the feature people would find creepy (an agent that knows who your friends know) becomes the feature they trust when a human approves every hop and only self-authored cards ever move.
What's next
Live-linked cards, multi-hop relays (friend-of-friend), consented QR handshake in person, an opt-in card directory, import of your own LinkedIn data export with swipe-to-confirm, public-web enrichment with a URL on every fact, and the privacy hardening that turns "isolated by construction" into "encrypted at rest."
Built With
- byllm
- capacitor
- claude-code
- cloudflare
- d3.js
- elevenlabs
- gemini
- google-search-grounding
- graph-database
- ios
- jac
- jac-client
- jac-scale
- jaclang
- jaseci
- linear
- object-spatial-programming
- python
- react
- shadcn
- tailwindcss
- typescript
- vite
- xcode
Log in or sign up for Devpost to join the conversation.