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

@onchainsuite/sdk

v0.5.0

Published

In-app, web push and mobile push notifications for any dApp — wallet-authenticated, real-time, and lightweight.

Readme

@onchainsuite/sdk

Dead-simple in-app push notifications for any dApp. Wallet-authenticated, real-time, and tiny. Built-in toast UI you can fully restyle or render your own.

import { OnchainSuite } from "@onchainsuite/sdk";

const os = new OnchainSuite("pk_live_yourorg_xxx");
await os.start(); // wallet signs in → notifications start showing

That's the whole integration. No framework required — socket.io-client is bundled as a dependency, nothing else to install.

OS notifications (web push)

The in-app toast only reaches someone while your page is open. Web push reaches them when the browser is closed — including on a phone, through a mobile browser or a home-screen PWA on iOS 16.4+.

It needs no configuration from you. OnchainSuite holds one platform-wide key.

1. Serve the service worker

Copy node_modules/@onchainsuite/sdk/public/onchainsuite-sw.js to the root of your site, so it is served at /onchainsuite-sw.js.

The path matters: a service worker only controls pages at or below its own URL, so one served from /static/sw.js covers /static and nothing else.

Already have a service worker? Do not add a second — import ours into yours:

importScripts("/onchainsuite-sw.js");

Two workers at the same scope fight over the same push events, and only one wins.

2. Ask, at the right moment

const client = createClient("pk_live_…", { apiBaseUrl: "https://api.onchainsuite.com" });
await client.start();

// Later — from a button, after the user did something a notification helps with
if (client.pushStatus() === "prompt") {
  const status = await client.enablePush();   // "granted" | "denied" | …
}

Do not call enablePush() on page load. A user who taps "Block" is never asked again — the browser remembers and there is no API to re-prompt. A prompt at a moment they have no context for does not cost you a retry, it costs you that user permanently.

The moment that works is right after they did something a notification would obviously help with — placed an order, opened a position, joined a list.

pushStatus() returns "unsupported" | "denied" | "prompt" | "granted". Treat the first two the same: do not show a "turn on notifications" button, because pressing it can no longer do anything.

3. That is it

start() re-registers a browser that has already granted permission, without prompting. That is not an optimisation — browsers expire push subscriptions without telling anyone, and re-registering on every load is the only way a silently-rotated subscription is ever noticed.

To turn it off:

await client.disablePush();

What arrives, and when

A notification takes exactly one path per recipient:

| | | | ------------------------- | ------------------------------------------ | | Your page is open | in-app toast only | | Page closed, push enabled | OS notification | | Neither | held, and delivered next time they connect |

Never both — a phone banner about the thing already on screen reads as a bug.

The OS notification carries a summary (title, body, link), not the full content: it renders on a lock screen where anyone holding the phone can read it. Both carry the same deliveryId, and the SDK dedupes on it — so a user who taps a notification and lands on your page does not see it twice.

Mobile (React Native)

Native OS notifications on iOS and Android. Same backend, same analytics, same deliveryId dedupe as web.

Why you fetch the token, not us

Getting an APNs or FCM token needs native code, and your app already has an opinion about how — expo-notifications, @react-native-firebase/messaging, or your own bridge. Bundling one would pin your React Native version, force a linking step, and break web builds of this package for everyone who never wanted mobile.

So you get the token however you already do, and hand it over:

import messaging from "@react-native-firebase/messaging";

await client.start(walletAddress);

// YOUR app chooses when to ask — see the warning below
await messaging().requestPermission();
const token = await messaging().getToken();
await client.registerDevice(token, "android");

// Tokens rotate. Re-register when they do:
messaging().onTokenRefresh((t) => client.registerDevice(t, "android"));

Call registerDevice on every app launch, not only after the permission prompt. Tokens rotate on reinstall and sometimes on OS upgrade, and nothing announces it — re-registering is the only way a rotated token is ever noticed. Repeat calls upsert; they do not accumulate devices.

The permission prompt is yours to time. A user who declines is never asked again on either platform. Prompting on first launch is how apps lose half their addressable audience permanently.

To stop notifications for one device:

await client.unregisterDevice(token);

What your customer has to set up

Native push needs a credential from your Apple or Google account, uploaded once in OnchainSuite settings — an APNs .p8, or a Firebase service account JSON. It is verified on upload, so you find out immediately whether it works.

Web push needs none of this. If you have a website as well as an app, web push reaches phones with zero setup — start there.

Handling a tap

Route the notification's deliveryId into your app and the SDK dedupes it against the in-app render, so a user who taps a notification and lands in your app does not see the same thing twice.

Options

createClient("pk_live_…", {
  push: { serviceWorkerPath: "/onchainsuite-sw.js" },  // default
  // push: false,                                       // disable entirely
});

Install

npm i @onchainsuite/sdk

No build step (plain HTML, PHP, any server-rendered page)

One self-contained <script> — dist/inapp.js bundles socket.io-client, so there's nothing else to load. Point the tag at a publishable pk_* key and it auto-inits window.onchainsuite:

<script src="https://cdn.jsdelivr.net/npm/@onchainsuite/sdk/dist/inapp.js"
        data-key="pk_live_yourorg_xxx"></script>
<script>
  // `window.onchainsuite` is a ready instance — start it once a wallet connects:
  document.querySelector("#connect").onclick = () =>
    window.onchainsuite.start(/* walletAddress?, or omit to auto-detect an injected wallet */);
  window.onchainsuite.on("notification", (n) => console.log(n));
</script>
  • data-key is a publishable pk_live_… / pk_test_… key — never the sk_* secret key (that's server-side only).
  • Optional: data-api="https://api.onchainsuite.com" to override the endpoint; data-autostart to begin the wallet handshake on load.
  • The same file also exposes the class, so you can construct it yourself instead: const os = new OnchainSuite("pk_live_…").

(PHP/any backend: render the tag above for the browser render, and use the plain REST API — sk_* Bearer — for server-side sends. The SDK is browser-only.)

ESM from a CDN, if you prefer modules (load socket.io-client first so the SDK finds window.io):

<script src="https://cdn.socket.io/4.8.3/socket.io.min.js"></script>
<script type="module">
  import { OnchainSuite } from "https://esm.sh/@onchainsuite/sdk";
  const os = new OnchainSuite("pk_live_yourorg_xxx", {
    apiBaseUrl: "https://api.onchainsuite.com",
  });
  await os.start();
</script>

Usage

1. Simplest (uses the connected wallet)

const os = new OnchainSuite("pk_live_...", {
  apiBaseUrl: "https://api.onchainsuite.com",
});
await os.start(); // discovers a browser-extension wallet and prompts it to sign

The zero-config path auto-detects injected wallets: EVM via EIP-6963 (MetaMask, Rabby, Coinbase, Brave, Zerion, Frame — it picks the wallet that holds your address, or the one already connected, rather than guessing window.ethereum), then Solana (Phantom, Solflare, Backpack, Glow). Pass an address to start(address) to skip discovery.

2. Bring your own signer (wagmi / WalletConnect / Privy / Dynamic / Solana adapter)

signMessage is the universal path — it works for every wallet, including ones that don't inject (WalletConnect, Privy, Dynamic, Coinbase Wallet SDK, smart/AA wallets, Solana wallet-adapter). Your connector signs; the SDK never touches the wallet.

const os = new OnchainSuite("pk_live_...", {
  apiBaseUrl: "https://api.onchainsuite.com",
  signMessage: async (message) => signMessageAsync({ message }), // your wallet lib
});
await os.start(walletAddress);

3. React

useEffect(() => {
  const os = new OnchainSuite("pk_live_...", {
    apiBaseUrl: "https://api.onchainsuite.com",
  });
  os.start();
  return () => os.stop();
}, []);

Send a notification (from your backend)

Sending is a server-to-server call authenticated with your secret key (sk_*) — never expose it in the browser. One POST:

// Node / any backend
await fetch("https://api.onchainsuite.com/api/v1/inapp/push", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer sk_live_xxx",
  },
  body: JSON.stringify({
    walletAddress: "0xabc...",
    title: "GM 👋",
    body: "Your rewards are ready to claim.",
    ctaLabel: "Claim",
    ctaUrl: "https://app.myprotocol.xyz/rewards",
  }),
});

The recipient's dApp (running the SDK from Usage above) receives it in real time — or on next connect if they're offline.

Make it yours — display is fully flexible

Everything about the built-in UI is overridable. Change timing, position, colors, or opt out entirely.

new OnchainSuite("pk_live_...", {
  display: {
    position: "top-right",      // bottom-right | bottom-left | top-right | top-left
    accent: "#00e0b8",          // CTA / accent color
    background: "#0b0d12",
    foreground: "#ffffff",
    duration: 12000,            // ms on screen; 0 = sticky until dismissed
    maxVisible: 4,
    cardStyle: { borderRadius: "20px" }, // any CSS on the card
  },
});

Per-notification display time — pass a function:

new OnchainSuite("pk_live_...", {
  display: {
    duration: (n) => (n.cta ? 0 : 6000), // CTAs stay sticky, others auto-hide
  },
});

Render it 100% yourself (your own toast/modal/banner) — return false to skip the built-in UI. Analytics still work via the provided actions:

new OnchainSuite("pk_live_...", {
  display: false, // turn the built-in UI off
  onNotification: (n, actions) => {
    myUI.toast({
      title: n.title,
      body: n.body,
      cta: n.cta,
      onShow: () => actions.report("viewed"),
      onClick: () => actions.click(), // reports "clicked" + opens the CTA url
      onClose: () => actions.dismiss(),
    });
  },
});

API

new OnchainSuite(publishableKey, options?)

publishableKey — pk_live_* / pk_test_* from Dashboard → Integrations → In-App.

| Option | Type | Default | Notes | | ---------------- | ---------------------------------- | ----------------- | --------------------------------------------------- | | apiBaseUrl | string | same-origin | API host, no /api/v1. | | signMessage | (msg, wallet) => Promise<string> | injected wallet | Custom signer — the universal path (WalletConnect / Privy / Dynamic / Solana adapter). | | provider | EIP-1193 | EIP-6963 / window.ethereum | Force a specific EVM provider; else auto-discovered. | | display | DisplayOptions \| false | enabled | false = headless. See table below. | | onNotification | (n, actions) => boolean \| void | — | Custom handler; return false to skip built-in UI. | | ioClient | io factory | auto | Provide socket.io-client's io explicitly. | | debug | boolean | false | Verbose logging. |

DisplayOptions: position, accent, background, foreground, duration (number | (n) => number, 0 = sticky), maxVisible, zIndex, cardStyle.

Methods

  • start(walletAddress?) → Promise<void> — auth + start receiving.
  • stop() — disconnect + clear toasts.
  • on(event, cb) → unsubscribe — "notification" | "connected" | "disconnected" | "error".
  • report(notification, type) — "delivered" | "viewed" | "dismissed" | "clicked".

Notification shape

interface Notification {
  deliveryId: string;
  campaignRunId: string;
  walletAddress: string;
  title: string;
  body: string;
  cta?: { label: string; url: string };
  createdAt: string;
  expiresAt: string;
}

How it works & security

  1. POST /api/v1/inapp/challenge (publishable-key auth) → a message to sign.
  2. Wallet signs (EIP-191); POST /api/v1/inapp/verify → short-lived session JWT + WebSocket URL.
  3. Socket.IO connects to /api/v1/inapp/register with the token, auto-reconnects, and replays notifications missed while offline.
  4. Each notification is reported delivered, then rendered (or handed to onNotification), then viewed/clicked/dismissed from the UI.

Security notes:

  • No secrets in the browser. Only the publishable key ships to the client; it's scoped by allow-listed origins (Dashboard → Integrations → In-App → Origins). Wrong origin → 401.
  • Wallet ownership is proven by an EIP-191 signature over a single-use, 5-minute nonce; the challenge/verify endpoints are rate-limited server-side.
  • XSS-safe rendering — content is set via textContent, never innerHTML.

Docs