Caspian
Give an AI agent real communication channels — Slack, Discord, Telegram, email, X, SMS — behind one on_message handler via caspian-sdk. Free channels need no...
Caspian
@trycaspian
What This Skill Does
Unifies Slack, Discord, Telegram, email, X, and SMS into a single on_message handler and reply() call, so an AI agent can communicate across all channels with one code path. Free channels (email, Telegram, Discord, Slack) require no signup; paid channels need a one-time login and prepaid credit.
Replaces building and maintaining separate integrations for each messaging platform by providing one SDK that handles all channels with a single handler.
When to Use It
- Connect an AI agent to Slack for team notifications and replies
- Add Discord bot capabilities to an agent without writing Discord-specific code
- Let users send messages to an agent via Telegram and receive automated responses
- Enable email-based interaction with an AI agent for support or workflows
- Integrate X (Twitter) DMs into an agent's communication loop
- Route SMS messages from a Twilio/Telnyx number through the agent's handler
Install
$ openclaw skills install @trycaspian/caspiancaspian-sdk: give an AI agent real communication channels
You are integrating messaging into an AI agent using caspian-sdk. It gives an
agent a real presence on many channels behind ONE on_message handler and one
message.reply() - the same code answers on every channel. The gateway is
hosted at https://api.trycaspianai.com.
How to drive this (read first)
The developer may just say "add caspian-sdk" without naming channels. Do this:
- Do steps 1-3 below (key, install, the shared handler) - always needed.
- Discover which channels are actually connectable RIGHT NOW:
curl -s https://api.trycaspianai.com/v1/channels -H "Authorization: Bearer $CASPIAN_API_KEY"That returns the live channels (currently: email, discord, slack, x, telegram, phone/SMS). Phone/SMS is BRING-YOUR-OWN (the developer's own Twilio/Telnyx number - see that section). Only offer those. Other channels (WhatsApp, Voice, RCS, Instagram, Facebook, iMessage) exist in the SDK but are NOT live on this gateway yet - do NOT try to connect them (they 400). If a developer asks for one of those, tell them it's "coming soon / being integrated" - don't attempt it and don't apologize at length; just say it's on the way. A channel may occasionally appear MORE THAN ONCE - each row is a differentproviderfor that channel; when that happens, ask which provider they want and pass it asprovider=toconnect_*(). - ASK the developer which of the live channels they want (email needs nothing; others need a token or an OAuth click - see the per-channel sections). Don't assume; list the live options and let them choose. Email is a safe default if they don't care.
- Connect each chosen channel (sections below), then verify with a test.
Every channel uses the SAME handler from step 3 - adding one is just another
client.connect_*() call, never new handler code.
1. Get an API key
If CASPIAN_API_KEY is already in the project's .env, SKIP this step.
Free channels need NO signup. Email, Telegram, Discord, Slack, Instagram, Facebook, and the developer's own Twilio/Telnyx number cost nothing — just mint an instant key and start building:
curl -s -X POST https://api.trycaspianai.com/v1/projects/sandbox \
-H 'Content-Type: application/json' -d '{"name":"my-agent"}'
Returns {"project_id":"...","api_key":"comm_sandbox_..."}. Write it to .env:
CASPIAN_API_KEY=<api_key>
CASPIAN_BASE_URL=https://api.trycaspianai.com
Free channels need no sign-in. Paid channels need a one-time sign-in, then credit. Free channels (email, Telegram, Discord, Slack) work instantly with just the API key. Channels on Caspian's paid network (X, WhatsApp, iMessage) are gated on two things, both surfaced as machine-actionable JSON:
a) One-time developer sign-in (401 account_required). The first paid action
returns 401 with login_options. Paid spend must be tied to a real account.
Get the developer signed in once (they never sign in again after this):
POST https://api.trycaspianai.com/v1/auth/device/startwith{"api_key": "<your key>"}(so THIS project binds to their account - nothing you built is lost).- SHOW the developer
verification_uri_completeand WAIT for them to sign in. - Poll
POST https://api.trycaspianai.com/v1/auth/device/tokenwith{"device_code": ...}untilstatusisapproved, then retry the call.
b) Prepaid credit (402 insufficient_credit). Once signed in, a paid action
returns 402 with payment_options if the balance is too low:
- Check the balance any time:
GET https://api.trycaspianai.com/v1/billing(Bearer auth). - The developer adds credit in the dashboard. SHOW them this link and WAIT:
Add credit to keep {channel} running: https://dashboard.trycaspianai.com
- Credit lands seconds after payment - poll
GET https://api.trycaspianai.com/v1/billinguntilbalance_centsrises (or watch for thebilling.creditedevent), then retry the call that 402'd.
Autopay + spend limits (set them, they protect the developer): after one checkout the card is saved automatically. The agent can then enable replenishment and caps over the API - a monthly cap is REQUIRED for autopay:
curl -s -X PUT https://api.trycaspianai.com/v1/billing/autopay -H "Authorization: Bearer $CASPIAN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"threshold_cents":500,"topup_cents":2000,"monthly_cap_cents":10000}'
PUT https://api.trycaspianai.com/v1/billing/limits sets monthly_cap_cents / per-channel
channel_caps. Watch for billing.low_balance and billing.limit_reached
events on the normal event stream and tell the developer when they fire.
2. Install the SDK
uv add caspian-sdk
# or: pip install caspian-sdk
3. Integrate - one handler for every channel
The SDK reads CASPIAN_API_KEY and CASPIAN_BASE_URL from the environment or ./.env automatically. Write the handler ONCE; it fires for whatever channels you connect below.
from caspian_sdk import CommClient
client = CommClient()
# Connect the channels the developer chose (see the per-channel sections).
# Email needs nothing and is the safe default:
email = client.connect_email(username="scout") # ASK the developer for the name
print("Agent email:", email["address"]) # scout@agents.trycaspianai.com
# client.connect_telegram(bot_token=...) # if they want Telegram
# client.connect_discord(bot_token=...) # if they want Discord
@client.on_message
def handle(message):
# Same for every channel: message.text, message.sender, message.conversation_id
# (conversation_id is stable per thread - key your agent's memory on it)
answer = your_agent_logic(message.text)
message.reply(answer) # replies on whichever channel the message arrived from
client.listen()
Wire your_agent_logic to whatever the developer's agent framework is
(OpenAI Agents SDK, LangGraph, plain LLM call). The reply must be plain text.
Make the agent platform-aware (optional, recommended)
Each channel behaves differently - Slack threads, WhatsApp's 24h window, SMS length limits, iMessage has no markdown, X caps public replies at 280. Caspian returns concise etiquette for the channels the developer connected; inject it into the agent's system prompt so replies fit each platform:
guide = client.behavior_prompt() # combined guide for connected channels
system_prompt = system_prompt + "\n\n" + guide
On any SDK version you can fetch it directly instead:
curl -s https://api.trycaspianai.com/v1/behavior-prompt -H "Authorization: Bearer $CASPIAN_API_KEY"
(or one channel: https://api.trycaspianai.com/v1/channels/slack/guide). It's a suggestion - the
developer can use it as-is, tweak it, or write their own. Offer it; don't force it.
STOP before connecting email - you MUST ask the developer what mailbox NAME they
want (the part before the @) and WAIT for their answer. Do NOT call a bare
connect_email() - that auto-generates an ugly random name like
scout-a3f9@... and is almost never what they want. Once they tell you the name:
client.connect_email(username="<their choice>") -> <choice>@agents.trycaspianai.com.
If that name is taken the API replies 409 with a suggestions list of readable
alternatives - show those and let them pick one. If they'd rather use their OWN
domain, follow the custom-domain section and pass domain= too (e.g.
username="kernel", domain="agents.acme.com" -> kernel@agents.acme.com).
4. Verify it works
With the integration running in one terminal, deliver a test email:
curl -s -X POST https://api.trycaspianai.com/v1/test-emails -H "Authorization: Bearer $CASPIAN_API_KEY" \
-H 'Content-Type: application/json' -d '{"text":"hello, are you alive?"}'
Within ~10 seconds the running integration must print the inbound message and send a reply. Confirm by checking events:
curl -s "https://api.trycaspianai.com/v1/events?type=message.sent" -H "Authorization: Bearer $CASPIAN_API_KEY"
If a message.sent event appears, the integration is complete. Tell the
developer their agent's email address; real emails to it from any mail client
now reach the handler.
If the developer sends a REAL email and says no reply arrived, FIRST tell them to
check their SPAM/Junk folder. A message.sent event (above) means the reply WAS
sent - a brand-new sending domain has no reputation yet, so Gmail/Outlook often
file the first replies as spam. Ask them to open it and click "Not spam"; that
trains the inbox and later replies land normally. This is deliverability warmup,
not a bug. Only if there's no message.sent event is something actually wrong.
Custom email domain (optional)
By default addresses live on the platform domain. If the developer wants their own brand (e.g. support-x@agents.acme.com), ask them:
Do you want agent email addresses on your own domain? If yes, tell me a dedicated subdomain of a domain you control (e.g. agents.yourcompany.com). Do not use your root domain - its existing mail would be affected.
Then:
- Register it:
client.add_domain("agents.acme.com")(REST: POST https://api.trycaspianai.com/v1/domains with{"domain": "agents.acme.com"}). - Show the developer the returned
dns_recordsand tell them to add each at their DNS provider. A zone file for bulk import is athttps://api.trycaspianai.com/v1/domains/{id}/zone-file. - Poll
GET https://api.trycaspianai.com/v1/domains/{id}untilstatusisactive(DNS can take minutes). Do not guess; wait for the developer to confirm the records are in. - Connect on the domain:
client.connect_email(domain="agents.acme.com")- the address is now on their domain, sending is DKIM-signed as their domain. To pick the exact address, addusername="kernel"-> kernel@agents.acme.com (custom domains only; 409 if taken).
Adding Slack (ask which kind of app)
Slack comes THREE ways - STOP and ask the developer:
Slack connects as an app installed to a workspace. Three options: a) Quick (one click) - add our shared app; nothing to create. Custom name + icon, so it still looks like your brand. b) Branded - your OWN Slack app in YOUR workspace (~5 min); you control the app name and the @mention. c) Distribute - your OWN Slack app that you PUBLISH and install into your CUSTOMERS' workspaces (the same way Caspian does its shared app). ~10 min once, then one "Add to Slack" link per customer. Which - Quick, Branded, or Distribute?
a) Quick - shared app (one-click OAuth, no app to create)
Ask the developer what name (and optional icon URL) the agent should post under:
What should the agent be called in Slack? (e.g. "Acme Support") Optionally an icon image URL too.
Then: result = client.install_slack(display_name="<their name>", icon_url="<url>")
returns a connection with an authorize_url ("Add to Slack"). Give the developer
that URL: "Open this, pick your workspace, and authorize." The shared app installs
into their workspace, the connection flips to active on its own (poll
GET https://api.trycaspianai.com/v1/connections/{id} or watch for connection.active), and the agent
posts under their name + icon. Messages in that workspace reach the same
on_message handler. If install_slack() 400s, the shared app isn't configured on
this gateway - use (b). NEVER default the name to "Caspian"/an infra name - ask.
b) / c) Your own Slack app (b = your workspace, c = distribute to customers)
Both use an app the developer OWNS; (c) just also turns on public distribution so they can install it into OTHER workspaces. Walk the developer through creating it - tell them to do EXACTLY this at api.slack.com/apps -> Create New App -> From scratch:
- OAuth & Permissions -> Redirect URLs -> add
https://api.trycaspianai.com/v1/oauth/slack/callback, Save. - OAuth & Permissions -> Bot Token Scopes -> add: chat:write, chat:write.customize, channels:history, im:history, app_mentions:read.
- Event Subscriptions -> toggle On -> Request URL:
https://api.trycaspianai.com/internal/providers/slack/webhooks(it verifies instantly). Under "Subscribe to bot events" add: message.channels, message.im, app_mention, Save. - (Option c ONLY) Manage Distribution -> finish the checklist -> "Activate Public Distribution". This is what lets the app install into your customers' workspaces, not just your own.
- Basic Information -> App Credentials -> copy Client ID, Client Secret, and Signing Secret, and paste all three to me.
Then connect:
- b) your workspace:
client.connect_slack(slack_client_id=..., slack_client_secret=..., slack_signing_secret=...)returns a connection with anauthorize_url. Open it, pick your workspace, Allow -> the connection goesactive. - c) per customer: call
connect_slack(...)with the SAME app creds but a DIFFERENTcustomer_idfor each customer (ask the developer for a label per customer, e.g. their company name). Each call returns its OWNauthorize_url("Add to Slack"). Send that link to that customer; they pick THEIR workspace and Allow. Every customer's workspace routes back to the sameon_messagehandler, isolated by customer_id, and inbound from all of them shares the one events URL - routed automatically by app + team.
CLI: caspian connect slack prints the authorize link.
Adding Instagram or Facebook (OAuth channels)
Same OAuth-link pattern (gateway-configured app):
client.connect_instagram(...)/connect_facebook(...)returns a connection with anauthorize_url.- Give the developer that URL: "Click this to authorize in your account."
- They approve; the platform redirects to our callback and the connection flips
to active. Poll
GET https://api.trycaspianai.com/v1/connections/{id}untilactive. - The same
on_messagehandler now answers on that channel.
Adding Discord (ask which kind of bot)
The agent connects to Discord as a bot (two-way: people message it, it replies). There are two ways to get that bot - STOP and ask the developer:
Discord connects as a bot. Two options: a) Quick (one click) - add our shared bot to your server; no token to create. You give it a custom name; it uses a shared avatar. b) Branded - create your OWN bot (~5 min) so it has your own name AND avatar. Which - Quick (shared) or Branded (your own)?
a) Quick - shared bot (one-click OAuth, no token)
First ASK the developer what name the bot should show in their server:
What should the bot be called in your server? (e.g. "Acme Support")
Then pass it as display_name:
result = client.install_discord(display_name="<their chosen name>") returns a
connection with an authorize_url. Give the developer that URL: "Open this, pick
your server, and authorize." Discord adds the shared bot, renames it to their
chosen name in that server, and the connection flips to active on its own (poll
GET /v1/connections/{id} or watch for connection.active). Messages in that
server then reach the same on_message handler; message.reply() answers. If
install_discord() 400s, the shared bot isn't configured on this gateway - use
(b). A 409 on the callback means that server is already linked to an agent. (The
custom name is a per-server nickname; avatar stays shared - use (b) for a custom
avatar too.) NEVER default the name to "Caspian" or an infra name - ask.
b) Branded - bring your own bot
Create a bot at discord.com/developers -> New Application -> Bot -> Reset Token -> copy it. Enable the "Message Content Intent" under Bot settings. Then paste me the token.
Then client.connect_discord(bot_token="<token>"). Same on_message handler
answers; message.reply() replies in Discord. A 409 means that bot is already
connected elsewhere - use a different one.
Either way, replies use the developer's agent - only the bot's name/avatar differs (shared name vs their own bot). Never brand the bot as Caspian/the gateway.
Adding Telegram (optional second channel)
Telegram needs one thing only a human can create: a bot token. STOP and ask the developer for it with exactly these instructions:
Open Telegram and message @BotFather. Send /newbot, choose a name and a username, and paste me the token it gives you (looks like 7123456789:AAE...).
Do not proceed without the token and never invent one. Once the developer provides it:
telegram = client.connect_telegram(bot_token="<token from developer>")
print("Telegram bot:", telegram["address"]) # @their_bot
Or via CLI: caspian connect telegram --bot-token <token>.
The same @client.on_message handler receives Telegram messages; message.reply() answers in the chat. No other setup - the gateway registers the webhook.
To verify: tell the developer to open Telegram, search for the bot's @username, and send it any message. The running integration must print it and reply within a few seconds.
Error handling: a 409 from connect means that bot token is already connected
elsewhere - ask the developer for a different bot. A 422 naming bot_token
means it was missing or malformed.
Adding WhatsApp (ask which provider first)
WhatsApp is served by TWO providers on this gateway, so it appears twice in /v1/channels. They behave differently, so STOP and ask the developer which one with exactly this choice:
WhatsApp has two options here:
- Twilio - fastest to test. A shared sandbox number works in minutes (great for a demo); a dedicated production number needs a paid Twilio account + WhatsApp sender approval.
- Meta (direct) - connect your OWN branded WhatsApp Business number through a one-time Meta popup (Embedded Signup). Best for production and brand. Which do you want - Twilio (quick test) or Meta (branded)?
Pass their choice as provider= on connect. Same on_message handler either way.
Option 1 - Twilio (test now)
wa = client.connect_whatsapp(provider="twilio-whatsapp")
print("WhatsApp on:", wa["address"]) # the Twilio number (sandbox by default)
For inbound to reach the agent, the developer must do two one-time things in the Twilio Console (Messaging -> Try it out -> Send a WhatsApp message):
- Set "When a message comes in" to
https://api.trycaspianai.com/internal/providers/twilio-whatsapp/webhooks(HTTP POST), and Save. - From their own WhatsApp, send the shown
join <two-words>to the sandbox number (+1 415 523 8886) to opt in.
Then any message they send that number reaches the same handler and gets a reply.
Option 2 - Meta (branded, Embedded Signup)
A one-time Meta popup connects the developer's (or their customer's) own WhatsApp Business number - no tokens to copy or paste:
session = client.start_whatsapp_onboarding()
print("Open this to connect WhatsApp:", session["launcher_url"])
Give the developer that launcher_url. They click through the Meta popup once
and their number provisions onto the agent. Poll GET https://api.trycaspianai.com/v1/connections/{id}
until status is active (or watch for the connection.active event). If
start_whatsapp_onboarding() returns 400, Embedded Signup isn't configured on
this gateway yet - fall back to Option 1 (Twilio).
Either way, WhatsApp business rules apply: the agent can reply freely for 24h
after a user messages it; starting a conversation cold needs an approved
template. The developer's on_message code doesn't change - the gateway handles
the difference.
Adding SMS / phone (bring your own number - ask which provider)
The agent gets a real phone line for two-way SMS. It's BRING-YOUR-OWN: the developer uses their OWN Twilio or Telnyx account + a number they own there, so their account handles billing and A2P/10DLC registration - Caspian just relays. STOP and ask which provider:
SMS runs through your own CPaaS account. Which do you use - Twilio or Telnyx? You'll need your account credentials and a phone number you own there.
Twilio
Ask for: Account SID + Auth Token (console.twilio.com) and your Twilio number (E.164).
sms = client.connect_phone(provider="twilio",
account_sid="AC...", auth_token="...", from_number="+1...")
Then tell the developer to set their Twilio number's inbound "A message comes in"
webhook (Messaging config) to:
https://api.trycaspianai.com/internal/providers/twilio/webhooks/<your +E.164 number>
Telnyx
Ask for: API Key (portal.telnyx.com) and your Telnyx number (E.164).
sms = client.connect_phone(provider="telnyx", api_key="KEY...", from_number="+1...")
Then set that number's inbound webhook to:
https://api.trycaspianai.com/internal/providers/telnyx/webhooks/<your +E.164 number>
The same on_message handler answers SMS; message.reply() texts back; it can
also initiate (cold-start) a text. A 409 means that number is already connected.
Note: US outbound SMS needs A2P 10DLC (or a verified toll-free) on THEIR Twilio/
Telnyx account - that's on the developer's CPaaS account, not this gateway.
Adding iMessage (no setup - like email)
iMessage needs NOTHING from the developer - the gateway owns the number (via an AgentPhone relay, since Apple has no public API). Just connect:
imessage = client.connect_imessage()
print("iMessage number:", imessage["address"])
The same on_message handler answers over iMessage. It also supports
cold-initiate (message a number that never messaged first), unlike WhatsApp.
To verify: text the printed number from an iPhone - the running integration prints the inbound message and replies within a few seconds.
Note: this gateway shares one iMessage number, so it's one connection at a time (fine for a demo; per-agent numbers are a follow-up). A 409 / "already connected" on connect means another agent currently holds it.
Adding X / Twitter (reactive DM bot - ask which kind)
The agent runs an X account as a bot: people DM the account, the agent replies. REACTIVE only - it answers DMs it receives, never cold-DMs (X ToS). The account should be labelled "Automated" in X settings. Two ways - STOP and ask:
X connects your account as a DM bot. Two options: a) Quick (one click) - authorize with "Sign in with X"; nothing to paste. b) Branded - paste your own X app's tokens (create one at developer.x.com).
a) Quick - one-click OAuth (no tokens to paste)
result = client.install_x()
print("Authorize here:", result["authorize_url"])
Give the developer the authorize_url, or open it. It says "Sign in with X ->
authorize." IMPORTANT - tell the developer clearly:
Whatever X account you are LOGGED INTO when you click "Authorize" becomes the bot - that handle is what your users will DM. If you don't want your personal account to be the bot, create a dedicated X account first (e.g. @yourbrandbot), log into it, THEN open this link.
Once they approve, that account becomes the bot and goes active. Then just run the
same on_message handler + client.listen(). If install_x() 400s, the shared X
app isn't configured on this gateway - use option (b).
b) Branded - bring your own account tokens
Ask the developer for their app's Access Token, Access Token Secret, and numeric user id (from developer.x.com, Keys & Tokens tab; App permissions "Read and write and Direct message"). The user id is the part before the dash in the access token.
x = client.connect_x(
access_token="<Access Token>",
access_secret="<Access Token Secret>",
user_id="<numeric user id>",
)
Either way: the same on_message handler answers X DMs and message.reply()
replies in the same DM thread. Inbound arrives by polling (a few seconds' latency,
not instant) - no webhook to set up. A 409 means that X account is already
connected to another agent.
To verify: DM the connected account from any other X account - the running integration prints the inbound DM and replies within ~10 seconds. Note: X DM reads/sends are pay-per-use, so the account needs a small credit balance.
Notes
connect_email()is idempotent: calling it again (e.g. on agent restart) returns the same connection and address instead of provisioning a new inbox.connect_email()with no arguments uses a default customer/agent scope. For multi-tenant products pass explicitcustomer_idandagent_id(create them viaclient.create_customer(name)/client.create_agent(name)).client.listen()blocks forever. For custom loops pollclient.events(after_seq=cursor); events are strictly ordered byseq.- Do not reply to the same message twice;
listen()already dedupes within a run. Persist your own cursor if you need replies across restarts. - A "typing…" indicator is shown automatically while your handler runs, on
channels that support it (Discord, Telegram). No code needed; for long work
call
message.typing()again to keep it alive (it lasts ~5-10s per call). - For channels WITHOUT a typing indicator (X, SMS, email), pass
client.listen(ack="On it, one moment…")to send an instant acknowledgement reply the moment a message arrives, so the human knows the agent is working while it thinks; the real answer follows from the handler. (caspian-sdk >= 0.1.2; JS:client.listen({ ack: "On it…" }).) - REST reference: https://api.trycaspianai.com/docs
Top skills in this category
Humanizer
@biostartechnologyRemove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's comprehensive "Signs of AI writing" guide. Detects and fixes patterns including: inflated symbolism, promotional language, superficial -ing analyses, vague attributions, em dash overuse, rule of three, AI vocabulary words, negative parallelisms, and excessive conjunctive phrases.
Slack
@steipeteUse when you need to control Slack from Clawdbot via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs.
PollyReach
@pollyreachPollyReach gives every AI agent a phone number and the ability to get things done over the phone — finding contacts, making calls, and completing tasks. Just...
imap-smtp-email
@gzlicanyiRead and send email via IMAP/SMTP. Check for new/unread messages, fetch content, search mailboxes, mark as read/unread, and send emails with attachments. Supports multiple accounts. Works with any IMAP/SMTP server including Gmail, Outlook, 163.com, vip.163.com, 126.com, vip.126.com, 188.com, and vip
Answer Overflow
@rhyssullivanSearch indexed Discord community discussions via Answer Overflow. Find solutions to coding problems, library issues, and community Q&A that only exist in Discord conversations.