1. Pick the model, and who this guide is for
Juicebox gives you a project with a balance, programmable rules, and project tokens. Owners can change permitted terms subject to ruleset timing and approval. A revnet commits its core economic schedule at launch and retains a limited set of operator controls.
This guide has paths for three kinds of builder, with audience tags on the sections specific to each path. Project builders launch and run a project from this site without writing code. App builders connect a product to Juicebox with the SDK and the indexer. Contract builders extend the protocol with hooks and their own contracts. The parts overlap; read the tags and skip what is not yours.
| You need to | Use |
|---|---|
| Pay a team a budget each cycle, from revenue or donations | Project (payout limits) |
| Keep the option to change rules, add tokens, or migrate later | Project (rulesets, superpowers) |
| Sell NFTs or memberships and treat the sales as revenue | Either, with a shop |
| Commit an issuance schedule and cash out terms at launch | Revnet (stages; inspect retained operator controls) |
| Let holders cash out against surplus | Project or revnet, when the terms allow it |
| Let holders borrow using project tokens as collateral | Revnet (loans, subject to available funds and terms) |
2. Choose your first useful result
You do not need to read this entire reference in order. Pick one result, follow its path, and return to the operation map when you need another feature. The Learn glossary explains terms such as ruleset, terminal, indexer, and SDK.
- Launch a project without code: open the wizard, choose testnet, configure a small example, review the terms, and complete a test payment and payout before launching on mainnet.
- Build an app: install the SDK, read a testnet project with the example below, then add indexed discovery and a quoted transaction flow.
- Build a contract: inspect the V6 interfaces and deployed addresses, implement one hook or integration, and verify it against the local workflow and a pinned fork.
3. Every operation, in one table
Start with the user action, then identify its contract entrypoint. Some flows require multiple transactions, such as approving an ERC-20 before paying or claiming a cross-chain transfer. SDK helper names are listed where available; the call outlines below are reference shapes, with placeholders to replace before use.
User action → contract call
- Launch a project
- JBController.launchProjectFor, or the 721 / omnichain deployers — buildLaunchProjectTx / buildOmnichainLaunchProjectTx
- Launch a revnet
- REVDeployer.deployFor — buildDeployRevnetTx
- Pay
- JBMultiTerminal.pay — buildPayTx
- Buy a shop item
- JBMultiTerminal.pay with 721 metadata — build721PayMetadata
- Add funds, no tokens
- JBMultiTerminal.addToBalanceOf
- Cash out
- JBMultiTerminal.cashOutTokensOf — prepareHookAwareCashOut
- Send payouts
- JBMultiTerminal.sendPayoutsOf
- Withdraw surplus allowance
- JBMultiTerminal.useAllowanceOf
- Send reserved tokens to splits
- JBController.sendReservedTokensToSplitsOf
- Queue a ruleset
- JBController.queueRulesetsOf — buildQueueRulesetsTx
- Edit splits
- JBController.setSplitGroupsOf — buildSetSplitGroupsTx
- Deploy the ERC-20
- JBController.deployERC20For — buildDeployErc20Tx
- Claim credits as ERC-20
- JBController.claimTokensFor — buildClaimTokensTx
- Mint
- JBController.mintTokensOf — buildMintTokensTx
- Grant an operator
- JBPermissions.setPermissionsFor — buildSetPermissionsTx
- Manage the shop
- JB721TiersHook.adjustTiers / mintFor
- Move tokens to another chain
- sucker.prepare → toRemote → claim
- Set the buyback pool
- JBBuybackHookRegistry.initializePoolFor / setHookFor
- Deploy a payer address
- JBProjectPayerDeployer — buildDeployProjectPayerTx
Amounts are bigint in the token’s own decimals until the display boundary. A project’s identity is chain ID plus project ID; a sucker group links the chains but never makes their addresses, balances, or ruleset IDs interchangeable.
4. The contracts, and where their addresses live
Shared protocol deployments are listed by chain in deploy-all-v6 artifacts, including their address and ABI. The SDK’s address map follows those deployments. Project-specific tokens, hooks, terminals, and bridge instances can have their own addresses: resolve a project’s current controller and terminals through JBDirectory before composing a transaction.
Who does what
- JBProjects
- The project NFT; whoever holds it owns the project. Charges the creation fee
- JBController
- Rulesets, token issuance, reserved splits, metadata URI
- JBMultiTerminal
- Takes payments, holds balances, sends payouts and allowances, executes cash outs, charges fees
- JBRulesets, JBSplits, JBFundAccessLimits, JBTokens, JBTerminalStore
- The storage each of the above reads and writes
- JBDirectory
- Which controller and terminals a project uses
- JBPermissions
- Operator grants, by permission ID
- JBPrices
- Currency conversion for issuance and payout limits
- JB721TiersHook + deployer
- The shop, and a launcher that sets it up as the data hook
- JBBuybackHookRegistry + JBBuybackHook
- Routes a payment to a Uniswap V4 pool when that beats issuing
- JBSucker + JBSuckerRegistry, JBOmnichainDeployer
- Multichain projects and the launcher that links them
- JBRouterTerminalRegistry
- Accepts tokens a project does not hold and swaps them in
- REVDeployer, REVOwner, REVLoans
- Revnets
- Chains: Ethereum, Optimism, Base, Arbitrum, plus Sepolia and the three L2 Sepolias. The SDK’s SUPPORTED_CHAINS and JB_CHAINS carry the list.
- Source: the repos ending in -v6 are current. Older Juicebox versions are not interchangeable with them, and this site redirects unknown routes to the legacy app for those.
5. Launch from the wizard
For: Project builders
The create page walks through five steps: Flavor, Look and feel, Rules (Stages for a revnet), Shop, Launch. The simple flavor skips Rules and launches with sensible defaults. Later rulesets can replace permitted project terms subject to the active timing and approval requirements. Revnet stage economics are committed at launch; metadata and other permitted operator controls remain editable.
The steps
- Flavor
- Simple, project, or revnet. Production or testnet. Which chains, whether to link them (suckers, over CCIP, the native bridge, or both). Accounting tokens: ETH, USDC, or a custom ERC-20 at the same address on every chain; “Accept any token” adds the router terminal. Owner: your connected wallet, another address, or per-chain addresses; turning “Allow changes” off hands ownership to a dead address
- Look and feel
- Name (100), ticker, tagline, description in markdown (10,000), logo and cover (25 MB), links, tags, a payment notice. Pinned to IPFS as one metadata file when you launch
- Rules
- The first ruleset, described in the next section, plus what happens afterwards (wait, terminate, cycle, or a custom follow-up) and the rule-change notice (an approval hook of 3 hours to 7 days, or none)
- Shop
- Optional items priced in the token, ETH, or USD, each with supply (per chain if you like), media, category, sale splits, discount, reserve inventory, voting, and flags. A shop contract deploys even with zero items
- Launch
- One transaction per chain, signed in sequence: simulate, review the decoded call, sign, wait. A Safe connected through the Safe App proposes instead of sending
6. The rules, field by field
For: Project builders
A ruleset defines project terms and can repeat across multiple cycles. These are the fields the wizard exposes, what they mean, and the units the contracts hold them in.
Rules step
- Duration
- How long the cycle lasts before the next queued ruleset can take over: presets from 1 day to a year, Flexible (0, replaced whenever you queue), or Forever. A cycle with a duration repeats itself until replaced
- Issuance
- Tokens issued per unit of the base currency paid, priced in ETH or USD. Default 10,000. Leave blank on a later ruleset to inherit the previous one’s decayed rate
- Issuance cut
- A percentage the rate drops by each cycle (a revnet says “cut X% every N days” instead)
- Reserved split
- The share of every issuance that goes to your reserved recipients instead of the payer. Set by adding recipients; the percent is their total
- Cash outs
- Off, or on with a tax: 0, 10, 30, 50%, or any value below 100. Off is written as a 100% tax. A revnet commits its tax schedule and may also have an initial cash out delay
- Payouts
- None; flexible (payouts can send the owner the amount within the configured limit); or routed to recipients as percentages or fixed amounts, per accounting token. Limits are per cycle and per chain
- Surplus allowance
- Whether the owner may also withdraw from the surplus above the payout limit: unlimited or capped
- Hold fees
- Delay eligible payout and allowance fee processing for 28 days; adding funds back with the return-held-fees option can restore a matching amount
- Accept payments
- Off pauses paying
- Owner can mint any time
- The allowOwnerMinting flag
- Superpowers
- Whether this ruleset lets the owner set terminals, set the controller, migrate terminals, set a custom token, add accounting contexts, or add price feeds. These actions require the relevant flag while that ruleset is active; check whether an owner can queue a later ruleset that enables it
- Afterwards, notice
- What follows this ruleset, and how much notice a rule change needs: an approval hook of 3 hours, 1 day, 3 days, or 7 days, or none
7. What happens when you launch
For: Project buildersApp builders
- Which contract: a revnet goes through REVDeployer.deployFor; a linked multichain project through JBOmnichainDeployer.launchProjectFor, which also deploys the chain’s suckers; anything else through JB721TiersHookProjectDeployer.launchProjectFor, which sets up the shop as the data hook. This site never calls JBController.launchProjectFor directly.
- Creation fee: read from JBProjects.creationFee() right before each send and passed as the transaction value. JBProjects forwards it to the configured fee receiver; when routed as a project payment, token rewards go to the resolved fee payer.
- One transaction per chain: signed in sequence, with a shared salt kept in your session so a linked project gets matching sucker addresses. The same wallet must sign on every chain.
- Afterwards: each chain has its own project ID; the page lives at /<chain>:<id>. The Rulesets (or Terms) tab shows the terms as the contracts hold them.
8. Running it: the owner’s tabs
For: Project builders
The project page groups available management actions into tabs. What you can use depends on the connected account, project rules, and installed extensions.
Project page
- Rulesets
- See the current and queued cycles. Queue a new ruleset to replace the current one, follow it, or start right away; the approval hook decides when it may take effect
- Funds
- Send payouts to the configured splits, withdraw surplus allowance, add to balance
- Owners
- Holders and balances; send reserved tokens to their splits; auto issuance for revnets
- Shop
- Add items, mint reserved copies, replace media, redeem items
- Admin
- Operators and permissions, transfer ownership, the ENS handle, metadata and links, deploy the ERC-20, token metadata, the ruleset-gated powers (mint, set terminals, and the rest), the buyback router and pool
- Extras
- Export a .jb of the live terms; deploy a payer address; liquidity positions
9. The fees
For: Project builders
The core protocol generally charges 2.5% on payouts, allowance withdrawals, and eligible cash out value. Exceptions depend on the route and fee-exempt addresses. Fees fund the Juicebox fee project, with its token rewards going to the operation’s designated beneficiary when processed as a payment. Network gas and extension or swap fees are separate.
Fees to expect
- Launch
- JBProjects.creationFee(), at most 0.001 ETH, per chain
- Payments in
- No core protocol fee; a swap or extension can add costs
- Payouts
- 2.5% on each payout, except to another Juicebox project through the same terminal and to feeless addresses
- Surplus allowance
- 2.5%, skipped if the owner or beneficiary is feeless
- Cash outs, tax above 0%
- 2.5% on the value returned
- Cash outs, 0% tax
- 2.5% only on the fee-free surplus portion, which is often zero
- Held fees
- With holdFees on, payout and allowance fees wait 28 days and can be returned by adding funds back; cash out fees are never held
- Revnets
- Add a 2.5% revnet fee on the tokens burned by a taxed cash out, and loan fees
10. Set up the SDK
For: App builders
@bananapus/nana-sdk-core carries the ABIs, the addresses, the reads, and pure transaction builders. Do not hand-maintain selectors or addresses in product code. The root entry exports every ABI, jbContractAddress, the chain list, the bendystraw helpers, and project-metadata reads; the /v6 entry exports the reads and builders.
Builders are pure: validated input in, a { chainId, address, abi, functionName, args, value } request out. Keep reads on a public client for the target chain and writes on a wallet client connected to that same chain.
mkdir juicebox-starter
cd juicebox-starter
npm init -y
npm install @bananapus/nana-sdk-core viemCreate a V6 project on Base Sepolia with the wizard, or use a known V6 test project on that network. Its URL contains the project ID. Save this as read-project.mjs; it reads chain state and needs no wallet or private key.
import { createPublicClient, http } from "viem";
import { baseSepolia } from "@bananapus/nana-sdk-core/chains";
import { getCurrentRuleset } from "@bananapus/nana-sdk-core/v6";
const projectIdText = process.env.JB_PROJECT_ID;
if (!projectIdText || !/^[1-9][0-9]*$/.test(projectIdText)) {
throw new Error("Set JB_PROJECT_ID to your Base Sepolia project ID.");
}
const client = createPublicClient({
chain: baseSepolia,
transport: http(process.env.BASE_SEPOLIA_RPC_URL),
});
const result = await getCurrentRuleset(client, {
chainId: baseSepolia.id,
projectId: BigInt(projectIdText),
});
if (result.ruleset.id === 0) {
throw new Error("No active V6 ruleset: check the network and project ID.");
}
console.dir(result, { depth: null });# Replace 123 with your Base Sepolia project ID.
JB_PROJECT_ID=123 node read-project.mjs
# Set BASE_SEPOLIA_RPC_URL as well to use your own RPC endpoint.A successful read prints ruleset and metadata, including issuance, timing, and cash out settings. Compare them with the project’s Rulesets tab. If the RPC fails, retry through a configured endpoint; if the ID is wrong or no V6 ruleset is active, the example stops with an explanation.
/v6 exports, grouped
- Reads
- getAccountingContexts, resolvePaymentTerminal, getCurrentRuleset, getUpcomingRuleset, getAllRulesets, previewPay, chooseBestPayRoute, getCashOutQuote, getHookAwareCashOutQuote, getV6SuckerPairs, getSuckerMovements, getTokenAddress, getCreditBalance, getProjectCreationFee, getProject721Shop, hasPermissions, getBorrowableAmount
- Builders
- buildLaunchProjectTx, buildOmnichainLaunchProjectTx, buildDeployRevnetTx, buildPayTx, buildCashOutTx, buildQueueRulesetsTx, buildOmnichainQueueRulesetsTx, buildSetSplitGroupsTx, buildSetPermissionsTx, buildDeployErc20Tx, buildClaimTokensTx, buildTransferCreditsTx, buildMintTokensTx, buildBurnTokensTx, buildDeployProjectPayerTx, buildBridgePrepareTx, buildToRemoteTx, buildBridgeClaimTx, buildSyncAccountingDataTx, buildDirectPaySwapTx, buildPermit2ApproveTx, buildBorrowTx, buildRepayLoanTx
- Config builders
- buildRulesetConfiguration, buildRulesetMetadata, buildAccountingContext, buildTerminalConfigurations, buildSplit, fillSplitPercents, buildTierMetadata, build721RulesetMetadata, build721PayMetadata, buildRevnetStageConfig
- Constants
- slippageFloor, RULESET_WEIGHT_INHERIT, STANDARD_FEE, MAX_FEE, MAX_RESERVED_PERCENT, MAX_CASH_OUT_TAX_RATE, SPLITS_TOTAL_PERCENT, RESERVED_TOKEN_SPLIT_GROUP_ID, payoutSplitGroupId, NATIVE_TOKEN, USDC_ADDRESSES, PERMIT2_ADDRESS, the uniswapV4* math family
- Sub-entries
- /v6/loans, /v6/cash-out, /v6/permit2, /v6/direct-pay, /v6/uniswap-v4, /chains, /jbcenter
import {
buildPayTx, buildQueueRulesetsTx, buildRulesetConfiguration, buildRulesetMetadata,
buildSetSplitGroupsTx, getCurrentRuleset, previewPay, slippageFloor,
} from "@bananapus/nana-sdk-core/v6";
import {
getJBContractAddress, JBCoreContracts, jbMultiTerminalAbi, type JBChainId,
} from "@bananapus/nana-sdk-core";
const terminal = getJBContractAddress(JBCoreContracts.JBMultiTerminal, 6, chainId);11. Read a project
For: App builders
Use the index (next section) to find and display projects. Use the chain for anything a signature depends on, and read it again right before signing.
What to read, and where
- Controller, terminals
- JBDirectory.controllerOf / terminalsOf / primaryTerminalOf
- Current, next, queued rulesets
- JBController.currentRulesetOf / upcomingRulesetOf / latestQueuedRulesetOf / allRulesetsOf
- Accepted tokens
- JBMultiTerminal.accountingContextsOf
- Balance and surplus
- JBTerminalStore.balanceOf / currentSurplusOf / usedPayoutLimitOf / usedSurplusAllowanceOf
- Limits
- JBFundAccessLimits.payoutLimitOf / surplusAllowanceOf
- Supply and balances
- JBTokens.totalSupplyOf / totalBalanceOf / creditBalanceOf / tokenOf
- Splits
- JBSplits.splitsOf(projectId, rulesetId, groupId)
- Pending reserved tokens
- JBController.pendingReservedTokenBalanceOf
- Cash out quote
- JBMultiTerminal.previewCashOutFrom
- Permissions
- JBPermissions.hasPermission / hasPermissions
- Chains
- JBSuckerRegistry.suckerPairsOf
12. Indexed data: Bendystraw
For: App builders
Bendystraw is the Juicebox indexer: a Ponder service that watches every V6 contract on every supported chain and serves the results over GraphQL. It is how this site lists projects, draws activity feeds and price charts, ranks trending projects, and reads LP positions — none of which are practical to assemble from RPC calls at page-load speed.
Use it for anything a person reads. Use the chain for anything a wallet signs: re-read balances, rulesets, allowances, and quotes onchain right before building a transaction. The index can lag the chain by a few blocks, and a lagging index looks like empty data, not an error.
ENDPOINTS
- https://bendystraw.up.railway.app/graphql
- Mainnets: Ethereum, Optimism, Base, Arbitrum. No API key needed
- https://testnet.bendystraw.xyz/graphql
- Testnets: Sepolia and the L2 Sepolias
- …/schema
- A playground with the schema explorer; POST an introspection query to the graphql URL for codegen (same schema on both databases)
POST https://bendystraw.up.railway.app/graphql
{
projects(where: { chainId: 8453, version: 6 }, orderBy: "balance", orderDirection: "desc", limit: 10) {
items { projectId chainId name balance suckerGroupId }
totalCount
}
}WHAT TO ASK IT FOR
- projects / project(chainId, projectId, version: 6)
- Name, metadata URI, balance, token, owner, and the sucker group that links its chains
- payEvents, cashOutTokensEvents, activityEvents
- The feed behind any project page; filter by projectId or suckerGroupId
- participants
- Token holders and their balances, per project or per sucker group
- buybackPools, swapEvents, buybackPoolPositions
- The AMM: pool identity, every trade’s post-trade price, and every LP range
- loans, borrowLoanEvents
- Revnet loans and their collateral
- nftTiers, mintNftEvents
- 721 shop tiers and purchases
- suckerTransactions
- Cross-chain moves and where each one is in its lifecycle
- Every V6 row is versioned: filter with version: 6, and key a project by chainId + projectId, never projectId alone — the same number exists on every chain.
- Numeric arguments on singular queries are Float!, not Int! (Ponder’s choice). Declare variables as Float! or the request fails validation with no data.
- Lists page with limit and offset and return totalCount; loop until you have them all rather than trusting one page.
- suckerGroupId is as-of-event: when chains are linked later, old event rows keep the group id they were written with. Query by every project in the group when you need the full history.
- The SDK’s requestBendystraw(endpoint, query, variables) handles the POST, error surfacing, and endpoint normalisation; selectBendystrawEndpoint picks mainnet vs testnet from a chainId.
13. Wallets, Safes, Relayr, and Permit2
For: App builders
Four kinds of signing show up in a Juicebox app: a plain wallet write, a Safe proposal, a Relayr bundle for many chains at once, and a Permit2 signature for pool swaps. This site routes every owner action through one dispatcher that picks the path from who is connected.
Signing paths in this site
- Wallet write
- submitReviewedContractWrite: review the decoded call → switch chain → simulate as the connected account → sign → wait for the receipt
- Authority calls
- runAuthorityCalls decides: an EOA signs directly; an ERC-2771-capable account signs forward requests that Relayr executes as one prepaid bundle across chains; a Safe gets proposals queued in its transaction service
- Safe
- Propose, confirm, execute; a same-address Safe can be deployed on a new chain; L1 and L2 singletons are allowlisted separately, so a Safe’s authority is checked per chain
- Permit2
- Only for direct Uniswap V4 swaps; pay uses a plain approve for the exact amount
RPC goes through Juicebox Center, an origin-allowlisted provider with no client-side key. Wallets connect through injected and EIP-6963 wallets, WalletConnect, Coinbase, the Safe App connector when framed, and an embedded Para wallet that also provides the card on-ramp.
14. Metadata and IPFS
For: App builders
- Shape: JBProjectMetadata: name, description, projectTagline, logoUri, coverImageUri, infoUri, payButton, payDisclosure, tags, twitter, telegram, discord, archived.
- Read: getProjectMetadata(publicClient, { jbControllerAddress, projectId }) resolves the project’s uri and fetches it; ipfsUri and cidFromIpfsUri handle the encoding.
- Pin: this site pins JSON, images (25 MB), and media (500 MB) straight from the browser to Juicebox Center, which is origin-allowlisted and guards against empty files. The gateway is juicebox.center/ipfs/.
- Write: the owner updates the pointer with JBController.setUriOf (SET_PROJECT_URI).
15. One transaction boundary
For: App builders
The request you quote, simulate, decode, show, and submit must be the same object, not five reconstructions of it. Right before signing, refresh the reads that set bounds and permissions, rebuild, simulate with the real account, then decode the calldata and present it. After submission, keep wallet rejection, Safe proposal, inclusion, revert, and confirmed success as separate states.
This site enforces it mechanically: the review step re-encodes the request after the user has seen it and aborts on any drift; simulation is a raw eth_call with a gas cap; a script fails the build if any write bypasses the three reviewed entry points, with the count of write sites pinned in a fixture.
Refresh chain state, build one request, simulate it for the actual account, decode and review it, then submit that same request. Report success only after a successful transaction receipt.
16. Run this site locally
For: App builders
Use the Node.js and npm versions declared in package.json. Copy the example public configuration before starting. NEXT_PUBLIC values are shipped to the browser, so they must never contain private keys or secrets.
git clone https://github.com/mejango/juicebox-money.git
cd juicebox-money
npm ci
cp .env.example .env.local
npm run devOpen http://localhost:3001. Confirm the configured indexer, RPC service, wallet providers, and allowed origin work in your environment. Start with project reads, then connect a testnet wallet for transaction flows.
Commands
- npm run dev
- Next.js on port 3001
- npm run check
- Full repository gate: dependencies, types, lint, source/deployment/schema checks, tests, production build, browser checks, and budgets
- npm test / npm run test:browser
- Vitest, then Playwright against the built app with a fixture server
- npm run schema:check
- Regenerates and verifies the Bendystraw operation registry
- Env: NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_BENDYSTRAW_URL, NEXT_PUBLIC_TESTNET_BENDYSTRAW_URL, NEXT_PUBLIC_PARA_API_KEY, NEXT_PUBLIC_PARA_ENV, NEXT_PUBLIC_VERSION; WalletConnect and the on-ramp provider are optional. No RPC or IPFS keys live in the client.
- Tests: the fixture-based browser suite isolates external services; use the documented fork and live-service checks separately when validating an integration.
17. Launch
For: App buildersContract builders
Everything starts with JBController.launchProjectFor(). This single call:
- Mints an ERC-721 project NFT to the owner address
- Configures initial rulesets (payout limits, token weights, reserved percents, etc.)
- Sets up terminal configurations (which tokens the project accepts)
- Registers the project in JBDirectory
launchProjectFor(
owner, // receives the project NFT
projectUri, // metadata (name, description, logo)
rulesetConfigurations[], // operational parameters
terminalConfigurations[], // payment processing setup
memo // transaction description
)
// Send exactly JBProjects.creationFee() as transaction value.18. Configure
For: App buildersContract builders
After launch, inspect and understand your project’s configuration:
READING PROJECT STATE
- JBProjects.ownerOf(projectId)
- Who owns the project NFT
- JBController.uriOf(projectId)
- Metadata link (name, description, logo)
- JBController.currentRulesetOf(projectId)
- Active ruleset and its metadata
- JBController.upcomingRulesetOf(projectId)
- What comes next (auto-cycled with weight decay)
- JBDirectory.terminalsOf(projectId)
- All active terminals
- JBDirectory.primaryTerminalOf(projectId, token)
- Default terminal for a specific token
- JBMultiTerminal.accountingContextsOf(projectId)
- Which tokens/currencies are accepted
- JBSplits.splitsOf(projectId, rulesetId, groupId)
- Payout and reserved token distribution rules
FUND ACCESS LIMITS
- JBFundAccessLimits.payoutLimitOf(...)
- Maximum distributable per cycle per token
- JBFundAccessLimits.surplusAllowanceOf(...)
- How much surplus the owner can withdraw
19. Get funded
For: App buildersContract builders
Once launched, anyone can contribute to the project through its configured terminals.
pay(
projectId,
token, // which token to pay with
amount, // how much
beneficiary, // who receives the minted tokens
minReturnedTokens, // slippage protection
memo, // message attached to the payment
metadata // extra data for hooks
)
// Returns: number of tokens minted for the beneficiaryCHECKING BALANCES
- JBTerminalStore.balanceOf(terminal, projectId, token)
- Terminal balance for a specific token
- JBTerminalStore.currentSurplusOf(...)
- Surplus across specified terminals and tokens
- JBTerminalStore.currentTotalSurplusOf(...)
- Surplus aggregated across ALL terminals
20. Manage tokens
For: App buildersContract builders
Tokens start as internal credits. Deploy an ERC-20 whenever you’re ready.
TOKEN OPERATIONS
- JBController.deployERC20For(projectId, name, symbol, salt)
- Deploy the project’s ERC-20 token
- JBTokens.tokenOf(projectId)
- Get the ERC-20 address (zero if not yet deployed)
- JBTokens.totalBalanceOf(holder, projectId)
- Complete holdings (credits + ERC-20)
- JBTokens.creditBalanceOf(holder, projectId)
- Internal credits only
- JBController.claimTokensFor(holder, projectId, count, beneficiary)
- Convert credits into ERC-20 tokens
MINTING & BURNING
- JBController.mintTokensOf(projectId, tokenCount, beneficiary, memo, useReservedPercent)
- Owner mints tokens on-demand (if ruleset allows)
- JBController.burnTokensOf(holder, projectId, tokenCount, memo)
- Holder burns their own tokens
21. Distribute
For: App buildersContract builders
Projects distribute funds through payouts and reserved tokens. By default anyone can trigger distribution; the ownerMustSendPayouts ruleset flag restricts payouts to the owner or an operator with SEND_PAYOUTS permission.
sendPayoutsOf(
projectId,
token,
amount, // up to the payout limit
currency,
minTokensPaidOut // slippage protection
)
// Distributes to splits, leftover to project owner
// 2.5% protocol fee on payouts, except same-terminal project payouts and eligible feeless recipientssendReservedTokensToSplitsOf(projectId)
// Anyone can call this at any time
// Mints accumulated reserved tokens and distributes to splitsTRACKING USAGE
- JBTerminalStore.usedPayoutLimitOf(...)
- How much of the payout limit has been used this cycle
- JBTerminalStore.usedSurplusAllowanceOf(...)
- How much surplus allowance has been used
- JBController.pendingReservedTokenBalanceOf(projectId)
- Undistributed reserved tokens
22. Cash out
For: App buildersContract builders
Token holders can cash out tokens against the project’s current surplus under its tax, hook, and fee settings. In a single accounting context, surplus is the terminal balance above the remaining payout limit. Read a hook-aware quote for the actual transaction.
At 0% cash out tax the core formula returns a proportional share before fees. Higher rates reduce the amount returned to a holder cashing out part of the supply; at 100% it returns zero. The tax is separate from protocol and hook fees.
cashOutTokensOf(
holder,
projectId,
cashOutCount, // how many tokens to burn
tokenToReclaim, // which token to receive
minTokensReclaimed, // slippage protection
beneficiary, // who receives the funds
metadata
)Below 100% tax, multiply surplus by the holder’s supply share, then by the curve adjustment shown. A 0% tax is proportional. At 100% tax the contract returns zero, and protocol or hook fees are separate.
23. Evolve
For: App buildersContract builders
Projects evolve by queuing new rulesets. A timed ruleset changes at an eligible cycle boundary. A flexible ruleset can change without a cycle boundary, but any configured start time, notice, or approval still applies.
queueRulesetsOf(
projectId,
rulesetConfigurations[], // new parameters
memo
)
// If an approval hook is configured, it must approve
// the changes before they can activate.INSPECTING QUEUED CHANGES
- JBController.latestQueuedRulesetOf(projectId)
- Latest ruleset in the queue and its approval status (may already be the active one)
- JBController.allRulesetsOf(projectId, startingId, size)
- Complete ruleset history
24. What’s a revnet?
A revnet is a Juicebox project owned by REVOwner, which locks its staged economic parameters at launch. Its operator retains specific permissions: review these alongside the stage schedule.
The token is deployed as an ERC-20 at launch. Revenue can back cash outs under the configured terms. Fixed issuance rules do not guarantee revenue or fix the token’s market price.
Revnets use stages to describe their predetermined economic progression. Operator controls can still include split recipients, metadata, buyback settings, router selection, and configured NFT features.
Projects can be owned by wallets or contracts with ruleset-dependent powers. Revnets use REVOwner, commit the core stage schedule at launch, and deploy their ERC-20 during launch while retaining limited operator controls.
25. Deploy a revnet
For: App buildersContract builders
Deploy with REVDeployer.deployFor():
deployFor(
revnetId, // project ID (or 0 for auto)
configuration, // REVConfig with stages
accountingContextsToAccept[], // tokens the terminal should accept
suckerDeploymentConfiguration, // cross-chain setup
tiered721HookConfiguration, // optional NFT tiers
allowedPosts[] // optional croptop posts
)
// New revnet (revnetId == 0): include the current project creation fee.
// Existing project ID: send no native value.A revnet uses the same core payment and cash out entrypoints as other projects, with REVOwner applying its revnet terms and fee logic. Quote through the configured hooks and inspect the operator’s retained permissions.
26. Stages
For: App buildersContract builders
Stages are pre-programmed rulesets. A revnet might start with high token issuance (bootstrapping), then reduce over time (scarcity), and eventually reach a steady state.
Each stage can configure: token weight, weight decay, reserved splits, cash out tax rate, and more. Once deployed, stages progress automatically at their configured boundaries.
READING STAGE STATE
- JBController.currentRulesetOf(projectId)
- Active stage parameters
- JBController.upcomingRulesetOf(projectId)
- Next ruleset, including an automatic cycle of the current stage; empty only when no ruleset follows.
- JBController.allRulesetsOf(projectId, startingId, size)
- Complete stage history
27. Revnet fees
Ordinary taxed revnet cash outs can incur both fees below. The revnet fee is skipped for zero-tax cash outs, feeless beneficiaries, or when the fee project has no compatible terminal.
- 2.5% protocol fee — taken from the reclaimed value by JBMultiTerminal, sent to the Juicebox protocol’s project
- 2.5% revnet fee — REVOwner allocates a share of the token count to the fee calculation and sends its associated reclaim value to the revnet fee project.
The protocol fee is based on reclaimed value. The revnet fee is calculated from a share of the token count; its recipient receives the associated reclaim value, not those project tokens. The rates have different bases, so do not simply subtract 5% from a core quote.
28. Go deeper on revnets
revnet.money carries the full revnet build guide: the wizard field by field, .jb drafts and agent launches, what deploy does, the operator’s nine permissions, loans from a contract, the extension points REVOwner allows and forbids, and the sharp edges. Use its linked source references to verify the configuration you plan to deploy.
29. Install the code and launch from Solidity
For: Contract builders
The V6 repos ship as npm packages and import by package path; remappings.txt in every repo only maps forge-std, and node_modules resolves the rest. Solidity 0.8.28.
Packages
- @bananapus/core-v6
- Terminals, controller, tokens, splits, permissions, prices, the hook interfaces
- @bananapus/721-hook-v6
- Shop tiers and the 721 project deployer
- @bananapus/omnichain-deployers-v6
- Launch plus suckers in one call
- @bananapus/buyback-hook-v6, @bananapus/suckers-v6, @bananapus/router-terminal-v6
- Pool routing, cross-chain, token routing
- @bananapus/project-payer-v6, @bananapus/project-handles-v6, @bananapus/distributor-v6
- Payer addresses, ENS handles, distributor
- @bananapus/permission-ids-v6
- The permission ID constants
- @rev-net/core-v6
- Revnets
launchProjectFor, field by field
- JBController.launchProjectFor
- (address owner, string projectUri, JBRulesetConfig[] rulesetConfigurations, JBTerminalConfig[] terminalConfigurations, string memo) payable → projectId. msg.value must equal JBProjects.creationFee() exactly
- JBRulesetConfig
- mustStartAtOrAfter (uint48; 0 = now), duration (uint32 seconds; 0 = until replaced), weight (uint112, 18-dec tokens per base-currency unit; 1 = inherit the decayed weight, 0 = no issuance), weightCutPercent (uint32, of 1e9), approvalHook, metadata, splitGroups[], fundAccessLimitGroups[]
- JBRulesetMetadata
- reservedPercent (uint16, of 10,000), cashOutTaxRate (uint16, of 10,000; 10,000 = cash outs off), baseCurrency (uint32: 1 = ETH, 2 = USD, or uint32(uint160(token))), pausePay, pauseCreditTransfers, allowOwnerMinting, allowSetCustomToken, allowTerminalMigration, allowSetTerminals, allowSetController, allowAddAccountingContext, allowAddPriceFeed, ownerMustSendPayouts, holdFees, scopeCashOutsToLocalBalances, useDataHookForPay, useDataHookForCashOut, dataHook, metadata (uint16, 14 usable bits)
- JBFundAccessLimitGroup
- terminal, token, payoutLimits[] and surplusAllowances[] as { amount (uint224, in the token’s decimals), currency (uint32) }, sorted by strictly increasing currency. Empty = no payouts; type(uint224).max = unlimited. Per chain, never aggregate
- JBTerminalConfig
- terminal, accountingContextsToAccept[] of { token, decimals (uint8), currency (uint32) }. NATIVE_TOKEN is 0x…EEEe with currency 61166
- JBSplitGroup / JBSplit
- groupId (1 = reserved tokens; uint256(uint160(token)) = payouts in that token), splits[] of { percent (uint32, of 1e9), projectId (uint64), beneficiary, preferAddToBalance, lockedUntil (uint48), hook }. Routing: hook, else projectId, else beneficiary
- JBOmnichainDeployer.launchProjectFor
- (owner, projectUri, JBOmnichain721Config, rulesetConfigurations, terminalConfigurations, memo, JBSuckerDeploymentConfig) payable → (projectId, hook, suckers[]). Also deploys this chain’s suckers; the sucker salt is keccak256(sender, salt), so the same sender must launch on every chain
- JB721TiersHookProjectDeployer.launchProjectFor
- (owner, JBDeploy721TiersHookConfig, JBLaunchProjectConfig, controller, salt) payable → (projectId, hook). Sets the hook as the data hook
30. Custom hooks
For: Contract builders
Build custom logic that executes at key moments in the payment lifecycle. Hooks are the primary extension mechanism.
HOOK INTERFACES
- IJBRulesetDataHook
- Intercepts pay and cash out BEFORE state changes. Can override the weight (pay) or the cash out tax rate / effective counts (cash out), and specify pay and cash out hook specifications.
- IJBPayHook
- Called AFTER payment recorded and tokens minted. Use for rewards, notifications, side effects.
- IJBCashOutHook
- Called AFTER tokens burned and funds transferred. Use for cleanup, analytics, conditional logic.
- IJBSplitHook
- Called when a split routes funds to a hook address. Use for auto-investing, compounding, forwarding.
- IJBRulesetApprovalHook
- Gates queued rulesets. Must return APPROVED before a queued ruleset can activate.
function afterPayRecordedWith(
JBAfterPayRecordedContext calldata context
) external payable;
// context includes:
// payer, projectId, rulesetId, amount,
// forwardedAmount, weight, newlyIssuedTokenCount,
// beneficiary, hookMetadata, payerMetadata31. Hook mechanics: funds, metadata, minting, reentrancy
For: Contract builders
- Installing a data hook: set metadata.dataHook and useDataHookForPay / useDataHookForCashOut on the ruleset. beforePayRecordedWith returns the weight to use and pay hook specifications; beforeCashOutRecordedWith returns the tax rate, count, supply, surplus, and cash out hook specifications.
- Funds: native value arrives at a pay or cash out hook as msg.value; ERC-20 arrives as an allowance you must transferFrom during the call, revoked afterwards. Fee treatment depends on the operation and recipient; quote the net amount delivered to the hook. A specification with noop = true is informational and never called.
- Two metadatas: hookMetadata is authored by the data hook, while payerMetadata / cashOutMetadata comes from the caller. Trust hookMetadata only after authenticating the calling terminal and the expected hook path; validate caller-supplied fields.
- Metadata format: JBMetadataResolver: a reserved first word, then a table of 4-byte ids with word offsets, then 32-byte-aligned blobs. createMetadata(ids, datas), addToMetadata, getDataFor(id, metadata). Ids are getId(purpose, target) = bytes4(bytes20(target) ^ bytes20(keccak256(purpose))).
- Minting from a hook: JBController.mintTokensOf lets the terminal, the ruleset’s data hook, or any address the data hook’s hasMintPermissionFor approves mint without a grant; everyone else needs MINT_TOKENS (10) and allowOwnerMinting.
- Split hooks: a split whose hook field is set receives its share through processSplitWith. On the reserved-token path the controller approves an ERC-20 for the amount and burns whatever the hook leaves unspent; on payouts the terminal checks ERC-165 first.
- Reentrancy: pay and cashOutTokensOf call hooks after recording state. Review every callback and protect your own state transitions against reentrancy; a blanket guard on core-dependent flows can also break intended composition. Use override(ERC165, IERC165) for supportsInterface.
32. Permissions
For: Contract buildersApp builders
Grant fine-grained access to other addresses with JBPermissions. Each permission is a bit in a 256-bit field.
setPermissionsFor(
account, // the address granting permission
permissionsData // { operator, projectId, permissionIds[] }
)
// projectId = 0 is the wildcard: every project `account` controls on this chainCHECKING PERMISSIONS
- JBPermissions.hasPermission(operator, account, projectId, permissionId, includeRoot, includeWildcard)
- Check a single permission
- JBPermissions.hasPermissions(operator, account, projectId, permissionIds[], includeRoot, includeWildcard)
- Check multiple permissions at once
- JBPermissions.WILDCARD_PROJECT_ID()
- Returns 0 — the wildcard project ID
PERMISSION IDS
- 1 - ROOT
- Grants all permissions. Use with extreme care.
- 2 - QUEUE_RULESETS
- Queue new rulesets for the project.
- 3 - LAUNCH_RULESETS
- Launch the project’s first rulesets.
- 4 - CASH_OUT_TOKENS
- Cash out (redeem) project tokens on a holder’s behalf.
- 5 - SEND_PAYOUTS
- Trigger payout distributions.
- 6 - MIGRATE_TERMINAL
- Migrate funds to a new terminal.
- 7 - SET_PROJECT_URI
- Update project metadata.
- 8 - DEPLOY_ERC20
- Deploy the project’s ERC-20 token.
- 9 - SET_TOKEN
- Set a custom token for the project.
- 10 - MINT_TOKENS
- Mint tokens on-demand.
- 11 - BURN_TOKENS
- Burn tokens from another holder.
- 12 - CLAIM_TOKENS
- Claim credits into ERC-20 tokens for a holder.
- 13 - TRANSFER_CREDITS
- Transfer a holder’s unclaimed credits.
- 14 - SET_CONTROLLER
- Change the project controller.
- 15 - SET_TERMINALS
- Set the project’s terminals.
- 16 - ADD_TERMINALS
- Add terminals to the project.
- 17 - SET_PRIMARY_TERMINAL
- Set the primary terminal for a token.
- 18 - USE_ALLOWANCE
- Withdraw surplus via the surplus allowance.
- 19 - SET_SPLIT_GROUPS
- Modify payout and reserved token splits.
- 20 - ADD_PRICE_FEED
- Add a price feed for a currency pair.
- 21 - ADD_ACCOUNTING_CONTEXTS
- Add accounting contexts (accepted tokens) to a terminal.
- 22 - SET_TOKEN_METADATA
- Set the project token’s name and symbol.
- 23 - SIGN_FOR_ERC20
- Sign ERC-20 permit approvals on the project’s behalf.
- 24 - ADJUST_721_TIERS
- Add or remove tiers on a 721 hook.
- 25 - SET_721_METADATA
- Update a 721 hook’s metadata (base URI, resolver, contract URI).
- 26 - MINT_721
- Mint NFTs directly, without a payment.
- 27 - SET_721_DISCOUNT_PERCENT
- Set the 721 hook’s discount percent.
- 28 - SET_BUYBACK_TWAP
- Set the buyback hook’s TWAP window.
- 29 - SET_BUYBACK_POOL
- Set the buyback hook’s Uniswap pool.
- 30 - SET_BUYBACK_HOOK
- Set which buyback hook the registry routes the project to.
- 31 - SET_ROUTER_TERMINAL
- Configure the project’s router terminal.
- 32 - MAP_SUCKER_TOKEN
- Map a token across a sucker pair.
- 33 - DEPLOY_SUCKERS
- Deploy cross-chain suckers for the project.
- 34 - SET_SUCKER_PEER
- Set a sucker’s cross-chain peer.
- 35 - SUCKER_SAFETY
- Emergency token recovery on a sucker.
- 36 - SET_SUCKER_DEPRECATION
- Deprecate a sucker.
- 37 - OPEN_LOAN
- Open a REVLoans loan against project tokens.
- 38 - REALLOCATE_LOAN
- Move collateral into a new loan and borrow its current capacity. The operator chooses where new proceeds go.
- 39 - REPAY_LOAN
- Repay or release collateral on a holder’s behalf. The operator chooses where returned collateral goes.
33. Test against the real thing
For: Contract builders
Foundry
- TestBaseWorkflow
- @bananapus/core-v6/test/helpers/TestBaseWorkflow.sol: deploys the full protocol locally with a mock USDC and two terminals, plus Permit2. Extend it for unit tests of hooks and integrations
- Fork tests
- Pin a block, fork through foundry.toml rpc_endpoints, and run against the deploy-all-v6 addresses before shipping
- Sizes
- forge build --sizes early; several V6 contracts sit near EIP-170 and were split or trimmed to fit. Plan for library extraction if you are close
- Bytecode parity
- verify a deployment against the deploy-all-v6 artifacts rather than trusting a source match; linked libraries change the hash
34. Sharp edges
For: Contract buildersApp builders
- Who is the payer: context.payer is the terminal’s msg.sender. Through a router, project payer, or wrapper it is that contract, unless the contract exposes originalPayer() (IJBPayerTracker), which the router registry probes. Expose the original payer where the integration expects it, and set beneficiary/refund addresses explicitly so rewards or refunds are not accidentally credited to the intermediary.
- Router terminal cold start: the router registry reverts accountingContextForTokenOf for projects below its threshold; it is not universally accepting. Probe with previewPayFor before assuming a route.
- Buyback metadata is three words: the pay metadata under getId("pay") for the buyback hook decodes as (amountToSwapWith, minimumSwapAmountOut, skipSplits). Always encode all three.
- Payouts to a project without a terminal: a payout split to a project that has no terminal for the token is caught, the balance is restored, and no fee is taken, but the payout limit is still consumed.
- Permissions die with the NFT: every check asks whether the current owner granted the permission; grants by a previous owner stop authorizing anything when the project NFT moves.
- Held fees: a failed processHeldFeesOf forgives the fee. Cash out fees are never held.
- Sucker salt: the omnichain deployer derives sucker addresses from the sender and the salt, so a different wallet on another chain produces a project that cannot be linked.
35. NFT tiers
For: Project buildersApp buildersContract builders
Deploy tiered NFTs as pay hooks using JB721TiersHook. Payers pick tiers in the pay metadata and receive the NFTs their payment covers.
launchProjectFor(
owner,
deployTiersHookConfig, // NFT name, symbol, tiers[]
launchProjectConfig, // standard project config
controller,
salt // CREATE2 salt for a deterministic hook address (0 for none)
)
// Always use this deployer, even with empty tiersTIER CONFIGURATION
- price
- What one NFT of this tier costs.
- initialSupply
- Max NFTs available. Must be at least 1; capped at 999,999,999. 0 is rejected.
- category
- Grouping ID. Tiers MUST be sorted by category (ascending).
- reserveFrequency
- Mint 1 reserved NFT every N minted.
- reserveBeneficiary
- Who receives reserved NFTs.
- votingUnits
- Governance weight (via JB721Checkpoints). Applies only when the tier’s flags.useVotingUnits is set — otherwise voting power tracks the tier price.
- encodedIpfsUri
- IPFS content hash for metadata.
- flags.cantBeRemoved
- If true, tier is permanent (one of the nested flags: allowOwnerMint, useVotingUnits, transfersPausable, cantBeRemoved, …).
READING NFT STATE
- JB721TiersHookStore.tiersOf(hook, categories[], includeResolvedUri, startId, size)
- List tiers with optional filters
- JB721TiersHookStore.tierOf(hook, tierId, includeResolvedUri)
- Single tier details
- JB721TiersHook.balanceOf(owner)
- NFTs held by an address
- JB721TiersHook.cashOutWeightOf(tokenIds[])
- Cash out weight of specific NFTs (divide by totalCashOutWeight() for the surplus fraction)
36. Buyback hook
For: App buildersContract builders
JBBuybackHook compares the mint price against a Uniswap V4 pool price and routes payments (and cash outs) to whichever gives better value. Slippage tolerance defaults to a TWAP-based sigmoid; a payer can override it with a quote and minimum in the pay metadata.
// Set up the buyback hook with a Uniswap V4 pool
JBBuybackHook.setPoolFor(
projectId,
fee, // Uniswap pool fee tier
tickSpacing, // pool tick spacing
twapWindow, // TWAP observation window (seconds)
terminalToken // the terminal token to route
)
// The pool key is once-only; the TWAP window stays mutable via setTwapWindowOf
// Requires SET_BUYBACK_POOL permission payment arrives
│
▼
query TWAP oracle for market price
│
├─ pool gives more tokens than minting
│ └─▶ swap on Uniswap V4, mint any unswapped remainder
│
└─ minting gives equal or more tokens
└─▶ normal mint flow (weight × amount)37. Router terminal
For: App buildersContract builders
JBRouterTerminal is a universal payment terminal: it accepts any token and automatically converts it into whatever token the destination project accepts, then forwards the result to that project’s primary terminal. It’s a pass-through — it never holds a balance.
There is no fixed output token. For each payment, a JBPayRouteResolver evaluates every token the destination project accepts and picks the route that yields the most project tokens for the payer — choosing among direct forwarding, a Uniswap V3 or V4 swap, a recursive cash out of JB tokens, or a combination. Pools and routes are discovered automatically, not configured per project.
ROUTER TERMINAL FUNCTIONS
- pay(...)
- Same IJBTerminal interface as JBMultiTerminal — resolves the best route, converts the input, then calls pay() on the destination terminal.
- addToBalanceOf(...)
- Same as pay() but forwards via addToBalanceOf() on the destination terminal (no token minting).
- previewPayFor(...)
- Preview the chosen route and expected output for a payment without executing it.
- bestPoolLiquidityOf(tokenA, tokenB)
- Report the deepest-liquidity Uniswap pool the router would use for a pair.
The router is reached through JBRouterTerminalRegistry, which is what a project adds to JBDirectory alongside JBMultiTerminal; the registry resolves to JBRouterTerminal. Routing is internal (JBPayRouteResolver) — there is no per-project pool configuration.
38. Payer address
For: Project buildersContract builders
A JBProjectPayer address is deployed as a minimal proxy (clone). The constructor takes only a JBDirectory address. After deployment, defaults are set via initialize() or setDefaultValues().
The receive path accepts the native token (ETH) only. When defaultAddToBalance is false, incoming native ETH triggers pay() — minting tokens for the beneficiary. When true, funds are added via addToBalanceOf() without minting. With no explicit or default beneficiary, the contract resolves the original payer, including supported upstream payer trackers. ERC-20 payments must call pay() or addToBalanceOf() after approval; direct ERC-20 transfers do not trigger either path.
// Set via initialize() after clone deployment:
defaultProjectId // which project to forward to
defaultBeneficiary // who gets the tokens (0 = msg.sender)
defaultMemo // attached to each payment
defaultMetadata // extra data for hooks
defaultAddToBalance // false = pay(), true = addToBalance()
// Anyone sends ETH to the payer address:
// → receive() fires
// → looks up DIRECTORY.primaryTerminalOf(projectId, token)
// → calls pay() or addToBalanceOf() with defaults39. Project handles
For: Project buildersApp builders
JBProjectHandles maps ENS names to Juicebox project IDs using bidirectional verification. Anyone can propose a handle, but only verified ones (where the ENS text record matches) are returned by handleOf().
All functions take a chainId parameter — handles are chain-aware. Storage is keyed by the setter address, so multiple addresses can propose different handles for the same project.
HANDLE FUNCTIONS
- setEnsNamePartsFor(chainId, projectId, parts[])
- Associate ENS name parts with a project. Anyone can call this — no access control.
- ensNamePartsOf(chainId, projectId, setter)
- Get the stored name parts as set by a specific setter address.
- handleOf(chainId, projectId, setter)
- Returns the verified handle string, or empty if ENS text record doesn’t match.
- TEXT_KEY
- The ENS text record key: "juicebox". Expected value: "{chainId}:{projectId}".
Name parts are in reverse order. handleOf returns the dot-joined labels without a .eth suffix; the suffix is only added when computing the namehash for verification. `_formatHandle` (JBProjectHandles.sol:220-232) walks the array from the LAST element to the first, so the innermost label goes last. For "myproject.eth" → ["myproject"]. For "sub.myproject.eth" → ["myproject", "sub"]. Parts cannot contain dots, ASCII control characters, DEL, "eth", or be empty. Unicode normalization (ENSIP-15) is the caller/client’s responsibility, not the contract’s.
40. Distributor
For: Contract builders
JBDistributor is an optional, project-deployed add-on (not part of the core protocol deployment). It distributes ERC-20 rewards to stakers in time-based rounds with linear vesting. Two implementations exist: JBTokenDistributor (for IJBActiveVotes token holders, e.g. a Juicebox JBERC20) and JB721Distributor (for NFT holders).
The distributor is funded via split hooks or direct deposits. Each round, a snapshot captures the distributable balance. Stakers claim their pro-rata share, which vests linearly over a configured number of rounds.
CORE FUNCTIONS
- fund(hook, token, amount)
- Directly deposit reward tokens for a specific hook’s staker pool
- beginVesting(hook, tokenIds[], tokens[])
- Snapshot and begin vesting for the specified token IDs
- collectVestedRewards(hook, tokenIds[], tokens[], beneficiary)
- Collect unlocked vested tokens (auto-vests current round too)
- releaseForfeitedRewards(hook, tokenIds[], tokens[], beneficiary)
- Return unvested rewards from burned tokens to the pool
- poke()
- Record the snapshot block for the current round early
READ STATE
- balanceOf(hook, token)
- Balance held for a hook’s staker pool
- collectableFor(hook, tokenId, token)
- How much is unlocked and ready to collect right now
- claimedFor(hook, tokenId, token)
- Total uncollected amount (vesting + vested-but-uncollected)
- currentRound()
- The current round number
- roundSnapshotBlock(round)
- The block number used for stake weight lookups
41. Test what can surprise you
- Launch: the encoded configuration round-trips through the ABI; the creation fee is read at send time; the same sender and salt yield the same sucker addresses on a second chain.
- Payments: the chosen route’s executable minimum is no worse than the alternatives shown; an empty pool falls back to issuance; a router route is probed, not assumed.
- Cash outs: the terminal or hook enforces the same minimum the confirmation shows, including at 0% fee-free surplus.
- Payouts: limits are per chain and per cycle; a recipient project without a terminal consumes the limit without paying; fees are held or not as the ruleset says.
- Rulesets: a queue with too little notice fails its approval hook; an inherited weight is decayed, not copied.
- Permissions: the narrowest ID is granted, scoped to the project; a Safe proposal is not success.
- Hooks: reentrancy from a hostile hook or token, an under-pulling split hook, hostile payer metadata.
Fork-test against the current deployments. Then publish the addresses, source, transaction map, and a human-readable ruleset schedule so users can check your product against the contracts themselves. The audit page has prompts for a whole-system review and for a single transaction.
42. Build from this client
Juicebox Money is a production V6 client you can study, fork, or use as a reference for your own product. Its Next.js interface combines server-assisted indexing and IPFS services with wallet flows that build and verify Juicebox transactions from the current V6 contracts.
Treat each working flow as an implementation example, not a black box. The project, account, shop, and create surfaces show how product interactions map to indexed reads, fresh onchain checks, transaction builders, ABI round trips, and clear signing previews. Give the relevant source and tests to your coding agent when you want to reuse one of those patterns.
- Start from the product flow closest to yours — such as Pay, Cash Out, project creation, ruleset editing, or the Shop — and identify its component, supporting reads, and transaction builder.
- Give your coding agent that source, its tests, this guide’s deep link, the current V6 contract repository below, and the Juicebox V6 skills library (github.com/mejango/juicebox-skills) — a Claude Code plugin whose skills carry the addresses, ABIs, fee math, and transaction-safety rules so the agent does not reconstruct them from memory.
- Keep indexed data for fast discovery and display, but re-read signing-critical state onchain immediately before building and submitting a transaction.
- Reuse the pure transaction-builder and ABI round-trip pattern, then add product-specific invariants and browser tests before asking a wallet to sign.
Use the app as a product reference and the repository as the implementation reference. Server routes improve indexing, search, media, and transaction preparation; signing remains explicit, and each wallet-bound action is decoded and checked against the V6 ABI before submission.