Skip to content
AstroWay/api v2.325.0 · integrations
all systems operational

Telegram bot

Last updated View as Markdown Agent setup

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. In Telegram send @BotFather the /newbot command and keep the token it gives you.
  2. In the dashboard create a Server only key as described in A key for an automation. Endpoint scope: public/horoscope/* and public/moon-phase. Neither endpoint spends credits.
  3. Set both values as environment variables, not in the code:
Terminal window
export TELEGRAM_BOT_TOKEN="123456:ABC..."
export ASTROWAY_API_KEY="aw_live_..."

Save it as bot.mjs:

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 Libra
The close of the cycle: rest, cleansing and preparing for the new.
Next new moon: 2026-11-09T07:02:07Z

LANG switches the language of the horoscope text and the names: en, uk and the other languages in the list.

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/chart costs 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, setWebhook removes the long-lived connection. The answer() logic does not change.