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}` },
);
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
| Field | Value |
|---|---|
| Version | 0.0.252 |
| Commit | 8f2a91c4 |
| Author | release-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 API | Use |
|---|---|
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
| Helper | Use |
|---|---|
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.
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
- Create a bot in Verdant and keep its token in a secret store.
- Run your bot process anywhere that can reach
https://api.verdant.chat. - Use REST for posting cards and the gateway only when your bot needs live events.
- Use idempotency keys for webhooks, scheduled posts, and retryable jobs.