Skip to content

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.

  1. Build the binary. Go 1.26+, single static executable, no cgo:

    Terminal window
    CGO_ENABLED=0 go build -o askrelay ./cmd/askrelay

    Planned · WP-14

    Building from source is the only way to get askrelay today. 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.

  2. Run it. -base-url is required and must be an absolute http(s) URL — it is the OAuth issuer/audience and the base for enrollment links. -listen defaults to 127.0.0.1:8080, -db to askrelay.db, -signing-key to signing.key beside the DB, and -invite-ttl to 24h. Every setting takes flag > env > default.

    Terminal window
    ./askrelay serve -base-url http://127.0.0.1:8080 -db ./askrelay.db

    TLS is the reverse proxy’s job and the relay listens on localhost by default, so plain http://127.0.0.1:8080 is fine for a try-out. For anything reachable by other people, see Self-host & operate.

  1. Mint two invites. Enrollment is invite-gated — no open signup. Run this on the relay host; invite opens 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, default 24h). Keep the two URLs straight — you are about to spend them on different people.

  2. 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 two device_credential values; label them. Each is long-lived (365 days) and each is what authorizes one client as one identity.

  3. 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.

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.

  1. Ada asks. In Ada’s client, call send_message with to: grace@example.com and your question as text; omit thread to start a new one. It returns a draft_id, a thread_id, and state: "pending_review". Nothing has left the relay yet.

  2. Ada releases her own ask. This is the outbound gate. Call approve_reply with that draft_id (optionally editing the text first). The state becomes sent and the relay delivers it. The only way a draft skips this step is an outbound set_thread_grant covering the thread — a fresh ask never has one.

  3. Grace looks. In Grace’s client, call check_inbox. It returns to_approve (inbound messages awaiting her verdict) and to_review (her own drafts). Ada’s ask is in to_approve. get_thread fetches the whole thread — messages from the other side arrive spotlighted, as untrusted data.

  4. Grace approves the inbound message. This is the inbound gate. Call approve_message with the message id; the thread moves input-required → working. (decline_message sends it to rejected instead.) Only now is her AI cleared to act on the ask.

  5. Grace answers. Call send_message with the thread id from step 3 and the reply as text. Exactly like step 1, it lands in pending_review.

  6. Grace releases the reply, Ada receives it. approve_reply on Grace’s draft delivers it. Back in Ada’s client, check_inbox shows it in to_approve, and approve_message clears 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.

Verified against askrelay 2e019ea on . Anything newer than that commit is not reflected here.