Bot SDK

Build self-hosted bots for Verdant.

Use the TypeScript SDK to post rich feed announcements, send clean cards into text channels, upload images through Verdant, and keep a bot online through the gateway when it needs to react to events.


Install

The SDK is published as @verdant/bot. Use it from a small process that you own: local Node, Bun, a VPS, a worker, or any host that can make HTTPS requests.

npm install @verdant/bot
# or
bun add @verdant/bot
# or
pnpm add @verdant/bot

Create a bot inside Verdant, mint a bot token, scope it to the server/feed/channel it needs, then pass the token through an environment variable.

Do not commit bot tokens. Put tokens in your host secret store or a local .env file that is ignored by git.

Quickstart

This posts one feed announcement. The card uses format() so dynamic values are escaped, while selected variables can still be styled.

import { VerdantBot, card, channel, format } from "@verdant/bot";

const bot = new VerdantBot({
  token: process.env.VERDANT_BOT_TOKEN!,
});

const release = {
  version: "0.0.252",
  title: "Desktop updater polish",
};

await bot.feeds.postAnnouncement(
  process.env.VERDANT_FEED_ID!,
  card()
    .title(format("Client {version} released", release, {
      version: { color: "success", weight: "bold" },
    }))
    .description(format("{title} is live.", release, {
      title: { color: "info" },
    }))
    .accent("success")
    .button("Discuss release", channel(process.env.VERDANT_CHANNEL_ID!)),
  { idempotencyKey: `release-${release.version}` },
);

Rendered example

Client 0.0.252 released

Desktop updater polish is live.

Discuss release

Examples

Webhook variables

Webhook payloads are just data. Pull out the fields you want, then map them into the builder. Keep static copy readable and pass dynamic values through format().

import { card, format } from "@verdant/bot";

export function releaseCard(payload: GitHubReleasePayload) {
  const values = {
    version: payload.release.tag_name,
    name: payload.release.name,
    author: payload.release.author.login,
    sha: payload.release.target_commitish.slice(0, 8),
  };

  return card()
    .title(format("Client {version} released", values, {
      version: { color: "success", weight: "bold" },
    }))
    .description(format("{name} by {author}", values, {
      name: { color: "info" },
      author: { color: "muted" },
    }))
    .accent("success")
    .table({
      columns: ["Field", "Value"],
      rows: [
        ["Version", format("{version}", values, { version: { color: "success", weight: "bold" } })],
        ["Commit", format("{sha}", values, { sha: { color: "info" } })],
        ["Author", format("{author}", values, { author: { color: "muted" } })],
      ],
    });

Rendered example

Client 0.0.252 released

Desktop updater polish by release-bot

FieldValue
Version0.0.252
Commit8f2a91c4
Authorrelease-bot

Channel cards

Feed announcements are good for long-lived posts. Text-channel cards are better for discussion, rankings, status updates, and short automation notices.

await bot.channels.postCard(
  process.env.VERDANT_CHANNEL_ID!,
  card()
    .title("Weekly ranking", { color: "purple", weight: "bold" })
    .accent("purple")
    .ranking("Top contributors", [
      { label: "Josh", value: 1280, detail: "12 commits" },
      { label: "Release Bot", value: 940, detail: "6 automations" },
    ]),
  { idempotencyKey: "rankings-week-2026-18" },
);

Images

Upload images through Verdant first, then place the returned CDN URL in the card.

const image = await bot.uploads.image(
  await Bun.file("release-badge.png").arrayBuffer(),
  { filename: "release-badge.png", contentType: "image/png" },
);

await bot.feeds.postAnnouncement(
  process.env.VERDANT_FEED_ID!,
  card()
    .title("Release badge")
    .image(image.url, "Release badge")
    .text("The image is served from Verdant's CDN."),
);

Gateway

Simple scheduled bots can use REST only. Connect to the bot gateway when your bot needs to appear online or react to events it is allowed to receive.

const ws = new WebSocket("wss://api.verdant.chat/bot-gateway");

ws.onopen = () => {
  ws.send(JSON.stringify({
    op: "IDENTIFY",
    d: {
      token: process.env.VERDANT_BOT_TOKEN,
      intents: ["FEEDS", "MESSAGES"],
      serverIds: [process.env.VERDANT_SERVER_ID],
    },
  }));
};

ws.onmessage = (event) => {
  const frame = JSON.parse(event.data);
  if (frame.op === "READY") console.log("bot online");
  if (frame.op === "DISPATCH") console.log(frame.t, frame.d);
};

setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ op: "PING", d: {} }));
  }
}, 25_000);

Component Reference

Builder APIUse
card() / announcement()Start a new card builder.
.title(text, style?)Set the card title.
.description(text, style?) / .summary()Add short intro copy below the title.
.accent(color) / .color(color)Set the card border/accent color.
.text(text, style?)Add a paragraph with markdown and inline styling.
.heading(text, level?, style?)Add a section heading.
.quote(text, style?)Add a callout block.
.bullets(items, style?)Add an unordered list.
.numbered(items, style?)Add an ordered list.
.table({ columns, rows })Add structured rows and columns.
.code(source, language?)Add a preserved code block.
.image(cdnUrl, alt?)Add a Verdant CDN image.
.divider()Add a section break.
.button(label, action, options?)Add a clickable action button.
.footer(text, style?)Add small trailing context.
.build() / .toJSON()Create the JSON payload sent to the API.

Helpers

HelperUse
format(template, values, styles?)Escape dynamic variables and optionally style selected variables.
richText(...parts)Build text from escaped plain parts and explicit styled spans.
span(text, style)Color, size, or weight a specific word or phrase.
escapeMarkdown(value)Escape untrusted text before placing it in markdown.
trustedMarkdown(text)Use text you fully control without escaping it.
themeColor(name)Resolve a named color token to a hex color.
channel(id)Create a button action that opens a Verdant channel.
externalUrl(url)Create a button action that opens an HTTP or HTTPS link.
invite(code)Create a button action for a Verdant invite code.

Styles

Named colors

Use a named token for Verdant-themed cards, or pass an exact #RRGGBB value when a brand color matters.

default#d1d5db
muted#9ca3af
success#22c55e
info#38bdf8
warning#f59e0b
danger#f43f5e
accent#14b8a6
purple#a855f7
pink#ec4899
cyan#06b6d4
card()
  .accent("success")
  .title("Release passed", { color: "success" })
  .text("Custom brand color", { color: "#7c3aed" });

Text controls

Use size values xs, sm, md, lg, xl. Use weight values normal, medium, semibold, bold.

Publishing your bot

  1. Create a bot in Verdant and keep its token in a secret store.
  2. Run your bot process anywhere that can reach https://api.verdant.chat.
  3. Use REST for posting cards and the gateway only when your bot needs live events.
  4. Use idempotency keys for webhooks, scheduled posts, and retryable jobs.