Getting started

One bot program, every channel

Caspian is the mouth and ears of an agent. You write a familiar bot API — on_message / onMessage, on_action / onAction, thread.post. Underneath, that becomes an inspectable list of rules so the same program can be tested, hosted, or self-hosted without rewriting it.

A handler looks like this

bot.py
from caspian import Caspian

cx = Caspian()

@cx.on_message({"channel": ["discord", "telegram"], "overlap": "queue"})
def handle(thread, msg, ctx):
    thread.typing()
    thread.post(f"you said: {msg.text}")
bot.ts
import { Caspian } from "caspian-sdk"

const cx = new Caspian()

cx.onMessage(
  { channel: ["discord", "telegram"], overlap: "queue" },
  async (thread, msg, { skipped }) => {
    await thread.typing()
    await thread.post(`you said: ${msg.text}`)
  },
)

Adding a channel is paperwork on the side — channels.add — never a new handler type. Adapters are the only code that knows Telegram’s Bot API or Discord’s gateway.

Hosted or self-host — always pass via

via="hosted" means Caspian owns inbound: construct Caspian(api_key=…), then channels.add(..., via="hosted"). via="self-host" means you own inbound: Caspian() with no key is enough; you pass the channel’s own secrets (BotFather token, Slack tokens, …). The handlers are the same either way.

cx = Caspian(api_key=KEY)
cx.channels.add("telegram", via="hosted", bot_token=TG)

cx = Caspian()   # no Caspian key
cx.channels.add("telegram", via="self-host",
                bot_token=TG, webhook_url=URL)
const cx = new Caspian()
await cx.channels.add("telegram", { via: "hosted", bot_token: TG })
await cx.run({ apiKey: KEY })

const cx = new Caspian()
await cx.channels.add("telegram", {
  via: "self-host",
  bot_token: TG,
  webhook_url: URL,
})

How a turn works

  1. The adapter turns a platform update into a plain event.
  2. The core picks the matching rule (channel, chat kind, command, overlap).
  3. Your handler gets a thread and calls thread.post — that enqueues a command; it does not call Telegram.
  4. The adapter turns the command into the right platform send.

Hosted: the platform talks to Caspian; Caspian acknowledges, then delivers the event to your app. Self-host: the platform hits your webhook (or you listen / poll).

Channels in this SDK

The catalog adapters are Telegram, Slack, Discord, email, WhatsApp, Messenger, SMS, voice, iMessage, X, and Linear. Hosted can also speak channels the gateway owns that have no local adapter.

Next steps

Quickstart →
A Telegram bot in a few minutes, self-host or hosted.
Core concepts →
App, adapters, thread, hosted vs self-host.
Overlap →
queue, drop, skipped — concurrent turns on one chat.
Channels →
Catalog matrix, inbound verb, credentials.
SDK reference →
Caspian, Thread, Message — Python and TypeScript.
MCP server →
Give Cursor or Claude Code a messaging presence by URL — no install.