Glimpse

Nobody's face shows up until you've both asked to see it. Every round, you answer a question, write down what you think the other person will say, and mark how confident you are. Then you find out.


Inspiration

Dating apps give you a face and ask you to decide. Then the chat dies somewhere around "hey / how's your day / cool."

People aren't bad at writing. They're stuck on what to ask. And a lot of us just don't know how to keep a conversation going with someone we've never met.

Every AI dating tool I looked at fixes this by writing the message. That's the wrong fix, and it costs you the thing you came for. If the agent does the talking, she's getting to know the agent.

I wanted something smaller. You reckon she'd text three people and go wherever the first one answers back. Turns out she'd sit in the front row of a cinema on her own and stay through the credits. Getting that wrong isn't a fail. It's the most interesting thing that can happen in the first ten minutes, and it hands you your next question.

WebMCP is why any of this works. The page is the only thing in the room holding both people. An agent sitting outside a website has one side of the story and can't do a single one of these tools.


What it does

Two people get the same question. You each write your answer, write a guess at the other person's answer, and pick hunch, fairly sure, or certain. Nothing shows until you've both hit send, and then all four blocks appear on the same frame. Nobody went first. Nobody got left on read.

Your agent can pull clues, think out loud about her, tell you how she's reading you, and point out what neither of you has brought up yet. It can't see her answer. It won't write you a message you didn't ask for.

Photos come into focus when you've both asked.


How I built it

Next.js App Router, exported static. No backend, no database, no accounts. Everything sits in localStorage. Tool registration only exists in the browser, so the interface is a client component with ssr: false.

desiredTools() works out which tools should exist right now. sync() compares that against what's actually registered and makes real registerTool() calls, killing tools by aborting their AbortSignal.

A round resolves on your confidence and whether you landed close:

guess confidence what you get
missed fairly sure or certain confidently wrong
missed hunch a hunch, and not this time
close anything you had that one

That top row is the entire product. It prints:

you were sure she'd stay in. she'd go to the cinema alone and stay for the credits.

Open a second window on ?side=them and it plays the other person. Both windows share one state object over the storage event, so the four blocks really do land together.


Challenges I ran into

Keeping her answer out of reach. I wanted to hand it to the agent about four separate times. Handlers can be edited, so I fixed it in the shape of the data instead: visibleRound() is the only view any tool ever gets, and it strips her side until you've both submitted. So when you ask your agent what she'll say, the two of you are guessing together. That accident turned out to be my favourite thing about using it.

Being useful without doing the talking. After the reveal, compose_message takes intents rather than sentences. Coffee on Sunday morning. Say something about the photo. Ask about Sarajevo. The agent picks what, the page picks the words, you press send.

Consent instead of a prize. get_reveal_status only ever tells you about you. Telling you she asked would make a much stickier loop, but then not asking becomes a rejection you can see, and I don't want to build that. Locking the photo behind good play would be worse. Attention turns into something you spend, and once you've spent it there's no reason to keep paying it.

Being generous with the scoring. Guess matching is crude on purpose. Any shared content word counts as close. It doesn't need to be clever, it needs to not be mean to someone who nearly had it.

Cutting the calendar import. I built it, it worked, and it made the demo confusing. Anything that needs explaining isn't done.


What I learned

Registration is the API. If a tool shouldn't work right now, don't register it. Don't register it and have it return an error. About thirty tools come and go as state changes. open_composer and set_confidence disappear the moment you hit send, so there's no revising. Not "you can't revise" — there's nothing there to call.

Most of the design was taking things out. Six tools are declared and then never registered: write_message, draft_reply, draft_answer, suggest_opener, improve_my_answer, rate_compatibility. The rail at the bottom shows each one as an empty slot that says writing is yours. Naming them and refusing to register them lands harder than quietly leaving them out.

Errors should be sayable out loud. Every failure comes back as something the agent can just tell you, not a code.


Accessibility

The whole thing works on a keyboard with no agent at all. The agent saves you effort, it isn't the interface. WebMCP shouldn't turn into an excuse for a page that's worse to use without it.

What's simulated

The second person is scripted and nothing leaves your browser. The tools, the register and unregister lifecycle, and the reveal gate are all real.

What's next

Two real people instead of a scripted one, which mostly means moving the shared state object onto a server and keeping the simultaneous reveal honest across a network.

After that, the things WebMCP makes possible that I ran out of hours for.

Questions that come from the gap. Right now the set is fixed. next_from_mismatch should read where you missed her and write the next question out of it. Guessed Europe, she said Sarajevo, so the next one is about the last time you went somewhere on almost no notice. The conversation builds itself out of your wrong answers.

A real blind spot tool. find_blind_spot can only see two rounds today. Over twenty it should be able to say something like: eleven rounds in and neither of you has mentioned family once. Nothing outside the page can spot that, because it needs both transcripts.

A cap on how many people you're talking to. Four threads, and find_matches unregisters at the fourth. Every app I know monetises infinite optionality. I'd rather the page just stop handing you more people until you've closed one, and enforce it by removing the tool rather than showing a nag screen.

Ending things properly. Ghosting happens because writing that message is hard, so nobody writes it. close_conversation sends the same neutral line for everyone, which means there's no subtext to read into it, and then the thread's tools unregister for both sides.

Built With

  • abortcontroller
  • chrome
  • css
  • document.modelcontext
  • gemini-api
  • google-gemini
  • html5
  • javascript
  • json-schema
  • localstorage
  • model-context-protocol
  • next.js
  • node.js
  • react
  • static-site-generation
  • svg
  • vercel
  • web-api
  • webmcp
Share this project:

Updates