What We Build Better Than OpenClaw and Hermes
Before writing a single line of channel integration code, we spent time studying the two best open-source AI agent frameworks that already solve multi-channel messaging: OpenClaw (TypeScript, 25+ channels) and Hermes Agent by Nous Research (Python, 23+ channels).
They are genuinely good. OpenClaw's typed plugin interface is clean. Hermes's canonical message struct is exactly the right abstraction. Both projects handle Slack, Telegram, WhatsApp, SMS, and email in ways that work.
So rather than build from scratch, we started with a question: what do they do that we should adopt directly, what do they do that we should improve, and what are they not designed to handle at all?
This post is the honest answer to that question.
What they do well
OpenClaw's best idea: the typed adapter contract.
Every channel in OpenClaw is a self-contained ChannelPlugin<Account> module. The plugin declares its capabilities — can it send typing indicators? Does it support approval buttons? How should long messages be chunked? — and the gateway calls only what exists. Plugins are lazy-loaded, so a Slack adapter doesn't increase startup time on a deployment that only uses Telegram.
The mention gating pattern is especially clean: a single shared function resolveInboundMentionDecision() handles the "should we respond to this group message?" logic for every channel. One implementation, zero duplication.
Hermes's best idea: the canonical message struct.
Hermes forces every channel adapter to produce an identical MessageEvent dataclass. Telegram messages, Slack messages, SMS messages, email replies — they all look the same to the gateway by the time they're processed. The gateway speaks one language internally. No if/elif chains on platform == 'slack' deep inside routing logic.
The session key builder is similarly clean: a single build_session_key() function produces deterministic, hierarchical keys (agent:main:telegram:chat:123456:topic:42) from any adapter's output. Sessions are recoverable, debuggable, and consistent across reconnects.
Hermes also gets the delivery side right. Its GatewayStreamConsumer sends streaming responses via progressive message edits — for Telegram and Slack, subsequent token chunks edit the existing message rather than posting a cascade of new ones. It reads like a human typing, not like a slot machine.
Where they stop
Both projects were designed for a single user operating their own agent. That design constraint shapes everything.
No multi-tenancy
In OpenClaw and Hermes, credentials are environment variables. One Slack bot token. One Telegram bot. The entire deployment is for one workspace, one operator. There's no concept of a tenant, and there's no way to install the same agent for 500 different companies with their own Slack workspaces, their own credentials, their own agent configurations.
This isn't a criticism — it's a scope decision. But it means neither project can be taken off-the-shelf and turned into a product.
No BYOK credentials
Related: both projects assume they own the API keys. The Slack token, the Twilio credentials, the Gmail OAuth token — they live in .env files on the server.
For a business running Ariftly, those credentials belong to the customer. Their Gmail OAuth token is theirs. Their Slack workspace is theirs. We never hold raw secrets; agents receive short-lived proxy tokens and make calls through our tool proxy. The customer can revoke at any time. This model simply doesn't exist in either framework.
No policy engine
Neither OpenClaw nor Hermes has a concept of a rule that governs what an agent can do. If the Slack integration receives a message that would trigger an outbound email campaign, it dispatches it. Full stop.
In an enterprise context, that's a problem. Different tenants have different rules. A regulated financial services company may require human approval before any external communication. A sales team may want to rate-limit outbound messages to 50 per day. An IT department may want to explicitly deny certain task types from channel triggers entirely.
No human-in-the-loop over the channel
Hermes and OpenClaw deliver responses back to the channel, but neither has a first-class concept of an approval gate that lives in the channel itself. If an agent proposes an action that needs a human decision, there's nowhere for that decision to land.
Our approval.requested event is a primitive in the protocol. When an agent requests approval and the session originated from Slack, the approval card — with Approve/Deny buttons — goes back to the same Slack thread. The human approves from where they already are. The agent resumes.
No event sourcing
Both projects maintain in-memory or SQLite session state. If the process restarts, or if you want to replay what happened in a session, or if you want to audit every message that passed through, the data is either gone or requires a separate logging integration.
Every event on Ariftly — including every inbound channel message, every outbound response, every approval, every tool call — appends to an immutable event_log. Sessions are projections. Audit is free.
The six decisions we made differently
1. Multi-tenant from the ground up
Every session, every channel binding, every credential, every event carries a tenantId. There is no shared global state between tenants. A single Slack webhook handler can serve 500 different companies simultaneously because the first thing it does is look up the binding by externalWorkspaceId and resolve the tenant.
This isn't a layer added on top — it's in the schema, the session key format, the rate limiter, and the event log from the first commit.
2. BYOK credentials through an encrypted vault
Tenants bring their own API keys and OAuth tokens. We store them AES-256-GCM encrypted in our credential vault. Agents never receive raw secrets — they get short-lived proxy tokens scoped to specific tools. The tool proxy makes the actual API call, logs it, and enforces the allowed-tools list from the agent's manifest.
The customer retains control. We never hold the underlying credential in plaintext at runtime.
3. Policy-gated channel actions
Before any inbound channel message dispatches to an agent, the policy engine evaluates tenant-defined rules. A rule can:
allow— proceedrequire_approval— gate the action behind a human decisiondeny— reject with a reason
This runs before any agent code executes. It's the same engine that governs scheduled tasks and API calls — channels aren't a special case.
4. Approval as a first-class channel primitive
When an agent emits approval.requested and the session has an originating channel, the approval goes back to that channel. Slack gets a Block Kit message with interactive buttons. Telegram gets an inline keyboard. SMS gets a APPROVE/DENY reply with a token. Email gets instructions and a reply token in the subject line.
The human approves from the same place they asked the question. No second system. No polling a dashboard. The loop closes in the channel.
5. Serverless-native, not daemon-based
OpenClaw and Hermes are long-running Node.js and Python processes. They need persistent connections to maintain Socket Mode or long-polling. For a multi-tenant platform serving many companies, that model requires dedicated infrastructure per deployment, complex connection management, and state recovery on restart.
Our channel handlers are stateless Vercel Functions. Inbound webhooks are HTTP POST requests. Outbound delivery is a function call that happens after the response is returned, via after(). Durable delivery (retries, ordering guarantees) comes from Vercel Queues, not from in-process state.
This means our channel layer scales horizontally without coordination, restarts cleanly, and costs nothing when idle.
6. Cross-agent routing in the channel binding
OpenClaw and Hermes route all messages to a single configured agent. We store routing configuration in the channel binding:
{
"agentRouting": {
"compliance": "ai-agent",
"sales": "sales-agent",
"default": "ai-agent"
}
}
A single Slack workspace can route #sales channel messages to sales-agent and #compliance channel messages to ai-agent, with the binding configuration controlling the mapping. No code changes. No redeployment.
The comparison in full
| OpenClaw | Hermes Agent | Ariftly | |
|---|---|---|---|
| Multi-tenant | ❌ | ❌ | ✅ tenantId on every row |
| BYOK credentials | ❌ env vars | ❌ env vars | ✅ encrypted vault, proxy tokens |
| Policy-gated actions | ❌ | ❌ | ✅ deny / require_approval / allow |
| Approval via channel | Partial | ❌ | ✅ Slack buttons, Telegram keyboard, SMS reply |
| Cross-agent routing | ❌ | ❌ | ✅ agentRouting config per binding |
| Event sourcing | ❌ | ❌ | ✅ append-only event_log |
| Serverless / edge | ❌ daemon | ❌ process | ✅ Vercel Functions + Queues |
| Durable delivery | ❌ | ❌ | ✅ Vercel Queues with retry |
| Streaming delivery | ❌ | ✅ editMessageText | ✅ same pattern |
| HMAC credential rotation | ❌ | ❌ | ✅ already built for agent callbacks |
| Rate limiting | ❌ | ❌ | ✅ per-tenant per-agent daily cap |
| Mention gating | ✅ | ❌ | ✅ adopted from OpenClaw |
| Canonical message struct | ❌ plugin-owned | ✅ MessageEvent | ✅ NormalizedMessage |
| Session persistence | In-memory | SQLite | ✅ Postgres |
| SMS | ❌ | ✅ Twilio | ✅ Twilio (in progress) |
| Email inbound | ❌ | ✅ IMAP | ✅ SendGrid + IMAP cron (in progress) |
| Channels supported | 25+ | 23+ | Slack, Telegram, SMS, Email (more coming) |
What we took from each
We didn't build from scratch. The best ideas from both projects are in our implementation:
From Hermes: the canonical NormalizedMessage struct, the PlatformRegistry.register() pattern, deterministic session key construction, Twilio HMAC validation, email thread tracking via In-Reply-To/References, and progressive streaming delivery.
From OpenClaw: the ChannelAdapter<T> typed interface, lazy-loaded adapter modules, mention gating via a shared decision function, and Socket Mode for Slack development (no public webhook URL needed locally).
The additions are ours: multi-tenancy, BYOK credentials, policy evaluation, channel-native approvals, cross-agent routing, event sourcing, and serverless execution.
A note on language
Hermes is Python. We're TypeScript. Both are valid choices — Hermes's Python makes sense for a research organization deep in the ML ecosystem.
For us, TypeScript wins on one specific point: the type TaskTriggerRequest is defined once in packages/agent-wire and is shared by Core, the agent SDK, every agent, and every test. There is no opportunity for the agent's understanding of a message format to drift from Core's understanding. In Python, equivalent Pydantic models in different services would need to be kept in sync manually.
Future agents that genuinely need Python — a PenAgent doing active scanning, an analytics agent running numpy — are standalone HTTP services. They speak the RAP v1 protocol. Language is irrelevant at the boundary.
The full picture
OpenClaw and Hermes solved the hard problem of getting many messaging platforms to work. We owe them that.
The problems they didn't solve — multi-tenancy, BYOK, policy, approvals, event sourcing — aren't hard algorithmic problems. They're design commitment problems. You can't add multi-tenancy to a single-user system as a feature. It has to be the foundation. Which is why we read both projects before writing code rather than after.
The channel layer we're building is those projects' good ideas, plus the architecture that makes them safe to run for paying customers at scale.
Ariftly is the intelligence layer above your stack. Cross-domain AI agents — compliance, sales, accessibility, security — that coordinate, get approvals, and act on your behalf. Start here →