The native gateway is the server's structured API for custom clients, bots, agents, and external-content tools. It is not another IRC port, an IRC bouncer, or an administrator back door. It reaches the same accounts, channels, events, retention policy, installation checkpoints, and authorization used by IRC clients.
Most people should continue using an ordinary IRC client. Use the native gateway when software needs structured channel state that IRC cannot express cleanly.
| Goal | Interface |
|---|---|
| Use Irssi, WeeChat, HexChat, or a mobile IRC client | TLS IRC |
| Build a browser client that behaves like IRC | IRC-over-WebSocket at /irc |
| Build a richer first-party channel client | Native gateway on the control listener |
| Connect a narrowly authorized bot or model-backed agent | Native gateway, usually through the reference MCP adapter |
| Onboard users or perform permitted HTTP administration | Native control API |
| Offer externally hosted XDCC-like packages over HTTPS and DCC | Native gateway plus the provider redemption API |
The native gateway is deliberately narrower than IRC today. It supports channel join, durable resume, delivery acknowledgement, channel messages, visible automation invocation, automation-policy disclosure, optional automation command declaration, catalogue search, content requests, provider package publication, and provider-side DCC transfer control. It does not currently provide direct messages, topics, presence commands, NAMES/WHOIS, channel moderation, or account-wide human read markers. A custom client needing those features should use IRC or combine the two adapters over one local model.
The gateway lives on the configured control listener, not the raw IRC port
and not the IRC-over-WebSocket /irc endpoint:
account password or automation app key
|
| POST /v1/native-sessions over HTTP Basic
v
one-use ticket (30 seconds, process memory only)
|
| WebSocket /v1/sync, subprotocol irc.native.v1
v
native session -> channel authority -> ordinary event log -> IRC and native clients
The authentication secret obtains a short-lived ticket. The first WebSocket frame consumes that ticket and claims a nickname. A ticket can be restricted to selected operations and channels, but software that still holds the account password or app key can request another ticket; durable bot and channel grants are the actual long-term authority boundary.
You need:
- an existing human account or automated-account app key;
- private local state for a human installation token;
- a configured control listener;
- TLS for any non-loopback listener; and
- Node.js 22 or newer to run the reference client.
Human clients use the bare account name and reusable account password. The
first ticket response creates an opaque dr1... installation token; save it
and return it in Telex-Installation on later ticket requests. It selects the
delivery checkpoint but cannot authenticate. Automated adapters instead use a
labeled account/key-id and ak1... app key; give each independently running
deployment its own key.
For local development, the loopback control listener may use plaintext:
IRC_CONTROL_LISTEN=127.0.0.1:8098 cargo run --lockedRemote plaintext control traffic is never allowed. A deployed control origin must use the daemon's TLS identity or a reviewed layer-4 pass-through topology.
From the repository root, start the server and then run:
cd clients
npm run native -- \
--url http://127.0.0.1:8098 \
--nick alice-native \
--auth-id alice \
--channel '#general'The client prompts for the account password or app key without echo. For non-interactive
local automation it can read IRC_CLIENT_PASSWORD once and delete it from the
process environment; passwords are never accepted on the command line.
For a deployed server, replace the URL with its HTTPS control origin, for
example https://irc.example.net:8098 or https://irc.example.net when the
listener is safely published on the default HTTPS port.
The reference terminal understands:
| Command | Effect |
|---|---|
| ordinary text | Send a message to the active channel |
/join #channel |
Join, restore cached events, and resume delivery |
/open #channel |
Select an already joined channel |
/cause EVENT-ID text |
Send a message with an explicit causal reference |
/invoke ACCOUNT question |
Post a visible question addressed to an admitted bot or agent |
/catalog [query] |
Search the active channel's external package catalogue |
/get PROVIDER-ID PACKAGE-ID |
Request a private one-use provider URL |
/conversations |
List joined native conversations |
/quit |
Disconnect |
The terminal currently has no syntax for a shared channel key. The JavaScript
library supports session.join("#room", {key: "..."}); otherwise use an IRC
client for keyed rooms.
The reference client stores events before acknowledging them. On restart it resumes after that device's last acknowledged event. A crash can therefore produce a harmless duplicate, but should not create a silent gap.
Its state file is plaintext and is private to the local OS account. By default
it is $XDG_STATE_HOME/telex/terminal-messages.json, falling back to
~/.local/state/telex/terminal-messages.json; --state PATH overrides
it. A content-epoch change clears only that server/device transcript scope
before resume; it cannot erase exports, other IRC client logs, model-provider
records, or downloaded files.
Native acknowledgement means “this device durably stored or processed the event.” It does not mean “the human read it” and does not advance the account's IRC read marker.
When a native client joins a channel, the server returns the complete durable automation roster before history replay. The reference terminal shows:
- whether each participant is a
botoragent; - its purpose;
- whether processing is local, elsewhere in the operator's infrastructure, or at a named external service;
- its data-handling statement;
- exactly which channel inputs and outputs it may use;
- any retained-history window;
- the configured invocation-context allowance;
- whether automation-to-automation chatter is enabled; and
- message/byte budgets, loop limits, moderator pause, and active cooldowns.
An offline agent with retained-history access is still shown because it may receive later content. The roster is disclosure by the operator, not proof that an outside processor honors its stated retention policy.
/invoke Helper what did we decide? commits a normal visible channel message.
An authenticated IRC user can create the same structured invocation with the
strict leading form @Helper: what did we decide?. Only the named, currently
admitted automation receives the private target marker and the zero-to-ten
preceding events allowed by its channel grant. Mid-sentence mentions and
malformed addresses remain ordinary chat. A leading mention also remains
ordinary when the named account is human, unknown, or lacks
invocation.receive; this is how an ambient participant agent can treat
mentions as highlights instead of private invocation gates.
Bots and agents use the same gateway but are more restricted than humans. An
automated account needs a manifest and at least one explicit channel grant
before it can obtain a session. Every join, replay, live event, acknowledgement,
and write rechecks that grant. See automated participants for
operator provisioning and the reference MCP adapter
for the easiest model/tool integration.
The CommandBot example is the
smallest deterministic invocation/reply scaffold.
The CatalogBot example is the
smallest complete leading-! consumer and publishes the declarations it
implements.
The command-catalogue publisher
shows how a leading-! bot can advertise the commands it already handles to
capable clients without changing ordinary channel-message delivery.
The ScheduledPublisher example
is the smallest timer-driven publish-only scaffold.
The RosterBot example composes a
minimal current roster page with a durable direct invocation/reply loop.
The DurableWorker example
persists work before acknowledgement and publishes asynchronous completion
without holding the delivery cursor open.
The Chatbot example is the ambient
model-backed scaffold whose downstream runner discovers its current resources
and tools through MCP.
The ArtifactProvider example
publishes a private file catalogue and exercises both one-use HTTPS retrieval
and traditional DCC SEND/resume slots.
The gateway's catalogue is an XDCC-like convenience layer; the IRC daemon never hosts, fetches, or relays package bytes.
/catalog displays bounded descriptors and the provider's data-handling
statement. /get requests a five-minute, one-use HTTPS capability privately.
The client prints the URL but never opens or previews it. Opening it normally
reveals the requester's network address to the named provider.
Provider software publishes descriptors through its granted native session.
For HTTPS it redeems capability tokens through the authenticated control API.
For traditional XDCC SEND, a content.transfer session receives one bounded
request, opens a port inside its operator-approved range, and returns the port;
Telex emits the conventional CTCP offer and the client downloads directly from
the provider. XDCC HTTPS remains the explicit private-link path and is never a
silent fallback from DCC. The complete trust, resume, and quota model is in
external content references.
The dependency-free ECMAScript library exports NativeSession and
NativeDurableSync. A minimal non-durable experiment looks like this:
import { NativeSession } from "./clients/lib/index.js";
let password = process.env.IRC_CLIENT_PASSWORD;
delete process.env.IRC_CLIENT_PASSWORD;
const session = new NativeSession({
url: "http://127.0.0.1:8098",
nickname: "alice-tool",
authenticationId: "alice/019-device-id",
password,
});
password = null;
session.addEventListener("event", ({ detail }) => {
console.log(detail.event);
});
await session.connect();
const channel = await session.join("#general");
await session.sendMessage(channel.id, "hello from the native gateway");This example intentionally does not acknowledge delivery. Production clients
should use NativeDurableSync or reproduce its ordering: persist a complete
page or live event transactionally, then acknowledge its final event. Never
acknowledge merely because a WebSocket frame arrived.
An automated session whose ticket allows automation.commands.set can publish
one complete leading-! catalogue after joining:
const desiredCommands = [
{
name: "latest",
usage: "<topic>",
description: "Find the latest item for a topic",
},
];
let catalogue = await botSession.setAutomationCommands(
botChannel.id,
desiredCommands,
{ expectedRevision: 0 },
);
if (catalogue.status === "stale") {
catalogue = await botSession.setAutomationCommands(
botChannel.id,
desiredCommands,
{ expectedRevision: catalogue.result.revision },
);
}The replacement is metadata, not a command route. The deployment still needs
live.human.read and must handle ordinary channel messages beginning with
!; the same permission is required to publish the declaration. Empty
desiredCommands clears the catalogue. See the
publisher example for a
bounded JSON-file tool and retry loop.
For a lower-level implementation, the sequence is:
- authenticate
POST /v1/native-sessionsand optionally request a restricted operation/channel scope; - open
/v1/syncwith WebSocket subprotocolirc.native.v1; - send
hellowith the one-use ticket and nickname within five seconds; - join a channel and retain its stable
conversation_id; - resume, commit each returned event batch, and acknowledge only durable work;
- use a fresh UUIDv7 operation ID for a new message or invocation, and reuse it only for an exact retry; and
- reconnect and resume after lag, transport loss, or restart.
The exact frame shapes, bounds, errors, and ticket attenuation rules are in the native gateway wire contract. The native control API documents the HTTP endpoints used for administration and providers.
- Use HTTPS/WSS everywhere except explicit loopback development.
- Persist a distinct human installation token or automation app key per process.
- Never put a password, app key, native ticket, installation token, or content capability in argv, a URL, chat history, logs, or source control.
- Treat the local transcript as plaintext sensitive data.
- Treat all channel text as hostile input to bots, models, and tools.
- Remember that ticket attenuation cannot confine software that still holds the account password or app key.
- Remember that the server operator can read retained plaintext; this is not end-to-end encryption.