Inspiration

Every May the dumpsters outside UNC dorms fill with lamps, fans, mini fridges and textbooks that still work, while a few hundred metres away someone is about to buy the same thing. Campus listing boards exist, but most posts stop at "posted": nobody matches the item to a real need, nobody agrees a pickup, and nobody knows whether the handoff happened. I wanted the whole loop to close, not just the listing.

What it does

Loop is a small web app plus an iMessage agent for passing usable belongings to the people who need them.

  1. Someone posts a need in plain words ("reading lamp for my desk").
  2. An owner photographs an item — or a whole pile. Gemini drafts the title, description and visible condition for each thing it finds (up to six per photo, each with its own box), and suggests at most one open request each item genuinely fits, with a one-sentence reason. When nothing fits, it says so.
  3. The owner checks and edits the draft (it stays marked "Drafted by Gemini — check it" until a person approves it), then offers it to that person or posts it to the board.
  4. The recipient accepts, and a handoff thread opens between the two people and Loop, the coordinator. Loop asks the giver when and where works (the giver's availability comes first), turns typed replies like "I can do 6 at the Union" into proposals with Gemini, asks the other person to confirm, reminds both before the pickup, and tells the giver when the recipient taps "I'm here".
  5. At pickup the recipient shows a 4-digit code; the giver types it in the app or texts it to Loop. Only then does the item become "Collected" and the request "Fulfilled".
  6. Free by default; a giver may set a price, paid in person at the handshake (Loop never touches money). And it loops: the taker can pass the item on again without a new photo, with a lap count on the card.
  7. No app needed: through Photon's Spectrum, the same thread reaches people's phones as iMessages, and a giver can text a photo to Loop's number, get the draft back, reply YES to offer it, settle the pickup by text (or with a 👍/👎 tapback on Loop's question) and send the code by text. To get texts, a person types their number on the item page and enters the code Loop texts back, or texts the page's 4-digit code to Loop; the number is never shown.

No identities at all: no accounts, names, nicknames, avatars or dorms. A tab or a phone is an anonymous session, the two sides of a handoff are just "the giver" and "the taker", and the 4-digit code proves identity when it matters. Gemini never publishes, reserves, contacts anyone or marks anything collected; a person taps or texts every state change. If Gemini cannot identify the object, is rate-limited, or times out, the owner gets a plain explanation and fills in the form by hand. If iMessage is unavailable, everything works in the browser.

How we built it

  • Node 22 + Express server with a JSON-file store; every state change is a pure transition function. Items: available → offered → reserved → collected (409 when someone else already claimed it, 403 on a wrong code). Threads: planning → set → meeting → done or cancelled.
  • Gemini 3.5 Flash-Lite through generateContent with image parts and JSON response schemas: one schema for a single item, one for a pile (boxes in 0–1000 units), one for the coordinator's actions (propose / accept / arrived / cancel / chat / decline). The server validates everything: unknown request ids are dropped, the schema asks for the reason before the id so the model commits to its reasoning first, strings are clamped, "not allowed" items (food, medicine, weapons) are blocked, and the coordinator can never confirm on someone's behalf or reveal the code.
  • Photon Spectrum (spectrum-ts) on a shared iMessage line: thread messages are forwarded to linked phones, replies are routed back into the same thread, and a texted photo runs the same draft pipeline. Linked numbers are added to Photon's user list through its API; iPhone HEIC photos are converted on the server with macOS sips.
  • Front end in plain HTML, CSS and JavaScript, no framework, no build step: hash routing, one render function, polling with a version counter so two people see each other's actions within about 1.5 seconds, View Transitions, prefers-reduced-motion respected, every text pair measured at 4.5:1 or better.
  • Visual language: a "poster, light" system — cream and ink, acid lime and coral only as fills, Space Grotesk + JetBrains Mono, hard offset shadows, sticker chips, a ticket-stub pickup code, a rubber-stamp "Kept in use", and the Knot mark (one continuous over-under line). No names, avatars or dorms anywhere.

Challenges we ran into

  • Proving the riskiest call first: the Gemini docs now lead with a newer Interactions API, so the first job was confirming the classic generateContent request shape (image part + JSON schema) on gemini-3.5-flash-lite. It worked first try, about 2 seconds per photo.
  • Honest drafts: the first prompt rated a visibly dusty fan "good"; telling the model to look for dust, scratches and dents, and to write its reason before it picks a request id, fixed both that and a false "coat" match in the pile photo.
  • An agent that never oversteps: the coordinator may only propose, ask, remind and relay. Clear agreements take a deterministic path (no model call), the model handles the messy sentences, and the server rejects anything the rules forbid (accepting for someone else, "I'm here" before a plan exists, revealing the code).
  • iMessage on a shared line: Photon's Free plan only texts numbers on the project's Users page, and the first send after connecting can fail, so sends retry and queue, and Loop adds each number to that list through Photon's API the moment a phone links.
  • Three looks in one day: pale "Frutiger Aero" glass, then a modern blue, then a warm paper zine, before settling on a bold poster on a light canvas. Each pass re-measured every text and edge for contrast (the poster pass: 20 pairs, none below target).
  • No identities, late: removing names, avatars and dorms touched every sentence the server writes. People became opaque session ids and the two sides of a handoff became "the giver" and "the taker"; the 4-digit code carries all the trust.

Accomplishments that we're proud of

  • A complete two-person loop that ends in a confirmed handoff, not a listing.
  • Honest AI: the model says "no suitable match" when nothing fits, and the UI treats its output as a draft.
  • Three consecutive scripted end-to-end runs pass (items, pickup threads, iMessage text flow), and the full loop was verified on the judging laptop with live Gemini and a real iMessage line.

What we learned

  • Prove the riskiest call with a ten-minute script before building anything on it.
  • Design every state before styling anything: uploading, drafting, no match, AI failed, offered, reserved, already claimed, collected, withdrawn, network down. The polish passes were fast because the states already existed.
  • An AI coordinator earns trust by what it refuses to do. The demo is strongest when Loop says "nothing to agree to yet" or "I can't share the code".
  • Anonymity simplifies the product: once nobody has a name, the code at the handoff is the only identity that matters, and the whole board becomes just things people have and things people need.

What's next for Loop

Real accounts and campus email verification, photos of the handoff spot, notifications when a new need matches an existing item, and a trial with one residence hall at move-out. Nothing here has been tested with real users yet.

AI and attribution disclosure

Built solo during the HackNC 2026 hacking window. AI engineering with a human in the loop: Aijun (Ethan) set the product, the design, every flow and rule, tested each step on the real services (live Gemini, the real iMessage line, his own phone) and sent work back until it met the bar; AI coding agents (Claude) wrote the code, the tests and most of the copy under that direction; he has not reviewed every line. The product calls Google's Gemini API (gemini-3.5-flash-lite) and Photon Spectrum (Loop's real iMessage number). The demo video's narration is Ethan's own voice, cloned with his consent from a 58-second voice memo (Higgsfield voice clone, rendered with MiniMax text-to-speech); the video was composited with HyperFrames (HTML rendered by Puppeteer and FFmpeg), product footage recorded with Playwright on the live app, a p5.js watercolor opening, a three.js knot, Remotion typography, an iPhone screen recording of real Messages, FFmpeg-synthesized sound effects and an AI-generated instrumental bed. Fonts: Space Grotesk and JetBrains Mono (SIL OFL). Seed photos: Wikimedia Commons, credited in the repo. No user data or impact figures are claimed beyond what the demo shows.

Built With

Share this project:

Updates

Submission history