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

@boostxyz/tbi-sdk

v0.4.0

Published

TypeScript SDK for Boost time-based incentives

Readme

@boostxyz/tbi-sdk

Framework-agnostic TypeScript SDK for Boost time-based incentives. It wraps the public /v1/ TBI API, returns SDK-native types (bigint, viem Address/Hex, Date | null), and includes viem-friendly claim helpers.

V1 does not ship React hooks, Vue/Svelte adapters, or UI components. Use the methods here with your framework's data-fetching layer of choice.

Documentation

  • Quickstart — build your first integration: discover a campaign, read a user's rewards, and claim, in one short walkthrough. Start here.
  • Concepts — how time-based incentives work, plus campaign lifecycle, modes, APY fields, merkle claims, and forwarder deposits. Read this to understand the domain.
  • API reference — every method, type, and field, generated from source. Regenerate after changing src/ with pnpm --filter @boostxyz/tbi-sdk docs. To browse everything — these guides plus the full reference — as one searchable HTML site, run pnpm run docs:open inside packages/sdk (written to the gitignored build/docs/).

This README is a cookbook of the common flows — install, discover campaigns, and claim. For a guided first integration, follow the Quickstart.

Install

pnpm add @boostxyz/tbi-sdk viem
npm install @boostxyz/tbi-sdk viem

viem is a peer dependency because claim helpers accept viem WalletClient instances and exported types use viem Address/Hex.

Create a Client

import { createTbiClient } from "@boostxyz/tbi-sdk";

const tbi = createTbiClient();

Use a non-production API or custom headers when needed:

const tbi = createTbiClient({
  baseUrl: "https://staging-api.example.com",
  refId: "partner_demo",
  retry: { attempts: 2, baseDelayMs: 100 },
});

Boost assigns partner refId values manually. When configured, the SDK sends it on every API request using the X-Boost-Ref-Id header so Boost can attribute SDK/API/Forwarder usage. You can override it per call when needed:

await tbi.campaigns.active(undefined, { refId: "partner_demo_experiment" });

Does This Vault Have a Boost Campaign?

const { data } = await tbi.campaigns.active({
  target: { chainId: 8453, address: vaultAddress },
});
const campaign = data[0];

target.chainId is the event chain where the vault, pool, or token position is tracked. campaign.id.chainId is the reward chain where users claim.

Campaign Discovery

const campaigns = await tbi.campaigns.list({
  chainId: 480,
  status: ["active", "ended"],
  userAddress,
  claimable: true,
  limit: 20,
  offset: 0,
});

Single-campaign and stats-only reads:

const campaign = await tbi.campaigns.get({ chainId: 480, campaignIndex: 1 });
const stats = await tbi.campaigns.stats({ chainId: 480, campaignIndex: 1 });

Some campaigns are omitted from discovery (campaigns.list, campaigns.active) by design. Direct campaigns.get / campaigns.stats, plus user reward and claim methods, still return them when you have the campaign id.

APY Fields

boostApyBps and protocolApyBps are separate basis-point fields:

  • boostApyBps: APY paid by the Boost TBI campaign.
  • protocolApyBps: APY from the underlying vault, pool, or protocol.
  • null: unavailable or not meaningful for the campaign state.

Do not add them together unless your UI explicitly wants to present a combined estimate.

function formatBps(value: bigint | null) {
  return value === null ? "N/A" : `${Number(value) / 100}%`;
}

console.log(formatBps(campaign.boostApyBps));
console.log(formatBps(campaign.protocolApyBps));

Forwarder-Required Campaigns

Discovery methods exclude forwarder-required campaigns by default. These campaigns only reward positions that entered through the Boost Forwarder contract.

Opt in when your integration will route deposits through the Forwarder:

const { data } = await tbi.campaigns.active({
  target: { chainId: 8453, address: vaultAddress },
  includeForwarderRequired: true,
});

const forwarderOnly = data.filter((campaign) => campaign.requiresForwarderDeposit);

Direct campaigns.get(id) returns the campaign regardless of this flag.

Build the ordered transaction list for a supported forwarder target:

const campaign = forwarderOnly[0];
if (!campaign) throw new Error("No forwarder campaign");

const deposit = await tbi.forwarder.buildDeposit({
  target: campaign.target,
  amount: 1_000_000n,
  sender: userAddress,
});

for (const tx of deposit.transactions) {
  await walletClient.sendTransaction({
    account: userAddress,
    chain: walletClient.chain,
    to: tx.to,
    data: tx.data,
    value: tx.value,
  });
}

buildDeposit currently supports direct same-chain deposits into registered Forwarder targets. It infers the accepted token from the server registry and includes an ERC-20 approval transaction only when the user's allowance is insufficient.

Multi-asset targets (e.g. Lido Earn) accept more than one input asset. Each target lists its directly depositable assets in acceptedTokens, with native ETH represented by the zero address. Pass one of them as inputToken; when omitted, the deposit uses the target's default acceptedToken:

const deposit = await tbi.forwarder.buildDeposit({
  target: campaign.target,
  amount: 1_000_000_000_000_000_000n,
  sender: userAddress,
  inputToken: "0x0000000000000000000000000000000000000000", // native ETH
});

List supported targets when you need to preflight UI state:

const targets = await tbi.forwarder.targets();
const supported = targets.find(
  (target) =>
    target.chainId === campaign.target.chainId &&
    target.targetAddress === campaign.target.address,
);

User Rewards and Claim Proofs

const rewards = await tbi.rewards.forUser(userAddress, {
  chainId: 480,
  status: ["active", "finalized"],
});

const claimable = rewards.data.filter((reward) => reward.claimable > 0n);

Fetch a claim proof:

const proof = await tbi.claims.get(
  { chainId: 480, campaignIndex: 1 },
  userAddress,
);

if (proof.claimable > 0n) {
  console.log(proof.cumulativeAmount, proof.proof);
}

Batch claim statuses:

const statuses = await tbi.claims.statuses(userAddress, [
  { chainId: 480, campaignIndex: 1 },
  { chainId: 480, campaignIndex: 2 },
]);

Claim with viem

Short full claim flow:

const proof = await tbi.claims.get(id, address);
if (walletClient.chain?.id !== id.chainId) throw new Error("Wrong chain");
if (proof.claimable === 0n) throw new Error("Nothing to claim");
const { hash } = await tbi.claim({ walletClient, id, address, proof });

tbi.claim() fetches the proof for you if proof is omitted:

const { hash } = await tbi.claim({ walletClient, id, address });

Dry-run before signing:

const result = await tbi.claim.simulate({ walletClient, id, address });

if (!result.willSucceed) {
  console.error(result.revertReason);
}

Raw Calldata

Use encodeClaim when your stack submits transactions outside viem, such as ethers, Gnosis Safe, or account abstraction.

const proof = await tbi.claims.get(id, address);
const { to, data, value } = tbi.encodeClaim({ id: proof.id, proof });

Ethers:

await signer.sendTransaction({ to, data, value });

Gnosis Safe:

await safeSdk.createTransaction({
  transactions: [{ to, data, value: value.toString() }],
});

Account abstraction:

await smartAccount.sendUserOperation({
  calls: [{ to, data, value }],
});

encodeClaim passes proof.cumulativeAmount verbatim. The TBI Manager contract subtracts the amount already claimed on-chain.

Claim Multiple Campaigns

Claim specific campaign IDs on the same reward chain:

const result = await tbi.claimAll({
  walletClient,
  address,
  ids: [
    { chainId: 480, campaignIndex: 1 },
    { chainId: 480, campaignIndex: 2 },
  ],
});

console.log(result.hash, result.claimed);

Or let the SDK fetch claimable rewards for one reward chain:

const result = await tbi.claimAll({
  walletClient,
  address,
  chainId: 480,
});

claimAll filters zero-claimable proofs, rejects mixed reward chains, and submits one Multicall3 transaction. V1 is all-or-nothing: if one subcall reverts, the whole transaction reverts.

Raw Multicall3 calldata:

const proofs = await Promise.all(ids.map((id) => tbi.claims.get(id, address)));
const { to, data, value } = tbi.encodeClaimAll({ proofs });

Referrals

Campaigns can fund one or more referral pools — each in its own token, not necessarily the reward token — that pay referrers a share of what their referred users earn. A referral "code" is just the referrer's wallet address — build links that carry it; there is nothing to mint or register.

Bind a referred user (the referee) before their first deposit. The referee signs an EIP-712 message with their wallet; the first accepted bind wins. A bind is global — one per referee wallet, applying to every campaign — and earns the referrer a share of each referral campaign the referee first deposits into after binding:

const bind = await tbi.referrals.bind({
  walletClient, // the referee's wallet, on any network
  referrer: referrerAddress, // from your referral link
});

console.log(bind.created); // false when the same bind already existed

Bind rejections are TbiApiErrors with machine-readable codes: INVALID_SIGNATURE, SELF_REFERRAL, WALLET_NOT_LINKED, REFERRER_NOT_LINKED, CYCLIC_REFERRAL, and ALREADY_BOUND.

Track a referrer's earnings:

const stats = await tbi.referrals.stats(referrerAddress);

for (const entry of stats) {
  // One entry per referral pool of each campaign (entry.poolIndex), paid in
  // entry.rewardToken. "estimated" while the campaign is live (entry.amount is
  // the projected share of the pool); "final" once the post-finalize payout is
  // settled. entry.amount is null only from servers that predate the live projection.
  console.log(entry.poolIndex, entry.status, entry.refereeCount, entry.amount, entry.referralPool);
  // entry.claimTxHash / entry.claimedAt are set once the payout is claimed on-chain.
}

Claim after the campaign finalizes and the pool's referral root is published. Referral payouts go through the TBI Manager's claimReferral, which pays out of the pool's own ReferralDistributor contract — the reward-claim methods tbi.claim and tbi.claimAll do not apply — and each is one-shot per pool, claimable only during the distributor's on-chain claim window. Pass poolIndex to claim from a pool other than the first:

const { hash } = await tbi.referrals.claim({
  walletClient,
  id: { chainId: 480, campaignIndex: 1 },
  poolIndex: 0, // from the stats entry; defaults to 0
  referrer: referrerAddress,
});

Dry-run first, or encode raw calldata for non-viem stacks:

const result = await tbi.referrals.claim.simulate({ walletClient, id, referrer });

const proof = await tbi.referrals.proof(id, referrer, { poolIndex: 0 }); // TbiNotFoundError until published
const { to, data, value } = tbi.referrals.encodeClaim({ proof });

Signing the bind outside viem? Build the EIP-712 payload with referralBindTypedData, sign it with the referee's EOA (smart-wallet EIP-1271 signatures are not supported), and submit the signed fields with tbi.referrals.submitBind.

See Concepts — Referrals for the full rules: global first-touch binding, no retroactive attribution, estimated vs. final earnings, and the claim window.

wagmi + TanStack Query

import { useQuery } from "@tanstack/react-query";
import { type Address, type CampaignId, createTbiClient } from "@boostxyz/tbi-sdk";
import { useAccount, useWalletClient } from "wagmi";

const tbi = createTbiClient();

export function useBoostCampaignForVault(chainId: number, vault: Address) {
  return useQuery({
    queryKey: ["tbi", "campaigns", "active", chainId, vault],
    queryFn: () => tbi.campaigns.active({ target: { chainId, address: vault } }),
  });
}

export function ClaimButton({ id }: { id: CampaignId }) {
  const { address } = useAccount();
  const { data: wallet } = useWalletClient({ chainId: id.chainId });
  const disabled = !wallet || !address || wallet.chain?.id !== id.chainId;

  return (
    <button
      disabled={disabled}
      onClick={() => tbi.claim({ walletClient: wallet!, id, address: address! })}
    >
      Claim
    </button>
  );
}

Footguns

Bigint Persistence

SDK responses contain bigint. In-memory caches like TanStack Query and SWR handle that fine, but JSON.stringify(1n) throws. If you persist cache data to localStorage or IndexedDB, add a serializer.

export function stringifyWithBigint(value: unknown) {
  return JSON.stringify(value, (_, entry) =>
    typeof entry === "bigint" ? `${entry.toString()}n` : entry,
  );
}

export function parseWithBigint(value: string) {
  return JSON.parse(value, (_, entry) => {
    if (typeof entry === "string" && /^\d+n$/.test(entry)) {
      return BigInt(entry.slice(0, -1));
    }
    return entry;
  });
}

Wallet Client Availability

walletClient can be missing until the wallet is connected and on the requested chain. Gate claim calls on all of these:

const canClaim =
  !!walletClient &&
  !!address &&
  walletClient.chain?.id === campaign.id.chainId &&
  proof.claimable > 0n;

Errors

API errors are typed:

  • TbiValidationError: invalid query/path/body params (INVALID_PARAMS).
  • TbiNotFoundError: campaign or proof not found.
  • TbiApiError: other API errors, including referral bind rejections — check error.code.
  • TbiNetworkError: fetch or JSON parsing failures.
  • TbiClaimError: SDK-side claim validation failures.

Wallet write failures are propagated from the wallet client so integrations can surface wallet-specific rejection or revert messages.