# 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

1. In Telegram send [@BotFather](https://t.me/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](/en/integrations/#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:

```bash frame="terminal"
export TELEGRAM_BOT_TOKEN="123456:ABC..."
export ASTROWAY_API_KEY="aw_live_..."
```

## 2. The code

Save it as `bot.mjs`:

```js title="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:

```text
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](/en/languages/).

## 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](/en/rate-limits-credits/).

**Caution:**
A wrong key on `/v1/public/*` does not give a `401`: the request is served as anonymous and gets the anonymous limit. If the bot suddenly answers `PUBLIC_RATE_LIMIT`, first check that `ASTROWAY_API_KEY` really is set in the process environment.

## Next

- **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](/en/integrations/#send-the-birth-moment-not-an-offset). The bot will need a dialogue of several steps or a [Mini App](https://core.telegram.org/bots/webapps) 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.

**Note:**
Checked on 2026-11-07: `answer()` and the calls to AstroWay ran against production with all five kinds of message from the example. Telegram itself was replaced in the check by a local server answering in the Bot API format: we did not run a real bot.
