arizukocomponents › channel adapters

channel adapters

What they are

A channel adapter is a small daemon that speaks one upstream platform’s protocol on one side and arizuko’s HTTP contract on the other. Telegram on the left, gated on the right.

Inbound: the adapter receives a message from the platform, signs the request with CHANNEL_SECRET, and POSTs it to gated. Outbound: gated calls back to the adapter’s POST /v1/send with the agent’s reply, and the adapter pushes it to the platform.

One adapter per platform. One container per adapter. Each runs on LISTEN_ADDR=:8080 inside its container; Docker networking keeps the ports collision-free.

The shared shape

Every adapter binds to the same Go interface in core/types.go:

Every adapter serves the same HTTP surface:

How they fit

platform (Slack, Telegram, …)
        |  platform-native protocol
        v
      <channel adapter>   (LISTEN_ADDR=:8080, own container)
        |  POST /v1/messages    (signed by CHANNEL_SECRET)
        v
      gated
        |  POST /v1/send         (callback to adapter)
        v
      <channel adapter>
        |  platform-native protocol
        v
      platform

The trust contract is symmetric: gated verifies inbound HMACs, the adapter verifies outbound HMACs. Neither trusts a request without a valid CHANNEL_SECRET signature. See auth.

The adapters

nameplatformlanguagenotes
slakdSlackGoAssistant pane — suggested prompts and conversation title; reactions map to likes.
teledTelegramGoLong-poll bot API, file uploads, native voice messages.
discdDiscordGoGateway over websocket, ActionRow buttons, channel threads.
mastdMastodonGoActivityPub via API token, replies and boosts.
bskydBlueskyGoAT Protocol, post threading.
reditdRedditGoComments, posts, crossposts; native dislike via downvote.
emaidemailGoIMAP receive, SMTP send, threading by Message-ID.
linkdLinkedInGoMessaging API, connection-scoped DMs.
whapdWhatsAppTypeScriptBaileys client, QR pairing, voice notes.
twitdX / TwitterTypeScriptMentions and DMs.

Capability gaps are honest: an adapter without a platform verb returns chanlib.ErrUnsupported. The agent gets a structured response and picks another action instead of silently dropping the call.

Per-platform quirks stay inside the adapter. Slack threads, Discord ActionRows, WhatsApp QR pairing, IMAP folder watching — none of it leaks into gated. The DB sees a uniform messages row no matter where the message came from.

Output rendering follows the same one-renderer rule. The agent picks a Claude Code output style per channel: slack emits Slack mrkdwn (*bold*, _italic_, <url|text>); other channels use CommonMark. The runner selects the style when it spawns the container (container/runner.go) so the adapter receives text already shaped for the surface.

Language choice follows the upstream SDK. Most platforms have a maintained Go client, so the adapter is Go. WhatsApp and X have richer TypeScript clients (Baileys, twitter-api-v2), so those adapters run on Node. The HTTP contract is identical either way.

Adding a new adapter

Two artifacts, no edits to proxyd or compose:

  1. Write the daemon. Implement Channel; implement Socializer for the verbs the platform exposes. Serve the standard HTTP surface on LISTEN_ADDR=:8080.
  2. Drop a template/services/<daemon>.toml with the compose env block and a [[proxyd_route]] entry. The next arizuko run picks it up.

Spec: specs/5/35-proxyd-standalone.md. Extension points: EXTENDING.md.

Asymmetric channels

An inbound channel and an outbound channel do not have to be the same daemon. A message can arrive on email and the reply can go out on Slack — the route table decides. See howto/asymmetric-channels.

Go deeper