-
-
Cover: A campus marketplace that lives in iMessage.
-
Live dashboard: Live stats from Tiger Data.
-
Haggles for you: Offers and counters, all through Karl.
-
Texts you first: Ask once. Karl texts you when it’s listed.
-
Every message scanned: Scams get flagged or blocked.
-
How it’s built: The LLM interprets. Code commits.
-
Sell with a photo: Send a photo. Karl drafts the listing.
-
Numbers stay private: Karl relays messages. Numbers stay hidden.
-
Semantic search: Matches by meaning, not keywords.
Inspiration
When our teammate Aayush was in elementary school, he really wanted a new Xbox. A middle schooler named Karl offered to sell him one. Aayush gave Karl his savings, and he never saw the money or the Xbox again.
Buying and selling on campus still works a lot like that. Posts get buried in GroupMe chats and Instagram stories, and you usually know nothing about the person on the other end. Someone almost always asks you to Venmo them first. We wanted to build the middleman Aayush needed back then, so we named him Karl too. This Karl has the same name, but his whole job is to make sure what happened to Aayush doesn't happen to anyone else.
What it does
Karl is a campus marketplace that lives entirely in iMessage. There's no app: you text one number.
- Sell: text a photo. Karl identifies the item, drafts a title, condition and price, and posts it when you say yes.
- Buy: text what you want ("need a desk lamp under $20"). If nothing matches, Karl remembers and texts you the moment one gets listed.
- Bid/ask matching: even when prices are far apart, Karl tells both the buyer and the seller that the other exists, so they can meet in the middle.
- Negotiation: "ask him for $35" becomes a real offer. Counter-offers go back and forth through Karl until someone says yes, even after a deal is agreed.
- Masked relay: buyer and seller never see each other's numbers. Karl forwards messages with a bold label and taps 👍 when a message is delivered.
- Scheduling by overlap: both people say when they're free ("tmr after 2"), Karl finds the overlap, and the meetup only locks when both tap 👍.
- Closing the loop: Karl checks in after the meetup, handles disputes ("they say it happened, you say it didn't") without picking a side, and asks every 48 hours whether old listings are still available, so nothing becomes a ghost listing.
- Safety: every relayed message is scanned (pay-first requests, gift cards, links and phone numbers get warned or blocked), every photo is screened, prohibited items are refused, and Karl never touches money.
How we built it
- Photon Spectrum connects Karl to iMessage: texts, photos, tapbacks and threaded replies.
- A single Bun + TypeScript server on Railway runs the message pipeline, background timers and a small HTTP API.
- Gemini reads item photos, understands free-form requests through function calling (about 20 tools), and produces embeddings. We used it deliberately: rules-first routing means plain code handles anything with real consequences (prices, confirmations, relaying), and Gemini only handles what rules can't.
- Tiger Data runs everything data-related in one Postgres database: pgvector with DiskANN for search by meaning ("something to keep drinks cold" finds a mini fridge), a TimescaleDB hypertable for events, and a continuous aggregate behind the live dashboard.
- A Next.js dashboard on Vercel shows live listings and stats (first names only).
- Four of us built in parallel on four branches against shared contracts, with automated tests (~490) and a review step before every merge.
Challenges we ran into
- No group chats. Our iMessage plan can't create group threads, so we built a masked relay instead. That constraint turned into our privacy feature.
- Who is this message for? Mid-deal, "can you ask him for 55" is for Karl, while "can you do 55?" is for the other person. We combined pattern rules with a quick Gemini check for the genuinely ambiguous cases.
- Real-world testing humbled us. Our first live tests found Gemini reading "tmr" as today, two people locking two different meetups, and Karl promising to "sort it out" with no way to do it. Each became a fix: date-aware parsing, overlap scheduling, and a hard rule that Karl never claims an action unless a tool actually did it.
- Free-tier limits. One Gemini model capped out at 20 requests a day mid-testing, so Karl now switches models automatically when a quota runs out.
Accomplishments that we're proud of
- Two real people negotiated a price, scheduled a meetup, and closed a sale entirely through Karl, without ever seeing each other's numbers.
- Karl texts you first. Most chatbots wait to be asked.
- Every bug we found in live testing became a test, so it can't come back.
What we learned
- The hardest part of a multi-party agent isn't the AI. It's deciding when not to use it.
- Human-in-the-loop isn't a limitation: a 👍 before anything locks is what makes people trust an agent with their stuff.
- Test with real phones early. Simulated conversations never text "we meat early."
What's next for Karl
- UNC email verification, so everyone on Karl is a verified student
- Ratings after each sale
- A move-out week push, when half of campus is selling at once
- More campuses
- Tickets, if UNC's resale policy ever allows it
Built With
- bun
- gemini
- github
- google-gemini-api
- gsap
- hono
- imessage
- next.js
- pgvector
- pgvectorscale
- photon
- postgresql
- railway
- react
- remotion
- spectrum-ts
- three.js
- tiger-data
- timescaledb
- typescript
- vercel
- vite
Log in or sign up for Devpost to join the conversation.