Building a Bot
Your whole integration surface is one token and one SDK. Logic lives on your server, in any shape you like — Atlas only ever sees the chat events you emit. The SDK is TypeScript, shipped compiled with full type declarations; write your bot in JavaScript or TypeScript.
-
Get a token from BotFather
/newbot, two answers, done — see BotFather. The token is the bot's chat identity, its platform-LLM access, and its notification access in one credential. It is also the only configuration a bot needs. -
Install the SDK
npm install atlas-bot-sdkCompiled ESM with full type declarations, zero runtime dependencies, Node 18+. Production endpoints are baked in — there is nothing to configure beyond the token.
-
Write the bot
import { createAtlasBot } from "atlas-bot-sdk"; const bot = createAtlasBot({ accessToken: process.env.ATLAS_BOT_TOKEN, // from BotFather — the whole config instructions: "You are a support assistant for Acme." }); bot.tools.register("lookup_order", { description: "Look up an order by id", parameters: { type: "object", properties: { id: { type: "string" } } }, handler: ({ id }) => myDatabase.orders.get(id) // your server, your code }); bot.onCommand("start", (ctx) => ctx.sendText("Hi! Ask me about your order.")); bot.onMessage(async (ctx) => { if (ctx.attachment) { // photos, documents, voice notes… const { bytes } = await ctx.attachment.download(); // …do something with it } const turn = await ctx.think(); // platform LLM + your tools if (turn.reply) await ctx.sendText(turn.reply); }); bot.start();Handlers receive a context bound to the chat:
sendText,sendChoices, the media senders,invite,createGroup,setRoomName,history(),members(),think(), the sender's verified AtlasphoneNumber, plus parsedcommand/argsandattachment.bot.dmRoom(userId)finds or creates the bot's 1:1 with someone — the dispatch primitive. The API reference lists everything. -
Run it — anywhere
ATLAS_BOT_TOKEN=… node server.jsThe loop is outbound-only, so it runs behind NAT on a laptop during development and unchanged on Railway, a VPS, or a Raspberry Pi in production. There is no webhook to expose and no endpoint to register — membership is the routing table.
Build native screens with the SDK UI libraries, then open pages or bottom sheets using existing chat buttons. The guides include import paths, types, and a complete food-ordering example.
Native app screens and wizards
Import atlas-bot-sdk/experiences to compose Atlas-native screens with ui, screen and page. Use sendExperience(ctx, definition) to send an installable experience card in the bot chat. Native headers use circular glass actions and morphing navigation; screens support photos, search, categories, forms and fixed bottom actions.
The SDK includes docs/native-experiences.md and runnable examples/native-ui/ for ride booking, parcel delivery and task tracking. The same vocabulary powers the food ordering demo. Experiences are always available; the user reviews grants and installs the pack before opening it. No developer setting is required. Ordinary chat widgets remain available on other clients.
Chat UI from your bot
Two lines turn a question into buttons — tappable chips in the app, a numbered list on older clients, and either way the answer arrives as an ordinary reply:
await ctx.sendChoices("Which order is this about?", ["Order #1042", "Order #1055"]);
// Labels and values can differ — great for commands:
await ctx.sendChoices("New case T001", [
{ label: "Claim", value: "/claim T001" },
{ label: "Pass", value: "/pass T001" }
]);
Media works in both directions — send photos, videos, voice notes, documents, contact cards, and locations; receive them via ctx.attachment. Every send also pushes a notification to the chat's humans by default (notifications: false opts out). See the API reference.
The platform LLM
ctx.think() calls the Atlas model through the platform, authenticated with the bot's token — it works out of the box with zero LLM configuration. You never hold a model-provider key, and the platform keeps per-bot attribution, quotas, the base-rules preamble, and the kill switch on its side.
- Instructions — your persona/system prompt, passed at construction. The platform enforces the base rules (identify as a bot; never claim actions without a tool result; never impersonate a person) — they aren't strippable by prompt.
- Tools — functions you register are offered to the model and executed on your server in a model→tool→model loop (max depth 6).
ctx.think({ tools })adds per-call tools that close over the current chat. The model decides when; your code decides what happens. - What the model never decides — keep contested or dangerous decisions deterministic: emergencies, assignments, money. The Doctor Finder example shows the split.
- Opting out — pass
llm: falseif your bot is fully deterministic;ctx.think()then throws instead of spending.
Reliability you don't have to build
- Downtime loses nothing. The chat history queues events; on restart, the message stream resumes from the persisted cursor and the stale guard keeps the bot from answering old mail.
- Restart safety is one file. The SDK persists the stream cursor (and DM-chat cache) to
data/bot-state.json; everything else derives from the chat timeline. - Backoff is built in. Stream errors retry with exponential backoff, capped at 30s.
- The guard kit is on by default — marker, stale, dedupe, cooldown, consecutive cap (details). Turning one off is a deliberate
guardOptionsopt-out. - Read ticks and pushes are automatic — the SDK marks handled messages read and notifies recipients per send; both are opt-out flags, not work.
Testing without the platform
Everything external is injectable. Pass a fake client and drive bot.handleSyncPayload(payload) directly with fixture events, and script the LLM the same way — your suite runs deterministically with no platform, no token, and no network.
Config files
Keep bot data in YAML next to the code — the SDK ships a dependency-free subset parser (yaml-lite): nested maps, lists, inline lists, quoted scalars, comments. The doctor roster is the worked example.