Skip to content

Build guide

Build on Juicebox.

Launch a project, build an app, or write contracts. Choose a path to get started.

New to building? Start with Your first test payment: read a project, preview a payment, and check what happened.

Build with an AI assistant

Describe your product in the prompt below, then give your assistant the Juicebox V6 skills to help it build with Juicebox. Review what each transaction will do before signing.

Read or manually copy the prompt
Start here
Choose a project or revnet

1. Choose a project or revnet

A Juicebox project collects money, shares tokens, and follows rules you set. Its owner can change the terms when those rules allow it. A revnet sets its core economic terms at launch and creates its transferable token then; its operator keeps a smaller set of controls.

Project builders can launch and manage funds without code. App builders can connect their product to Juicebox. Contract builders can add new behavior. Start with the path you need and use the reference as you go.

Pick the model
You need toUse
Pay a team from revenue or donations on a scheduleProject with a spending limit
Keep the option to change rules or contracts laterProject with those owner powers enabled
Sell collectibles or membershipsEither, with a shop
Set how new tokens and cash outs work at launchRevnet; check the controls its operator keeps
Let holders exchange tokens for available project fundsProject or revnet, when the terms allow it
Let holders use tokens to borrow project fundsRevnet with funds available to lend
Start with one working exampleStart with one working example

2. Start with one working example

Choose one result below. Each path explains the tools you need as you use them. The Learn glossary is there when you need a definition.

  1. Launch a project: use the setup form on a test network. Review the terms, then try a small payment and payout before using real funds.
  2. Build an app: follow Your first test payment. Read a project, preview a payment, and check the result.
  3. Write a contract: choose one behavior to add. Read the V6 contract reference, build it, and test against a local copy of the chain.

3. Find the right contract

The deploy-all-v6 repository lists deployed contracts by chain. It includes each address and the format used to call it, called an ABI. The SDK includes the same address map. A project can use its own contracts, so use JBDirectory to find the contracts currently managing its rules and payments before building a transaction.

Who does what

JBProjects
Records each project’s owner using a unique token (NFT)
JBController
Manages rules, creates project tokens, and records project details
JBMultiTerminal
Receives payments, holds funds, sends payouts, and handles cash outs
JBRulesets, JBSplits, JBFundAccessLimits, JBTokens, JBTerminalStore
Store rules, recipients, spending limits, tokens, and balances
JBDirectory
Finds a project’s rules manager (controller) and payment contracts (terminals)
JBPermissions
Records permission to act for another account
JBPrices
Converts currencies when calculating new tokens and spending limits
JB721TiersHook + deployer
Adds a shop to project payments
JBBuybackHookRegistry + JBBuybackHook
Buys existing tokens on Uniswap V4 when that gives the payer more tokens
JBSucker + JBSuckerRegistry, JBOmnichainDeployer
Links projects and moves tokens between chains
JBRouterTerminalRegistry
Finds supported ways to convert a payment into an accepted token
REVDeployer, REVOwner, REVLoans
Revnets
  • Chains: Ethereum, Optimism, Base, and Arbitrum, plus their Sepolia test networks. Find the list in the SDK’s SUPPORTED_CHAINS and JB_CHAINS.
  • 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.

4. Find a contract call

Use this table once you know the action you want to build. The second column names its contract call and any matching helper in the Juicebox software library (SDK). Examples later in this guide contain placeholders; replace them before use. Some actions need more than one transaction, such as approving token spending before paying.

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 tokens set aside for other recipients
JBController.sendReservedTokensToSplitsOf
Schedule new terms
JBController.queueRulesetsOf — buildQueueRulesetsTx
Edit recipients and shares
JBController.setSplitGroupsOf — buildSetSplitGroupsTx
Create a transferable token contract (ERC-20)
JBController.deployERC20For — buildDeployErc20Tx
Move tokens tracked in Juicebox into the ERC-20
JBController.claimTokensFor — buildClaimTokensTx
Create more tokens
JBController.mintTokensOf — buildMintTokensTx
Give someone permission
JBPermissions.setPermissionsFor — buildSetPermissionsTx
Manage the shop
JB721TiersHook.adjustTiers / mintFor
Move tokens to another chain
sucker.prepare → toRemote → claim
Choose a market for buying existing tokens
JBBuybackHookRegistry.initializePoolFor / setHookFor
Create an address that forwards payments
JBProjectPayerDeployer — buildDeployProjectPayerTx

Store amounts as whole numbers in the token’s smallest unit, using bigint. Convert only for display. Identify each project by both chain ID and project ID. Linked projects share a group ID, called a sucker group, but keep separate addresses, balances, and rule IDs on each chain.

Project builders

5. Launch with the setup form

For: Project builders

The setup form covers Flavor, Look and feel, Rules or Stages when needed, Shop, and Launch. The simple flavor skips Rules and uses preset terms. Other projects can schedule new terms when their rules allow it. A revnet fixes its main financial terms at launch, while its operator keeps a limited set of controls.

The steps

Flavor
Choose a project type, real or test networks, accepted assets, and owner. Link chains if needed. A custom token must use the same address on each chain. “Accept any token” adds support for asset conversion. Turning “Allow changes” off sends ownership to an address nobody can use
Look and feel
Name (100 characters), token symbol, tagline, description (10,000 characters), logo and cover (25 MB each), links, tags, and payment notice. Saved when you launch to IPFS, a network that identifies files by their content
Rules
Set the first terms, what follows them, and how much notice changes require. The next section explains each setting
Shop
Add items with prices, supply, media, and categories. Choose who receives sales, discounts, reserved copies, and any voting rights. Launch creates a shop contract even with no items
Launch
One transaction per chain: preview, review, sign, and wait. A shared Safe wallet creates a proposal that still needs approval and execution
Drafts save in your browser. Import or export the form as a .jb file. A live project’s Extras tab can export its current settings so you can compare them with your draft.

6. Choose the project’s rules

For: Project builders

A group of project terms is called a ruleset. It can repeat on a schedule, with each repeat called a cycle. These are the settings in the setup form.

Rules step

Duration
How long each cycle lasts: 1 day to a year, Flexible (0), or Forever. Timed cycles repeat until replaced. Flexible rules have no cycle boundary; notice and approval still apply
Issuance
New tokens per ETH or USD paid, before reserved tokens. Default 10,000. Leave blank on a later ruleset to continue the previous rate after any scheduled cuts
Issuance cut
How much the new-token rate falls each cycle. Revnets express this as “cut X% every N days”
Reserved split
The share of new tokens set aside for other recipients. Add recipients to set the total
Cash outs
Let holders exchange tokens for available project funds. The cash out tax controls how much stays for remaining holders: 0% gives a proportional share; 100% turns cash outs off. Revnets may also delay when cash outs begin
Payouts
None, payments to the owner within a limit, or payments to named recipients. Set amounts or percentage shares for each accepted asset. Limits apply per cycle and per chain
Surplus allowance
How much the owner can withdraw beyond the regular spending limit
Accept payments
Off pauses paying
Owner can mint any time
Whether the owner can create tokens without a payment (allowOwnerMinting)
Superpowers
Launch contracts and settings the owner can change: payment contracts, rules manager, project token, accepted assets, and currency feeds. Check both the current powers and whether later rules can enable more
Afterwards, notice
What follows these terms, and how much notice a change needs: 3 hours, 1 day, 3 days, 7 days, or none
The form sets up the shop extension automatically. The Learn guide explains how the cash out tax changes the amount holders receive, with a worked example.

7. What happens when you launch

For: Project buildersApp builders

  • Launch contract: the app uses REVDeployer.deployFor for revnets, JBOmnichainDeployer.launchProjectFor for linked chains, and JB721TiersHookProjectDeployer.launchProjectFor for other projects. These launchers set up the required extensions.
  • Transaction value: read JBProjects.creationFee() right before each launch and pass that exact amount as value. Show it in the review before signing.
  • One transaction per chain: sign each launch in order with the same wallet. The app keeps a shared value, called a salt, so linked bridge contracts get matching addresses.
  • 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. Manage your project

For: Project builders

The project page groups available management actions into tabs. Available actions depend on your account, project rules, and extensions.

Project page

Rulesets
Read current and upcoming terms. Schedule a change under the project’s timing and approval rules
Funds
Send funds to chosen recipients, use the owner’s extra withdrawal allowance, or add funds
Owners
See holders and balances. Send tokens set aside for recipients, or create tokens scheduled by a revnet
Shop
Add items, create reserved copies, replace media, and redeem items
Admin
Manage permissions, ownership, names, links, and token details. Use the contract and market controls allowed by the current rules
Extras
Export current terms as a .jb file, create a payment-forwarding address, and manage funds supplied to markets
New terms must meet the start time, notice, and approval requirements. Timed rules also require an eligible cycle boundary. A change with too little notice is skipped.
App builders

9. Use the Juicebox software library

For: App builders

The Juicebox software library (SDK), @bananapus/nana-sdk-core, helps you read projects and prepare transactions. It includes addresses and contract call formats (ABIs). Import shared addresses, formats, and project details from the package root; import V6 reads and transaction builders from /v6.

A builder prepares a request without sending it: { chainId, address, abi, functionName, args, value }. Read with a public client and sign with a wallet client on the same chain.

The tutorial uses an existing project on the Base Sepolia test network. It includes the full working example, package versions, expected output, and help when a step fails.

/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);
Read a project

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

Owner and project details
JBProjects.ownerOf / JBController.uriOf
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.
Search project data with Bendystraw

11. Search project data with Bendystraw

For: App builders

Bendystraw gathers Juicebox chain records into a searchable database. This service, called an indexer, supplies project lists, activity, charts, and market positions through GraphQL queries.

Use it for fast browsing. Before signing, read balances, terms, spending approvals, and quotes directly from the chain again. The index can be a few blocks behind, so missing activity does not prove that a transaction failed.

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
Market pools, prices after trades, and the price ranges funded by liquidity providers
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.
Give your assistant the /jb-bendystraw skill for help with queries, charts, and holder lists. It is part of the Juicebox V6 skills library. Bendystraw source: github.com/peripheralist/bendystraw.

12. Connect wallets and request approval

For: App builders

A wallet can send a transaction directly. A shared Safe wallet needs a proposal and approvals before execution. Relayr can send a group of approved calls across chains. Permit2 grants limited token spending for swaps. This site chooses the flow based on the connected account.

Signing paths in this site

Wallet write
submitReviewedContractWrite: review the decoded call → switch chain → simulate as the connected account → sign → wait for the receipt
Actions on behalf of an owner
runAuthorityCalls selects the flow. A directly controlled wallet signs itself. An account that supports forwarded calls (ERC-2771) can approve a prepaid Relayr bundle. A Safe receives proposals in its transaction service
Safe
Propose, approve, then execute. A Safe can use the same address on another chain, but its deployment and signing authority must be checked on each chain
Permit2
Used for direct Uniswap V4 swaps. Other token payments use approve for the exact amount

Chain requests (RPC) go through Juicebox Center, which allows approved website origins. Connections include browser wallets, WalletConnect, Coinbase, and Safe. Para provides an embedded wallet and card purchases.

13. Save names, descriptions, and media

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

14. Review, send, and confirm a transaction

For: App builders

Use one request from preview through submission. Just before signing, refresh balances, limits, and permissions. Prepare the request, run a trial with the actual account, and show what it does and the minimum it will return. After sending, distinguish a rejected wallet request, a shared-wallet proposal, a pending transaction, and a confirmed success or failure.

The app checks that the encoded request still matches what the user reviewed. Its trial uses eth_call with a gas limit. A source check also verifies that every contract write uses one of the reviewed submission functions.

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.

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

16. Get funded

For: App buildersContract builders

A project receives payments through its payment contracts, called terminals. Use the current terms and a fresh quote to show the tokens the payer will receive.

JBMultiTerminal.pay (call outline)
pay(
  projectId,
  token,              // which token to pay with
  amount,             // how much
  beneficiary,        // who receives the minted tokens
  minReturnedTokens,  // minimum amount the recipient must receive
  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
Use addToBalanceOf() to add funds without receiving tokens, such as a grant, donation, or returned funds.

17. Manage tokens

For: App buildersContract builders

Juicebox can track token balances itself; these balances are called credits. A project can also create a separate token contract using the ERC-20 standard. Holders can then move credits into that contract. Count both forms when showing a holder’s total balance.

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

18. Send funds and reserved tokens

For: App buildersContract builders

Payouts send funds to the project’s chosen recipients. Reserved tokens are new tokens set aside for other recipients. Anyone can normally trigger these distributions, but the ownerMustSendPayouts setting can restrict 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     // minimum amount the recipient must receive
)
// Distributes to splits, leftover to project owner
// Review the final amount each recipient will receive
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
Recipients and their shares are stored as splits. Calling sendPayoutsOf() sends funds to those recipients; any leftover goes to the project owner.

19. Cash out

For: App buildersContract builders

Holders can exchange tokens for available project funds, called surplus. With one asset in one terminal, surplus is the balance above the remaining payout limit. A cash out removes the exchanged tokens from supply.

The cash out tax controls how much stays for remaining holders. At 0%, the base formula returns a proportional share. Higher rates return less when only part of the supply is exchanged; 100% returns zero. Use a quote that includes the project’s extensions to find the final amount.

JBMultiTerminal.cashOutTokensOf (call outline)
cashOutTokensOf(
  holder,
  projectId,
  cashOutCount,         // how many tokens to burn
  tokenToReclaim,       // which token to receive
  minTokensReclaimed,   // minimum amount the recipient must receive
  beneficiary,          // who receives the funds
  metadata
)
Set minTokensReclaimed to the minimum the recipient accepts. The contract must enforce the same minimum shown in the review.
Life of a revnet
Deploy a revnet

20. Deploy a revnet

For: App buildersContract builders

Create a revnet and its stage schedule with REVDeployer.deployFor().

Stages set how a revnet’s terms change over time. For example, an early stage can create more tokens per payment, followed by stages that gradually create fewer.

Each stage sets the new-token rate, scheduled cuts, reserved share, and cash out tax. Stages begin automatically at their scheduled times.

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 payment and cash out calls as other projects. REVOwner applies its terms through an extension. Include that extension when requesting a quote, and check the operator’s remaining permissions.

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

21. Build with revnets

The Revnet build guide covers launch settings, saved drafts, operator controls, loans, and custom extensions. Use its source references to check the terms you plan to launch.

Contract builders
Install the code and launch from SolidityInstall the code and launch from Solidity

22. 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
JBController.queueRulesetsOf
(uint256 projectId, JBRulesetConfig[] rulesetConfigurations, string memo). Schedules new terms. The current timing and approval requirements decide when they can begin
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.

23. Add custom behavior with hooks

For: Contract builders

An extension can change a payment’s token output, send rewards, or check a rule change. These extensions are called hooks. Choose the interface for the moment when your code needs to run.

Hook interfaces

IJBRulesetDataHook
Runs before a payment or cash out is recorded. Can change the new-token rate, cash out inputs, and which later hooks run.
IJBPayHook
Runs after a payment is recorded and tokens are created. Can deliver rewards or trigger other actions.
IJBCashOutHook
Runs after a cash out is recorded. Can receive funds and run additional actions.
IJBSplitHook
Runs when a recipient’s share is sent to an extension. Can forward or use the funds.
IJBRulesetApprovalHook
Approves new project terms before they can take effect. Must return APPROVED.
IJBPayHook interface (call outline)
function afterPayRecordedWith(
  JBAfterPayRecordedContext calldata context
) external payable;

// context includes:
//   payer, projectId, rulesetId, amount,
//   forwardedAmount, weight, newlyIssuedTokenCount,
//   beneficiary, hookMetadata, payerMetadata
Use a data hook to change the values recorded by the payment or cash out. Use later hooks to receive funds or trigger additional actions.

24. Implement an extension

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. Quote the amount the hook will actually receive. 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: a hook can call back into your contract before its first call finishes, called reentrancy. Protect your state updates and test each callback. A guard that blocks every nested call can break intended flows. Use override(ERC165, IERC165) for supportsInterface.

25. Permissions

For: Contract buildersApp builders

Give another address permission for a specific action with JBPermissions. Choose only the permissions it needs and the project they apply to. The contract stores each permission as one 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 every permission. Prefer the specific actions needed.
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
Create tokens without a payment.
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 period used to calculate the buyback market’s average price (TWAP).
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
Link matching assets across a pair of bridge contracts (suckers).
33 - DEPLOY_SUCKERS
Create bridge contracts between the project’s chains.
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.

26. Test against deployed contracts

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
Create a local copy of the chain at one fixed block, called a fork. Configure foundry.toml rpc_endpoints and test against the deploy-all-v6 addresses
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
compare the deployed machine code with deploy-all-v6 artifacts. Matching source alone is insufficient because linked libraries change the resulting code

27. Behavior to check carefully

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", hook) decodes as (amountToSwapWith, minimumSwapAmountOut, skipSplits). Always encode all three; two-word quotes revert. Buyback 1.4.0 falls back to minting when the TWAP floor cannot be filled.
  • Router gateway custody: resolve registry.terminalOf(projectId), then gateway.ROUTER() when the selected terminal is a gateway. Eligible failed fee routes remain held for retry, not paid or forgiven. Track QueuePendingCall, ProcessPendingCall, RecordTerminalCallFailure and RefundPendingCall; pendingCallCount is the lifetime count of issued IDs, not the number currently pending.
  • Per-chain rollout and prices: use canonical executed deployment records, preserving previous and v1 addresses for history. Mainnet proposals do not activate the new stack. JBRatioPriceFeed supplies project-zero defaults for USDC→NATIVE and USDC→ETH; addresses vary by chain and OP Sepolia has the feed without hook/router/gateway.
  • Payouts to a project without a terminal: if the recipient project has no payment contract for the asset, its payout fails and the balance is restored. The payout limit is still consumed.
  • Ownership changes reset permissions: permissions must come from the current owner. A previous owner’s grants stop working after ownership transfers.
  • 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

28. Sell collectibles and memberships

For: Project buildersApp buildersContract builders

A project shop can sell uniquely identified tokens, called NFTs. Items are grouped into tiers with a price and supply. The JB721TiersHook extension delivers the selected items when a payment covers their price.

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
Voting weight through JB721Checkpoints. Used only when flags.useVotingUnits is set; otherwise the tier’s price sets its voting weight.
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)
Sort tiers by category. An out-of-order list fails with InvalidCategorySortOrder.

29. Buy existing tokens when they offer more

For: App buildersContract builders

A payment can create new tokens or buy existing ones from a market. JBBuybackHook compares both and uses the configured Uniswap V4 pool when it gives the payer more tokens. It can also route cash outs.

To limit price changes during a swap, the hook uses an average price over time, called TWAP, to calculate its default minimum. Buyback 1.4.0 falls back to minting when the swap cannot meet that floor. Pay metadata under getId("pay", hook) must encode (amountToSwapWith, minimumSwapAmountOut, skipSplits); the third word is a bool, and a two-word quote reverts.

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)
For cash outs, the hook can sell tokens through the pool when that returns more than the project’s cash out quote. Callers can supply their own quote and minimum in the transaction metadata.

30. Accept payments in other assets

For: App buildersContract builders

A router can convert a payment into an asset the project accepts. JBRouterTerminal finds a supported route, makes the conversion, and forwards the result to the project’s payment contract. It does not keep a project balance.

JBPayRouteResolver compares available routes by the tokens the payer would receive. A route may forward an accepted asset, swap through Uniswap V3 or V4, cash out other Juicebox tokens, or combine these steps. Availability depends on the project, asset, and market; check previewPayFor before offering the route.

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.

Projects attach JBRouterTerminalRegistry to JBDirectory alongside JBMultiTerminal. Read terminalOf(projectId) to find the selected route: current deployments use JBRouterTerminalGateway → ROUTER(), while unmigrated projects can still select the previous router. The gateway takes custody before routing. Eligible failed fee routes remain pending for retry instead of being settled or forgiven; finalization can refund the source project after qualified failures. Use canonical deployment records per chain: a proposed mainnet deployment is not a live gateway.

31. Create an address that forwards payments

For: Project buildersContract builders

A payer address forwards incoming ETH to a project. Deploy JBProjectPayer as a small copy of a shared implementation, called a clone. Its constructor takes JBDirectory; set defaults through initialize() or setDefaultValues().

When defaultAddToBalance is false, incoming ETH calls pay() and delivers project tokens to the beneficiary. When true, it adds funds without creating tokens. With no chosen beneficiary, the contract finds the original payer, including through supported forwarding contracts. ERC-20 payments need an explicit pay() or addToBalanceOf() call after approval; sending those tokens directly will not trigger a payment.

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
The payer address looks up the project’s current payment contract through JBDirectory each time it receives a payment.

32. Give a project a name

For: Project buildersApp builders

A project can use a readable Ethereum Name Service (ENS) name. JBProjectHandles checks that the name’s record also points back to the project before returning it from handleOf().

Anyone can propose a name. The registry stores each proposal by its setter, chain ID, and project ID. Apps should also check that the project’s current owner or operator claimed the name.

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.

33. Share rewards over time

For: Contract builders

A distributor shares reward tokens with eligible holders over time. Rewards unlock gradually, called vesting. Deploy this optional extension for your project; it is separate from the shared core contracts.

JBTokenDistributor supports tokens with IJBActiveVotes, such as Juicebox JBERC20. JB721Distributor supports NFT holders. Fund either through project distributions or direct deposits. Each round records the available rewards and holders’ eligible balances, then unlocks their shares evenly over the configured 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
Reward shares use voting weight at a recorded block. Token distributors read IVotes.getPastVotes(); NFT distributors use tier voting units. Token holders must assign their votes, even to themselves, to count. IJBActiveVotes.getPastTotalActiveVotes excludes balances whose votes are unassigned.
Ship it safely

34. 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 zero-tax cash outs.
  • Payouts: limits apply per chain and cycle; a recipient project without a payment contract can consume the limit without receiving funds.
  • 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.

Test against a local copy of the current deployments. Publish the addresses, source, user actions, and schedule of terms so people can check how your product works. The audit page has prompts for reviewing a whole product or one transaction.

35. Use this app as a reference

Use Juicebox Money’s source to build your own product. Its Next.js app shows how to find projects, load details and media, and prepare transactions against the current V6 contracts.

Start with a flow close to yours, such as Pay, Cash Out, Create, or Shop. Read its source and tests to see how it checks current data, prepares a request, and shows what the user will sign.

  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 assistant the relevant source, tests, guide link, V6 contracts, and Juicebox V6 skills library (github.com/mejango/juicebox-skills) to help it explain anything Juicebox.
  3. Use indexed data for browsing. Refresh the chain data the transaction depends on just before building and sending it.
  4. Prepare one request and check that encoding and decoding preserves its fields. Test the behavior your product depends on before asking a wallet to sign.

Use the app to study the experience and its source to see how it works. The server helps with search, media, and request preparation. The wallet signs only after the action is shown and checked against the V6 contract format.