/holders
`/holders` replies with the live holder count of one token, configured per bot from the dashboard. It answers in a group, in a group-admin chat, and in a…
/holders
/holders replies with the live holder count of one token, configured per bot from the dashboard. It answers in a group, in a group-admin chat, and in a DM with the bot — only the Telegram command menu is scoped by chat type.
Setup
Turn /holders on, in order
- Open Settings → Features in the dashboard — the page itself is titled Bot Utilities.
- Find the
/holderscard and turn on its toggle. - With no contract address saved yet, the toggle opens the Configure Holders modal instead of switching the card on directly.
- Pick the token's chain from the Blockchain dropdown — 8 chains, listed below.
- Enter the token's contract address under Contract Address. The field's placeholder text changes with the chosen chain.
- Tap Enable /holders — disabled until the address matches the selected chain's address format.
- Send
/holdersin the chat.
Once an address is saved, a pencil button on the /holders card reopens the same modal, titled Edit Holders Config in this mode, to change the chain or address without turning the feature off first.
Chains and holder sources
/holders counts on 8 of the 9 chains the bot otherwise recognises for contract addresses (ca.CA_SUPPORTED). Each chain races its own list of free, keyless sources at once; the first to answer, in the order below, wins.
The 8 chains, in holders.py's SOURCES order
| Chain | Holder-count sources, in order | Address placeholder |
|---|---|---|
| Ethereum | GoPlus → Blockscout → CoinGecko | 0x… token contract |
| BNB Chain | GoPlus → CoinGecko | 0x… token contract |
| Base | GoPlus → Blockscout → CoinGecko | 0x… token contract |
| PulseChain | Blockscout | 0x… token contract |
| Robinhood Chain | CoinGecko → GoPlus | 0x… token contract |
| Solana | GoPlus → Jupiter → CoinGecko | Token mint address |
| TON | tonapi → CoinGecko | Jetton master EQ… |
| Sui | GoPlus → CoinGecko | 0x…::module::COIN |
HyperEVM:
—The dashboard's Blockchain dropdown never lists it, and
/holderscannot be pointed at a HyperEVM token.
PulseChain has no CoinGecko fallback
Every other chain above falls back to CoinGecko when its free sources don't answer. PulseChain has no
coingeckoentry inSOURCES— Blockscout is its only source, and that fallback is never attempted.
All of a chain's configured sources are queried at once. The first source in preference order to answer within 2 seconds wins; if none has by then, the reply waits for whichever source answers first, up to 6 seconds total (PREFER_WAIT_S / HARD_WAIT_S, holders.py).
Address validation
The Contract Address field is checked against the selected chain's address family before Enable /holders or Save Changes unlocks, and re-checked on the server when the dashboard saves it.
Address shape per chain family
| Family | Chains | Shape |
|---|---|---|
| EVM | Ethereum, BNB Chain, Base, PulseChain, Robinhood Chain | 0x + 40 hex characters |
| Solana | Solana | a base58 public key (32 bytes decoded) |
| TON | TON | EQ or UQ + 46 characters |
| Sui | Sui | a coin type, 0x<hex>::module::Type — not a plain address |
Validation error text
| Condition | Message |
|---|---|
| No chain picked yet | Pick the token's chain first |
| Address fits a different chain's family | That looks like {an EVM|a Solana|a TON|a Sui} address, not {ChainName} |
| Address fits no known family | Not a {ChainName} token address |
The dashboard's save endpoint re-checks the address on the way in — 256 characters or fewer, matching [a-zA-Z0-9._:-]+ — and rejects any chain outside the 8 above with Unsupported chain. Supported: ethereum, bsc, base, pulsechain, robinhood, solana, ton, sui.
The reply
Every /holders reply is sent as a reply to the triggering message, with notifications suppressed.
Every outcome
| Case | Reply |
|---|---|
| Configured; a source answers | 📊 Holders: 12,483 |
| Configured; every live source fails but a cached count exists | 📊 Holders: 12,483 (cached) |
| Enabled, but the chain/address pair fails validation | 📊 Holders not configured yet. |
| Enabled; every source fails and nothing is cached | 📊 Holder data temporarily unavailable. Try again later. |
| Feature toggle off | No reply — silent |
Holders: is sent bold (parse_mode=HTML); a cached answer appends (cached) in italics after the number. Both failure texts above are plain, unformatted strings, and both also delete the /holders command message a short time later — the code does not fix that delay at one number, so none is given here.
A resolved count is cached 15 seconds per chain-and-address pair and single-flighted: overlapping /holders sends inside that window share one fetch instead of re-querying every source.
When /holders is visible
A disabled
/holdersanswers nothingWith the toggle off, sending
/holdersproduces no reply and no error — the message is consumed rather than left for anything else to answer.
/holders is added to Telegram's own command menu only once it is both enabled and pointed at a chain-and-address pair that passes validation. It's added to the group menu and the group-admin menu (all_group_chats, all_chat_administrators); it is never added to the private-chat menu (all_private_chats), including the owner's own DM.
It still works in a DM
The route itself runs at scope
any— every chat type. Sending/holdersdirectly in a DM with the bot answers exactly as it does in a group; only the menu suggestion is scoped to groups and admins.
Groups and access
A group is authorized for a bot when its chat id sits in `allowedGroups`, in the bot's `admin_groups`, or is inherited from a migrated ancestor chat.…
Custom commands
Text commands and button commands are built from the same modal, in the dashboard's **Commands** tab. Whether a saved command becomes one or the other is…