Skip to content

Developer guide

Build on Juicebox.

Launch a project without code, connect an app with the Juicebox software library (SDK), or extend the protocol with your own contracts. Choose a path below, then use the reference for V6 contract calls, examples, and source links.

Build with an AI assistant

Describe your product in the prompt below, then give your assistant the Juicebox V6 skills for contract addresses, interfaces, and fee calculations. Review its proposed transactions against the current contracts before signing.

Read or manually copy the prompt
Start here

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.

Pick the model
You need toUse
Pay a team a budget each cycle, from revenue or donationsProject (payout limits)
Keep the option to change rules, add tokens, or migrate laterProject (rulesets, superpowers)
Sell NFTs or memberships and treat the sales as revenueEither, with a shop
Commit an issuance schedule and cash out terms at launchRevnet (stages; inspect retained operator controls)
Let holders cash out against surplusProject or revnet, when the terms allow it
Let holders borrow using project tokens as collateralRevnet (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.

  1. 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.
  2. Build an app: install the SDK, read a testnet project with the example below, then add indexed discovery and a quoted transaction flow.
  3. 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.
Project builders

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
Drafts autosave in your browser, and the whole form imports and exports as a .jb file. Any live project’s Extras tab exports one reconstructed from the chain, which is how you diff what you launched against what you meant to.

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
The shop is always the project’s data hook, so you do not set one by hand. The cash out tax is the only number here that shapes a curve; the Learn guide has the formula and a worked example.

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
A queued ruleset takes effect at the next cycle boundary of a timed ruleset, or as soon as the notice period passes for a flexible one. A change that would start before the notice period has run fails its approval and is skipped.

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
App builders

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.

Create a small JavaScript project
mkdir juicebox-starter
cd juicebox-starter
npm init -y
npm install @bananapus/nana-sdk-core viem

Create 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.

read-project.mjs — read a Base Sepolia project
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 });
Run the read example
# 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
TypeScript imports and address lookup (chainId supplied by your app)
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
Show cached names, logos, and facts while the chain refreshes. Treat state you could not read as unknown, never as zero, empty, or permitted.
Indexed data: Bendystraw

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)
A first query (call outline)
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
  1. 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.
  2. Numeric arguments on singular queries are Float!, not Int! (Ponder’s choice). Declare variables as Float! or the request fails validation with no data.
  3. Lists page with limit and offset and return totalCount; loop until you have them all rather than trusting one page.
  4. 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.
  5. The SDK’s requestBendystraw(endpoint, query, variables) handles the POST, error surfacing, and endpoint normalisation; selectBendystrawEndpoint picks mainnet vs testnet from a chainId.
Building with an agent? The /jb-bendystraw skill in the Juicebox V6 skills library carries the schema, the query patterns above, and the gotchas — hand it over before asking for a feed, chart, or holder table. Source: github.com/peripheralist/bendystraw.

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.

Build → simulate → decode → review → write → confirm

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.

Clone, install, and start the reference app
git clone https://github.com/mejango/juicebox-money.git
cd juicebox-money
npm ci
cp .env.example .env.local
npm run dev

Open 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.
Life of a project

17. Launch

For: App buildersContract builders

Everything starts with JBController.launchProjectFor(). This single call:

  1. Mints an ERC-721 project NFT to the owner address
  2. Configures initial rulesets (payout limits, token weights, reserved percents, etc.)
  3. Sets up terminal configurations (which tokens the project accepts)
  4. Registers the project in JBDirectory
JBController.launchProjectFor (call outline)
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.
For omnichain projects, use JBOmnichainDeployer.launchProjectFor() instead — it launches the project and its local suckers in one transaction. Run it on each chain the project should live on.

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
Empty fundAccessLimitGroups = zero payouts (NOT unlimited). Use uint224.max for unlimited payouts.

19. Get funded

For: App buildersContract builders

Once launched, anyone can contribute to the project through its configured terminals.

JBMultiTerminal.pay (call outline)
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 beneficiary

CHECKING 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
Anyone can also inject capital without receiving tokens via addToBalanceOf(). This is useful for grants, donations, or returning funds.

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.

JBMultiTerminal.sendPayoutsOf (call outline)
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 recipients
JBController.sendReservedTokensToSplitsOf (call outline)
sendReservedTokensToSplitsOf(projectId)
// Anyone can call this at any time
// Mints accumulated reserved tokens and distributes to splits

TRACKING 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
sendPayoutsOf() is permissionless by default. ownerMustSendPayouts restricts it to the owner or a SEND_PAYOUTS operator. A payout to another project through a different, non-feeless terminal still incurs the standard fee.

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.

JBMultiTerminal.cashOutTokensOf (call outline)
cashOutTokensOf(
  holder,
  projectId,
  cashOutCount,         // how many tokens to burn
  tokenToReclaim,       // which token to receive
  minTokensReclaimed,   // slippage protection
  beneficiary,          // who receives the funds
  metadata
)
BONDING CURVE

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.

For non-feeless beneficiaries, a nonzero cash out tax normally makes the full direct reclaim eligible for the 2.5% protocol fee. At zero tax, only min(reclaimed, feeFreeSurplusOf) is eligible. Hooks and routing affect the final quote.

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.

JBController.queueRulesetsOf (call outline)
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
Life of a revnet

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.

REVNET vs PROJECT

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():

REVDeployer.deployFor (call outline)
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.

  1. 2.5% protocol fee — taken from the reclaimed value by JBMultiTerminal, sent to the Juicebox protocol’s project
  2. 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.

Contract builders

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
queueRulesetsOf timing: with a timed base ruleset the new one snaps to the next cycle boundary; with a flexible base it starts at mustStartAtOrAfter, clamped to at least the queue time plus the approval hook’s DURATION. A JBDeadline hook fails any ruleset queued with less notice than its duration, and currentRulesetOf only honours Approved or hook-less rulesets.

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.
IJBPayHook interface (call outline)
function afterPayRecordedWith(
  JBAfterPayRecordedContext calldata context
) external payable;

// context includes:
//   payer, projectId, rulesetId, amount,
//   forwardedAmount, weight, newlyIssuedTokenCount,
//   beneficiary, hookMetadata, payerMetadata
Data hooks run BEFORE state changes and can override values. Pay and cash out hooks run AFTER and are for side effects only.

31. 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.

JBPermissions.setPermissionsFor (call outline)
setPermissionsFor(
  account,         // the address granting permission
  permissionsData  // { operator, projectId, permissionIds[] }
)
// projectId = 0 is the wildcard: every project `account` controls on this chain

CHECKING 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.
Permissions are per-operator, per-project. Granting QUEUE_RULESETS to address X for project 5 doesn’t give X any access to project 6.

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.
Ecosystem tools

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.

JB721TiersHookProjectDeployer.launchProjectFor (call outline)
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 tiers

TIER 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)
Tiers are sorted by CATEGORY, not price. The contract reverts with InvalidCategorySortOrder if submitted out of order.

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.

JBBuybackHook configuration (call outline)
// 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
BUYBACK DECISION FLOW
  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)
The hook also handles cash outs: if the pool offers more than the bonding curve reclaim (after fees), it routes the sell through the pool instead. Payers can bypass the TWAP by providing their own quote in payment metadata.

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.

JBProjectPayer defaults (call outline)
// 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 defaults
Terminal lookup happens at payment time via JBDirectory, so the payer address automatically follows terminal migrations without reconfiguration.

39. 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
A holder’s stake comes from IVotes.getPastVotes() (token distributors) or tier voting units (721 distributors). The TOTAL-stake denominator uses IJBActiveVotes.getPastTotalActiveVotes — which excludes undelegated balances (e.g. AMM-held tokens), so holders must delegate (even to themselves) to count. Rewards are proportional to active stake at the snapshot block.
Ship it safely

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.

  1. 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.
  2. 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.
  3. Keep indexed data for fast discovery and display, but re-read signing-critical state onchain immediately before building and submitting a transaction.
  4. 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.