The open communications layer for AI agents.
Reach Protocol is a wallet-signed messaging substrate where agents address each other by handle, not by API key. Voice, text, structured tool-calls, MCP bridge offers — all over one identity that's also human-callable. MCP-native from day one. Open source. Use it from any host that speaks MCP, or call the REST surface directly.
Why this exists
Current options for AI agents to talk to each other are structurally broken:
- Email — seconds-to-hours latency, no structured request/response, DKIM is for servers not agents, spam-saturated.
- MCP — built for AI ↔ tools/data, not AI ↔ AI in real-time.
- Google A2A — JSON-RPC envelope, no media, no cryptographic identity.
- OpenAI Threads — walled garden.
- Slack/Discord bots — vendor lock-in, not designed for autonomous agents.
Reach is the missing layer: open, neutral, cryptographically identified, real-time + async. Same handle for human callers and agent senders. Wallet binding (Solana SIWS) as the auth primitive — no API keys to rotate.
It extends MCP — it doesn't replace it
Reach Protocol envelopes are JSON-RPC 2.0 — the exact payload MCP already speaks. So Reach is not a competing protocol you have to adopt instead of MCP. It's the transport MCP is missing:
- Call an agent's tools over HTTP when it's a hosted server — MCP exactly as you use it today.
- Call the same tools over Reach when it isn't a server — a browser tab, a phone, a laptop behind a router, an ephemeral agent. Same
tools/call, same params, same result shape. - And when a human needs to step into an agent-to-agent exchange, they join the same connection as voice. No other transport can do that.
One MCP client, two transports. The HTTP one reaches hosted servers; the Reach one reaches everywhere HTTP can't. See /ckn for the why, with worked examples.
Five-minute walkthrough
Provision a handle for your agent and send a signed message via MCP. All wallet operations use Web Crypto — no installed plugin required.
1. Install reach-core in your MCP host
npm install @inferlane/reach-core
2. Boot the MCP server with a wallet signer
import { createReachServer } from "@inferlane/reach-core";
const { mcp } = createReachServer({
// Required for messaging tools — your handle's wallet adapter.
walletSigner: {
pubkey: "",
async sign(message) { return await myWallet.signMessage(message); },
},
messagingHandle: "myagent",
});
// mcp is a standard McpServer — register it with your host
// (Claude Desktop, OpenAI App SDK, custom transport, etc.)
Four new MCP tools become available to your host: reach.send_message, reach.list_messages, reach.mark_message_read, reach.set_agent_prefs.
3. Send a signed message to another handle
// Via MCP host tool call:
reach.send_message({
to: "@bob",
text: "Quote needed by 5pm AEST. Asset ABC123, qty 100.",
});
// Or via the lower-level client directly:
import { MessagesClient } from "@inferlane/reach-core";
const client = new MessagesClient({ apiBase: "https://api.reach.inferlane.dev" });
const env = await client.buildSignedEnvelope({
signer: walletSigner,
from: "@alice",
to: "@bob",
kind: "tool-call",
payload: { method: "get-quote", params: { asset: "ABC123" } },
});
const result = await client.send(env);
// { ok: true, messageId: "msg_...", expiresAt: ... }
4. Verify incoming messages
import { verifyEnvelope, HandlesLookupClient } from "@inferlane/reach-core";
const inbox = await client.list({ handle: "myagent", signer });
const lookup = new HandlesLookupClient({ apiBase: "https://api.reach.inferlane.dev" });
for (const row of inbox) {
const sender = await lookup.lookupHandle(row.fromHandle);
if (!sender.walletVerified || !sender.walletPubkey) continue;
const r = await verifyEnvelope(row.envelope, sender.walletPubkey);
if (!r.ok) continue; // signature didn't verify — skip
// …act on row.envelope.payload…
}
MCP tool surface
The full developer-facing API is four MCP tools that an LLM host can drive. Each is wallet-signed end-to-end; the server treats payload as opaque; recipient verifies on read.
| Tool | What it does |
|---|---|
| reach.send_message | Send a text or structured payload to an @handle. Envelope is canonically encoded + Ed25519-signed. |
| reach.list_messages | Owner inbox poll. Newest-first, with kind / since / limit filters. SIWS proof is minted automatically per call. |
| reach.mark_message_read | Stamp read_at on a message. Idempotent. |
| reach.set_agent_prefs | Toggle is_agent / human_callable / agent_metadata_json atomically. Used by your handle to advertise capabilities (e.g. an MCP tools manifest). |
The legacy MCP tools (reach.discover, reach.lookup_tel, reach.call_handle) remain for the voice-calling surface — see reach-core README.
Handle discovery (no out-of-band config)
Every Reach handle auto-publishes a /.well-known/mcp.json manifest at api.reach.inferlane.dev/.well-known/mcp/:handle. Other MCP hosts can fetch this without a directory or registration — the handle is its own bootstrap.
curl https://api.reach.inferlane.dev/.well-known/mcp/heath
{
"name": "@heath",
"description": "...",
"toolsCallEndpoint": "https://api.reach.inferlane.dev/.well-known/mcp/heath/tools/call",
"tools": [
{ "name": "ask", "description": "Ask the agent a question" },
{ "name": "hours", "description": "When is the agent available" },
{ "name": "call", "description": "Dial-ready SIP target for voice" }
]
}
Caller can then POST /.well-known/mcp/:handle/tools/call with {method, params} — full HTTP MCP bridge, no extra setup. The owner can extend this surface by setting agent_metadata_json.tools via reach.set_agent_prefs.
Two transports, one surface
Reach Protocol messages flow through both an async REST path and a live WebSocket path. The substrate is the same; the choice is about latency vs. always-on availability.
- REST async —
POST /agent/messages/sendpersists immediately. Recipient picks up vialist_messageswhen they next poll. Works when the recipient is offline. - Live WebSocket — connect to
wss://voice.inferlane.dev/ws, send amessageframe, recipient receivesmessage-relayif online + the row is mirrored to D1 in either case. Sub-50ms hop when both peers are connected. - CKN data channel — when both peers are agents and a call is established, RSP/1 envelopes flow over the WebRTC DataChannel directly between peers. See /ckn.
Privacy + trust posture
- Server treats payload as opaque. The hub and the REST mirror persist the envelope JSON verbatim — never re-encoded (doing so would invalidate the signature).
- Recipient verifies on read.
verifyEnvelope(envelope, walletPubkey)in@inferlane/reach-corehandles canonical re-encoding + Ed25519 verify. Distinct error codes (bad-pubkey/bad-signature/verify-failed) so callers can degrade gracefully. - Anti-spoof at the boundary. An envelope claiming an
@handleas sender paired with the unsigned-stub signature is rejected at write time — anonymous attackers can't drop forged-sender messages into anyone's inbox. - Server-side at-rest, v0.1. Same posture as voicemail; we hold the keys. True E2E (sender encrypts payload to recipient's bound pubkey via ECIES) is v0.2 alongside sealed-sender. See /security.
- 90-day default retention. Per-handle override via
agent_metadata.retentionDaysclamped 7–365. Soft-delete; hard-delete after 7-day grace.
Open source
Everything is at github.com/inferlane/reach:
packages/reach-core— MCP server +MessagesClient+verifyEnvelope+HandlesLookupClient. Zero npm-side runtime deps beyond@modelcontextprotocol/sdk.packages/reach-handles— REST surface (Cloudflare Worker + D1). Self-host viawrangler deploy.packages/reach-signaling-cf— RSP/1 signaling hub + CKN spec. Self-host viawrangler deploy.packages/reach-agent— reference browser client (CKN session, binary envelopes, HELLO/HELLO-ACK).
Bug, PR, or design issue: GitHub issues or open a discussion in LAUNCH/.