Telegram bot
A bot with two commands: /horoscope leo returns today’s horoscope for the sign, /moon the current Moon phase. It fetches messages from Telegram itself (long polling), so it needs no webhook, domain or HTTPS: any machine with Node.js 22 will do.
1. A bot and a key
Section titled “1. A bot and a key”- In Telegram send @BotFather the
/newbotcommand and keep the token it gives you. - In the dashboard create a Server only key as described in A key for an automation. Endpoint scope:
public/horoscope/*andpublic/moon-phase. Neither endpoint spends credits. - Set both values as environment variables, not in the code:
export TELEGRAM_BOT_TOKEN="123456:ABC..."export ASTROWAY_API_KEY="aw_live_..."2. The code
Section titled “2. The code”Save it as bot.mjs:
// Node 22 or newer, no dependencies.const TELEGRAM = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`;const ASTROWAY = 'https://api.astroway.info/v1';const LANG = 'en';const SIGNS = ['aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo', 'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces'];
// A day's answer per path: 12 signs and one Moon are at most 13 calls a day.const cache = new Map();
async function astroway(path) { const key = `${new Date().toISOString().slice(0, 10)} ${path}`; if (cache.has(key)) return cache.get(key); const res = await fetch(`${ASTROWAY}${path}`, { headers: { 'X-Api-Key': process.env.ASTROWAY_API_KEY }, }); const json = await res.json(); if (!json.ok) throw new Error(`${json.error.code}: ${json.error.message}`); cache.set(key, json.data); return json.data;}
async function answer(text) { const [word, arg = ''] = text.trim().split(/\s+/); const command = word.split('@')[0]; // in a group the command arrives as /horoscope@YourBot if (command === '/horoscope') { const sign = arg.toLowerCase(); if (!SIGNS.includes(sign)) return `Send a sign: /horoscope leo\n${SIGNS.join(', ')}`; const data = await astroway(`/public/horoscope/daily?sign=${sign}&lang=${LANG}`); return `${data.date}, ${sign}\n\n${data.horoscope}`.slice(0, 4000); } if (command === '/moon') { const m = await astroway(`/public/moon-phase?lang=${LANG}`); return `${m.localized.phaseName}, ${m.illuminationPercent}%, Moon in ${m.localized.moonSign}\n` + `${m.localized.meaning}\nNext new moon: ${m.next.newMoon}`; } return 'Commands: /horoscope <sign>, /moon';}
async function telegram(method, body) { const res = await fetch(`${TELEGRAM}/${method}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return res.json();}
let offset = 0;for (;;) { const updates = await telegram('getUpdates', { offset, timeout: 30 }); for (const update of updates.result ?? []) { offset = update.update_id + 1; const message = update.message; if (!message?.text) continue; let reply; try { reply = await answer(message.text); } catch (err) { reply = `AstroWay: ${err.message}`; } await telegram('sendMessage', { chat_id: message.chat.id, text: reply }); }}Run node bot.mjs and send the bot /moon. The reply looks like this:
Waning Crescent, 7.85%, Moon in LibraThe close of the cycle: rest, cleansing and preparing for the new.Next new moon: 2026-11-09T07:02:07ZLANG switches the language of the horoscope text and the names: en, uk and the other languages in the list.
Why the code has a cache
Section titled “Why the code has a cache”A sign’s horoscope is the same for everyone who asks that day, and so is the Moon phase. A one-day cache means at most 13 calls to the API a day, however many people write to the bot.
That matters on Free: a Free key on /v1/public/* counts against the same limit as a call with no key, 30 requests an hour from your server’s IP address. Without the cache a bot with a few dozen users runs into it within the hour. A key on a paid plan is counted against the plan’s limit.
- A natal chart. Endpoints that compute from a birth moment (
POST /v1/chartcosts 20 credits) take the same date, time, zone and coordinates described in No-code integrations. The bot will need a dialogue of several steps or a Mini App with a form. - A webhook instead of polling. Once the bot runs on a server with HTTPS,
setWebhookremoves the long-lived connection. Theanswer()logic does not change.