Build a Telegram token scanner bot for Robinhood Chain
Build a Telegram bot that scans Robinhood Chain tokens: price, liquidity, holders, taxes and a security score from two Crius API calls, with real examples.
When a message contains a token address, call GET /v1/robinhood/token/{address} (price, market cap, liquidity, holders, launchpad) and GET /v1/robinhood/token/{address}/security (score, risk, flags, taxes, honeypot) in parallel and reply with a summary; for a name, call /search first and let the user pick. Two credits per scan, and both calls work without a key.
A Telegram token scanner is a bot that answers “is this token OK?” in a chat: someone pastes a Robinhood Chain token address, and the bot replies with its price, market cap, liquidity, holders, launchpad, taxes and a security verdict. With the Crius API that’s two calls per scan, the token profile and security checks, plus search when someone types a name instead of an address. All three are on the anonymous plan, so the bot runs without a key until it gets busy.
Architecture
-
Telegram side. Long-poll
getUpdatesfrom the Telegram Bot API (no webhook or server needed) and reply withsendMessage. Create the bot with @BotFather to get its token. -
Parse the message. An address (
0xand 40 hex characters) is scanned directly. A word after/scanis a name: search for it and reply with the matches. -
Scan. Call the token profile and security in parallel, then format one message. Cache results for a minute so ten people pasting the same token in a group cost one scan.
-
Optional feed. A second process listens to
launches:allon the stream and posts new tokens that pass the scan to a channel.
Endpoints used: GET/token/{address} GET/token/{address}/security GET/search
Names are not unique: search, then pick
Three tokens called Housecoin
A user types /scan housecoin. Which token do they mean?
curl "https://api.crius.sh/v1/robinhood/search?q=housecoin&limit=3"const res = await fetch("https://api.crius.sh/v1/robinhood/search?q=housecoin&limit=3");const { data, meta } = await res.json();import requests
r = requests.get( "https://api.crius.sh/v1/robinhood/search?q=housecoin&limit=3",)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 73 lines (arrays trimmed)
{ "data": [ { "address": "0x578dfaf61257ea702b8834bb037740a6368947bd", "label": null, "launchpad": null, "launchpad_name": null, "links": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": null, "website": null }, "liquidity_usd": 22978.7, "logo": null, "name": "Housecoin", "price_usd": 0.0000846805, "symbol": "HOUSECOIN", "type": "token" }, { "address": "0x34c21f10ea7ec052ed878d157f40fb4160730b24", "label": null, "launchpad": "pons", "launchpad_name": "Pons", "links": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": "https://x.com/aryonserwin/status/2093942789784637480", "website": "https://fs.blog/knowledge-project-podcast/vlad-tenev/?utm_source=chatgpt.com" }, "liquidity_usd": 4978.47, "logo": "https://api.crius.sh/v1/robinhood/logo/0x34c21f10ea7ec052ed878d157f40fb4160730b24.png?size=128", "name": "Housecoin", "price_usd": 0.00000374137, "symbol": "housecoin", "type": "token" }, { "address": "0xc19b982b971642aa6cc5346f5a15cd0ccf4626cc", "label": null, "launchpad": "pons", "launchpad_name": "Pons", "links": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": "https://x.com/coinbase/status/2093710406695125093", "website": "https://www.ponsfamily.com/launchpad/0x992f10d2c751ec875d6a2834a70f730d030a25f9" }, "liquidity_usd": 3876.33, "logo": "https://api.crius.sh/v1/robinhood/logo/0xc19b982b971642aa6cc5346f5a15cd0ccf4626cc.png?size=128", "name": "Housecoin", "price_usd": 0.00000218996, "symbol": "Housecoin", "type": "token" } ], "meta": { "block": 100410183, "block_age_ms": 555, "block_ts": 1793225424000, "chain": "robinhood", "finalized_block": 100401329, "lag_ms": 9, "safe_block": 100405028 }}What this tells you
- Three different tokens share the name:
0x578dfaf61257ea702b8834bb037740a6368947bd(no launchpad,liquidity_usd22,978.7), and two Pons tokens,0x34c21f10ea7ec052ed878d157f40fb4160730b24(4,978.47) and0xc19b982b971642aa6cc5346f5a15cd0ccf4626cc(3,876.33). - Their
symbols differ only in case (HOUSECOIN,housecoin,Housecoin), and their links point to unrelated pages. A bot that scans “the first result” would be wrong as often as right. - Reply with every match: name,
launchpad_name, liquidity and the full address, and let the user choose.type=tokenrestricts search to tokens;limitgoes up to 20.
The scan: profile plus security
Housecoin's security: unlocked liquidity in one wallet
The user picks 0x578dfaf61257ea702b8834bb037740a6368947bd. What should the bot warn about?
curl "https://api.crius.sh/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd/security"const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd/security");const { data, meta } = await res.json();import requests
r = requests.get( "https://api.crius.sh/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd/security",)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 119 lines (arrays trimmed)
{ "data": { "contract": { "blacklist": false, "can_change_limits": false, "can_change_tax": false, "cooldown": false, "implementation": null, "is_proxy": false, "mintable": false, "owner": null, "ownership": "none", "pausable": false, "proxy_kind": null, "self_destruct": false, "trading_switch": false, "upgradeable": false, "verified": "none" }, "flags": [ "lp_unlocked", "unverified", "lp_single_wallet" ], "honeypot": { "is_honeypot": false, "reason": null, "selective": false }, "hook": null, "known_asset": false, "launch": { "flags": [ "lp_single_wallet" ], "lp_top_wallet": { "address": "0x52c6e90a0928df4d6e4a6164ee0c7851144b1454", "liquidity_usd": 23239.6, "pct": 100 }, "sniper_supply_pct": 0.0000145583, "sniper_top_wallet_pct": 0.0000145583, "sniper_wallets": 1, "sniper_window_blocks": 10, "uniform_holders": null, "wallet_farm": null }, "limits": { "max_tx": "1000000000", "max_tx_getter": "maxTxAmount()", "max_tx_pct": 100, "max_wallet": "1000000000", "max_wallet_getter": "maxWalletAmount()", "max_wallet_pct": 100 }, "liquidity": { "status": "unlocked", "summary": { "burned_pct": 0, "locked_pct": 0, "next_unlock_ts": null, "protocol_fee_pct": 0, "unlocked_in_wallets_pct": 100, "weighting": "by pool liquidity_usd" } }, "primary_dex": "uniswap_v4", "risk": "medium", "score": 50, "tax": { "buy_pct": 0, "confidence": "high", "realized": { "buy_pct": 0, "by_wallet_class": { "dev_sell_pct": null, "others_sell_pct": 0 }, "last_sample_ts": 1793225424000, "max_sell_pct_seen": 0, "samples": { "buys": 53, "sells": 48 }, "sell_pct": 0, "window": "30d" }, "sell_pct": 0, "simulated": null, "source": "realized", "transfer_pct": null }, "tax_history": [ { "from_block": 100408848, "pct": 0, "side": "buy" }, { "from_block": 100408910, "pct": 0, "side": "sell" } ], "tax_history_total": 2, "tax_mode": "static", "token": "0x578dfaf61257ea702b8834bb037740a6368947bd" }, "meta": { "block": 100410199, "block_age_ms": 1166, "block_ts": 1793225425000, "chain": "robinhood", "finalized_block": 100401329, "lag_ms": 97, "safe_block": 100405028, "simulation": "no V2 WETH pair: realized tax only" }}What this tells you
score: 50,risk: "medium",flags:lp_unlocked,unverified,lp_single_wallet.launch.lp_top_walletis0x52c6e90a0928df4d6e4a6164ee0c7851144b1454withpct: 100of the liquidity (liquidity_usd23,239.6), andliquidity.statusisunlocked: one wallet can pull the whole pool. That’s the line to put first in the reply.- No tax:
tax.buy_pctandtax.sell_pctare 0 over 53 buys and 48 sells (tax.realized.samples,confidence: "high"), andhoneypot.is_honeypotis false. meta.simulationsays"no V2 WETH pair: realized tax only": the taxes come from real trades, not a simulation.contract.ownership: "none", with no mint, pause or blacklist: the contract itself isn’t the risk here.
Minutes later: the pool is empty
The same token, scanned again twelve minutes after the security check above. This is what the lp_single_wallet warning was about.
Housecoin after its liquidity was pulled
Someone pastes 0x578dfaf61257ea702b8834bb037740a6368947bd again. What does the profile show now?
curl "https://api.crius.sh/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd"const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd");const { data, meta } = await res.json();import requests
r = requests.get( "https://api.crius.sh/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd",)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 267 lines (arrays trimmed)
{ "data": { "address": "0x578dfaf61257ea702b8834bb037740a6368947bd", "admin_flags": null, "bundles": { "see": "/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd/bundles" }, "burned": 771605584, "chain_id": 4663, "circulating_supply": 228394416, "creation": { "attribution": "unattributed_factory", "block": 100408825, "deployed_via": "factory", "deployer": "0x52c6e90a0928df4d6e4a6164ee0c7851144b1454", "factory": "0x1cbaf24d53fe930fce8eff149fa797d2611da149", "source": "trace", "tx": "0xba98b7479b2bf5896021262176c4c92f149df0750f93a75b2fcc83869d10d51a" }, "decimals": 18, "dexscreener": { "status": "checking" }, "first_seen_block": 100408825, "history": "complete", "holders": { "burned_pct": 77.1606, "count": 166, "dev_holding": null, "dev_pct": null, "locked_pct": 0, "top10_pct": 16.4376 }, "launch": null, "liquidity": { "see": "/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd/liquidity", "status": "no_liquidity", "summary": { "burned_pct": null, "locked_pct": null, "next_unlock_ts": null, "protocol_fee_pct": null, "unlocked_in_wallets_pct": null, "weighting": "by pool liquidity_usd" } }, "locked": 0, "market": { "fdv_usd": null, "liquidity_usd": 0, "market_cap_usd": null, "pools_count": 1, "quote_liquidity_usd": 0 }, "metadata": { "banner": null, "description": null, "links": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": null, "website": null }, "logo": null, "logo_original": null, "socials": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": null, "website": null }, "source": null, "sources": { "links": null, "logo": null } }, "name": "Housecoin", "price": { "ath_market_cap_usd": 21314.9, "ath_scope": "all_time", "ath_ts": 1793225465000, "ath_usd": 0.0000933247, "change_pct": { "1h": 271.103, "1m": 0, "1s": 0, "24h": 271.103, "30s": 0, "4h": 271.103, "5m": 0, "6h": 271.103 }, "confidence": null, "dex": null, "eth": null, "liquidity_usd": null, "quote": null, "quote_liquidity_usd": null, "source_pool": null, "thin_liquidity": null, "usd": null }, "security": { "see": "/v1/robinhood/token/0x578dfaf61257ea702b8834bb037740a6368947bd/security" }, "standard": "erc20", "stats": { "all_time": { "buy_volume_usd": 17761, "buys": 70, "first_trade_ts": 1793225291000, "sell_volume_usd": 11036.9, "sells": 62, "since": "chain_start", "trades": 132, "volume_usd": 28797.9 }, "avg_trade_usd": { "1h": 218.166, "1m": null, "1s": null, "24h": 218.166, "30s": null, "4h": 218.166, "5m": null, "6h": 218.166 }, "buy_volume_usd": { "1h": 17761, "1m": 0, "1s": 0, "24h": 17761, "30s": 0, "4h": 17761, "5m": 0, "6h": 17761 }, "buyers": { "1h": 52, "1m": 0, "1s": 0, "24h": 52, "30s": 0, "4h": 52, "5m": 0, "6h": 52 }, "buys": { "1h": 70, "1m": 0, "1s": 0, "24h": 70, "30s": 0, "4h": 70, "5m": 0, "6h": 70 }, "last_trade_ts": 1793225465000, "sell_volume_usd": { "1h": 11036.9, "1m": 0, "1s": 0, "24h": 11036.9, "30s": 0, "4h": 11036.9, "5m": 0, "6h": 11036.9 }, "sellers": { "1h": 31, "1m": 0, "1s": 0, "24h": 31, "30s": 0, "4h": 31, "5m": 0, "6h": 31 }, "sells": { "1h": 62, "1m": 0, "1s": 0, "24h": 62, "30s": 0, "4h": 62, "5m": 0, "6h": 62 }, "traders": { "1h": 52, "1m": 0, "1s": 0, "24h": 52, "30s": 0, "4h": 52, "5m": 0, "6h": 52 }, "trades": { "1h": 132, "1m": 0, "1s": 0, "24h": 132, "30s": 0, "4h": 132, "5m": 0, "6h": 132 }, "txns": { "1h": 132, "1m": 0, "1s": 0, "24h": 132, "30s": 0, "4h": 132, "5m": 0, "6h": 132 }, "volume_eth": { "1h": 12.2259, "1m": 0, "1s": 0, "24h": 12.2259, "30s": 0, "4h": 12.2259, "5m": 0, "6h": 12.2259 }, "volume_usd": { "1h": 28797.9, "1m": 0, "1s": 0, "24h": 28797.9, "30s": 0, "4h": 28797.9, "5m": 0, "6h": 28797.9 } }, "symbol": "HOUSECOIN", "total_supply": 1000000000, "type": "generic", "ui_multiplier": null, "verification": { "compiler": null, "contract_name": null, "implementation": null, "is_proxy": false, "sourcify": "none" } }, "meta": { "block": 100417059, "block_age_ms": 1310, "block_ts": 1793226138000, "chain": "robinhood", "finalized_block": 100408671, "lag_ms": 100, "pending": [], "safe_block": 100412483 }}What this tells you
market.liquidity_usdis 0,price.usdisnullandliquidity.statusisno_liquidity: the token’s one pool (market.pools_count: 1) has nothing left to trade against.- No trade happened after the all-time high:
price.ath_tsandstats.last_trade_tsare both 1793225465000 (22:11:05 UTC), less than a minute after the security check. creation.deployeris0x52c6e90a0928df4d6e4a6164ee0c7851144b1454, the same wallet the security check reported as holding 100% of the liquidity (launch.lp_top_wallet).holders.countis still 166: the holders keep their tokens, but there’s no market to sell them in.creation.attribution: "unattributed_factory"andlaunch: null: no launchpad, so no launch locker either.
The bot
Node.js 22+, no dependencies. Set TELEGRAM_TOKEN from @BotFather; CRIUS_API_KEY is optional.
// scanner-bot.mjs: TELEGRAM_TOKEN=... node scanner-bot.mjsconst TG = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}`;const API = "https://api.crius.sh/v1/robinhood";const HEADERS = process.env.CRIUS_API_KEY ? { "x-api-key": process.env.CRIUS_API_KEY } : {};const cache = new Map(); // address -> { at, text }
const esc = (s) => String(s ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]); // names are user-madeconst usd = (n) => (n == null ? "?" : n >= 1000 ? `$${Math.round(n).toLocaleString("en-US")}` : `$${Number(n).toPrecision(4)}`);async function get(path, tries = 3) { const res = await fetch(`${API}${path}`, { headers: HEADERS }); if (res.status === 429 && tries > 1) { await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 1) * 1000)); // rate limited: wait, retry return get(path, tries - 1); } return (await res.json()).data;}
async function scan(address) { const hit = cache.get(address); if (hit && Date.now() - hit.at < 60_000) return hit.text; const [t, s] = await Promise.all([get(`/token/${address}`), get(`/token/${address}/security`)]); if (!t) return "Token not found on Robinhood Chain."; const warn = { critical: "DANGER: honeypot or scam", high: "HIGH RISK", medium: "Medium risk", low: "Low risk" }[s?.risk] ?? "Risk unknown"; const lines = [ `<b>${esc(t.name)}</b> (${esc(t.symbol)})`, `<code>${address}</code>`, `Price ${usd(t.price?.usd)} · 24h ${t.price?.change_pct?.["24h"]?.toFixed(1) ?? "?"}%`, `Market cap ${usd(t.market?.market_cap_usd)} · Liquidity ${usd(t.market?.liquidity_usd)}`, `Holders ${t.holders?.count ?? "?"} · Top 10 hold ${t.holders?.top10_pct?.toFixed(1) ?? "?"}%`, t.launch ? `Launchpad ${esc(t.launch.launchpad_name)} · ${t.launch.status}` : "No launchpad", "", `${warn} · score ${s?.score ?? "?"}/100`, s?.flags?.includes("lp_single_wallet") ? "One wallet holds the unlocked liquidity and can pull it at any time." : null, `Tax buy ${s?.tax?.buy_pct ?? "?"}% · sell ${s?.tax?.sell_pct ?? "?"}% · LP ${s?.liquidity?.status ?? "?"}`, s?.flags?.length ? `Flags: ${s.flags.join(", ")}` : "No flags", ]; const text = lines.filter((l) => l != null).join("\n"); cache.set(address, { at: Date.now(), text }); return text;}
async function search(q) { const hits = (await get(`/search?q=${encodeURIComponent(q)}&type=token&limit=5`)) ?? []; if (!hits.length) return "No token with that name."; return ["Which one? Send the address:", ...hits.map((h) => `${esc(h.name)} (${esc(h.symbol)}) · ${esc(h.launchpad_name ?? "no launchpad")} · liq ${usd(h.liquidity_usd)}\n<code>${h.address}</code>`)].join("\n\n");}
async function reply(chatId, text) { await fetch(`${TG}/sendMessage`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: "HTML", link_preview_options: { is_disabled: true } }), });}
let offset = 0;for (;;) { try { const { result = [] } = await (await fetch(`${TG}/getUpdates?timeout=30&offset=${offset}`)).json(); for (const u of result) { offset = u.update_id + 1; const text = u.message?.text ?? ""; const addr = text.match(/0x[0-9a-fA-F]{40}/)?.[0]?.toLowerCase(); const name = text.match(/^\/scan(?:@\w+)?\s+(.+)/)?.[1]; if (addr) await reply(u.message.chat.id, await scan(addr)); else if (name) await reply(u.message.chat.id, await search(name.trim())); } } catch (e) { console.error(e); await new Promise((r) => setTimeout(r, 3000)); }}What it costs
| Operation | Calls | Credits |
|---|---|---|
| Scan an address | /token/{address} + /token/{address}/security |
2 |
| Look up a name | /search |
1 |
| Repeat scan within a minute | cached | 0 |
| New-launch feed | 1 stream connection + 2 per posted token | 1 + 2 each |
Without a key, scans are limited to 2 requests per second per IP, which a small group won’t reach. With a Free key, 1,000,000 credits cover about 500,000 scans a month. See Plans & limits.
Frequently asked questions
Do I need a Crius API key for a Telegram scanner?
Not to start. Token profile, security and search are on the anonymous plan at 2 requests per second per IP. A busy group needs more: a Free key gives 10 requests per second and 1,000,000 credits a month, about 500,000 scans.
How do I stop users scanning the wrong token with the same name?
Treat names as a search, never as an identity. /search returns every token with that name or symbol; show each one's address, launchpad and liquidity_usd, and scan only after the user sends or taps an address.
Can the bot post new launches to a channel automatically?
Yes. Subscribe to launches:all on the WebSocket stream (Free key with the stream scope), wait a minute, run the same scan and post tokens that pass. The new tokens guide has the stream client.
Does this work for Discord too?
The Crius part is the same: two calls per scan. Only the chat side changes: replace getUpdates and sendMessage with your Discord library's message events and replies.
Related
Get your free Crius API key
Every example on this page works with a free key: higher rate limits than the anonymous plan, holders, wallets and the WebSocket stream. No card needed.
- Robinhood Chain (4663) only, from our own archive nodes
- REST + WebSocket
- Pay on-chain or by card