nativeads-telegram
v0.1.2
Published
NativeAds SDK for Telegram bots — inject ads into LLM replies (Telegraf / grammY / raw).
Maintainers
Readme
nativeads-telegram
Node/TypeScript SDK for NativeAds — monetize your Telegram
bot by weaving native ads into LLM replies. Works with Telegraf, grammY, or raw
bot code. Written in TypeScript — types are bundled, no @types/* needed.
npm i nativeads-telegramQuick start
Generate your LLM answer as usual, then pass it through inject() — you get the text back
with a native ad woven in and the ad button merged into your keyboard. inject() never
throws: on any error it returns your original message untouched.
import { NativeAds } from 'nativeads-telegram';
const ads = new NativeAds({
apiKey: 'sk_live_xxx', // your publisher key
platformId: 'plt_xxx',
showEvery: 5, // ad on every 5th message (see "Ad frequency" below)
});
// inside your message handler, after generating the LLM answer:
const result = await ads.inject({ userId: ctx.from.id, message: llmReply });
// entities make the ad text a clickable link; replyMarkup adds the button
await ctx.reply(result.message, {
entities: result.entities,
reply_markup: result.replyMarkup,
});That's the whole integration. Everything below is optional tuning.
Ad frequency
Showing an ad on every reply hurts retention. Control cadence per user with two
options on the constructor — enforced client-side with an in-memory per-user counter, so
on a skipped turn inject() returns instantly without calling the server (no wasted
latency, no impression logged):
| option | meaning |
|--------|---------|
| showEvery: 5 | ad on every 5th message. 5 is the recommended sweet spot. 1 = every message, 0 = ads off. |
| skipFirst: 2 | never show an ad on a user's first 2 messages (let them get value first). |
const ads = new NativeAds({ apiKey: 'sk_live_xxx', showEvery: 5, skipFirst: 1 });
// message 1 → no ad (skipFirst) · 2,3,4 → no ad · 5 → ad · 10 → ad · …With showEvery > 1 the user's first message is always ad-free.
Passing your own keyboard
If you already send inline buttons, pass them — the ad button is added as its own row;
your buttons are never moved or removed. Raw markup, Telegraf Markup, and grammY
InlineKeyboard are all accepted and returned in the same shape:
const result = await ads.inject({
userId: ctx.from.id,
message: llmReply,
keyboard: myKeyboard, // raw / Telegraf / grammY
adButtonPosition: 'top', // 'top' or 'bottom' (default)
});adButtonPosition: 'top' puts the ad row above your buttons; 'bottom' (default) below.
Rendering the reply
Send result.message with result.entities (turns the ad text into a clickable link)
and result.replyMarkup (the button). No parse_mode — so your LLM text is never
re-parsed or broken:
await ctx.reply(result.message, {
entities: result.entities,
reply_markup: result.replyMarkup,
});result.replyMarkupis a plain{ inline_keyboard } | undefined, drop-in for any framework.result.entitiesisundefinedwhen no ad was injected — safe to always pass.- Already send answers with
parse_mode: 'HTML'? Telegram ignoresentitiesthen — instead appendresult.ad.adTextFormatted(a ready<a href>snippet) to your HTML and keepparse_mode: 'HTML'.
Privacy-friendly mode (fetch)
Don't want to send the LLM reply to the server? Fetch the ad as separate fields and render it yourself:
const { hasAd, ad } = await ads.fetch({ userId, languageCode: 'ru' });
if (hasAd && ad) {
await ctx.reply(`${llmReply}\n\n${ad.adText}`, {
reply_markup: { inline_keyboard: [[{ text: ad.buttonText, url: ad.buttonUrl }]] },
});
}Guarantees
- Never throws. On any error (timeout, network, 5xx)
inject()/fetch()return your original message and keyboard unchanged — ads can never break your bot. Default timeout 3s. - Your buttons are preserved. The ad button is always a separate row (top/bottom), respecting Telegram's 4096-char and 13-row limits.
- Click tracking is automatic —
buttonUrlis a tracking redirect to the advertiser.
API reference
new NativeAds(options)
| option | type | default | notes |
|--------|------|---------|-------|
| apiKey | string | — | required — your publisher key (sk_live_…) |
| platformId | string | — | optional public platform id (plt_…) |
| baseUrl | string | https://nativeads.cloud | API base; override for self-hosting |
| timeoutMs | number | 3000 | per-request timeout |
| platform | string | "telegram" | source platform label |
| showEvery | number | 1 | ad on every Nth message (5 optimal, 0 off) |
| skipFirst | number | 0 | no ad on a user's first N messages |
inject(params): Promise<InjectResult>
params: { userId, message, languageCode?, isPremium?, keyboard?, adButtonPosition?, parseMode? }
→ InjectResult: { message, hasAd, impressionId, ad, keyboard }
fetch(params): Promise<FetchResult>
params: { userId, languageCode?, isPremium?, parseMode? }
→ FetchResult: { hasAd, impressionId, ad }
Ad: { adText, buttonText, buttonUrl, adTextFormatted? }
All types are exported: NativeAds, NativeAdsOptions, InjectParams, InjectResult,
FetchParams, FetchResult, Ad, mergeKeyboard, InlineButton.
