EN

Theme

Accent colour

Your theme and colour carry over to satelit.md and the app.

Dashboard Choose a plan

Automatic blogger

Every morning, a Markdown post with the most serious news, the BNM rate and the ANRE cap. It saves it to a file and, if you want, sends it to a Telegram channel.

How it works

  1. Fetch the news of the last 24 hours with gravity of at least 50.
  2. Fetch the rate and the cap of the day.
  3. Write the post, with links to the originals and the attribution line received in meta.attribution.
  4. Save it in posts/ and send it to Telegram, if you set the token.

What it costs: 2 credits for news, 1 for the rate, 1 for fuel: 4 credits a day, 120 a month. It fits in the Start plan. On every plan news arrives without delay and with a summary.

How to run it

bash
export SATELIT_KEY=sat_live_...
POST_LANG=ro MIN_GRAVITY=50 node blogger.mjs

What it outputs

stdout
posts/2026-11-02-ro.md: 4 stories, 4 credits

The full script

blogger.mjs
#!/usr/bin/env node
// Automated blogger: one Markdown post a day from the Satelit API.
//   SATELIT_KEY=sat_live_... node blogger.mjs
// Optional: TELEGRAM_TOKEN and TELEGRAM_CHAT send the post to a channel.
// No dependencies, Node 18 or newer. Put it in cron: 0 8 * * * node /path/blogger.mjs
import { mkdirSync, writeFileSync } from "node:fs";

const BASE = process.env.SATELIT_BASE ?? "https://api.satelit.md/v1";
const KEY = process.env.SATELIT_KEY;
const LANG = process.env.POST_LANG ?? "ro";
const MIN_GRAVITY = Number(process.env.MIN_GRAVITY ?? 50);
const OUT_DIR = process.env.OUT_DIR ?? "posts";
if (!KEY) throw new Error("Set SATELIT_KEY");

const WORDS = {
  ro: { title: "Ziua în Moldova", news: "Știrile zilei", money: "Curs și carburanți", cap: "Plafon A-95", gravity: "gravitate" },
  ru: { title: "День в Молдове", news: "Новости дня", money: "Курс и топливо", cap: "Предел A-95", gravity: "серьёзность" },
  en: { title: "The day in Moldova", news: "Today's news", money: "Rates and fuel", cap: "A-95 cap", gravity: "gravity" },
  uk: { title: "День у Молдові", news: "Новини дня", money: "Курс і пальне", cap: "Межа A-95", gravity: "серйозність" },
};
const w = WORDS[LANG];

async function api(path, params = {}) {
  const url = new URL(BASE + path);
  for (const [k, v] of Object.entries({ lang: LANG, ...params })) url.searchParams.set(k, v);
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
    const retry = Number(res.headers.get("retry-after"));
    if ((res.status === 429 || res.status === 503) && retry && attempt < 2) {
      await new Promise((ok) => setTimeout(ok, retry * 1000));
      continue;
    }
    const body = await res.json();
    if (!res.ok) throw new Error(`${path}: ${body.error.code}: ${body.error.message}`);
    return body;
  }
}

const today = new Date().toISOString().slice(0, 10);
const since = new Date(Date.now() - 86400e3).toISOString().slice(0, 10);

const [news, rates, fuel] = await Promise.all([
  api("/news", { min_gravity: MIN_GRAVITY, from: since, per: 10 }),
  api("/rates"),
  api("/fuel"),
]);

const lines = [`# ${w.title}: ${today}`, "", `## ${w.news}`, ""];
for (const n of [...news.data].sort((a, b) => b.gravity - a.gravity)) {
  const link = n.sources[0]?.url;
  lines.push(`- **${n.title}** (${w.gravity} ${n.gravity})${link ? ` [${n.sources[0].name}](${link})` : ""}`);
  if (n.summary ?? n.dek) lines.push(`  ${n.summary ?? n.dek}`);
}

const eur = rates.data.find((r) => r.code === "EUR");
const usd = rates.data.find((r) => r.code === "USD");
lines.push("", `## ${w.money}`, "", `- EUR: ${eur.value} (${eur.change_7d_pct}%, 7d)`, `- USD: ${usd.value} (${usd.change_7d_pct}%, 7d)`, `- ${w.cap}: ${fuel.data.cap.a95} lei/l`);

// The plan's attribution line, when it asks for one
const attribution = news.meta.attribution ?? rates.meta.attribution;
if (attribution) lines.push("", `*${attribution}*`);

const post = lines.join("\n") + "\n";
mkdirSync(OUT_DIR, { recursive: true });
const file = `${OUT_DIR}/${today}-${LANG}.md`;
writeFileSync(file, post);

if (process.env.TELEGRAM_TOKEN && process.env.TELEGRAM_CHAT) {
  const res = await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chat_id: process.env.TELEGRAM_CHAT, text: post.slice(0, 4000) }),
  });
  if (!res.ok) throw new Error(`Telegram: ${res.status}`);
}

const credits = news.meta.credits + rates.meta.credits + fuel.meta.credits;
console.log(`${file}: ${news.data.length} stories, ${credits} credits`);