Quickstart
This gets a relay running and takes one question all the way through: ask → approve → answer → approve. Not “connected, with tools” — the actual loop.
That means two identities. A thread is 1:1, and send_message refuses a
send to yourself, so there is no single-user shortcut. You need two email
addresses, two invites, two enrollments, two device credentials, and two client
sessions. One person can do all of it alone — the second identity is part of the
exercise, not a colleague you have to recruit. Below they are ada@example.com
(who asks) and grace@example.com (who answers).
The email is only a roster identity. The relay hands you the invite URL and you carry it to the other side out-of-band; nothing is mailed for you.
Set up the relay
Section titled “Set up the relay”-
Build the binary. Go 1.26+, single static executable, no cgo:
Terminal window CGO_ENABLED=0 go build -o askrelay ./cmd/askrelayPlanned · WP-14
Building from source is the only way to get
askrelaytoday. There are no prebuilt binaries, no container images, and no packages — Docker, GoReleaser, a brew tap, and an npm wrapper are all packaging work that has not landed. -
Run it.
-base-urlis required and must be an absolutehttp(s)URL — it is the OAuth issuer/audience and the base for enrollment links.-listendefaults to127.0.0.1:8080,-dbtoaskrelay.db,-signing-keytosigning.keybeside the DB, and-invite-ttlto24h. Every setting takes flag > env > default.Terminal window ./askrelay serve -base-url http://127.0.0.1:8080 -db ./askrelay.dbTLS is the reverse proxy’s job and the relay listens on localhost by default, so plain
http://127.0.0.1:8080is fine for a try-out. For anything reachable by other people, see Self-host & operate.
Enroll two identities
Section titled “Enroll two identities”-
Mint two invites. Enrollment is invite-gated — no open signup. Run this on the relay host;
inviteopens the SQLite file directly. The email may come before or after the flags.Terminal window ./askrelay invite ada@example.com -base-url http://127.0.0.1:8080 -db ./askrelay.db./askrelay invite grace@example.com -base-url http://127.0.0.1:8080 -db ./askrelay.db# → http://127.0.0.1:8080/enroll/<token> (one line per invite)Each token is single-use and expires (
-ttl, default24h). Keep the two URLs straight — you are about to spend them on different people. -
Enroll a device for each. Generate an Ed25519 keypair locally per identity and register only the public key, base64-std encoded. The endpoint is unauthenticated, so every failure is deliberately opaque: a bad token, an expired invite, and an already-registered key all look alike.
Terminal window curl -sX POST http://127.0.0.1:8080/enroll/<ada-token> \-H 'content-type: application/json' \-d '{"pubkey":"<base64-std of the 32-byte Ed25519 public key>","label":"ada-laptop"}'# → {"person_id":"…","device_id":"…","base_url":"…","device_credential":"…"}Repeat with
<grace-token>and a second, different keypair — a key that is already registered is refused. You now hold twodevice_credentialvalues; label them. Each is long-lived (365 days) and each is what authorizes one client as one identity. -
Connect two clients. Add the relay’s URL as a remote MCP server in each client. The client discovers the relay’s OAuth metadata, registers itself (Dynamic Client Registration or a Client ID Metadata Document), opens the relay’s login page, and you paste that identity’s device credential.
Two identities means two client sessions that do not share state — two browser profiles, or a browser client for one and Claude Code for the other. Paste Ada’s credential in one and Grace’s in the other; per-client steps are in Connect a client.
Once a client is authorized, ten tools appear: find_people, check_inbox,
get_thread, wait_for_activity, approve_message, decline_message,
set_thread_grant, send_message, approve_reply, and discard_reply.
The round trip
Section titled “The round trip”Planned · WP-12
There is no human-facing way to drive the exchange from a shell: inbox,
approve, and status are not built, so every step below is a tool call you
make from inside an AI client. The verbs that do exist are serve, invite,
enroll, device list|revoke, daemon, mcp, version, and help.
Both directions are gated. A message you send is held for your review before it leaves, and a message you receive is held for your verdict before your AI acts on it. A reader who sends and then waits will conclude the relay is broken; the draft is sitting in their own review queue. One question and one answer therefore cross four approvals.
-
Ada asks. In Ada’s client, call
send_messagewithto: grace@example.comand your question astext; omitthreadto start a new one. It returns adraft_id, athread_id, andstate: "pending_review". Nothing has left the relay yet. -
Ada releases her own ask. This is the outbound gate. Call
approve_replywith thatdraft_id(optionally editing the text first). The state becomessentand the relay delivers it. The only way a draft skips this step is an outboundset_thread_grantcovering the thread — a fresh ask never has one. -
Grace looks. In Grace’s client, call
check_inbox. It returnsto_approve(inbound messages awaiting her verdict) andto_review(her own drafts). Ada’s ask is into_approve.get_threadfetches the whole thread — messages from the other side arrive spotlighted, as untrusted data. -
Grace approves the inbound message. This is the inbound gate. Call
approve_messagewith the message id; the thread movesinput-required→working. (decline_messagesends it torejectedinstead.) Only now is her AI cleared to act on the ask. -
Grace answers. Call
send_messagewith thethreadid from step 3 and the reply astext. Exactly like step 1, it lands inpending_review. -
Grace releases the reply, Ada receives it.
approve_replyon Grace’s draft delivers it. Back in Ada’s client,check_inboxshows it into_approve, andapprove_messageclears it — the fourth approval, and the round trip is closed.
Instead of polling check_inbox, either side can call wait_for_activity, which
returns when there is something to look at or when its timeout expires.
To pre-approve one direction on this thread and shorten the loop, use
set_thread_grant; grants are revocable and append-only. See
Concepts for what that changes.
- Connect a client — per-client setup
- Self-host & operate — TLS, config, backup, rate limiting
- Concepts — how the approval gate and identities work
Verified against askrelay 2e019ea on . Anything newer than that commit is not reflected here.

