npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dotbots-boutique/ads-sdk

v0.1.2

Published

Official SDK for showing, reporting and managing dotAds advertisements in a DotBots Boutique application.

Readme

@dotbots-boutique/ads-sdk

The official SDK for showing, reporting and managing dotAds advertisements in a DotBots Boutique application. dotAds is a platform service under the provider contract; this package is the consumer side of it.

You show ads that are counted and attributed correctly in under twenty lines of code, without ever having a service token, a trackingId or a dotAds endpoint in the browser, and without fetching a token yourself.


Architecture

  Browser (React)                      Your Deno backend                      dotAds
┌──────────────────────┐            ┌───────────────────────────┐        ┌────────────────┐
│ @dotbots-boutique/   │            │ @dotbots-boutique/        │        │                │
│   ads-sdk/react      │            │   ads-sdk                 │        │  /public/v1/*  │
│                      │            │                           │        │                │
│ DotAdsProvider       │ auth.fetch │ createDotAdsHandler       │ service│  /ads/batch    │
│ AdSlot               ├───────────►│   POST {prefix}/batch     │ token  │  /ads          │
│ useDotAdsClaims      │            │   POST {prefix}/events    ├───────►│  /events       │
│ useRedeemClaim       │            │   POST/GET {prefix}/claims│        │  /claims       │
│ useAdvertiser        │◄───────────┤   PUT  {prefix}/claims/:id│◄───────┤  /advertisers  │
│                      │   JSON     │   GET  {prefix}/advertisers        │  /setup        │
└──────────────────────┘            │   GET  {prefix}/status    │        └────────────────┘
                                    │                           │
                                    │ DotAdsClient              │
                                    │   dotbots.service('dotAds',│
                                    │     { orgId, onBehalfOf })│
                                    └───────────────────────────┘
                                                │
                                    @dotbots-boutique/server-sdk
                                    holds the URL, the token and the environment

The browser talks only to your own backend, with the fetch your application already uses. Your backend talks to dotAds through a service handle. No token ever crosses that first line.


Install

npm install @dotbots-boutique/ads-sdk

The package has no dependencies and no peer dependencies, and ships ESM plus its own types. What each entry expects from your application:

| Entry | Expects | Why it is not a peer dependency | | --- | --- | --- | | server | @dotbots-boutique/server-sdk 1.0 or newer | Nothing is imported from it. Your application passes in its own initialised dotbots, and the contract is typed structurally. | | react | react 18 or newer | Only the browser entry imports it, and a React application already has React installed. |

Declaring those as peer dependencies costs more than it buys: Deno resolves peers per dependency path, so a package that declares them gets a separate copy per peer combination ([email protected][email protected] next to [email protected]_@[email protected]). A deno compile of an application backend then has to read a copy directory such as 0.1.0_1 from the Deno cache, which is not always materialised, and the build fails on a cold cache with Building npm vfs ... No such file or directory. Without peer dependencies there are no copies, so that failure cannot happen. Check the versions above yourself; npm will not warn you.

| Entrypoint | Runs in | Talks to | | --- | --- | --- | | @dotbots-boutique/ads-sdk | the Deno backend of your app | dotAds /public/v1/* through dotbots.service('dotAds') | | @dotbots-boutique/ads-sdk/react | the browser | your own backend, through the fetch you pass in |

The server entry refuses to load where window exists.


Declare the service

dotAds is a platform service, so declare it in dotbots.boutique.json before you call it. A grant is approved once, by the organisation, and the platform issues the service token from there on.

{
  "platformServices": {
    "dotAds": {
      "description": "Advertisements in the application",
      "scopes": ["ads.serve", "ads.report", "ads.claim", "ads.redeem", "ads.setup"],
      "access": "required"
    }
  }
}

| Function | Scope | | --- | --- | | serve.batch, serve.one | ads.serve | | events.report | ads.report | | claims.create, claims.list | ads.claim | | claims.update (redeeming) | ads.redeem | | catalog.* | ads.serve | | setup.* | ads.setup |

Declare only the scopes you use. A scope you did not declare comes back as SCOPE_NOT_DECLARED; one that was declared but not granted as SCOPE_NOT_GRANTED.


Quickstart

Backend (Deno):

import { DotAdsClient, createDotAdsHandler } from "@dotbots-boutique/ads-sdk";

const client = new DotAdsClient({ dotbots, orgId: Deno.env.get("APP_ORG_ID")! });
const ads = createDotAdsHandler({ client, resolveUser, corsHeaders });

Deno.serve(async (request) => await ads(request) ?? new Response("Not found", { status: 404 }));

Browser (React):

import { AdSlot, DotAdsProvider } from "@dotbots-boutique/ads-sdk/react";

const auth = useDotBotsAuth();

<DotAdsProvider endpoint={`${BACKEND_URL}/api/ads`} fetch={auth.fetch} serviceStatus={auth.serviceStatus("dotAds")}>
  <AdSlot placement="home_top" markets={["BE", "EU"]} />
</DotAdsProvider>;

That is the whole integration. Slots rendered together share one request, impressions and clicks are counted once each, and nothing is stored in the browser.


orgId is the organisation of the maker

orgId is the organisation that owns the application and its dotAds inventory. It is fixed for the life of the client and comes from your own configuration.

It is never user.orgId. A visitor from another organisation is still shown the inventory of the organisation that built the app. Passing the visitor's organisation makes dotAds answer ORGANISATION_MISMATCH.

The visitor travels as onBehalfOf, per call. The SDK passes it to dotbots.service('dotAds', { orgId, onBehalfOf }), where it becomes act in the service token. It never goes into a request body.

await client.serve.batch({ slots, language: "nl", onBehalfOf: user.userId });

Handles are cached per acting person, so a screen with ten slots costs one token request, not ten. The platform caps at 600 requests per minute per application.


Serving

const result = await client.serve.batch({
  slots: [
    { key: "top", placement: "home_top", markets: ["BE", "EU"], context: { page: "home" } },
    { key: "side", placement: "home_side", markets: ["BE"], count: 2 },
  ],
  language: "nl",
  onBehalfOf: user.userId,
});

result.slotsByKey.top.ads; // the ads for the 'top' slot

markets is in order of preference. The dotAds answer is passed through unchanged (requestId, advertiserCap, slotCount, filledCount, and per slot key, placement, format, markets, requested, count, reason, ads); slotsByKey is added purely as an index.

An empty slot carries a reason, and a reason is a normal result, not an error:

| reason | Means | | --- | --- | | unknown_placement | the placement does not exist in dotAds | | unknown_market | the market does not exist in dotAds | | no_eligible_ads | nothing qualified right now |

serve.one covers a single placement through GET /ads. dotAds answers 404 for an unknown placement or market there; the SDK translates that into the same reason.


Reporting

Every event carries an eventId: the idempotency key dotAds deduplicates on. dotAds deduplicates on that key alone, never on trackingId plus type, so without an eventId a repeat counts twice. The SDK therefore always fills one in when you do not:

| Event | Generated eventId | | --- | --- | | impression | {trackingId}:impression (stable: one view per served ad) | | click | {trackingId}:click:{uuid} | | advertiser_page_view, offer_view | {advertiserId}:{type}:{uuid} |

const outcome = await client.events.report([
  { type: "impression", trackingId: ad.trackingId },
  { type: "click", trackingId: ad.trackingId },
], { onBehalfOf: user.userId });

Before anything is sent, the SDK refuses what dotAds could never store. Those come back as skipped results rather than as an exception:

| Situation | reason | | --- | --- | | impression or click without a trackingId | tracking_required | | type: 'claim' | claim_not_reportable | | an empty eventId, or one over 200 characters | invalid_event_id |

More than 100 events are split into blocks of 100 and sent one after another, and the results are merged into one { received, stored, skipped, results }. An empty batch is never sent. A batch fails only when dotAds rejects it as a whole. Each result's eventKey (the application code plus your eventId) is linked back to the eventId the SDK sent.


Claims

const claim = await client.claims.create({ promotionId, trackingId, onBehalfOf: user.userId });
const mine = await client.claims.list({ onBehalfOf: user.userId });
const redeemed = await client.claims.update(claim.id, { status: "redeemed" });

create and list require onBehalfOf: a claim always belongs to a person. Without it the SDK refuses locally with ACT_REQUIRED, before a request leaves the process. Redeeming through update is deliberately without a person: there the application acts, not the visitor, which is why the handler puts it behind allowRedeem and refuses it by default.

A marketplace key cannot claim. Claiming happens for a signed-in person of the application, through the service token with act.


Never in a body

Nothing about the application, the organisation, the environment or the acting person belongs in a request body: the service token carries all of it. Passing one anyway is an error, not a silent strip:

await client.serve.batch({ slots, userId: user.id });      // DotAdsError CONFIG_INVALID
await client.serve.batch({ slots: [{ ..., context: { orgId } }] }); // DotAdsError CONFIG_INVALID

The check runs over the whole argument, nested context included. The one exception is the document of setup.put: that is dotAds content and is passed through exactly as authored, only the envelope is checked.


Errors

DotBotsServiceError from the server SDK travels through unchanged, so your application can act on the grant flow: SERVICE_GRANT_PENDING, SERVICE_NOT_GRANTED, SERVICE_GRANT_DENIED, SERVICE_GRANT_REVOKED, SCOPE_NOT_GRANTED, SERVICE_NOT_DECLARED, SCOPE_NOT_DECLARED, TEST_ORG_NOT_ALLOWED, TEST_USAGE_QUOTA_EXCEEDED, RATE_LIMIT_EXCEEDED, SERVICE_UNAVAILABLE.

Everything dotAds itself answers arrives as a DotAdsError:

class DotAdsError extends Error {
  status: number;    // HTTP status, 0 on a network failure or timeout
  code: string;      // INVALID_SERVICE_TOKEN, SCOPE_NOT_GRANTED, GRANT_REVOKED,
                     // ORGANISATION_MISMATCH, tracking_other_app, ACT_REQUIRED, CONFIG_INVALID, ...
  details?: unknown; // for example { missingScopes: ['ads.serve'] }
  retryable: boolean; // true on 5xx, timeout and network failure; false on every 4xx
}

| Code | What your app does | | --- | --- | | SERVICE_GRANT_PENDING | show "waiting for approval", do not retry; an administrator approves the grant | | SERVICE_NOT_GRANTED, SERVICE_GRANT_DENIED, SERVICE_GRANT_REVOKED | hide the ad surfaces, carry on without dotAds | | SCOPE_NOT_GRANTED, SCOPE_NOT_DECLARED | a scope is missing; fix the declaration or ask for approval | | ORGANISATION_MISMATCH | orgId is not the owner of the inventory; fix your configuration | | ACT_REQUIRED | the call needs onBehalfOf; pass the signed-in person | | CONFIG_INVALID | a forbidden field was passed; remove it | | tracking_other_app | the trackingId came from another application; serve again | | TEST_USAGE_QUOTA_EXCEEDED | the day quota on test is spent; wait for the next day, never retry | | RATE_LIMIT_EXCEEDED | the SDK already retried once; back off | | SERVICE_UNAVAILABLE | dotAds is down; show the fallback |

Retry rules: never on 403, never on TEST_USAGE_QUOTA_EXCEEDED, never on claims.create. One retry on RATE_LIMIT_EXCEEDED after the indicated wait; one retry on a retryable failure for GET requests and for events.report (its eventId makes a repeat safe).


The handler

const ads = createDotAdsHandler({
  client,
  resolveUser: (request) => sessionFor(request),      // { userId, orgId, roles } | null
  allowClaims: (user) => true,                        // default: every signed-in person
  allowRedeem: (user) => user.roles.includes("staff"), // default: nobody
  corsHeaders,                                         // your platform-conforming function
  prefix: "/api/ads",                                  // default
});

| Route | Does | | --- | --- | | OPTIONS * | 200 with body ok and your CORS headers | | POST {prefix}/batch | validates the slots, takes onBehalfOf from resolveUser, calls serve.batch | | POST {prefix}/events | validates, calls events.report, answers 202 with the merged result | | POST {prefix}/claims, GET {prefix}/claims | only when allowClaims; the person comes from the session, never from the body | | PUT {prefix}/claims/:id | redeeming; only when allowRedeem | | GET {prefix}/advertisers/:id | the public advertiser page | | GET {prefix}/status | { status } derived from the last platform answer |

No user is 401 { error: 'unauthorized' }. Platform errors are translated the way the developer guide prescribes: SERVICE_GRANT_PENDING to 409 awaiting_approval, a missing grant to 403 not_available, SCOPE_NOT_GRANTED to 403 with scopes, SERVICE_UNAVAILABLE to 503. A DotAdsError keeps its status and code. Always JSON, always CORS, never a token in an answer.


React

<DotAdsProvider
  endpoint={`${BACKEND_URL}/api/ads`}  // absolute URL of your backend
  fetch={auth.fetch}                    // required
  batchWindowMs={16}                    // default
  flushIntervalMs={2000}                // default
  labels={{ nl: "Advertentie" }}        // default: en, nl, fr, de
  serviceStatus={auth.serviceStatus("dotAds")}
  onError={(error) => report(error)}    // default console.warn, never a throw
/>

A relative endpoint fails clearly at mount: it cannot be resolved from inside the marketplace iframe. While the grant is pending, denied or revoked the provider requests nothing at all. When you pass no serviceStatus, it reads GET {endpoint}/status once. The language comes from ?lang=, defaults to en, and travels as language on the batch. Everything lives in React state and refs.

<AdSlot
  placement="home_top"
  markets={["BE", "EU"]}
  count={2}
  context={{ page: "home" }}
  render={(ad) => <MyAd ad={ad} />}   // optional; do not render your own anchor inside it
  fallback={<HouseAd />}              // shown when nothing was served
  onServed={(result) => track(result)}
/>
  • Every slot rendered within batchWindowMs shares one POST {endpoint}/batch. Each slot gets its own key, derived from React's useId() but stripped to letters, digits and underscores (slot_r1), because a key with punctuation can be dropped by dotAds.
  • Nothing served means null or your fallback, never a placeholder larger than the format.
  • An impression is counted per ad after one uninterrupted second at half visibility (IntersectionObserver, threshold 0.5), once, also across re-renders and scrolling back.
  • A click is queued, flushed with keepalive: true, and only then opened with window.open(url, '_blank', 'noopener,noreferrer'). A second click within a second still navigates but is not reported again.
  • The queue is flushed on the interval, at 100 events, on pagehide and on visibilitychange: hidden. One retry, capped at 500 events, oldest dropped first.
  • Changing placement, markets, count or context refreshes the slot: new trackingIds, so new impressions. Never a silent reload.
  • A failure shows the fallback and calls onError. Never a throw: dotAds may not break your app.
  • Clickable ads are at least 44 by 44 pixels and never hover-only.
  • Media comes straight from the absolute URL in the ad (token-free on GET /public/v1/media/:id), with max-width: 100%. That URL is the capability: it comes from the ad and goes nowhere else.

Hooks:

useDotAdsStatus();  // { status, grantId?, canCurrentUserApprove? }
useDotAdsClaims();  // { claimPromotion(promotionId, trackingId), myClaims, refresh, loading, error }
useRedeemClaim();   // { redeem(claimId), loading, error }  -- only useful behind allowRedeem
useAdvertiser(id);  // { advertiser, loading, error }

Default renderers are chosen per format kind and show every mandatory field in full, with the label from labels and the promotion whenever ad.promotion exists. No text-overflow: ellipsis and no line-clamp on mandatory text: an advertiser paid for the whole sentence.


Logging

The server entry writes one JSON line per event to stdout, or to the logger you pass in.

{ "level": "info",  "message": "dotads ads served", "slots": 3, "filled": 2, "durationMs": 120 }
{ "level": "info",  "message": "dotads events reported", "received": 40, "stored": 39, "skipped": 1 }
{ "level": "error", "message": "dotads request failed", "code": "SCOPE_NOT_GRANTED", "status": 403, "endpoint": "/ads/batch", "stacktrace": "..." }

Never logged: the service token, DOTBOTS_APP_SECRET, full trackingIds (the first eight characters only, through redactTrackingId), user ids, request bodies.


Test and production

The SDK contains no environment code: the server SDK stamps env from the deployment. What differs on test:

  • @test is a separate application row, with its own inventory and its own grants.
  • High scopes must be switched on by hand on test.
  • Everything is measured and nothing is ever charged.
  • No network campaigns on test: only your own inventory.
  • Test data lives 30 days, with a limit of 100,000 events.
  • TEST_USAGE_QUOTA_EXCEEDED is a day quota. Wait for the next day; never retry it.

What the SDK never does

  • Put a service token, a dotAds endpoint or a secret in the browser.
  • Fetch or refresh a token itself: that is dotbots.service('dotAds', ...).
  • Put a user id, an organisation, an application or an environment in a request body.
  • Truncate advertisement text, or shrink a format to fit.
  • Report a claim as an event (type: 'claim' is skipped locally).
  • Retry a 403, a TEST_USAGE_QUOTA_EXCEEDED, or claims.create.
  • Write to localStorage, sessionStorage or a cookie, or open a native dialog (lint enforces it).
  • Proxy media: image URLs come straight from the ad.
  • Break your application. A dotAds failure is a fallback, never a crash.

Development

deno task lint     # includes the no-browser-storage plugin
deno task check    # type-checks both entrypoints
deno task test     # the full suite, no network calls of its own
npm run build      # emits dist/ for npm consumers

The tests mock DotBotsBackend.service() and dotAds itself, so the suite makes no calls to either. The npm packages the React tests use (react, jsdom, @testing-library/react) are fetched into the Deno cache on the first run.