Skip to main content

SDK

The Juicebox SDK provides React hooks and utilities for building frontend applications that interact with the Juicebox protocol. It handles contract interactions, data fetching, and provides type-safe interfaces for all protocol operations.

Quick Links

Most developers need these:

Packages

PackageDescriptionNPM
juice-sdk-reactReact hooks and context providersnpm
juice-sdk-coreCore utilities, types, and constantsnpm
revnet-sdkRevnet-specific hooks and utilitiesnpm

Installation

npm install juice-sdk-react juice-sdk-core wagmi viem @tanstack/react-query

For Revnet functionality:

npm install revnet-sdk

Requirements:

  • Node.js 20+
  • React 18.3+
  • wagmi 2.10+
  • viem 2.14+

Quick Start

1. Configure Wagmi

Create a wagmi configuration with the chains you want to support:

// lib/wagmi.ts
import { createConfig, http } from 'wagmi';
import { mainnet, optimism, base, arbitrum, sepolia } from 'wagmi/chains';

export const config = createConfig({
chains: [mainnet, optimism, base, arbitrum, sepolia],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_MAINNET_RPC),
[optimism.id]: http(process.env.NEXT_PUBLIC_OPTIMISM_RPC),
[base.id]: http(process.env.NEXT_PUBLIC_BASE_RPC),
[arbitrum.id]: http(process.env.NEXT_PUBLIC_ARBITRUM_RPC),
[sepolia.id]: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC),
},
});

2. Set Up Providers

Wrap your application with the required providers:

// app/providers.tsx
'use client';

import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from '@/lib/wagmi';

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}

3. Add JBProjectProvider

For project-specific pages, wrap your content with JBProjectProvider:

// app/project/[id]/page.tsx
import { JBProjectProvider } from 'juice-sdk-react';
import { sepolia } from 'viem/chains';

export default function ProjectPage({ params }: { params: { id: string } }) {
return (
<JBProjectProvider
projectId={BigInt(params.id)}
chainId={sepolia.id}
>
<ProjectDashboard />
</JBProjectProvider>
);
}

4. Use Hooks in Components

import {
useJBProjectMetadataContext,
useJBTokenContext,
useJBRulesetContext
} from 'juice-sdk-react';

function ProjectDashboard() {
const { metadata } = useJBProjectMetadataContext();
const { token } = useJBTokenContext();
const { ruleset, rulesetMetadata } = useJBRulesetContext();

if (metadata.isLoading) return <div>Loading...</div>;

return (
<div>
<h1>{metadata.data?.name}</h1>
<p>{metadata.data?.description}</p>
<p>Token: {token.data?.symbol}</p>
</div>
);
}

Common UI Patterns

The most common UI components developers need are Pay and Cash Out modals. Here are complete, copy-paste ready implementations.

Pay Modal

A complete pay form with amount input, token quote, and transaction execution:

'use client';

import { useState, useMemo } from 'react';
import { parseEther, formatEther } from 'viem';
import { useAccount, useWaitForTransactionReceipt } from 'wagmi';
import {
useJBContractContext,
useJBChainId,
useJBRulesetContext,
useJBTokenContext,
useWriteJbMultiTerminalPay,
} from 'juice-sdk-react';
import { getTokenAToBQuote, NATIVE_TOKEN } from 'juice-sdk-core';

interface PayModalProps {
projectId: bigint;
onSuccess?: () => void;
}

export function PayModal({ projectId, onSuccess }: PayModalProps) {
const [amount, setAmount] = useState('');
const [memo, setMemo] = useState('');

const { address, isConnected } = useAccount();
const chainId = useJBChainId();
const { contracts } = useJBContractContext();
const { ruleset, rulesetMetadata } = useJBRulesetContext();
const { token } = useJBTokenContext();

const {
writeContractAsync,
isPending: isWriting,
data: txHash,
} = useWriteJbMultiTerminalPay();

const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash: txHash,
});

// Calculate how many tokens user will receive
const tokensReceived = useMemo(() => {
if (!amount || !ruleset.data || !rulesetMetadata.data) return 0n;
try {
return getTokenAToBQuote(parseEther(amount), {
weight: ruleset.data.weight,
reservedPercent: rulesetMetadata.data.reservedPercent,
});
} catch {
return 0n;
}
}, [amount, ruleset.data, rulesetMetadata.data]);

const handlePay = async () => {
if (!address || !contracts.primaryNativeTerminal.data || !amount) return;

try {
const amountWei = parseEther(amount);

await writeContractAsync({
chainId,
address: contracts.primaryNativeTerminal.data,
args: [
projectId,
NATIVE_TOKEN,
amountWei,
address, // beneficiary - receives tokens
0n, // minReturnedTokens
memo || '',
'0x', // metadata
],
value: amountWei,
});

// Reset form on success
setAmount('');
setMemo('');
onSuccess?.();
} catch (error) {
console.error('Payment failed:', error);
}
};

const isLoading = isWriting || isConfirming;
const buttonDisabled = !isConnected || !amount || isLoading;

return (
<div className="pay-modal">
{/* Amount Input */}
<div className="input-group">
<label>Amount</label>
<div className="input-with-suffix">
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
step="0.01"
min="0"
disabled={isLoading}
/>
<span>ETH</span>
</div>
</div>

{/* Token Quote */}
{amount && Number(amount) > 0 && (
<div className="quote">
<span>You will receive:</span>
<strong>
{formatEther(tokensReceived)} {token.data?.symbol || 'tokens'}
</strong>
</div>
)}

{/* Memo Input */}
<div className="input-group">
<label>Memo (optional)</label>
<textarea
value={memo}
onChange={(e) => setMemo(e.target.value)}
placeholder="Leave a message..."
disabled={isLoading}
/>
</div>

{/* Pay Button */}
<button
onClick={handlePay}
disabled={buttonDisabled}
className="pay-button"
>
{!isConnected
? 'Connect Wallet'
: isWriting
? 'Confirm in wallet...'
: isConfirming
? 'Confirming...'
: isSuccess
? 'Success!'
: `Pay ${amount || '0'} ETH`}
</button>

{/* Success Message */}
{isSuccess && txHash && (
<p className="success">
Payment successful!{' '}
<a href={`https://etherscan.io/tx/${txHash}`} target="_blank">
View transaction
</a>
</p>
)}
</div>
);
}

Cash Out Modal

A complete redemption form with balance display, quote, and multi-chain support:

'use client';

import { useState, useMemo } from 'react';
import { parseEther, formatEther } from 'viem';
import { useAccount, useWaitForTransactionReceipt } from 'wagmi';
import {
useJBContractContext,
useJBChainId,
useJBTokenContext,
useNativeTokenSurplus,
useSuckersUserTokenBalance,
useWriteJbMultiTerminalCashOutTokensOf,
} from 'juice-sdk-react';
import {
NATIVE_TOKEN,
getTokenCashOutQuoteEth,
applyJbDaoCashOutFee,
} from 'juice-sdk-core';

interface CashOutModalProps {
projectId: bigint;
onSuccess?: () => void;
}

export function CashOutModal({ projectId, onSuccess }: CashOutModalProps) {
const [amount, setAmount] = useState('');

const { address, isConnected } = useAccount();
const chainId = useJBChainId();
const { contracts } = useJBContractContext();
const { token, totalOutstanding } = useJBTokenContext();
const { data: surplus } = useNativeTokenSurplus();
const { data: userBalance } = useSuckersUserTokenBalance(address);

const {
writeContractAsync,
isPending: isWriting,
data: txHash,
} = useWriteJbMultiTerminalCashOutTokensOf();

const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash: txHash,
});

// Calculate ETH received when cashing out
const ethReceived = useMemo(() => {
if (!amount || !surplus || !totalOutstanding.data) return 0n;
try {
const tokenAmount = parseEther(amount);
// Simple calculation: (tokens / totalSupply) * surplus
// This is a simplified version - production should use getTokenCashOutQuoteEth
const share = (tokenAmount * surplus) / totalOutstanding.data;
return applyJbDaoCashOutFee(share); // Apply 2.5% JB DAO fee
} catch {
return 0n;
}
}, [amount, surplus, totalOutstanding.data]);

const handleCashOut = async () => {
if (!address || !contracts.primaryNativeTerminal.data || !amount) return;

try {
const tokenAmount = parseEther(amount);

await writeContractAsync({
chainId,
address: contracts.primaryNativeTerminal.data,
args: [
address, // holder
projectId,
tokenAmount,
NATIVE_TOKEN, // token to receive (ETH)
0n, // minTokensReclaimed
address, // beneficiary
'0x', // metadata
],
});

setAmount('');
onSuccess?.();
} catch (error) {
console.error('Cash out failed:', error);
}
};

// Quick select buttons
const setPercentage = (percent: number) => {
if (!userBalance) return;
const tokenAmount = (userBalance * BigInt(percent)) / 100n;
setAmount(formatEther(tokenAmount));
};

const isLoading = isWriting || isConfirming;
const buttonDisabled = !isConnected || !amount || isLoading;

return (
<div className="cash-out-modal">
{/* Balance Display */}
<div className="balance">
<span>Your balance:</span>
<strong>
{formatEther(userBalance || 0n)} {token.data?.symbol || 'tokens'}
</strong>
</div>

{/* Quick Select Buttons */}
<div className="quick-select">
{[10, 25, 50, 100].map((pct) => (
<button
key={pct}
onClick={() => setPercentage(pct)}
disabled={!userBalance || isLoading}
>
{pct === 100 ? 'Max' : `${pct}%`}
</button>
))}
</div>

{/* Amount Input */}
<div className="input-group">
<label>Amount to redeem</label>
<div className="input-with-suffix">
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
step="0.01"
min="0"
max={formatEther(userBalance || 0n)}
disabled={isLoading}
/>
<span>{token.data?.symbol || 'tokens'}</span>
</div>
</div>

{/* ETH Quote */}
{amount && Number(amount) > 0 && (
<div className="quote">
<span>You will receive:</span>
<strong>~{formatEther(ethReceived)} ETH</strong>
<small>(after 2.5% fee)</small>
</div>
)}

{/* Cash Out Button */}
<button
onClick={handleCashOut}
disabled={buttonDisabled}
className="cash-out-button"
>
{!isConnected
? 'Connect Wallet'
: isWriting
? 'Confirm in wallet...'
: isConfirming
? 'Confirming...'
: isSuccess
? 'Success!'
: 'Cash Out'}
</button>

{/* Success Message */}
{isSuccess && txHash && (
<p className="success">
Cash out successful!{' '}
<a href={`https://etherscan.io/tx/${txHash}`} target="_blank">
View transaction
</a>
</p>
)}
</div>
);
}

Combined Pay/Cash Out Card

A single component that toggles between pay and cash out modes:

'use client';

import { useState } from 'react';
import { PayModal } from './PayModal';
import { CashOutModal } from './CashOutModal';

interface PayCashOutCardProps {
projectId: bigint;
}

export function PayCashOutCard({ projectId }: PayCashOutCardProps) {
const [mode, setMode] = useState<'pay' | 'cashout'>('pay');

return (
<div className="pay-cashout-card">
{/* Mode Toggle */}
<div className="mode-toggle">
<button
className={mode === 'pay' ? 'active' : ''}
onClick={() => setMode('pay')}
>
Pay
</button>
<button
className={mode === 'cashout' ? 'active' : ''}
onClick={() => setMode('cashout')}
>
Cash Out
</button>
</div>

{/* Content */}
{mode === 'pay' ? (
<PayModal projectId={projectId} />
) : (
<CashOutModal projectId={projectId} />
)}
</div>
);
}

Dashboard Data

Fetch and display all the key project metrics:

'use client';

import { formatEther } from 'viem';
import {
useJBProjectMetadataContext,
useJBTokenContext,
useJBRulesetContext,
useNativeTokenSurplus,
useSuckers,
useSuckersNativeTokenSurplus,
} from 'juice-sdk-react';

export function ProjectDashboard() {
const { metadata } = useJBProjectMetadataContext();
const { token, totalOutstanding } = useJBTokenContext();
const { ruleset, rulesetMetadata } = useJBRulesetContext();
const { data: surplus } = useNativeTokenSurplus();
const { data: suckers } = useSuckers();
const { data: totalSurplus } = useSuckersNativeTokenSurplus();

const isOmnichain = suckers && suckers.length > 0;

if (metadata.isLoading) return <div>Loading...</div>;

return (
<div className="dashboard">
{/* Project Header */}
<header>
{metadata.data?.logoUri && (
<img src={metadata.data.logoUri} alt="" />
)}
<h1>{metadata.data?.name}</h1>
<p>{metadata.data?.description}</p>
</header>

{/* Treasury Stats */}
<section className="stats">
<div className="stat">
<label>Treasury</label>
<value>
{formatEther(isOmnichain ? (totalSurplus || 0n) : (surplus || 0n))} ETH
</value>
{isOmnichain && <small>across {suckers.length + 1} chains</small>}
</div>

<div className="stat">
<label>Total Supply</label>
<value>
{formatEther(totalOutstanding.data || 0n)} {token.data?.symbol}
</value>
</div>

<div className="stat">
<label>Token Price</label>
<value>
{ruleset.data?.weight
? `${formatEther(ruleset.data.weight)} ${token.data?.symbol}/ETH`
: '—'}
</value>
</div>
</section>

{/* Ruleset Info */}
<section className="ruleset">
<h3>Current Rules</h3>
<div className="rules">
<div>
<label>Reserved Rate</label>
<value>{(rulesetMetadata.data?.reservedPercent || 0) / 100}%</value>
</div>
<div>
<label>Redemption Rate</label>
<value>
{100 - (rulesetMetadata.data?.cashOutTaxRate || 0) / 100}%
</value>
</div>
<div>
<label>Duration</label>
<value>
{ruleset.data?.duration
? `${Number(ruleset.data.duration) / 86400} days`
: 'Open-ended'}
</value>
</div>
</div>
</section>

{/* Connected Chains (if omnichain) */}
{isOmnichain && (
<section className="chains">
<h3>Available on {suckers.length + 1} chains</h3>
<ul>
{suckers.map((s) => (
<li key={s.peerChainId}>Chain {s.peerChainId}</li>
))}
</ul>
</section>
)}
</div>
);
}

User Holdings Component

Display a user's token balance and value:

'use client';

import { formatEther } from 'viem';
import { useAccount } from 'wagmi';
import {
useJBTokenContext,
useNativeTokenSurplus,
useSuckersUserTokenBalance,
} from 'juice-sdk-react';

export function UserHoldings() {
const { address, isConnected } = useAccount();
const { token, totalOutstanding } = useJBTokenContext();
const { data: surplus } = useNativeTokenSurplus();
const { data: userBalance, isLoading } = useSuckersUserTokenBalance(address);

if (!isConnected) {
return <p>Connect wallet to see your holdings</p>;
}

if (isLoading) {
return <p>Loading...</p>;
}

// Calculate user's share of the treasury
const userValue = useMemo(() => {
if (!userBalance || !surplus || !totalOutstanding.data) return 0n;
if (totalOutstanding.data === 0n) return 0n;
return (userBalance * surplus) / totalOutstanding.data;
}, [userBalance, surplus, totalOutstanding.data]);

return (
<div className="user-holdings">
<h3>Your Holdings</h3>

<div className="holding">
<label>Tokens</label>
<value>
{formatEther(userBalance || 0n)} {token.data?.symbol}
</value>
</div>

<div className="holding">
<label>Value</label>
<value>~{formatEther(userValue)} ETH</value>
</div>

<div className="holding">
<label>Share</label>
<value>
{totalOutstanding.data && userBalance
? ((Number(userBalance) / Number(totalOutstanding.data)) * 100).toFixed(2)
: '0'}
%
</value>
</div>
</div>
);
}

Omnichain Payments

Juicebox V5 supports omnichain projects - projects deployed across multiple chains that share the same token and treasury. Users can pay the project on any chain and receive tokens, which can be bridged between chains via suckers.

How Omnichain Works

  1. Suckers connect project instances across chains (Ethereum, Optimism, Arbitrum, Base)
  2. Tokens can be bridged between any connected chains
  3. Treasury is aggregated across all chains for redemption calculations
  4. Users can pay on any chain and cash out from any chain

Checking if a Project is Omnichain

import { useSuckers } from 'juice-sdk-react';

function OmnichainBadge() {
const { data: suckers, isLoading } = useSuckers();

if (isLoading) return null;

const isOmnichain = suckers && suckers.length > 0;

if (!isOmnichain) return null;

return (
<span className="badge">
🌐 Available on {suckers.length + 1} chains
</span>
);
}

Paying on Any Chain

Users can pay your project on whichever chain they prefer. The JBProjectProvider can be configured for any chain:

import { JBProjectProvider } from 'juice-sdk-react';
import { useWriteJbMultiTerminalPay } from 'juice-sdk-react';
import { optimism, base, arbitrum, mainnet } from 'viem/chains';

// Chain selector for omnichain projects
function OmnichainPayPage({ projectId }: { projectId: bigint }) {
const [selectedChain, setSelectedChain] = useState(mainnet);

return (
<div>
{/* Chain selector */}
<ChainSelector
projectId={projectId}
selectedChain={selectedChain}
onChainSelect={setSelectedChain}
/>

{/* Project provider for selected chain */}
<JBProjectProvider
projectId={projectId}
chainId={selectedChain.id}
>
<PayForm />
</JBProjectProvider>
</div>
);
}

// Chain selector showing available chains
function ChainSelector({ projectId, selectedChain, onChainSelect }) {
const chainOptions = [
{ chain: mainnet, name: 'Ethereum', icon: '⟠' },
{ chain: optimism, name: 'Optimism', icon: '🔴' },
{ chain: base, name: 'Base', icon: '🔵' },
{ chain: arbitrum, name: 'Arbitrum', icon: '🔷' },
];

return (
<div className="chain-selector">
<p>Pay on:</p>
<div className="chain-options">
{chainOptions.map(({ chain, name, icon }) => (
<button
key={chain.id}
className={selectedChain.id === chain.id ? 'selected' : ''}
onClick={() => onChainSelect(chain)}
>
{icon} {name}
</button>
))}
</div>
</div>
);
}

Complete Omnichain Pay Component

This component lets users pay on their preferred chain with full token quote support:

import {
JBProjectProvider,
useJBContractContext,
useJBProjectId,
useJBChainId,
useSuckers,
useWriteJbMultiTerminalPay,
} from 'juice-sdk-react';
import { getTokenAToBQuote, NATIVE_TOKEN } from 'juice-sdk-core';
import { useState, useMemo } from 'react';
import { parseEther, formatEther, zeroAddress } from 'viem';
import { useAccount, useSwitchChain } from 'wagmi';
import { mainnet, optimism, base, arbitrum } from 'viem/chains';

const SUPPORTED_CHAINS = [
{ id: mainnet.id, name: 'Ethereum', icon: '⟠' },
{ id: optimism.id, name: 'Optimism', icon: '🔴' },
{ id: base.id, name: 'Base', icon: '🔵' },
{ id: arbitrum.id, name: 'Arbitrum', icon: '🔷' },
];

function OmnichainPayWidget({ projectId }: { projectId: bigint }) {
const [chainId, setChainId] = useState(mainnet.id);

return (
<JBProjectProvider projectId={projectId} chainId={chainId}>
<OmnichainPayForm
selectedChainId={chainId}
onChainChange={setChainId}
/>
</JBProjectProvider>
);
}

function OmnichainPayForm({ selectedChainId, onChainChange }) {
const { address } = useAccount();
const { switchChain } = useSwitchChain();
const { data: suckers } = useSuckers();
const projectId = useJBProjectId();
const currentChainId = useJBChainId();
const { contracts } = useJBContractContext();

const [amount, setAmount] = useState('');
const [memo, setMemo] = useState('');

// Get token quote
const payAmountWei = amount ? parseEther(amount) : 0n;
const tokenQuote = useMemo(() => {
if (!contracts.primaryNativeTerminal.data || payAmountWei === 0n) return 0n;
// Simplified - in production use ruleset weight
return payAmountWei; // 1:1 default
}, [payAmountWei, contracts]);

// Get available chains from suckers
const availableChains = useMemo(() => {
const chains = [{ id: currentChainId, ...SUPPORTED_CHAINS.find(c => c.id === currentChainId) }];
suckers?.forEach(s => {
const chain = SUPPORTED_CHAINS.find(c => c.id === s.peerChainId);
if (chain) chains.push(chain);
});
return chains;
}, [suckers, currentChainId]);

const { writeContract, isPending, isSuccess } = useWriteJbMultiTerminalPay();

async function handlePay() {
if (!address || !contracts.primaryNativeTerminal.data) return;

// Switch chain if needed
if (currentChainId !== selectedChainId) {
await switchChain({ chainId: selectedChainId });
}

writeContract({
address: contracts.primaryNativeTerminal.data,
args: [
projectId, // projectId
NATIVE_TOKEN, // token (ETH)
payAmountWei, // amount
address, // beneficiary
0n, // minReturnedTokens
memo, // memo
'0x', // metadata
],
value: payAmountWei,
});
}

return (
<div className="omnichain-pay">
{/* Chain selector */}
<div className="chain-selector">
<label>Pay on chain:</label>
<div className="chain-buttons">
{availableChains.map(chain => (
<button
key={chain.id}
className={selectedChainId === chain.id ? 'active' : ''}
onClick={() => onChainChange(chain.id)}
>
{chain.icon} {chain.name}
</button>
))}
</div>
</div>

{/* Amount input */}
<div className="amount-input">
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
step="0.01"
/>
<span>ETH</span>
</div>

{/* Token quote */}
{tokenQuote > 0n && (
<p className="token-quote">
You'll receive: {formatEther(tokenQuote)} tokens
</p>
)}

{/* Memo */}
<input
type="text"
value={memo}
onChange={(e) => setMemo(e.target.value)}
placeholder="Add a memo (optional)"
/>

{/* Pay button */}
<button
onClick={handlePay}
disabled={!amount || isPending}
>
{isPending ? 'Processing...' : `Pay on ${SUPPORTED_CHAINS.find(c => c.id === selectedChainId)?.name}`}
</button>

{isSuccess && <p className="success">Payment successful!</p>}
</div>
);
}

Aggregated Cross-Chain Dashboard

Display treasury and user data aggregated across all chains:

import {
useSuckers,
useSuckersUserTokenBalance,
useSuckersNativeTokenSurplus,
useSuckersCashOutQuote,
useNativeTokenSurplus,
} from 'juice-sdk-react';
import { formatEther } from 'viem';
import { useAccount } from 'wagmi';

function OmnichainDashboard() {
const { address } = useAccount();
const { data: suckers, isLoading: suckersLoading } = useSuckers();

// Single-chain data
const { data: localSurplus } = useNativeTokenSurplus();

// Cross-chain aggregated data
const { data: totalSurplus } = useSuckersNativeTokenSurplus();
const { data: userBalance } = useSuckersUserTokenBalance(address);
const { data: cashOutQuote } = useSuckersCashOutQuote(userBalance || 0n);

const isOmnichain = suckers && suckers.length > 0;

if (suckersLoading) return <div>Loading...</div>;

return (
<div className="omnichain-dashboard">
{/* Omnichain indicator */}
{isOmnichain && (
<div className="omnichain-badge">
🌐 Omnichain Project
<span className="chain-count">
Active on {suckers.length + 1} chains
</span>
</div>
)}

{/* Treasury */}
<div className="stat-card">
<label>Total Treasury</label>
<value>
{formatEther(isOmnichain ? (totalSurplus || 0n) : (localSurplus || 0n))} ETH
</value>
{isOmnichain && (
<small>Aggregated across all chains</small>
)}
</div>

{/* Connected chains */}
{isOmnichain && (
<div className="connected-chains">
<label>Connected Chains</label>
<ul>
{suckers.map((sucker) => (
<li key={sucker.peerChainId}>
Chain {sucker.peerChainId} → Project #{sucker.projectId.toString()}
</li>
))}
</ul>
</div>
)}

{/* User holdings */}
{address && (
<div className="stat-card">
<label>Your Balance (All Chains)</label>
<value>{formatEther(userBalance || 0n)} tokens</value>
{cashOutQuote && cashOutQuote > 0n && (
<small>
Worth {formatEther(cashOutQuote)} ETH if redeemed
</small>
)}
</div>
)}
</div>
);
}

Cross-Chain Token Bridging

Tokens earned on any chain can be bridged to other chains via suckers. See Suckers documentation for details on how token bridging works.

import { useSuckers } from 'juice-sdk-react';

function BridgeOptions() {
const { data: suckers } = useSuckers();

if (!suckers || suckers.length === 0) {
return <p>This project is not omnichain</p>;
}

return (
<div className="bridge-options">
<h3>Bridge Tokens To:</h3>
<ul>
{suckers.map((sucker) => (
<li key={sucker.peerChainId}>
<button onClick={() => handleBridge(sucker.peerChainId)}>
Bridge to Chain {sucker.peerChainId}
</button>
</li>
))}
</ul>
</div>
);
}

Context Providers

The SDK uses React Context to provide project data throughout your component tree.

JBProjectProvider

The main provider that composes all project-specific contexts. Use this to wrap any page or component that needs access to project data.

import { JBProjectProvider } from 'juice-sdk-react';

<JBProjectProvider
projectId={1n} // Required: Project ID as bigint
chainId={1} // Required: Chain ID
ctxProps={{ // Optional: Configure nested contexts
metadata: {
ipfsGatewayHostname: 'jbm.infura-ipfs.io' // Custom IPFS gateway
},
token: {
withTotalOutstanding: true // Include total token supply
}
}}
>
{children}
</JBProjectProvider>

Nested Contexts:

ContextHookData Provided
JBChainContextuseJBChainId()Current chain ID
JBContractContextuseJBContractContext()Contract addresses (Controller, Terminal, etc.)
JBRulesetContextuseJBRulesetContext()Current ruleset and metadata
JBProjectMetadataContextuseJBProjectMetadataContext()Project metadata from IPFS
JBTokenContextuseJBTokenContext()Token info and supply
JBTerminalContext-Primary native terminal

Hooks Reference

Context Hooks

These hooks access data from the JBProjectProvider context.

useJBChainId

Get the current chain ID from the project context.

import { useJBChainId } from 'juice-sdk-react';

function MyComponent() {
const chainId = useJBChainId();
return <span>Chain: {chainId}</span>;
}

useJBProjectId

Get the project ID, optionally for a different chain (useful for omnichain projects).

import { useJBProjectId } from 'juice-sdk-react';
import { optimism } from 'viem/chains';

function MyComponent() {
// Get project ID on current chain
const { projectId } = useJBProjectId();

// Get project ID on a different chain
const { projectId: opProjectId } = useJBProjectId(optimism.id);
}

useJBContractContext

Access contract addresses for the current project.

import { useJBContractContext } from 'juice-sdk-react';

function MyComponent() {
const { contracts } = useJBContractContext();

// Available contracts:
// - contracts.controller
// - contracts.primaryNativeTerminal
// - contracts.fundAccessLimits
// - contracts.tokens
// - contracts.splits
// - contracts.rulesets

return <span>Terminal: {contracts.primaryNativeTerminal.data}</span>;
}

useJBRulesetContext

Access the current ruleset and its metadata.

import { useJBRulesetContext } from 'juice-sdk-react';
import { formatEther } from 'viem';

function RulesetInfo() {
const { ruleset, rulesetMetadata } = useJBRulesetContext();

if (ruleset.isLoading) return <div>Loading...</div>;

return (
<div>
<p>Duration: {ruleset.data?.duration.toString()} seconds</p>
<p>Weight: {formatEther(ruleset.data?.weight || 0n)}</p>
<p>Reserved Rate: {rulesetMetadata.data?.reservedPercent}%</p>
<p>Redemption Rate: {100 - (rulesetMetadata.data?.cashOutTaxRate || 0) / 100}%</p>
</div>
);
}

useJBProjectMetadataContext

Access project metadata from IPFS (name, description, logo, links).

import { useJBProjectMetadataContext } from 'juice-sdk-react';

function ProjectHeader() {
const { metadata } = useJBProjectMetadataContext();

if (metadata.isLoading) return <div>Loading...</div>;
if (!metadata.data) return <div>No metadata</div>;

return (
<div>
{metadata.data.logoUri && (
<img src={metadata.data.logoUri} alt={metadata.data.name} />
)}
<h1>{metadata.data.name}</h1>
<p>{metadata.data.description}</p>
{metadata.data.twitter && (
<a href={`https://twitter.com/${metadata.data.twitter}`}>Twitter</a>
)}
</div>
);
}

useJBTokenContext

Access project token information.

import { useJBTokenContext } from 'juice-sdk-react';
import { formatEther } from 'viem';

function TokenInfo() {
const { token, totalOutstanding } = useJBTokenContext();

return (
<div>
<p>Symbol: {token.data?.symbol}</p>
<p>Decimals: {token.data?.decimals}</p>
<p>Total Supply: {formatEther(totalOutstanding.data || 0n)}</p>
</div>
);
}

Token & Balance Hooks

useNativeTokenSurplus

Get the native token (ETH) surplus available for redemptions.

import { useNativeTokenSurplus } from 'juice-sdk-react';
import { formatEther } from 'viem';

function TreasurySurplus() {
const { data: surplus, isLoading } = useNativeTokenSurplus();

if (isLoading) return <div>Loading...</div>;

return <p>Available to redeem: {formatEther(surplus || 0n)} ETH</p>;
}

useTokenCashOutQuoteEth

Calculate how much ETH a token holder would receive when cashing out.

import { useTokenCashOutQuoteEth } from 'juice-sdk-react';
import { parseEther, formatEther } from 'viem';

function RedemptionQuote({ tokenAmount }: { tokenAmount: string }) {
const { data: ethAmount } = useTokenCashOutQuoteEth(parseEther(tokenAmount));

return (
<p>
Redeeming {tokenAmount} tokens = {formatEther(ethAmount || 0n)} ETH
</p>
);
}

Omnichain Hooks

These hooks work with projects deployed across multiple chains via suckers.

useSuckers

Get cross-chain token pairs for the project.

import { useSuckers } from 'juice-sdk-react';

function ChainSelector() {
const { data: suckers, isLoading } = useSuckers();

if (isLoading) return <div>Loading chains...</div>;

return (
<select>
{suckers?.map((sucker) => (
<option key={sucker.peerChainId} value={sucker.peerChainId}>
Chain {sucker.peerChainId}
</option>
))}
</select>
);
}

useSuckersUserTokenBalance

Get a user's token balance aggregated across all connected chains.

import { useSuckersUserTokenBalance } from 'juice-sdk-react';
import { formatEther } from 'juice-sdk-core';
import { useAccount } from 'wagmi';

function CrossChainBalance() {
const { address } = useAccount();
const { data: totalBalance, isLoading } = useSuckersUserTokenBalance(address);

if (isLoading) return <div>Loading...</div>;

return (
<p>Your total balance: {formatEther(totalBalance || 0n, { fractionDigits: 4 })} tokens</p>
);
}

useSuckersNativeTokenSurplus

Get the total surplus across all connected chains.

import { useSuckersNativeTokenSurplus } from 'juice-sdk-react';
import { formatEther } from 'viem';

function TotalSurplus() {
const { data: surplus } = useSuckersNativeTokenSurplus();

return <p>Total surplus (all chains): {formatEther(surplus || 0n)} ETH</p>;
}

useSuckersCashOutQuote

Get a cash-out quote considering multi-chain surplus.

import { useSuckersCashOutQuote } from 'juice-sdk-react';
import { parseEther, formatEther } from 'viem';

function OmnichainRedemptionQuote({ tokenAmount }: { tokenAmount: string }) {
const { data: quote } = useSuckersCashOutQuote(parseEther(tokenAmount));

return (
<p>
Redeeming across all chains: {formatEther(quote || 0n)} ETH
</p>
);
}

NFT (721 Hook) Hooks

useFind721DataHook

Find the 721 data hook address for a project that has NFT tiers.

import { useFind721DataHook } from 'juice-sdk-react';

function NFTHookInfo() {
const { data: hookAddress } = useFind721DataHook();

if (!hookAddress) return <div>No NFT hook configured</div>;

return <p>NFT Hook: {hookAddress}</p>;
}

usePreparePayMetadata

Prepare metadata for payments that include NFT tier minting.

import { usePreparePayMetadata } from 'juice-sdk-react';

function PayWithNFT({ hookAddress, tierIds }: { hookAddress: string; tierIds: bigint[] }) {
const metadata = usePreparePayMetadata({
jb721Hook: {
dataHookAddress: hookAddress,
tierIdsToMint: tierIds,
}
});

// Use metadata in your pay transaction
// ...
}

Utility Hooks

useEtherPrice

Get the current ETH price in USD.

import { useEtherPrice } from 'juice-sdk-react';

function EthPrice() {
const { data: ethPrice } = useEtherPrice();

return <p>ETH Price: ${ethPrice?.toFixed(2)}</p>;
}

Auto-Generated Contract Hooks

The SDK auto-generates type-safe hooks for all Juicebox contracts using Wagmi CLI. These follow the pattern:

  • useRead{Contract}{Function} - Read contract state
  • useWrite{Contract}{Function} - Write transactions
  • useWatch{Contract}{Event} - Watch for events

Payment Example

import { useWriteJbMultiTerminalPay } from 'juice-sdk-react';
import { useJBContractContext, useJBChainId } from 'juice-sdk-react';
import { parseEther } from 'viem';
import { useAccount } from 'wagmi';

const NATIVE_TOKEN = '0x000000000000000000000000000000000000EEEe';

function PayButton({ projectId, amount }: { projectId: bigint; amount: string }) {
const { address } = useAccount();
const chainId = useJBChainId();
const { contracts } = useJBContractContext();
const { writeContractAsync, isPending, isSuccess } = useWriteJbMultiTerminalPay();

const handlePay = async () => {
if (!address || !contracts.primaryNativeTerminal.data) return;

const amountWei = parseEther(amount);

await writeContractAsync({
chainId,
address: contracts.primaryNativeTerminal.data,
args: [
projectId, // projectId
NATIVE_TOKEN, // token (ETH)
amountWei, // amount
address, // beneficiary (receives project tokens)
0n, // minReturnedTokens (0 = no slippage protection)
'Payment from app', // memo
'0x', // metadata
],
value: amountWei,
});
};

return (
<button onClick={handlePay} disabled={isPending || !address}>
{isPending ? 'Paying...' : `Pay ${amount} ETH`}
</button>
);
}

Cash Out (Redemption) Example

import { useWriteJbMultiTerminalCashOutTokensOf } from 'juice-sdk-react';
import { useJBContractContext, useJBChainId } from 'juice-sdk-react';
import { useAccount } from 'wagmi';

const NATIVE_TOKEN = '0x000000000000000000000000000000000000EEEe';

function RedeemButton({ projectId, tokenAmount }: { projectId: bigint; tokenAmount: bigint }) {
const { address } = useAccount();
const chainId = useJBChainId();
const { contracts } = useJBContractContext();
const { writeContractAsync, isPending } = useWriteJbMultiTerminalCashOutTokensOf();

const handleRedeem = async () => {
if (!address || !contracts.primaryNativeTerminal.data) return;

await writeContractAsync({
chainId,
address: contracts.primaryNativeTerminal.data,
args: [
address, // holder
projectId, // projectId
tokenAmount, // tokenCount to redeem
NATIVE_TOKEN, // token to receive (ETH)
0n, // minTokensReclaimed (0 = no slippage protection)
address, // beneficiary
'0x', // metadata
],
});
};

return (
<button onClick={handleRedeem} disabled={isPending || !address}>
{isPending ? 'Redeeming...' : 'Redeem Tokens'}
</button>
);
}

Read Current Ruleset Example

import { useReadJbControllerCurrentRulesetOf } from 'juice-sdk-react';
import { useJBContractContext, useJBChainId } from 'juice-sdk-react';

function CurrentRuleset({ projectId }: { projectId: bigint }) {
const chainId = useJBChainId();
const { contracts } = useJBContractContext();

const { data, isLoading } = useReadJbControllerCurrentRulesetOf({
chainId,
address: contracts.controller.data,
args: [projectId],
});

if (isLoading) return <div>Loading...</div>;

const [ruleset, metadata] = data || [];

return (
<div>
<p>Ruleset ID: {ruleset?.id.toString()}</p>
<p>Start: {new Date(Number(ruleset?.start) * 1000).toLocaleDateString()}</p>
<p>Duration: {ruleset?.duration.toString()} seconds</p>
</div>
);
}

Core Utilities

The juice-sdk-core package provides utilities for calculations, formatting, and constants.

Token Math

getTokenAToBQuote

Calculate how many project tokens a payment will mint.

import { getTokenAToBQuote } from 'juice-sdk-core';
import { parseEther } from 'viem';

// How many tokens for 1 ETH?
const tokensReceived = getTokenAToBQuote(
parseEther('1'), // payment amount in wei
{
weight: ruleset.weight,
reservedPercent: rulesetMetadata.reservedPercent,
}
);

getTokenBtoAQuote

Calculate how much to pay for a specific number of tokens.

import { getTokenBtoAQuote } from 'juice-sdk-core';
import { parseEther } from 'viem';

// How much ETH for 1000 tokens?
const paymentRequired = getTokenBtoAQuote(
parseEther('1000'), // desired token amount
{
weight: ruleset.weight,
}
);

getTokenBPrice

Get the current price of project tokens.

import { getTokenBPrice } from 'juice-sdk-core';

const price = getTokenBPrice({ weight: ruleset.weight });
console.log(`Token price: ${price.toString()} ETH`);

Ruleset Math

getNextRulesetWeight

Calculate what the weight will be in the next ruleset (after decay).

import { getNextRulesetWeight } from 'juice-sdk-core';

const nextWeight = getNextRulesetWeight(
currentRuleset.weight,
currentRuleset.decayPercent
);

Formatting

import { formatEther, formatUnits, formatEthAddress } from 'juice-sdk-core';

// Format ETH with custom precision
formatEther(weiAmount, { fractionDigits: 4 }); // "1.2345"

// Format any token
formatUnits(tokenAmount, 18); // "1000.00"

// Truncate addresses
formatEthAddress('0x1234567890abcdef...', { truncateTo: 4 }); // "0x1234...cdef"

Fixed-Point Math Classes

The SDK uses fixed-point math for precise decimal calculations:

import {
ReservedPercent,
CashOutTaxRate,
RulesetWeight,
Ether,
JBProjectToken
} from 'juice-sdk-core';

// Percentages (4 decimal places, max 10,000 = 100%)
const reserved = new ReservedPercent(5000); // 50%
const taxRate = new CashOutTaxRate(2500); // 25%

// Token weights and amounts (18 decimal places)
const weight = new RulesetWeight(1_000_000_000_000_000_000n); // 1.0
const ethAmount = new Ether(parseEther('1.5'));
const tokens = new JBProjectToken(tokenAmount);

// Convert to human-readable
console.log(weight.toString()); // "1.0"

Constants

import {
NATIVE_TOKEN,
JB_CHAINS,
ETH_CURRENCY_ID,
USD_CURRENCY_ID,
MAX_RESERVED_PERCENT,
MAX_CASH_OUT_TAX_RATE
} from 'juice-sdk-core';

// Native token address (ETH)
NATIVE_TOKEN // "0x000000000000000000000000000000000000EEEe"

// Supported chains
JB_CHAINS // { 1: {...}, 10: {...}, 8453: {...}, 42161: {...}, ... }

// Currency IDs for price feeds
ETH_CURRENCY_ID // 1
USD_CURRENCY_ID // 2

// Max values for percentages
MAX_RESERVED_PERCENT // 10_000 (100%)
MAX_CASH_OUT_TAX_RATE // 10_000 (100%)

Revnet SDK

For Revnet-specific functionality, use revnet-sdk:

import { calcPrepaidFee } from 'revnet-sdk';

// Calculate prepaid fee for a Revnet operation
const fee = calcPrepaidFee(amount);

Auto-Generated Revnet Hooks

import {
useWriteRevDeployerDeployFor,
useReadRevLoansLoanOf,
useWriteRevLoansBorrowFrom,
useWriteRevLoansRepayLoan
} from 'revnet-sdk';

See Life of a Revnet and Borrow from Revnet for detailed examples.


Complete Examples

Full Project Dashboard

'use client';

import { JBProjectProvider } from 'juice-sdk-react';
import {
useJBProjectMetadataContext,
useJBTokenContext,
useJBRulesetContext,
useNativeTokenSurplus,
useWriteJbMultiTerminalPay,
useJBContractContext,
useJBChainId
} from 'juice-sdk-react';
import { formatEther, getTokenAToBQuote } from 'juice-sdk-core';
import { parseEther } from 'viem';
import { useAccount } from 'wagmi';
import { useState } from 'react';

const NATIVE_TOKEN = '0x000000000000000000000000000000000000EEEe';

function ProjectInfo() {
const { metadata } = useJBProjectMetadataContext();
const { token, totalOutstanding } = useJBTokenContext();
const { ruleset, rulesetMetadata } = useJBRulesetContext();
const { data: surplus } = useNativeTokenSurplus();

if (metadata.isLoading) return <div>Loading project...</div>;

return (
<div>
<header>
{metadata.data?.logoUri && (
<img src={metadata.data.logoUri} alt="" width={64} height={64} />
)}
<h1>{metadata.data?.name}</h1>
<p>{metadata.data?.description}</p>
</header>

<section>
<h2>Treasury</h2>
<p>Surplus: {formatEther(surplus || 0n, { fractionDigits: 4 })} ETH</p>
<p>Total tokens: {formatEther(totalOutstanding.data || 0n, { fractionDigits: 2 })} {token.data?.symbol}</p>
</section>

<section>
<h2>Current Ruleset</h2>
<p>Reserved rate: {(rulesetMetadata.data?.reservedPercent || 0) / 100}%</p>
<p>Redemption rate: {100 - (rulesetMetadata.data?.cashOutTaxRate || 0) / 100}%</p>
</section>
</div>
);
}

function PayForm({ projectId }: { projectId: bigint }) {
const [amount, setAmount] = useState('0.1');
const { address } = useAccount();
const chainId = useJBChainId();
const { contracts } = useJBContractContext();
const { ruleset, rulesetMetadata } = useJBRulesetContext();
const { writeContractAsync, isPending } = useWriteJbMultiTerminalPay();

// Calculate token quote
const tokensReceived = ruleset.data && rulesetMetadata.data
? getTokenAToBQuote(parseEther(amount || '0'), {
weight: ruleset.data.weight,
reservedPercent: rulesetMetadata.data.reservedPercent,
})
: 0n;

const handlePay = async () => {
if (!address || !contracts.primaryNativeTerminal.data) return;

await writeContractAsync({
chainId,
address: contracts.primaryNativeTerminal.data,
args: [projectId, NATIVE_TOKEN, parseEther(amount), address, 0n, '', '0x'],
value: parseEther(amount),
});
};

return (
<div>
<h2>Contribute</h2>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
step="0.01"
min="0"
/>
<span> ETH</span>
<p>You'll receive: {formatEther(tokensReceived, { fractionDigits: 2 })} tokens</p>
<button onClick={handlePay} disabled={isPending || !address}>
{!address ? 'Connect Wallet' : isPending ? 'Paying...' : 'Pay'}
</button>
</div>
);
}

export default function ProjectDashboard({
projectId,
chainId
}: {
projectId: number;
chainId: number;
}) {
return (
<JBProjectProvider projectId={BigInt(projectId)} chainId={chainId}>
<ProjectInfo />
<PayForm projectId={BigInt(projectId)} />
</JBProjectProvider>
);
}

Multi-Chain Balance Display

import {
useSuckers,
useSuckersUserTokenBalance,
useSuckersNativeTokenSurplus
} from 'juice-sdk-react';
import { formatEther } from 'juice-sdk-core';
import { useAccount } from 'wagmi';

function OmnichainDashboard() {
const { address } = useAccount();
const { data: suckers, isLoading: suckersLoading } = useSuckers();
const { data: totalBalance } = useSuckersUserTokenBalance(address);
const { data: totalSurplus } = useSuckersNativeTokenSurplus();

if (suckersLoading) return <div>Loading chains...</div>;

return (
<div>
<h2>Cross-Chain Overview</h2>

<div>
<h3>Connected Chains</h3>
<ul>
{suckers?.map((sucker) => (
<li key={sucker.peerChainId}>
Chain {sucker.peerChainId} - Project #{sucker.projectId.toString()}
</li>
))}
</ul>
</div>

{address && (
<div>
<h3>Your Balance (All Chains)</h3>
<p>{formatEther(totalBalance || 0n, { fractionDigits: 4 })} tokens</p>
</div>
)}

<div>
<h3>Total Surplus (All Chains)</h3>
<p>{formatEther(totalSurplus || 0n, { fractionDigits: 4 })} ETH</p>
</div>
</div>
);
}

TypeScript Types

The SDK exports TypeScript types for all protocol data structures:

import type {
JBProjectMetadata,
JBRulesetData,
JBRulesetMetadata,
JBSplit,
JBChainId,
} from 'juice-sdk-core';

Key Types

TypeDescription
JBProjectMetadataProject metadata (name, description, logoUri, links)
JBRulesetDataRuleset configuration (duration, weight, decayPercent)
JBRulesetMetadataRuleset settings (reservedPercent, cashOutTaxRate, hooks)
JBSplitSplit configuration for payouts or reserved tokens
JBChainIdUnion of supported chain IDs

Resources