{"_id":"@boostxyz/tbi-sdk","name":"@boostxyz/tbi-sdk","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@boostxyz/tbi-sdk","version":"0.1.0","private":false,"description":"TypeScript SDK for Boost time-based incentives","license":"UNLICENSED","type":"module","sideEffects":false,"main":"./dist/cjs/index.js","module":"./dist/esm/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"peerDependencies":{"viem":">=2.21.3 <3"},"devDependencies":{"@types/node":"^22.15.21","typedoc":"0.28.19","typedoc-plugin-markdown":"4.12.0","typescript":"5.9.2","viem":"^2.21.3","vitest":"^4.0.18"},"scripts":{"build":"node scripts/clean.mjs && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json && tsc -p tsconfig.types.json && node scripts/write-cjs-package-json.mjs","docs":"typedoc","docs:check":"typedoc --emit none","docs:site":"typedoc --options typedoc.site.json","docs:open":"typedoc --options typedoc.site.json && open build/docs/index.html","typecheck":"tsc --noEmit","lint":"biome lint .","test":"vitest run","test:watch":"vitest"},"_id":"@boostxyz/tbi-sdk@0.1.0","_integrity":"sha512-7FCllG7PPQktsKAwTV57+TU+Y8mmb4ebDNfbX6f/yeWJwryipq56EfuKe59y23dPcxG0J3g11kOJi4FUQJux4w==","_resolved":"/private/var/folders/rk/pjkv91zd4ms0lg48dp0407j80000gn/T/38f80c274505c9ed846454221c1ea0f8/boostxyz-tbi-sdk-0.1.0.tgz","_from":"file:boostxyz-tbi-sdk-0.1.0.tgz","_nodeVersion":"24.10.0","_npmVersion":"11.6.1","dist":{"integrity":"sha512-7FCllG7PPQktsKAwTV57+TU+Y8mmb4ebDNfbX6f/yeWJwryipq56EfuKe59y23dPcxG0J3g11kOJi4FUQJux4w==","shasum":"122a06e0e06d80e390790ea132a5654c523f4665","tarball":"https://registry.npmjs.org/@boostxyz/tbi-sdk/-/tbi-sdk-0.1.0.tgz","fileCount":138,"unpackedSize":331300,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDXCCZ1IpX3lEOJN+7LEXZCRIu8Bjujnh0vYimAErIeyQIgVd0bwseAOAopbxGkqP3HkZ3WHv8HQMiddoS9nOzYaHM="}]},"_npmUser":{"name":"jon-boost","email":"jonathan@boost.xyz"},"directories":{},"maintainers":[{"name":"mmackz","email":"matthew@boost.xyz"},{"name":"jon-boost","email":"jonathan@boost.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/tbi-sdk_0.1.0_1781563783299_0.3666777265546892"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-15T22:49:43.079Z","0.1.0":"2026-06-15T22:49:43.487Z","modified":"2026-06-15T22:49:43.786Z"},"maintainers":[{"name":"mmackz","email":"matthew@boost.xyz"},{"name":"jon-boost","email":"jonathan@boost.xyz"}],"description":"TypeScript SDK for Boost time-based incentives","license":"UNLICENSED","readme":"# @boostxyz/tbi-sdk\n\nFramework-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.\n\nV1 does not ship React hooks, Vue/Svelte adapters, or UI components. Use the methods here with your framework's data-fetching layer of choice.\n\n## Documentation\n\n- [Quickstart](./docs/quickstart.md) — build your first integration: discover a campaign, read a user's rewards, and claim, in one short walkthrough. Start here.\n- [Concepts](./docs/concepts.md) — how time-based incentives work, plus campaign lifecycle, modes, APY fields, merkle claims, and forwarder deposits. Read this to understand the domain.\n- [API reference](./docs/reference/README.md) — 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/`).\n\nThis README is a cookbook of the common flows — install, discover campaigns, and claim. For a guided first integration, follow the [Quickstart](./docs/quickstart.md).\n\n## Install\n\n```bash\npnpm add @boostxyz/tbi-sdk viem\n```\n\n```bash\nnpm install @boostxyz/tbi-sdk viem\n```\n\n`viem` is a peer dependency because claim helpers accept viem `WalletClient` instances and exported types use viem `Address`/`Hex`.\n\n## Create a Client\n\n```ts\nimport { createTbiClient } from \"@boostxyz/tbi-sdk\";\n\nconst tbi = createTbiClient();\n```\n\nUse a non-production API or custom headers when needed:\n\n```ts\nconst tbi = createTbiClient({\n  baseUrl: \"https://staging-api.example.com\",\n  refId: \"partner_demo\",\n  retry: { attempts: 2, baseDelayMs: 100 },\n});\n```\n\nBoost 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:\n\n```ts\nawait tbi.campaigns.active(undefined, { refId: \"partner_demo_experiment\" });\n```\n\n## Does This Vault Have a Boost Campaign?\n\n```ts\nconst { data } = await tbi.campaigns.active({\n  target: { chainId: 8453, address: vaultAddress },\n});\nconst campaign = data[0];\n```\n\n`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.\n\n## Campaign Discovery\n\n```ts\nconst campaigns = await tbi.campaigns.list({\n  chainId: 480,\n  status: [\"active\", \"ended\"],\n  userAddress,\n  claimable: true,\n  limit: 20,\n  offset: 0,\n});\n```\n\nSingle-campaign and stats-only reads:\n\n```ts\nconst campaign = await tbi.campaigns.get({ chainId: 480, campaignIndex: 1 });\nconst stats = await tbi.campaigns.stats({ chainId: 480, campaignIndex: 1 });\n```\n\n### APY Fields\n\n`boostApyBps` and `protocolApyBps` are separate basis-point fields:\n\n- `boostApyBps`: APY paid by the Boost TBI campaign.\n- `protocolApyBps`: APY from the underlying vault, pool, or protocol.\n- `null`: unavailable or not meaningful for the campaign state.\n\nDo not add them together unless your UI explicitly wants to present a combined estimate.\n\n```ts\nfunction formatBps(value: bigint | null) {\n  return value === null ? \"N/A\" : `${Number(value) / 100}%`;\n}\n\nconsole.log(formatBps(campaign.boostApyBps));\nconsole.log(formatBps(campaign.protocolApyBps));\n```\n\n### Forwarder-Required Campaigns\n\nDiscovery methods exclude forwarder-required campaigns by default. These campaigns only reward positions that entered through the Boost Forwarder contract.\n\nOpt in when your integration will route deposits through the Forwarder:\n\n```ts\nconst { data } = await tbi.campaigns.active({\n  target: { chainId: 8453, address: vaultAddress },\n  includeForwarderRequired: true,\n});\n\nconst forwarderOnly = data.filter((campaign) => campaign.requiresForwarderDeposit);\n```\n\nDirect `campaigns.get(id)` returns the campaign regardless of this flag.\n\nBuild the ordered transaction list for a supported forwarder target:\n\n```ts\nconst campaign = forwarderOnly[0];\nif (!campaign) throw new Error(\"No forwarder campaign\");\n\nconst deposit = await tbi.forwarder.buildDeposit({\n  target: campaign.target,\n  amount: 1_000_000n,\n  sender: userAddress,\n});\n\nfor (const tx of deposit.transactions) {\n  await walletClient.sendTransaction({\n    account: userAddress,\n    chain: walletClient.chain,\n    to: tx.to,\n    data: tx.data,\n    value: tx.value,\n  });\n}\n```\n\n`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.\n\nMulti-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`:\n\n```ts\nconst deposit = await tbi.forwarder.buildDeposit({\n  target: campaign.target,\n  amount: 1_000_000_000_000_000_000n,\n  sender: userAddress,\n  inputToken: \"0x0000000000000000000000000000000000000000\", // native ETH\n});\n```\n\nList supported targets when you need to preflight UI state:\n\n```ts\nconst targets = await tbi.forwarder.targets();\nconst supported = targets.find(\n  (target) =>\n    target.chainId === campaign.target.chainId &&\n    target.targetAddress === campaign.target.address,\n);\n```\n\n## User Rewards and Claim Proofs\n\n```ts\nconst rewards = await tbi.rewards.forUser(userAddress, {\n  chainId: 480,\n  status: [\"active\", \"finalized\"],\n});\n\nconst claimable = rewards.data.filter((reward) => reward.claimable > 0n);\n```\n\nFetch a claim proof:\n\n```ts\nconst proof = await tbi.claims.get(\n  { chainId: 480, campaignIndex: 1 },\n  userAddress,\n);\n\nif (proof.claimable > 0n) {\n  console.log(proof.cumulativeAmount, proof.proof);\n}\n```\n\nBatch claim statuses:\n\n```ts\nconst statuses = await tbi.claims.statuses(userAddress, [\n  { chainId: 480, campaignIndex: 1 },\n  { chainId: 480, campaignIndex: 2 },\n]);\n```\n\n## Claim with viem\n\nShort full claim flow:\n\n```ts\nconst proof = await tbi.claims.get(id, address);\nif (walletClient.chain?.id !== id.chainId) throw new Error(\"Wrong chain\");\nif (proof.claimable === 0n) throw new Error(\"Nothing to claim\");\nconst { hash } = await tbi.claim({ walletClient, id, address, proof });\n```\n\n`tbi.claim()` fetches the proof for you if `proof` is omitted:\n\n```ts\nconst { hash } = await tbi.claim({ walletClient, id, address });\n```\n\nDry-run before signing:\n\n```ts\nconst result = await tbi.claim.simulate({ walletClient, id, address });\n\nif (!result.willSucceed) {\n  console.error(result.revertReason);\n}\n```\n\n## Raw Calldata\n\nUse `encodeClaim` when your stack submits transactions outside viem, such as ethers, Gnosis Safe, or account abstraction.\n\n```ts\nconst proof = await tbi.claims.get(id, address);\nconst { to, data, value } = tbi.encodeClaim({ id: proof.id, proof });\n```\n\nEthers:\n\n```ts\nawait signer.sendTransaction({ to, data, value });\n```\n\nGnosis Safe:\n\n```ts\nawait safeSdk.createTransaction({\n  transactions: [{ to, data, value: value.toString() }],\n});\n```\n\nAccount abstraction:\n\n```ts\nawait smartAccount.sendUserOperation({\n  calls: [{ to, data, value }],\n});\n```\n\n`encodeClaim` passes `proof.cumulativeAmount` verbatim. The TBI Manager contract subtracts the amount already claimed on-chain.\n\n## Claim Multiple Campaigns\n\nClaim specific campaign IDs on the same reward chain:\n\n```ts\nconst result = await tbi.claimAll({\n  walletClient,\n  address,\n  ids: [\n    { chainId: 480, campaignIndex: 1 },\n    { chainId: 480, campaignIndex: 2 },\n  ],\n});\n\nconsole.log(result.hash, result.claimed);\n```\n\nOr let the SDK fetch claimable rewards for one reward chain:\n\n```ts\nconst result = await tbi.claimAll({\n  walletClient,\n  address,\n  chainId: 480,\n});\n```\n\n`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.\n\nRaw Multicall3 calldata:\n\n```ts\nconst proofs = await Promise.all(ids.map((id) => tbi.claims.get(id, address)));\nconst { to, data, value } = tbi.encodeClaimAll({ proofs });\n```\n\n## wagmi + TanStack Query\n\n```tsx\nimport { useQuery } from \"@tanstack/react-query\";\nimport { type Address, type CampaignId, createTbiClient } from \"@boostxyz/tbi-sdk\";\nimport { useAccount, useWalletClient } from \"wagmi\";\n\nconst tbi = createTbiClient();\n\nexport function useBoostCampaignForVault(chainId: number, vault: Address) {\n  return useQuery({\n    queryKey: [\"tbi\", \"campaigns\", \"active\", chainId, vault],\n    queryFn: () => tbi.campaigns.active({ target: { chainId, address: vault } }),\n  });\n}\n\nexport function ClaimButton({ id }: { id: CampaignId }) {\n  const { address } = useAccount();\n  const { data: wallet } = useWalletClient({ chainId: id.chainId });\n  const disabled = !wallet || !address || wallet.chain?.id !== id.chainId;\n\n  return (\n    <button\n      disabled={disabled}\n      onClick={() => tbi.claim({ walletClient: wallet!, id, address: address! })}\n    >\n      Claim\n    </button>\n  );\n}\n```\n\n## Footguns\n\n### Bigint Persistence\n\nSDK 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.\n\n```ts\nexport function stringifyWithBigint(value: unknown) {\n  return JSON.stringify(value, (_, entry) =>\n    typeof entry === \"bigint\" ? `${entry.toString()}n` : entry,\n  );\n}\n\nexport function parseWithBigint(value: string) {\n  return JSON.parse(value, (_, entry) => {\n    if (typeof entry === \"string\" && /^\\d+n$/.test(entry)) {\n      return BigInt(entry.slice(0, -1));\n    }\n    return entry;\n  });\n}\n```\n\n### Wallet Client Availability\n\n`walletClient` can be missing until the wallet is connected and on the requested chain. Gate claim calls on all of these:\n\n```ts\nconst canClaim =\n  !!walletClient &&\n  !!address &&\n  walletClient.chain?.id === campaign.id.chainId &&\n  proof.claimable > 0n;\n```\n\n## Errors\n\nAPI errors are typed:\n\n- `TbiValidationError`: invalid query/path params.\n- `TbiNotFoundError`: campaign or proof not found.\n- `TbiApiError`: other API errors.\n- `TbiNetworkError`: fetch or JSON parsing failures.\n- `TbiClaimError`: SDK-side claim validation failures.\n\nWallet write failures are propagated from the wallet client so integrations can surface wallet-specific rejection or revert messages.\n","readmeFilename":"README.md","_rev":"1-a442dbc6bdd344d8ff6d1bf401d9e43a"}