spicyapiDocs
Main content

Telegram and Discord bot

Run your own private bot that makes images and videos, animates pictures and chats with characters you define, paid from your SpicyAPI balance with spending limits checked before every charge.

The SpicyAPI bot is a small program you run yourself. It connects to Telegram (and, if you want, Discord) and lets the people you allow generate images, make videos, turn images into videos and chat with characters you define. Everything is paid from your own SpicyAPI balance, and every request is priced before anything is charged.

The bot is free and open source (MIT) at github.com/spicyapi-ai/spicyapi-bot. It is private by default: only you can use it until you allow other people or groups.

The short version

  1. In the console, create an API key just for the bot and give it a daily spend cap.
  2. In Telegram, message @BotFather, send /newbot and copy the token.
  3. Clone the repository, copy .env.example to .env, fill in SPICY_API_KEY, TELEGRAM_BOT_TOKEN and OWNER_ID, then run docker compose up -d --build.
  4. Open your bot in Telegram, send /start, accept the rules and try /pic a cozy cabin in the snow.

What the bot does

  • Images: /pic a lighthouse at dusk
  • Videos: /vid waves rolling onto a beach at sunset
  • Animate: tap Animate under any image the bot made, or reply to it with /animate <how it should move>.
  • Character chat: type in a private chat, or use /talk. Replies stream into one message.
  • Spend protection: each request is priced first (quotes are free), expensive ones need a tap on Confirm, and per-person and whole-bot daily limits are enforced.
  • Private by default: nobody else can use it until you allow them.

SpicyAPI's catalogue includes uncensored models that add no content filter of their own, and the bot works with any model you pick. The law and SpicyAPI's content policy still apply to everything you generate. See Policy.

Commands

CommandWhat it does
/pic <prompt>Generate an image
/vid <prompt>Generate a video from text
/animate <motion>Reply to an image to turn it into a video. Images the bot made always work; your own photos only if the operator set ALLOW_PHOTO_UPLOADS=true
Animate buttonUnder every generated image: animates it, reusing its prompt
/talk <message>Talk to the current character (in a private chat you can simply type)
/personaChoose a character
/newchatForget the conversation and start over
/modelsChoose your image, video, animate and chat models
/spendWhat you have spent today; the owner also sees the whole bot and the account balance
/report <text>Send a report to the operator
/helpSummary of all of this

The names are deliberately not /image, /video or /chat, which other popular bots use, so this bot can share a group with them. In a Telegram group, reply to one of the bot's chat messages to keep talking to the character.

Before you start

  • A computer or server that stays on, with Docker installed. (Or a Railway account, see below.)
  • A SpicyAPI account with a balance. See Account setup and Billing.
  • A Telegram account, to create the bot with BotFather.

Deploy with Docker Compose

Create a key for the bot

In the console, create a new API key just for this bot. Give that key a daily spend cap and, if you like, a list of allowed models.

The bot has its own limits too, but the key's cap is enforced by SpicyAPI itself, so it still holds if the bot is misconfigured or its data is lost. New keys have no cap unless you set one. See Authentication.

Create the Telegram bot

Open @BotFather, send /newbot, pick a name and a username, and copy the token it gives you. It looks like 123456789:AA....

Configure

git clone https://github.com/spicyapi-ai/spicyapi-bot.git
cd spicyapi-bot
cp .env.example .env

Open .env and fill in the three required lines:

SPICY_API_KEY=sk-spicy-...
TELEGRAM_BOT_TOKEN=123456789:AA...
OWNER_ID=your-telegram-user-id

Every other line has a safe default. .env.example explains each setting.

Find your Telegram user ID (if you do not know it)

Put OWNER_ID=0 for now and finish the next step. Then send your bot any message and it replies with your ID. Put that number in OWNER_ID and restart:

docker compose up -d

Start

docker compose up -d --build
docker compose logs -f

The log shows Telegram bot @yourbot is running when it is ready.

Try it

Open your bot in Telegram, send /start, accept the rules once, and send /pic a cozy cabin in the snow. The default image model costs about $0.01 per image.

To update later, run git pull && docker compose up -d --build.

Deploy on Railway

Railway can run the bot without a server of your own. There is no one-click template: you deploy from your own copy of the repository.

Create the project

In Railway choose New Project > Deploy from GitHub repo and pick your fork. Railway finds the Dockerfile and builds it.

Add the variables

Open the service's Variables tab and add SPICY_API_KEY, TELEGRAM_BOT_TOKEN and OWNER_ID, plus any other setting from .env.example.

Attach a volume

Right-click the service, choose Attach volume and use the mount path /data. Without a volume, every redeploy forgets today's spending and everyone's settings.

Deploy

Deploy and check the logs for Telegram bot @yourbot is running.

Keep the service at one replica: two copies polling the same Telegram token fight over updates. The bot needs no public domain and no open port.

Add Discord (optional)

Create the application

In the Discord Developer Portal, create an application, open Bot, reset and copy the token, and turn Public Bot off so only you can add it to servers. No privileged intents are needed.

Invite it to your server

Under OAuth2 > URL Generator, tick the scopes bot and applications.commands, and the permissions Send Messages, Attach Files, Embed Links and Read Message History. Open the generated link and add the bot to your server.

Configure

In .env, set:

  • DISCORD_TOKEN to the bot token.
  • DISCORD_GUILD_ID to your server ID, if you want the commands to appear instantly instead of after a short global rollout.
  • OWNER_ID to include your Discord user ID, for example OWNER_ID=tg:123,dc:456. The tg: and dc: prefixes pin an ID to one platform.
  • ALLOWED_CHAT_IDS to the channel or server ID where the bot should answer.

To find IDs, turn on Developer Mode (User Settings > Advanced), then right-click a user, channel or server and choose Copy ID. Restart the bot.

Mark the channel age-restricted

Images and videos only work in age-restricted channels: Edit Channel > Overview > Age-Restricted Channel. The /pic and /vid commands are registered as age-restricted, so Discord only shows them there.

Chat (/talk, or mentioning the bot) works in any allowed channel unless you set DISCORD_CHAT_REQUIRES_NSFW=true, which limits chat to age-restricted channels too.

Discord does not allow apps with age-restricted commands in its App Directory, so keep the bot private. .env.example also has DISCORD_MEDIA_IN_DMS, which is false by default: images and videos then work only in age-restricted server channels, not in direct messages.

Who can use it

SettingDefaultEffect
OWNER_IDrequiredYou. Always allowed, not bound by the per-person daily limit, and the only one who sees the account balance. Separate several IDs with commas.
ALLOWED_USER_IDSemptyOther people allowed in private chats. Empty means only you.
ALLOWED_CHAT_IDSemptyGroups (Telegram) or channels and servers (Discord) where the bot answers. Empty means no groups at all.
REQUIRE_CONSENTtrueEveryone confirms once that they are 18+ and will follow the content rules.

Strangers who find the bot are told it is private and given their user ID to send you. Groups are ignored until listed in ALLOWED_CHAT_IDS; once a group is listed, every member of it can use the bot, within the daily limits. Telegram group IDs are negative, for example -1001234567890.

Do not set ALLOWED_USER_IDS=*

ALLOWED_USER_IDS=* makes the bot public and lets anyone who finds it spend your balance. Only use it if you mean to.

You operate this bot and are responsible for who you let use it. SpicyAPI's terms apply, including the regions SpicyAPI does not serve.

How spending works

  1. Price first. Each image or video is quoted by SpicyAPI before anything happens. Quotes are free.
  2. Hard ceiling. If a request could cost more than MAX_COST_PER_REQUEST_USD (default $1), it is refused. Nothing is charged.
  3. Confirmation. If it could cost more than CONFIRM_ABOVE_USD (default $0.10), the bot shows the price with Confirm and Cancel buttons. Only the person who asked can confirm.
  4. Daily limits. Each person may spend USER_DAILY_BUDGET_USD (default $1) per UTC day and the whole bot DAILY_BUDGET_USD (default $5). Generations still running count as spent until they finish, so several requests at once cannot slip past a limit. Owners are only bound by the whole-bot limit.
  5. No double charges. Each request carries an idempotency key derived from the chat message or button tap, so a duplicated Telegram update or a double tap returns the task that already exists.
  6. Results. Progress is shown by editing one message every few seconds. The result shows the actual charge and the task ID, which you can look up under Logs in the console.

An accepted generation cannot be cancelled. Successful runs are charged; ordinary failures release the hold. Certain content refusals are charged only when the model page explicitly says so. See Billing.

Use a capped key

The bot keeps its own ledger in DATA_DIR/state.json. If that file is lost, the bot forgets what was spent today. The cap on the API key in the console is the limit that can never be lost, because SpicyAPI enforces it. Create a separate key for the bot, set a daily spend cap, and optionally allow only the models you want. When the cap is reached, the bot tells you the key has reached its spend cap.

Other useful settings:

SettingDefaultWhat it does
MAX_CONCURRENT_TASKS_PER_USER2Images and videos one person may have running at the same time
VIDEO_SECONDS / VIDEO_RESOLUTION5 / 480pVideo length and size, snapped to what each model allows. Shorter and lower is cheaper
ALLOWED_MODELSemptyLimit the /models menus. Exact IDs or prefixes ending in *, for example alibaba/*

Models and prices

Each person can change their models with /models. The operator sets the defaults:

VariableDefaultCost at the time of writing
DEFAULT_IMAGE_MODELZ-Image Turbo, text to imageAbout $0.01 per image
DEFAULT_VIDEO_MODELMiniMax H3, text to videoAbout $0.037 per second at 768p
DEFAULT_ANIMATE_MODELMiniMax H3, image to videoAbout $0.037 per second at 768p
DEFAULT_CHAT_MODELDeepSeek V4.1 FlashAny chat model in the catalogue

The exact model ids are in .env.example; to switch, copy another id from the live catalog (in the character example below, MODEL_ID_FROM_CATALOG marks where such an id goes). Prices are from the public catalogue and can change; the bot always shows the live quote. Browse models at spicyapi.ai/models or see Models. For role-play, the roleplay leaderboard helps you choose a chat model.

Characters

Characters live in a JSON file (CHARACTERS_FILE, default characters/default.json, which has two example characters):

{
  "characters": [
    {
      "id": "mira",
      "name": "Mira",
      "system": "You are Mira, ... Never speak or act for {{user}}.",
      "greeting": "Welcome aboard, {{user}}!",
      "model": "MODEL_ID_FROM_CATALOG"
    }
  ]
}
  • system is the character's instructions. greeting (optional) is their first line. model (optional) pins a chat model for this character.
  • {{char}} and {{user}} are replaced with the character's name and the person's first name.
  • Entries can also be Character Card V2 JSON, the format most character sites and SillyTavern export. The description, personality, scenario, example dialogue and first message are used.

With Docker, mount your own file and point the setting at it. In docker-compose.yml:

volumes:
  - ./my-characters.json:/app/characters/custom.json:ro

And in .env:

CHARACTERS_FILE=characters/custom.json

The bot remembers HISTORY_TURNS exchanges per conversation (default 20) and limits replies to CHAT_MAX_TOKENS tokens (default 2048). Reasoning models count their thinking toward that limit, so a very small value can leave no room for the answer. See Chat recipes for more on writing characters.

Photo uploads are off by default

By default, only images the bot generated can be animated. Turning photos of real people into videos is the main way bots like this get abused, and a person in a photo has not agreed to appear in your video.

If you set ALLOW_PHOTO_UPLOADS=true, anyone you allow can animate a photo they send. Only turn it on for people you trust, and only for photos of adults who agreed. When it is on, uploaded images are downloaded by the bot and stored with SpicyAPI. Telegram file links are never passed on, because they contain your bot token.

Privacy: what is stored where

WhatWhereHow long
Daily spend totals, each person's model and character choice, who accepted the rulesstate.json in DATA_DIR (a volume in Docker)Until you delete it
Chat historyMemory onlyForgotten on restart, after a day of inactivity, or with /newchat
Bot logsYour machineTask IDs and amounts only, never prompts or media
Prompts and resultsSpicyAPIYour account's retention period; see Retention

Set RETENTION_SECONDS to ask SpicyAPI to delete results and prompts sooner than your account default.

In groups, results are hidden behind a spoiler (SPOILER_MEDIA, default groups; other values are always and never). /report forwards a message to the owner. Content that breaks SpicyAPI's rules can also be reported at spicyapi.ai/abuse.

Troubleshooting

Message or symptomWhat to do
"out of balance" (40201)Top up your SpicyAPI balance.
"reached the spend cap" (40202)The key's cap in the console was reached. Raise it or wait.
"not allowed to use that model" (40301)Allow the model on the key in the console, or pick another with /models.
"has not verified its email" (40310)Open the verification link from SpicyAPI.
"The model declined this request"That model refused the prompt. Adjust the request or choose another model. Check the model page’s refusal billing terms.
Conflict: terminated by other getUpdates requestTwo copies of the bot use the same token. Stop one.
Discord commands missing/pic and /vid only show in age-restricted channels. Global commands can take a moment to appear; set DISCORD_GUILD_ID for instant registration.
Network errors behind a proxyNode's fetch ignores HTTPS_PROXY unless the bot starts with NODE_USE_ENV_PROXY=1.
The bot replies with a number when you message itThat is your user ID. Put it in OWNER_ID and restart.

Error codes are explained in Errors.

On this page