LiteBits Ads Docs

Publisher SDK · v1

Rewarded ads for Telegram Mini Apps

Games, wallets, tools — anything that runs inside Telegram and wants to reward its users. One script tag, one call, one server callback. The SDK draws a full-screen ad, runs the countdown, confirms the view with our server and hands you the result.

  • Telegram Mini Apps
  • ~25 KB minified · ~9.5 KB gzipped
  • 0 dependencies
  • ES2018 vanilla JS
The one rule

Your server pays your user when our signed callback arrives — never from the browser. Everything the SDK resolves or emits is a UI hint; the callback is the receipt.

The SDK runs inside a Telegram Mini App. It identifies the viewer from the signed Telegram.WebApp.initData your Mini App already has — there is no user id to pass and nothing for you to look up. Plain web pages are not supported in v1; outside Telegram only sandbox works.

When we have no campaign for a user, the SDK can run your own accounts on other networks (Gigapub, Adsgram, TowerAds, Telega, RichAds) in the order you set per placement — one SDK, one fill report. Those views are paid by that network, not by us. See Fallback networks.

Quick start #

Three steps. The first is a one-time dashboard setting; the other two are code.

  1. Tell us which bot your Mini App belongs to (once)

    Open the dashboard (@litebits_ads_bot → Open dashboard) → Settings → Telegram bot and enter your bot's id or @username. No token: Telegram signs every Mini App's initData with its own key for your bot id, and we check that signature, so nobody can fake your users.

    Only know the @username? Forward any message from your bot to @litebits_ads_bot and it connects the bot for you. Until a bot is connected every real ad request answers 409 publisher_bot_missing, which the SDK surfaces as NOT_CONFIGURED (not as a no-fill). Sandbox requests work without it.

  2. Install the script

    html
    <script src="https://ads.litebits.io/sdk/v1.js"></script>

    Exposes window.LiteBitsAds. The SDK talks to the origin it was loaded from, so no baseUrl is needed unless you self-host the file. Third-party network scripts are fetched only when a fallback step is actually reached, never on page load.

  3. Show an ad when the user opts in — e.g. on a claim, a level-up, a daily bonus

    js
    const ads = new LiteBitsAds({ apiKey: 'lba_…', placementId: '…' });
    ads.on('rewarded', (r) => console.log('server says', r));   // informational only
    try {
      const r = await ads.show();   // resolves when the user taps Done (or a fallback network showed)
      if (r.fallback) return;       // shown by YOUR account on r.network — that network pays this user, we do not
      if (r.verified) showToast('Thanks! Your reward is on its way');   // credit happens via callback
    } catch (err) {
      if (err.code === 'NO_FILL') showSomethingElse();  // neither we nor any of your fallback networks had an ad
    }

    show() reads window.Telegram.WebApp.initData fresh on every call and sends it to our server, which verifies Telegram's signature for your bot and takes the Telegram user id from it. There is nothing else to wire up on the client.

    Then set a callback URL in the Integration tab and credit the user when our signed impression.verified POST arrives — see Server callback.

Requirements #

A Telegram Mini App

The SDK only works inside a Telegram Mini App (WebView opened from your bot). It reads Telegram.WebApp.initData on every show(), sends platform: 'telegram', opens advertiser links with Telegram.WebApp.openLink / openTelegramLink, disables vertical swipe-to-close while the ad is open, and fires a haptic on completion. Outside Telegram a real show() rejects with CONFIG ("LiteBits Ads runs inside a Telegram Mini App"); only sandbox: true works there.

Do not parse or forward initData yourself — the SDK sends the raw string and our server does the verification.

Your bot on file

Since Bot API 8.0 every initData carries a signature: Telegram's Ed25519 signature over the data and the id of the bot the Mini App was opened from. We check it with Telegram's public key for the bot id you saved in Settings → Telegram bot, and reject initData older than 48 h. That is what makes telegram_user_id in the callback trustworthy, and it needs no secret from you.

Accounts that saved a bot token earlier keep working unchanged: with a token on file we check Telegram's HMAC hash instead. The token is stored encrypted and never returned by any endpoint; you can remove it and connect by id at any time.

Same bot

The bot in the dashboard must be the bot this Mini App is opened from. A mismatch shows up as AUTH (server 401 init_data_invalid); no bot at all as NOT_CONFIGURED (409 publisher_bot_missing).

Your API key and a placement

Both come from the dashboard: the publisher key (lba_…) is shown once on the Integration tab and can be rotated (the old key stops working immediately); placement ids are UUIDs, one per ad slot, each with its own countdown length, per-user daily limit and fallback chain.

An active account

New publishers start as pending. Sandbox works right away; real requests answer no-fill until LiteBits approves the account. See Approval & payouts.

API reference #

Constructor #

js
const ads = new LiteBitsAds({
  apiKey: 'lba_…',        // required
  placementId: '…',       // required, placement UUID
  baseUrl: undefined,     // optional, default: origin of the <script> tag
  lang: undefined,        // optional, two-letter code for targeting
  sandbox: false,         // optional, demo creative, nothing billed or earned
  fallbacks: true,        // optional, false = never run the placement's fallback chain
});
OptionRequiredDefaultNotes
apiKeyyes—Publisher key from the dashboard (Integration tab). Publishable: it lives in your Mini App code and can only request, show and confirm ads. Stats, earnings, payout address and callback settings open only in the dashboard, from your Telegram account.
placementIdyes—Placement UUID.
baseUrlnoorigin of the <script> tage.g. http://localhost:4200 when self-hosting the file.
langnoTelegram user's language_code, else navigator.languageTwo-letter code, used for campaign targeting.
sandboxnofalsetrue → fixed demo creative, nothing billed, nothing earned; works outside Telegram too. Sandbox never runs fallbacks.
fallbacksnotruefalse → never run the placement's fallback networks; a LiteBits no-fill rejects NO_FILL straight away.

There is no userId option. The user is whoever Telegram says it is.

Methods #

ads.show()
Requests, draws and confirms one ad. Resolves { verified, earning, campaign_id, impression_id, tapped, visited, reason? } for a LiteBits ad (tapped: the user opened the ad's link; visited: they also stayed 10 s on the advertiser's page through our visit page), or { verified: false, fallback: true, network } when one of your fallback networks showed instead. Rejects with an Error carrying .code (see Errors).
ads.on(event, fn)
Subscribes to an event. Returns an unsubscribe function.
ads.destroy()
Removes an open ad (rejecting the pending promise with ABANDONED) and drops all listeners. The instance cannot be reused.
LiteBitsAds.ERROR_CODES
Array of every error code the SDK can reject with.

Only one ad can be on screen at a time, across all instances on the page; a second show() rejects with BUSY.

show() resolves even when verified is false — the user did watch, but the view will not be paid (frequency cap hit, daily cap, too fast, …). The reason tells you why; whether you show the user anything is your call. A resolved value with fallback: true is a different thing entirely: one of your other networks showed the ad.

What the SDK sends

POST /v1/ad/request with { placement_id, init_data, lang, platform: 'telegram' } — init_data is the raw Telegram.WebApp.initData string, untouched. In sandbox mode outside Telegram it sends sandbox_user_id: 'demo' instead. The request phase has a 20 s overall timeout.

What the user sees

A full-screen dark overlay: the LiteBits Ads · Sponsored badge plus a countdown (min_seconds, set per placement, default 8). The creative is rendered inside it in one of four ways (table below). Tapping the creative opens the advertiser via Telegram.WebApp.openLink / openTelegramLink without closing the ad. The countdown pauses while the tab is hidden. There is no close control until the view has been confirmed by our server; then a full-width "Done" button appears. The ad never disappears on its own — the user closes it when they are done.

These rules are deliberate: on our own faucet they cut ad abandonment by about 29%.

How creatives render #

The server probes every url campaign — can the page be framed, and what does its t.me / Open Graph card say? — and sends the result with the ad as framable and card ({ kind: 'telegram' | 'web', title, description, image_url, handle } or null). The SDK takes the first row that matches. Nothing here touches the countdown, the badge or the Done rule.

CreativeWhat the user seesTap / CTA
creative_type: 'image'The image, full-bleed, with a CTA button under it (cta_text, default "Open").Opens url externally.
url to t.me/… (or card.kind === 'telegram')Telegram card: round 64 px avatar (first letter on a Telegram-blue disc when there is no photo), a "Telegram Channel" / "Telegram Bot" chip, title, @handle in Telegram blue, description (3 lines max), then a full-width "Open in Telegram" ("Open Bot" for a bot; cta_text overrides) inside the card. The whole card is tappable. Never an iframe.Telegram.WebApp.openTelegramLink, else openLink / window.open.
url with framable: true (only ever sent inside LiteBits' own Mini App)The advertiser's page in a sandboxed iframe, plus a small "Open in browser ↗" link in the footer. If the page has not loaded after 6 s (or errors), the web card below takes over — the countdown keeps running, it is not restarted.openLink / window.open.
url with visit_url (SDK 1.7.0)The web card below, never an iframe: the page is shown on our visit page.Opens visit_url instead of url.
any other urlWeb card: the card's image (16:9, at most 40% of the screen), title and description when the server has a card, else the campaign title and the host name; "Open" button (cta_text overrides).openLink / window.open.

Only the boolean true frames; false, null or a missing field all mean the web card. In your Mini App every page ad arrives with framable: false and renders as the web card: we cannot see your page's Content-Security-Policy or what the advertiser's server sends to each of your users, and a blocked frame still looks "loaded" to the SDK, so the card is the version that always shows the ad. Keeping the user inside the app (iframe or Telegram card) instead of sending them to a browser is where the ~29% comes from.

The visit page #

For some page ads a tap opens our visit page, https://ads.litebits.io/v/…, in Telegram's browser instead of the advertiser directly. The user sees the advertiser's page full screen and a small ring in the corner that counts 10 seconds while the page is on screen ("Stay on the page"; "Paused" when they switch away). Then it turns green ("Visit verified") and offers Continue to <site> (the advertiser opened normally, so sign-ups and referral links work) and Back to <your app>.

It applies only to page ads we can show in a frame (never t.me links or images) and only while LiteBits has it switched on. Your reward is unchanged: the countdown in your app keeps running while the user is on the page, and complete, earnings and the callback are the same. A visit is measured, not billed or paid. When the user comes back the SDK fires visit and show() resolves with visited; your dashboard shows "% visited" under the tap rate. You add nothing, and SDKs before 1.7.0 simply open the link directly.

Events #

EventPayloadWhen
loadedad (id, creative_type, title, url, image_url, framable, card, min_seconds, …)The server returned a creative. framable and card decide how a url creative renders (see How creatives render).
openedadThe overlay is on screen, countdown running.
tap{ url, campaign_id, impression_id }The user opened the ad's link (CTA, card, image or "Open in browser"), once per tap; the badge does not fire it. Informational — never pay on it.
visit{ url, campaign_id, impression_id }SDK 1.7.0: the user stayed 10 s on the advertiser's page through our visit page and our server verified it. Once per view, usually when the user comes back to your app. Informational — never pay on it.
rewarded{ verified, earning, campaign_id, impression_id, tapped, visited, reason? }/v1/ad/complete answered. verified: false carries a reason (too_fast, frequency, user_cap, expired, mismatch, …). Informational only. Never fired for a fallback network.
nofill{ placementId }LiteBits had no eligible campaign (server answered 204). Nothing of ours was drawn. If the placement has fallback networks, the chain starts right after this event.
fallback{ network, outcome, error? }One fallback step finished: outcome is shown | no-fill | timeout | error. Fired once per step, in chain order, until one shows.
errorError with .codeAny failure; also fired before a rejection.
closed{ outcome: 'shown' | 'error' | 'abandoned' }The overlay left the screen.

Listener exceptions are logged and swallowed — they can never break an ad in progress.

Errors (err.code) #

CodeMeaningWhat to do
NO_FILLLiteBits had no ad and none of your fallback networks showed one either (or fallbacks: false).Show your own placeholder; the fallback events tell you what each network said.
TIMEOUTThe ad request took longer than 20 s.Treat like no fill.
ABANDONEDdestroy() was called while an ad was open.Nothing.
NETWORKRequest failed, or the view could not be confirmed after retries.Treat like no fill; do not pay.
CONFIGBad/missing option, invalid key, unknown placement, or not running inside a Telegram Mini App.Fix the integration; check the console.
NOT_CONFIGUREDYour publisher account has no bot connected yet (server 409 publisher_bot_missing).Enter your bot id or @username in the dashboard; until then treat like no fill.
AUTHOur server rejected the Telegram initData (server 401 init_data_invalid): Telegram's signature is not for the bot in your dashboard, or auth_date is older than 48 h.Check that the dashboard bot is the one this Mini App is opened from; otherwise treat like no fill.
BUSYAn ad is already open.Ignore the second call.

Every rejection is also emitted as an error event first.

Rendering an ad you already chose #

This entry point exists for LiteBits' own faucet, which sells and bills its own campaigns and only wants them drawn the same way as network ads. It is not part of the publisher integration: if you are a publisher, use show(). render() does not request, bill or pay anything, and views drawn with it never reach your LiteBits balance or your callback.

js
const { completed, tapped, visited, frame } = await LiteBitsAds.render(ad, {
  complete: () => bankReward(),  // required, () => Promise<{ ok: boolean }>, called when the countdown ends
  onOpen: (url) => {},           // optional, notified on every tap-through (CTA, creative, card, badge)
  onFrame: (f) => {},            // optional (SDK 1.9.0), once per framed ad: { state: 'loaded' | 'fallback', ms, reason? }
  minSeconds: 10,                // optional, overrides ad.min_seconds
  badgeCampaign: 'faucet',       // optional, ?p= on the badge link
});

Static method, no instance, no API key, and the SDK makes no request of its own (SDK 1.7.0: except the visit status, when the ad carries visit_url and the user tapped). ad has the network ad shape (creative_type, url, image_url, cta_text, title, min_seconds, framable, card) and renders exactly like show() (see How creatives render). When the countdown ends the SDK awaits complete(): { ok: true } shows "Done"; a rejection or anything else shows "Close" so the user is never stuck. The promise resolves when the user taps it: completed is whether complete() said ok, tapped whether the user opened the ad's url (badge taps fire onOpen but do not count). It shares the one-ad-at-a-time rule with show() (BUSY) and rejects CONFIG only before anything is drawn. No events are emitted.

Sandbox #

sandbox: true (or the header x-lba-sandbox: 1 on the API) returns a fixed demo creative; complete answers { verified: true, earning: 0, sandbox: true } and nothing is written or billed.

js
const ads = new LiteBitsAds({ apiKey: 'lba_…', placementId: '…', sandbox: true });
const r = await ads.show();   // { verified: true, earning: 0, sandbox: true }

The demo page

The demo Mini App at /demo/?key=<api key> has sandbox on by default, exercises every event (including fallback), logs timings, prints the placement's fallback chain from /v1/ad/config, and has a "Skip fallbacks" toggle (fallbacks: false). To test a real ad or the fallback chain, open the same page inside your bot: the banner switches to "Telegram user detected" and you can untick Sandbox.

Fallback networks #

We cannot fill every request. Instead of wiring a second SDK for the misses, add your own publisher ids for other networks to the placement (dashboard → Placements → Fallback networks, up to 6, ordered). After a LiteBits no-fill the SDK runs them in that order and stops at the first one that shows. LiteBits bills nothing and pays nothing for those views.

NetworkParams you enterWhat the SDK does
gigapubproject_idLoads ad.gigapub.tech/script?id=<project_id>, calls showGiga().
adsgramblock_idLoads sad.adsgram.ai/js/sad.min.js, Adsgram.init({ blockId }).show() — resolves on { done: true }.
toweradsapi_key, placement_idLoads uslads.com/sdk/tower-ads-v4.js, new TowerAds({ apiKey, placementId, … }).loadAndShow() — resolves on onRewardEarned or a settled loadAndShow().
telegaad_block_uuid (+ token)Loads inapp.telega.io/sdk/v1/sdk.js, TelegaIn.AdsController.create_miniapp({ token }).ad_show({ adBlockUuid }). The Telega miniapp token is required to build the controller; if your page already initialises window.TelegaInAds itself, the SDK reuses it and no token is needed.
richadspub_id, app_idLoads richinfo.co/richpartners/telegram/js/tg-ob.js, new TelegramAdsController(), awaits initialize({ pubId, appId }), then triggerInterstitialBanner() — resolves when the creative was shown and closed. Both ids are the pubId and appId in the initialize() snippet RichAds gives you; neither is a secret. Ask your RichAds manager to turn off “automatic ad display”: once their script is initialised it listens for clicks on the whole page, and with that setting on it opens ads wherever your users tap.

How the chain runs

  1. POST /v1/ad/request answers 204 → nofill is emitted.
  2. The chain comes from GET /v1/ad/config?placement_id=…, fetched once per LiteBitsAds instance, lazily on the first show(). If that fetch fails the SDK logs a console warning and behaves as if the placement had no fallbacks — it never blocks or throws on it.
  3. Each step is bounded at 20 s. The network script is loaded on first use (once per page), then its show call runs. The step ends as:
    • shown — the network reported completion/reward, or its creative is still on screen at the bound (the screen decides, not the promise — some SDKs never settle while their ad plays);
    • no-fill — the network declined (rejected, skipped, blocked);
    • timeout — nothing settled and nothing is on screen after 20 s;
    • error — script failed to load, SDK global missing, unknown network or missing params.
    Every step is reported to POST /v1/ad/outcome with its network, so the dashboard's fill breakdown shows LiteBits vs each of your networks.
  4. Per step the SDK emits fallback { network, outcome, error? }. The first shown resolves show() with:
js
{ verified: false, fallback: true, network: 'gigapub', earning: 0, campaign_id: null, impression_id: null, reason: 'fallback' }

rewarded is not emitted and no closed event follows: rewarded is reserved for views LiteBits verified, and the third-party network drew its own UI (our overlay never appears for a fallback). If every step fails, show() rejects with NO_FILL exactly as before.

Your accounts, your payouts

You pay fallback users from that network's own callback, exactly as you would if you had integrated it directly. A fallback: true result is a UI hint from the browser — it tells you which network took the slot so you can show a "thanks" toast — not a receipt. Our signed callback never fires for fallback views, and verified is always false on them.

Opting out and edge cases

Server callback #

Set a callback URL in the dashboard (Integration tab). You get a callback_secret once — store it server-side. For every verified impression we POST to that URL:

http
x-lba-timestamp:   1726000000            (unix seconds)
x-lba-event-id:    5d5c…                 (uuid, stable across retries — dedupe on this)
x-lba-delivery-id: 9f1e…                 (uuid per attempt)
x-lba-signature:   <base64url(HMAC-SHA256(callback_secret, `${timestamp}.${rawBody}`))>
Content-Type:      application/json

{ "event": "impression.verified", "event_id": "5d5c…", "placement_id": "…",
  "telegram_user_id": 123456789, "ext_user_id": "123456789",
  "campaign_id": "…", "earning": 1, "occurred_at": "2026-09-12T10:00:00Z" }

telegram_user_id (number) is the verified Telegram user id taken from initData — the same id your Mini App sees in initDataUnsafe.user.id, so you can credit the account directly. ext_user_id carries the same value as a string for compatibility with earlier integrations. earning is your share for this view, in sats.

Answer with any 2xx within 10 s. Non-2xx or no answer → we retry after 1 min, 5 min, 30 min, 2 h, 12 h with the same event_id.

Verify the signature (Node / Express)

js · node
import crypto from 'node:crypto';
import express from 'express';

const SECRET = process.env.LBA_CALLBACK_SECRET;
const app = express();

// Raw body is required: the signature covers the exact bytes we sent.
app.post('/lba/callback', express.raw({ type: '*/*', limit: '64kb' }), async (req, res) => {
  const ts = Number(req.get('x-lba-timestamp'));
  const eventId = req.get('x-lba-event-id');
  const sig = req.get('x-lba-signature') || '';
  if (!ts || !eventId) return res.status(400).end();
  if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(400).end();   // replay window: 5 min

  const expected = crypto.createHmac('sha256', SECRET)
    .update(`${ts}.${req.body.toString('utf8')}`).digest('base64url');
  const a = Buffer.from(sig), b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).end();

  const evt = JSON.parse(req.body.toString('utf8'));
  // Idempotent: the same event_id may arrive more than once (retries).
  const fresh = await db.insertIgnore('lba_events', { event_id: eventId, payload: evt });
  if (fresh && evt.event === 'impression.verified') {
    await creditTelegramUser(evt.telegram_user_id, YOUR_REWARD_FOR_ONE_AD);   // your own amount, your own ledger
  }
  res.status(200).end();
});

Rules

  1. Pay from the callback, not from the client. rewarded and the resolved promise are UI hints from a browser you do not control. The signed callback is the only thing that proves the view was verified and earned.
  2. Dedupe on x-lba-event-id. Retries carry the same id; credit once.
  3. Reject skew over 300 s and always compare signatures in constant time.
  4. Keep callback_secret on the server. Rotating the callback URL in the dashboard issues a new secret; the old one stops validating immediately.
  5. Credit by telegram_user_id. It is the verified identity; frequency capping is keyed on it network-wide, so the same person cannot be paid twice for one campaign across two Mini Apps.

Approval & payouts #

Approval

Registering in the dashboard creates your publisher account as pending with a "Default" placement and your API key (shown once). LiteBits reviews every new publisher; until the account is switched to active, real ad requests answer no-fill (204) — the SDK emits nofill and, with no fallbacks, rejects NO_FILL. Sandbox works the whole time, so you can finish the integration before approval lands. Your status is the pill at the top of the dashboard.

The dashboard's next-step list runs in the order that unblocks you fastest: connect your bot, add the SDK, test in sandbox, set your payout address.

How earnings are computed

Money is sats, integer, everywhere. Advertisers buy verified impressions at a CPM (sats per 1,000). For each verified view your earning is your revenue share of the advertiser's charge, adjusted by your account's quality score, floored to a whole sat. Both figures are set per account by LiteBits and shown in the dashboard. The per-view amount arrives as earning in the callback and in rewarded.

A view is verified only if the countdown was fully watched (too_fast otherwise), the token was used within 15 minutes (expired), the request and completion came from the same device (mismatch), the user had not seen that campaign in the last 24 h anywhere on the network (frequency), the placement's per-user daily limit was not hit (user_cap), your account's daily cap was not reached (publisher_cap), the campaign's own daily limit was not reached (campaign_cap) and the campaign still had budget (exhausted). Unverified views earn nothing and trigger no callback.

Hold and balance

Every earning is credited to your ledger immediately but stays on hold for a number of days set on your account (the default is 7), after which it becomes available. The dashboard shows available, on_hold and paid_out; the Earnings tab lists every ledger entry.

Payouts

Fallback views

Views filled by your own accounts on Gigapub, Adsgram, TowerAds, Telega or RichAds never appear in your LiteBits ledger — that network pays you directly under its own terms.

FAQ #

Does the SDK work on a normal website?

No. v1 is Telegram Mini Apps only: identity comes from the signed initData. Outside Telegram a real show() rejects with CONFIG; only sandbox: true works there.

Do I need to pass a user id?

No. There is no userId option. The SDK sends the raw initData, our server verifies it against your bot token and extracts the Telegram user id, and the callback delivers it back to you as telegram_user_id.

Why did show() resolve with verified: false?

The user watched, but the view is not payable: reason says why (frequency — same campaign seen in the last 24 h; user_cap — placement's per-user daily limit; too_fast; expired; mismatch; …). No callback is sent for it. Whether you tell the user anything is your call.

Can I pay the user when rewarded fires?

Please don't. It is a browser event anyone can fake with devtools. Pay only from the signed server callback, dedupe on x-lba-event-id, and pick your own reward amount.

Do fallback views come through the callback?

No. Fallback views are served and paid by that network under your own account; LiteBits neither bills nor pays them. The fallback: true result and fallback events exist so you can update the UI and see the fill split in the dashboard.

Can the user close the ad early?

No. There is no close control until our server has confirmed the view; then a "Done" button appears and the ad stays until the user taps it. Tapping the creative opens the advertiser without closing the ad, and the countdown pauses while the tab is hidden.

What if I call show() twice?

The second call rejects with BUSY. Only one ad can be on screen per page, across all instances, including during a fallback chain.

How long is the countdown?

min_seconds is set per placement in the dashboard (default 8). Completing before it has elapsed is too_fast and unverified.

I lost my API key or callback secret.

Both are shown once. Rotate the API key from the Integration tab (the old key stops working immediately). Saving a callback URL again issues a new callback_secret and invalidates the previous one.

I set a Content-Security-Policy in my Mini App.

Allow script-src for ads.litebits.io and for the hosts of any fallback networks you configured (ad.gigapub.tech, sad.adsgram.ai, uslads.com, inapp.telega.io, richinfo.co) and for whatever those scripts load in turn (see each network's own docs). Nothing is fetched from them unless that step is reached.

Where do I develop locally?

Load http://localhost:4200/sdk/v1.js from a local LiteBits Ads server; the SDK talks to the origin it was loaded from, or pass baseUrl explicitly. The demo page at /demo/ exercises every event in sandbox.