Cosmos Airdrops Chat administration bot built with Telegraf in TypeScript. Combines advanced moderation features with a unified wallet system for JUNO cryptocurrency, using an internal ledger architecture backed by SQLite.
- Four-tier role hierarchy:
owner>admin>elevated>pleb - Owner: Full control including wallet/treasury access, role promotions, bot configuration
- Admin: Moderation powers (jail, restrictions) but NO access to funds, treasury, or config
- Elevated: Basic user with wallet access, can view lists and statistics
- Pleb: Default role for all users
- Single JUNO wallet with internal ledger for all users
- Automatic deposit detection via RPC monitoring with memo-based routing
- Instant, fee-free internal transfers between users
- Secure withdrawal flow with transaction locking
- Complete audit trail of all financial operations
- See LEDGER.md for technical details
- See ADMIN_MANUAL.md for operational documentation
- Any user can create giveaways funded from their own balance
- Owners/admins can fund giveaways from treasury
- Configurable slot counts (10, 25, 50, 100 users)
- Users claim slots via inline button
- Per-giveaway escrow accounts for fund isolation
- Creators can cancel and reclaim unclaimed funds
- User Restrictions: Block specific users from stickers, URLs, media, regex patterns, or chance-based random deletes
- Global Restrictions: Apply content rules to all non-elevated users
- Jail System: Temporary mutes with configurable duration and bail payments
- Safe Regex: Timeout-protected pattern matching prevents ReDoS attacks
- See REGEX_PATTERNS.md for pattern examples
- Log user violations with configurable penalties
- Fine system with USD-to-JUNO conversion
- Bail payments for early jail release
git clone <repo-url> && cd cacmin-bot
bun install
cp .env.example .env
# Edit .env with required configuration
bun run setup-db
bun run build
bun run dev # DevelopmentRequired Environment Variables:
BOT_TOKEN: Telegram Bot API tokenOWNER_ID: Primary owner Telegram user IDBOT_TREASURY_ADDRESS: Juno wallet addressBOT_TREASURY_MNEMONIC: Wallet seed phrase (24 words)JUNO_RPC_URL: Primary Juno RPC endpoint
Telegram uses https://api.telegram.org by default. A deployment using the
local Telegram Bot API and its media gateway can set:
TELEGRAM_API_ROOT=http://tgbotapi.lxd:8081
TELEGRAM_FILE_ROOT=http://tgbotapi.lxd:8082Both values must be bare HTTP(S) origins. TELEGRAM_FILE_ROOT is optional with
the public API, but is required if getFile returns an absolute local path.
Optional MEMBER_TAGS assigns a custom member tag whenever a listed user joins
a group. Value is comma-separated <user_id>:<tag> pairs, keyed by the numeric
Telegram user id (usernames are not used, they are mutable). The bot must be a
group admin with the "Manage Tags" permission (can_manage_tags); tag values
are 0-16 characters with no emoji. Example:
MEMBER_TAGS=123456789:Neil,987654321:VIPRebuild Options: ./rebuild.sh [--dev|--quick|--full]
wget https://github.com/cac-group/cacmin-bot/releases/latest/download/cacmin-bot-dist.tar.gz
tar -xzf cacmin-bot-dist.tar.gz
sudo ./install.shThe production installer is intentionally preparation-first. It builds or
validates the release, installs root-owned code at /opt/cacmin-bot, installs
both update units, disables/stops the bot and update timer, stops any running
updater invocation, and verifies all three CACMin units are inactive. It does
not touch unrelated services. Production runs with the managed Bun binary at
/opt/bun/bin/bun; install it before running install.sh.
Configuration lives outside the code tree at
/etc/cacmin-bot/cacmin-bot.env (root:cacmin-bot, mode 0640). Runtime data
and file logs remain service-owned under /opt/cacmin-bot/data and
/opt/cacmin-bot/logs. At the approved cutover, activate the prepared service
and updater explicitly:
sudo ./install.sh --activateFor the tgbotapi.lxd deployment, add the local API and media gateway origins
to /etc/cacmin-bot/cacmin-bot.env:
TELEGRAM_API_ROOT=http://tgbotapi.lxd:8081
TELEGRAM_FILE_ROOT=http://tgbotapi.lxd:8082The API root handles Bot API calls. The file root handles media whose getFile
response contains an absolute server path; CACMin sends that path only as an
encoded gateway query parameter. Do not expose the media gateway outside the
trusted service network.
Preparation warns if an existing environment file has insecure metadata but never prints its contents. Activation refuses a missing environment file and normalizes then verifies its ownership and mode before enabling either unit.
The installer refuses its production paths on hosts other than tgbot. The
update timer executes the root-owned
/usr/local/libexec/cacmin-bot-auto-update, never a script writable by the bot
service. Timer runs follow the rolling latest release. Tag-triggered GitHub
deployments pass that exact tag with --release TAG --force, so production
installs the artifact published by the triggering workflow rather than a newer
or older latest artifact. A read-only release check does not call systemd or
change the install:
sudo /usr/local/libexec/cacmin-bot-auto-update --checkSee DEPLOYMENT.md for complete deployment documentation.
bun install && bun run build
sudo ./install.shWhen INDEXER_ENABLED=true, cacmin-bot is the single live writer for the shared telegram-chat-explorer dataset. Point INDEXER_DB_PATH at the active explorer SQLite DB, set INDEXER_DATASET_ID to the matching dataset ID, and set GROUP_CHAT_ID to the Telegram supergroup ID. The museum/API service must use LIVE_INDEX_ENABLED=false; running two Telegram consumers or two SQLite writers during migration can lose updates or double-handle bot actions.
For the CAC Museum deployment, keep INDEXER_EMBEDDINGS_ENABLED=false and use INDEXER_EMBED_TRIGGER_FILE=/opt/telegram-chat-explorer/state/embed-missing.trigger. After every INDEXER_EMBED_TRIGGER_BATCH_SIZE eligible text/caption inserts, the bot touches that trigger so teleindexer-embed-missing.path can run the explorer embedding worker and keep pgvector in sync.
The indexer also maintains the explorer dataset counters used by Grafana metrics (dataset_meta and live_index_state). Duplicate Telegram message IDs return before these counters are advanced.
Live author identity, ID-backed mentions, edits, and message_reaction old/new state are written into the explorer's immutable-user and interaction queue tables. Each distinct active reaction from the same user counts separately; repeated Telegram delivery is idempotent and reaction removal deletes only the removed active reaction. The greatest Telegram update_id is persisted per message/reactor so delayed or replayed older transitions cannot restore stale active-reaction state.
On tgbot, the cacmin-bot account owns only its mutable data/log directories
and joins teleindexer-data to write the shared explorer database, downloaded
media, and embedding trigger. Application code remains root:root and is
read-only to the service. The unit's write allowlist is limited to those own and
shared paths.
Install and updater replacements move the previous code into
/opt/cacmin-bot/.rollback/ before replacing it. If an updated active service
does not start, the updater restores the previous code and attempts to restart
that version. Rollback is armed before the first code move; partial move or
activation failure restores every displaced entry. A failed activation leaves
the bot, updater, and timer inactive and leaves the bot and timer disabled, so
a reboot cannot create a second writer. For manual recovery, stop
cacmin-bot.service, restore one
rollback directory's dist, node_modules, package.json, bun.lock, and
version.txt, confirm they are root:root without group/other write bits, then
start the service and verify the writer counters and a read-only wallet/ledger
status command before reopening moderation traffic.
Database recovery is separate from code rollback. Never replace
/opt/cacmin-bot/data/bot.db while the service is running. Restore the database
and its WAL/SHM-consistent backup as one maintenance operation, then verify the
ledger, moderation state, and explorer writer freshness before starting the bot.
sudo systemctl status cacmin-bot
sudo systemctl restart cacmin-bot
sudo journalctl -u cacmin-bot -fUse /help in a DM with the bot for the interactive, role-based reference. The menu is keyboard-navigated: categories split into subcategories, and each section renders with a back button to the parent menu. Content is filtered by role, so sensitive topics and functions (treasury operations, wallet tests, moderation tools, and the like) are only visible to the tiers that may use them. The list below is the full current slash-command surface.
/help- Open the DM help menu/menu- Open the DM button menu (Wallet, Shared Accounts, Moderation, Lists, Roles, Statistics, Help)/wallethelp- Show detailed wallet and treasury help
/balanceor/bal- View your internal JUNO balance/deposit- Get your deposit address and required memo/verifydeposit <txhash>- Verify and credit a deposit by transaction hash/withdraw <amount> <junoAddress>- Withdraw JUNO to an external wallet/send <amount> <recipient>or/transfer <amount> <recipient>- Send to@username, user ID, orjuno1...address/transactions [@user|userId]or/history [@user|userId]- View your recent transactions; owners can target another user/verifydeposit <txhash>- Verify and credit your own deposit by transaction hash/claimdeposit <txhash>- If a deposit was missed, verify it on-chain and credit the user id in the memo/checkdeposit <txhash>or/checktx <txhash>- Check whether a deposit was already processed/unclaimeddeposits- View the current UNCLAIMED deposit pool and recent invalid-memo deposits/fundtreasury <amount>- Move funds from your balance into the game treasury/fundtreasury deposit- Get external deposit instructions for the game treasury/treasurybalanceor/gamebalance- View the game treasury balance (admin+)/contributetreasury <amount>- Contribute to the treasury from your balance (admin+)/withdrawtreasury <amount>- Withdraw treasury funds back to your balance (owner)/treasurytransfer <@user|userId> <amount>- Send treasury funds to a user's balance (owner)/treasurywithdraw <amount> <address>- Withdraw treasury funds to an external Juno address (owner)/treasuryreserve <amount> <to|from>- Move funds between the treasury and reserve accounts (owner)/treasurydetails- View full treasury details: address, balances, and recent transactions (owner)
/createshared <name> <display_name> [description]- Create a shared account; quote multi-word display names or descriptions (elevated+)/listshared- List all shared accounts (elevated+)/myshared- List the shared accounts you can access/sharedbalance <account>- View a shared account balance/sharedinfo <account>- View account metadata and member permissions/sharedsend <account> <@username|user_id> <amount> [description]- Send from a shared account to another bot user/shareddeposit <account> <amount>- Move funds from your balance into a shared account/sharedhistory <account> [limit]- View shared-account transaction history/grantaccess <account_name> <@username|user_id> <level> [spend_limit]- Grant shared-account access/revokeaccess <account_name> <@username|user_id>- Remove shared-account access/updateaccess <account_name> <@username|user_id> <level> [spend_limit]- Change a member's access level/deleteshared <account>- Delete an empty shared account
/mystatus- View your role, warnings, jail status and bail, activity stats, and unpaid fines/jails- View currently jailed users/violations- View your violation history/viewwhitelist- View whitelisted users/viewactions- View active global restrictions/ratelimit [user]- View your 15-minute, 1-hour, and 24-hour character usage, including rollover capacity (admins may pass a user to view another user's usage)/ratelimits- Explain rate-limit windows, rollover, enforcement, and paid resets/ratelimitreset [user] <15m|1h|24h>- Purchase a JUNO reset for one usage window/verifyratelimitreset <txhash>- Verify a purchased rate-limit reset/listadmins- View all elevated, admin, and owner users (elevated+)/jailstats [@user|userId]- View active jail info or a specific user's jail history (elevated+)
Rate limits are opt-in per-user moderation controls. Configure a user's 15-minute base
character budget with /setratelimit <user> <15m_chars> (admin+). The default derived
budgets are 2x the base for one hour and 4x the hourly budget for 24 hours. Emoji count as
2 characters, stickers as 5 characters, and a shared image (photo or file) as 25 characters.
Each window can carry unused capacity into only its immediately following matching window. Current-period capacity is consumed first; rollover never compounds. A message that would exceed any active window is deleted, the user's current Telegram permissions are captured, and the user is muted until the relevant window resets. The scheduled cleanup restores the captured permissions without overriding an active jail.
Use /ratelimit for current usage and /ratelimits for a concise explanation. Users can
purchase a reset with /ratelimitreset <15m|1h|24h> in a DM or group. A reset request may
target another configured user, so anyone can pay the applicable JUNO fee. Reply to the
payment instructions with the transaction hash, or send it in a DM. The bot verifies an
exact successful transfer to the treasury and rejects transaction-hash reuse before clearing
the selected window.
/cac- Send the first sticker from the CACGifs pack/sendsticker [name]- Send a sticker from the CACGifs pack by name (currently onlyfirstis configured;/sendsticker firstmatches/cac)/getsticker- Reply to a sticker to get itsfile_id
/giveaway <amount>- Create an open giveaway funded from your balance/cancelgiveaway [id]- Cancel one of your active giveaways/roll <amount>- Play the roll game/rollstats- View your roll-game statistics/rollodds- View roll-game rules and probabilities/duel <@username|userId> <amount>- Challenge another user to a wagered duel/duelstats- View your duel stats/duelhistory [limit]- View your recent completed duels/duelcancel- Cancel your pending outgoing duel
/paybail [<@username|userId>]- Show the Jailing ID, bail amount, and payment address; reply/paybailto a jailed user's message to pay for them/verifybail <txhash>- Verify a bail payment (the tx memo must contain the Jailing ID) and release the user/bailhelp- Get complete bail payment and verification details in a DM/payfines,/payfine- Legacy aliases that show the same bail instructions/verifypayment <txhash>- Legacy alias for/verifybail
/jail <@username|userId> <minutes> ["reason"]or/silence <@username|userId> <minutes> ["reason"]- Jail a user for a fixed duration, optionally recording a quoted reason (admin+)/unjail <@username|userId>or/unsilence <@username|userId>- Release a jailed user (admin+)/warn <@username|userId> <reason>- Issue a warning and violation (admin+)/addrestriction <@username|userId> <type> [action] [until] [severity] [threshold] [jailDuration]- Add a user restriction (admin+)/listrestrictions <@username|userId>- View a user's restrictions (elevated+)/removerestriction <@username|userId> [type]- Remove one restriction, or omit[type]to remove them all (elevated+)/clearrestrictions <@username|userId>- Remove all restrictions from a user (elevated+)/bangif [@username|userId] [gif_id] [-g]- Ban a GIF: reply to it, or name a user and id; add-gfor a global ban (admin+)/getgifid- Reply to a GIF to get itsfile_unique_id(admin+)/regexhelp- Show regex restriction examples (admin+)/addaction <restriction> [restrictedAction]- Add a global restriction (admin+)/removeaction <restriction>- Remove a global restriction (admin+)/setratelimit <user> <15m_chars>- Configure a user's base character limit; 1-hour is 2x and 24-hour is 4x the hourly limit. One period of unused capacity rolls over without compounding (admin+)/listratelimits- List every user with an active rate limit and how much of each window remains (admin+)/clearratelimit <user>- Remove a user's character rate limit (admin+)/resetratelimit <user>- Clear a user's accumulated rate-limit usage and any active rate-limit mute without changing their configured limits (admin+)/addwhitelist <@username|userId>and/removewhitelist <@username|userId>- Manage whitelist entries (admin+)
/processdeposit <txhash> [userId|@username]- Recover or allocate any deposit: the memo user by default, or a target user for an unclaimed deposit (admin+)/botbalance- View the bot's on-chain wallet balance (owner)/treasury- View treasury and ledger status (owner)/walletstats- View wallet and reconciliation stats (owner)/reconcile- Force a reconciliation check (owner)/adjustbalance <amount> <debit|credit> [reason]- Correct ledger discrepancies by adjusting the bot treasury balance (owner)
/setowner- Register yourself as owner if your Telegram ID is already configured inOWNER_ID/OWNER_IDs/grantowner <@username|userId>- Grant owner privileges to another user (owner)/makeadmin <@username|userId>- Promote a user to admin (owner)/elevate <@username|userId>- Promote a user to elevated (admin+)/revoke <@username|userId>- Demote an elevated or admin user (admin+)
/payallfines <@username|userId>- Release a jailed user by covering their bail from the system reserve (admin+)/junoprice- View the current JUNO price (owner)/clearviolations <@username|userId>- Clear all violations for a user (owner)/crawlidentities [count|reset]- Backfill missing usernames and profiles for known members (owner)/stats- View overall bot statistics (owner)
/listspamreacts- View active custom spam-reaction patterns plus built-ins (admin+)/spamreacthelp- Show the spam-reaction field and pattern guide (admin+)/addspamreact [pattern] [bio|channel|both]- Add a custom spam-reaction pattern (owner)/removespamreact <id>- Remove a custom spam-reaction pattern (owner)/testspamreact <pattern> <sample>- Test a spam-reaction pattern without saving it (owner)
/listidentityblocks- View active name and username block patterns plus built-ins (admin+)/identityblockhelp- Show the identity-block field and pattern guide (admin+)/addidentityblock <pattern> [name|username|both]- Add a custom identity block pattern (owner)/removeidentityblock <id>- Remove a custom identity block pattern (owner)/testidentityblock <pattern> <sample>- Test an identity block pattern without saving it (owner)
src/
bot.ts # Entry point, handler registration, periodic tasks
config.ts # Environment configuration
database.ts # SQLite schema and query functions
commands/ # Command handlers by feature
giveaway.ts # Giveaway creation and management
wallet.ts # Balance, deposit, withdraw, send
moderation.ts # Jail, restrictions, warnings
roles.ts # Role management
handlers/ # Feature-specific handlers
callbacks.ts # Inline keyboard callback handlers
restrictions.ts # Content filter logic
services/ # Business logic layer
ledgerService.ts # Internal balance operations
unifiedWalletService.ts # On-chain wallet operations
jailService.ts # Jail/bail management
transactionLockService.ts # Concurrency control
middleware/ # Request pipeline
auth.ts # User identification
permissions.ts # Role-based access control
messageFilter.ts # Content restriction enforcement
utils/ # Shared utilities
precision.ts # Cryptocurrency math (6 decimals)
safeRegex.ts # Timeout-protected regex
keyboards.ts # Inline keyboard builders
- Service-oriented: Stateless services with static methods
- Double-entry accounting: Every transaction creates balanced ledger entries
- Transaction locks: Prevent double-spending during withdrawals
- Escrow accounts: Per-giveaway fund isolation
- Memo decoding: Canonical
decodeTxRawmemo extraction for deposit allocation
See LEDGER.md for detailed token flow documentation.
bun run test # Run all tests
bun run test:watch # Watch mode
bun run test:coverage # Coverage report
bun run test tests/unit # Unit tests only
bun run test tests/integration # Integration tests only- LEDGER.md - Internal ledger and token management system
- ADMIN_MANUAL.md - Operational guide for administrators
- DEPLOYMENT.md - Production deployment instructions
- REGEX_PATTERNS.md - Content filter pattern examples
- CLAUDE.md - Development guidelines and codebase reference
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Make granular commits with clear messages
- Open a pull request
MIT License. See LICENSE for details.