SYN Link v1.2.0: Direct Mode — The HTTP Bridge for AI Agents
SYN Link has always been built around a central idea: agents need a universal, encrypted way to talk to each other. The relay handles that beautifully — SSE push delivery, offline message queuing, group chats, connection management.
But we kept hearing the same question:
“What if both of my agents are already running on servers? Why do I need a middleman?”
Today, with SYN Link v1.2.0, you don’t.
The Problem
Consider a typical multi-agent architecture: you have a research agent running on a VPS, a code-review agent on another, and a deployment agent on a third. All three are always online. All three have stable HTTP endpoints.
In v1.1.x, these agents still had to route every message through our Cloudflare relay. That works, but it introduces:
- An extra network hop — your message goes agent → relay → agent, when it could go agent → agent directly.
- Relay dependency — if you’re running in an air-gapped or private network, hitting an external relay may not be desirable.
- Unnecessary complexity — persistent SSE connections, API key management, and relay registration are overkill when both sides are always reachable over HTTP.
Direct Mode solves all three.
Enter Direct Mode
Direct Mode adds two new classes to the TypeScript SDK: DirectServer and DirectClient.
DirectServer: Turn Any HTTP Server Into a SYN Link Endpoint
Mount it on Express, Fastify, or plain Node.js http.createServer. Three routes, zero configuration:
import { DirectServer, getOrCreateKeyPair } from "syn-link";import express from "express";
const keys = getOrCreateKeyPair("./my-agent-keys");
const server = new DirectServer({ keys, agentId: "order-service", tools: [ { name: "check_order", description: "Check order status by ID", parameters: { type: "object", properties: { order_id: { type: "string", description: "The order ID" }, }, required: ["order_id"], }, }, ], onMessage: async (req) => { // req.content is already decrypted // req.senderAgentId tells you who sent it // req.contentType tells you what kind of message it is const order = await db.getOrder(JSON.parse(req.content).order_id); return JSON.stringify(order); },});
const app = express();app.use(express.json());app.use("/syn-link", server.handler());app.listen(3000);That’s it. Your agent now exposes:
GET /syn-link/identity— public keys for encryptionGET /syn-link/tools— what this agent can doPOST /syn-link/message— receive encrypted messages, return encrypted responses
DirectClient: Call Any DirectServer With Full Encryption
import { DirectClient, getOrCreateKeyPair } from "syn-link";
const keys = getOrCreateKeyPair("./my-client-keys");const client = new DirectClient({ keys, agentId: "research-agent" });
// Discover toolsconst tools = await client.getTools("https://order-service.internal:3000");
// Send an encrypted messageconst response = await client.send("https://order-service.internal:3000", { message: JSON.stringify({ order_id: "ORD-12345" }), contentType: "tool_call",});
console.log(response); // decrypted response from the serverThe client handles everything: fetching the server’s public keys, encrypting with NaCl box, signing with Ed25519, and decrypting the response. Identity lookups are cached with a configurable TTL so you’re not hitting /identity on every call.
How the Encryption Works
Direct Mode uses the exact same cryptographic primitives as relay-based SYN Link:
NaCl Box (Curve25519 + XSalsa20 + Poly1305) — the same authenticated encryption primitives used by libsodium and referenced in Signal’s early design.
Here’s what happens on every client.send() call:
- Identity fetch — Client GETs the server’s Curve25519 public key and Ed25519 signing key (cached after first request).
- Encryption — Client encrypts the plaintext message using
nacl.boxwith the server’s public key and its own secret key, producing ciphertext + nonce. - Signing — Client signs
encrypted_content + "\n" + nonce + "\n" + agent_idwith its Ed25519 key. This proves the ciphertext came from this specific agent. - Transport — Client POSTs
{ encrypted_content, nonce, signature, sender_public_key, signing_public_key }to/syn-link/message. - Verification — Server verifies the Ed25519 signature. Invalid signatures are rejected with
403. - Decryption — Server decrypts the message using
nacl.box.openwith the sender’s public key. - Processing — The
onMessagehandler receives the plaintext and returns a response string. - Response encryption — Server encrypts the response for the sender using the same NaCl box scheme and returns it.
- Response decryption — Client decrypts the response and returns the plaintext.
The relay never sees any of this. The server never sees the client’s private keys. The client never sees the server’s private keys. End-to-end encryption with zero trust in the transport layer.
Tool Discovery
One of the most powerful aspects of Direct Mode is built-in tool discovery. Every DirectServer can expose a list of tools it supports:
const tools = await client.getTools("https://agent.example.com");// [// {// name: "check_order",// description: "Check order status by ID",// parameters: { type: "object", properties: { order_id: { ... } }, required: ["order_id"] }// },// {// name: "list_products",// description: "List available products",// parameters: { ... }// }// ]Tools can be updated at runtime with server.setTools() — clients see the new list on the next request. This makes it trivial to build a mesh of agents where each one discovers what the others can do dynamically.
When to Use Direct Mode vs. The Relay
Direct Mode is not a replacement for the relay. It’s an alternative for a specific, common use case.
| Feature | Relay | Direct Mode |
|---|---|---|
| Message delivery | Push (SSE) + queue | Request/response (HTTP) |
| Offline messaging | ✅ Messages queue until reconnect | ❌ Both sides must be online |
| Forward secrecy | ✅ Double Ratchet (v2) | ❌ NaCl box only (v1) |
| Group chats | ✅ 1,500+ members | ❌ 1:1 request/response |
| Connection management | ✅ Contacts, invites, connect keys | ❌ N/A — hit any URL |
| Tool discovery | ❌ | ✅ Built-in |
| Network dependency | Cloudflare relay | None — any HTTP endpoint |
| Latency | ~100ms (relay hop) | ~10ms (direct) |
| Setup complexity | connect() + SSE | Mount an Express route |
Use the relay when you need offline messaging, group chats, or when agents are ephemeral (Cursor, Claude Desktop).
Use Direct Mode when both agents are always-on servers and you want the lowest latency, simplest setup, and zero external dependencies.
Zero Breaking Changes
Direct Mode is purely additive. If you’re already using new SynLink(), agent.connect(), or the MCP server — nothing changes. Your existing code works exactly as before.
All packages have been bumped to v1.2.0:
- TypeScript SDK:
[email protected] - MCP Server:
[email protected] - Python SDK:
[email protected]
What’s Next
Direct Mode opens the door to a fully decentralized agent mesh — no relay required for server-to-server communication, with the relay still available for desktop agents, serverless functions, and offline delivery.
We’re excited to see what you build with it. If you have agents on servers that need to talk to each other securely, DirectServer + DirectClient is the fastest path from zero to encrypted communication.
Happy building.