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

@tipitip/embed

v0.1.1

Published

Drop-in React component for per-paragraph cUSD tipping on Celo. Add a tippable article block to any blog with two lines of code.

Readme

@tipitip/embed

npm version license: MIT

Drop-in React component for embedding a TipiTip article in any blog, newsletter, or React app. Each paragraph gets a live tip counter pulled directly from the on-chain TipJar contract on Celo Mainnet.

Add tippable paragraphs to your existing blog with two lines of code — no wallet integration in your codebase, no smart-contract knowledge needed. The host stays where their audience already is (Substack, dev.to, personal Next.js site, WordPress, etc.).

import { TipParagraphs } from "@tipitip/embed";

export default function MyArticle() {
  return <TipParagraphs articleId="0x73e8…" />;
}

Why use this

| If you're already writing on... | What this embed does for you | |---|---| | Substack / Ghost / Medium | Add an iframe wrapper that lets readers tip you in cUSD without ever leaving the article page (tip transactions happen on tipitip-sable.vercel.app in a new tab; tip counts stream back to your post). | | dev.to / Hashnode / WordPress | Same as above — drop the component into your custom code block and ship. | | Your own Next.js / Vite / Remix blog | Native React install, zero config. Live tip counters under each paragraph. |

You publish on TipiTip once (free, ~10 seconds), get an articleId, then embed the same articleId everywhere. Every paragraph carries its own on-chain identity, so tips made via the embed and tips made on tipitip-sable.vercel.app aggregate to the same balance.


Install

pnpm add @tipitip/embed
# or
npm i @tipitip/embed
# or
yarn add @tipitip/embed

Peer dependencies you must already have: react >= 18 and react-dom >= 18. No viem, no wagmi, no @noble/hashes in your bundle. The embed is intentionally dependency-light so it loads cleanly inside MDX, Substack custom HTML, and other constrained surfaces.


Usage

1. Basic

import { TipParagraphs } from "@tipitip/embed";

export function ArticleEmbed() {
  return (
    <TipParagraphs articleId="0x73e89882ff0c550111e5b4b5a1967582bdda9cb8000000000000000000000000" />
  );
}

2. Custom origin (for testing against a staging deploy)

<TipParagraphs
  articleId="0x…"
  baseUrl="https://my-tipitip-fork.vercel.app"
/>

3. Read from Celo Sepolia (testnet) instead of Mainnet

<TipParagraphs articleId="0x…" chainId={11142220} />

4. Faster polling

{/* Refresh tip counters every 10s instead of the default 30s */}
<TipParagraphs articleId="0x…" pollIntervalMs={10_000} />

{/* Disable polling — counters only update on mount */}
<TipParagraphs articleId="0x…" pollIntervalMs={0} />

5. Customize the loading + error states

<TipParagraphs
  articleId="0x…"
  loadingFallback={<MySpinner />}
  errorFallback={<MyEmptyState />}
/>

6. Style the wrapper

<TipParagraphs
  articleId="0x…"
  className="prose prose-lg dark:prose-invert"
  style={{ maxWidth: 720, margin: "0 auto" }}
/>

The embed renders semantic HTML (<article> + <section> per paragraph + <a> for tip buttons) and ships its own minimal inline styles. You can override anything via class selectors:

.tipitip-embed { /* outer wrapper */ }
.tipitip-embed__paragraph { /* one paragraph block */ }
.tipitip-embed__body { /* rendered markdown */ }
.tipitip-embed__tip { /* heart counter button */ }

Props

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | articleId | `0x${string}` | yes | — | The 32-byte article id registered on-chain. Get one by publishing an article at tipitip-sable.vercel.app/write. | | baseUrl | string | no | https://tipitip-sable.vercel.app | Origin serving the TipiTip API. | | chainId | 42220 \| 11142220 | no | 42220 | Which Celo network to read tip events from. | | pollIntervalMs | number | no | 30000 | How often to refresh tip counts. Set 0 to disable. | | loadingFallback | React.ReactNode | no | "Loading article…" | Rendered while the body is fetching. | | errorFallback | React.ReactNode | no | "Failed to load article: …" | Rendered if the body fetch fails. | | className | string | no | — | Applied to the outer <article>. | | style | React.CSSProperties | no | — | Inline styles on the outer <article>. |


How tipping actually flows

This is a read-and-redirect embed. The clickable heart counter under each paragraph is a deep link to the canonical TipiTip article at {baseUrl}/a/{articleId}#p-{N}. When the reader clicks it:

  1. A new tab opens to TipiTip with the targeted paragraph scrolled into view.
  2. The reader connects their Celo wallet (MiniPay is auto-detected — no popup).
  3. They approve a one-time cUSD allowance, then tap the heart to fire tipParagraph(...).
  4. The on-chain Tipped event is emitted with the same paragraphKey your embed already knows about.
  5. Within 30 seconds the embedded counter on your blog reflects the new tip.

This split keeps your embed dependency-free and your wallet-aware UI on the verified TipiTip domain — your readers' trust signals (verified contract, brand recognition, multi-wallet support) all live at the canonical destination.

A future v0.2 will add an opt-in <TipParagraphsConnected> component that lifts the entire flow inline using a wagmi peer dependency, for hosts that already ship a wallet stack.


How tip counts get aggregated

The embed calls GET {baseUrl}/api/tip-stats/{articleId}?chainId={chainId}. The TipiTip server:

  1. Reads the article body from decentralized storage (Vercel Blob).
  2. Re-splits it with the exact same algorithm the embed uses (splitParagraphs).
  3. Scans the last ~200,000 blocks of Tipped events on the configured network.
  4. Maps each event's paragraphKey back to the corresponding paragraph index.
  5. Returns { paragraphs: { "0": { count, total }, "2": { count, total }, ... } }.

The endpoint is public, CORS-permissive, and cached for 30 seconds at the edge.


Caveats and limits

  • Body edits. If the author edits the article body so a paragraph's text changes, old tips on that paragraph stay claimable on-chain by the author but stop showing up under the new text. That's intentional — paragraphs are tipped because of what they say.
  • Block range. The default scan window is the last ~3-4 days of blocks. Tips made before that aren't reflected (yet — a subgraph migration is on the roadmap for unbounded history once volume warrants it).
  • No SSR. The component uses useEffect for both the body fetch and the stats poll, so it always renders the loading state on the server. If you need SSR-rendered article text, fetch the body yourself from {baseUrl}/api/articles/{articleId} and pass it through.

Demo

A worked example with a real on-chain article:

<TipParagraphs articleId="0x7ff67b58d8cbc9deaa11b8e1f6cf95dba0e2f97d8d2f8a8a9e51ba4e6df8a25f" />

The corresponding rendering on the canonical site: https://tipitip-sable.vercel.app/a/0x7ff67b58d8cbc9deaa11b8e1f6cf95dba0e2f97d8d2f8a8a9e51ba4e6df8a25f


License

MIT © TipiTip