Build

Build with revnets

Launch and run a revnet from this site, connect an app to one with the SDK and the indexer, or extend one with contracts. Each section is tagged with who it is for. Every fact here is checked against the V6 contracts, and every section links to the source it came from.

Revnets are built on Juicebox. The Juicebox build guide covers the protocol-level calls, hooks, and permissions.

Building with an agent? , and give it the Juicebox V6 skills so it works from the deployed addresses, ABIs, and economics rather than from memory. The revnet ones are jb-revnet-deploy, revnet-economics, jb-revloans, jb-suckers, jb-bendystraw and jb-tx-safety.

Start here

1

When to use a revnet

Use a revnet when your product earns revenue and you want that revenue to back a token nobody has to trust you with. Use a plain Juicebox project when you need an owner who can change course.

A revnet is a Juicebox V6 project whose owner is a contract (REVOwner) that narrows the project to one intent and never changes its economics. Around it sit a deployer that writes the stage schedule, loans, and a narrowly scoped operator. From your product's side it is a project whose rules you can read once and rely on.

This guide is written for three kinds of builder, and each section is tagged with who it is for. Project builders launch and run a revnet from this site without writing code. App builders connect a product to one with the SDK and the indexer. Contract builders extend one with Solidity. The parts overlap, so read the tags and skip what is not yours.

Pick the model

You need to

Use

Pay contributors from revenue, forever

Revnet (split share)

Let customers hold a stake they can exit

Revnet (cash outs, loans)

Run a token with a published schedule

Revnet (stages)

Pay out a budget to a team each month

Juicebox project (payouts)

Change the rules after launch

Juicebox project (rulesets)

How a revnet worksJuicebox build guide

2

Every operation, in one table

Each thing a user can do maps to one contract call. This site exposes each of them as a button; the SDK ships a builder for each; the sections below show how to quote, bound, and sign it.

Amounts are bigint in the token's own decimals until the display boundary. A revnet's identity is chain ID plus project ID; a sucker group links the chains but never makes their addresses, balances, or stage IDs interchangeable.

User action → contract call

Launch a revnet
REVDeployer.deployFor — buildDeployRevnetTx
Pay / buy tokens
JBMultiTerminal.pay — buildPayTx
Buy a shop item
JBMultiTerminal.pay with 721 metadata — build721PayMetadata
Add funds, no tokens
JBMultiTerminal.addToBalanceOf
Cash out
JBMultiTerminal.cashOutTokensOf — prepareHookAwareCashOut
Claim credits as ERC-20
JBController.claimTokensFor — buildClaimTokensTx
Collect auto issuance
REVOwner.autoIssueFor — buildAutoIssueTx
Borrow
REVLoans.borrowFrom — buildBorrowTx
Repay
REVLoans.repayLoan — buildRepayLoanTx
Move a loan's collateral
REVLoans.reallocateCollateralFromLoan — buildReallocateCollateralTx
Move tokens to another chain
sucker.prepare → toRemote → claim
Trade on the pool directly
Uniswap V4 Universal Router — buildDirectPaySwapTx
Add pool liquidity
Uniswap V4 PositionManager.modifyLiquidities
Operator: rename, redirect splits
JBController.setUriOf / setSplitGroupsOf
Operator: manage shop
JB721TiersHook.adjustTiers / mintFor
Operator: set up the pool
JBBuybackHookRegistry.initializePoolFor / setHookFor
Operator: add a chain
REVDeployer.deploySuckersFor
Operator: hand over
REVOwner.setOperatorOf
3

The contracts, and where their addresses live

A revnet is a handful of contracts working together. Every one has a single address per chain, and there is one source of truth for those addresses.

  • Addresses: deploy-all-v6 publishes one artifact per contract per chain (address, ABI, source name) under deployments/<chain>/. Everything else derives from it: the SDK's jbContractAddress map and the skills library's chain-config.json.
  • 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 -v6 repos are current; older Juicebox versions are not interchangeable with them.

Who does what

REVDeployer
Writes the stage schedule and the sucker setup at launch; adds chains later
REVOwner
Owns the project NFT, acts as its data hook, holds the operator's permissions, collects the revnet fee on cash outs
REVLoans
Lends against revnet tokens; each loan is an NFT
JBMultiTerminal
Takes payments, holds the balance, executes cash outs
JBController
Issues tokens, distributes the split share, holds the ruleset (stage) data
JBBuybackHookRegistry + JBBuybackHook
Routes a payment to the Uniswap V4 pool when that is better than issuing
JB721TiersHook
The shop: tiers, prices, media
JBSucker + JBSuckerRegistry
Moves tokens and balance between chains
JBRouterTerminalRegistry
Accepts tokens the revnet does not hold directly and swaps them in

deploy-all-v6 (addresses)revnet-core-v6nana-core-v6Every V6 repo

Project builders

4

Launch from the wizard

Project builders

The create page walks through six sections, then deploys to every chain you picked. Everything in Terms is permanent, so this is the part to get right.

In Terms, each stage has an issuance rate (tokens per unit of base currency; a later stage can pick up where the previous one left off), an optional automatic cut (a percentage every N days, defaults 10% every 30 days), a cash out tax (a 0–80% slider in steps of 5, default 20%; the contracts allow anything below 100%), a split share with recipients (percent of every issuance, optionally different recipients per chain), auto issuance rows (an amount, a beneficiary, and the chain it mints on), and a start: the first stage starts about ten minutes after deploy unless you set a future time, later stages start after a number of cuts or days.

The six sections

1. Look
Name (up to 50 characters), ticker (2–10), logo, an About in markdown, optional links. Pinned to IPFS as one metadata file at deploy time.
2. Settlement
The reserve asset the revnet holds: ETH, USDC, both, or a custom ERC-20 checked on each chain. The base currency issuance is quoted in: ETH or USD. Which chains to run on.
3. Terms
The stages, described above. Every stage field is written into the contracts and cannot be edited afterwards.
4. Store
Optional items to sell, a pricing currency, and which store powers the operator keeps. A shop contract deploys even with zero items.
5. Operator
Off by default, which writes 0xdead… as the operator on every chain, so nobody ever has the role. On: one address for all chains, or one per chain.
6. Deploy
Get a quote, then sign. One chain is a normal wallet transaction; two or more go through Relayr so you pay gas once.
Stage starts must strictly increase, a split share above 0% needs at least one recipient, and the cut cadence should stay at a day or more. If a first-stage start is already in the past when the transaction lands, cash outs and loans lock for seven days. The wizard leaves that buffer for you.

Open the wizardHow the form becomes a deployFor call

5

Draft files, and launching with an agent

Project builders

The whole wizard round-trips through a .jb file. That is how you save a draft, share one, or have an agent write one for you to review and sign.

A .jb file is JSON: the form's fields in the units you see on screen (days, percentages from 0 to 100, human token amounts). Import it at the top of the create page and every section fills in. Any live revnet's Extras tab exports one reconstructed from the chain, which is also the easiest way to diff what you launched against what you meant to.

An agent has two ways to launch. It can produce a .jb for you to import, check, and sign, which keeps the keys with you. Or it can call REVDeployer.deployFor itself through the SDK's buildDeployRevnetTx, which is the path for automated or scripted launches.

  • Limits: up to 32 stages, 16 chains, 64 store items, 2 MB. Media cannot travel inside JSON; item images arrive as already-pinned ipfs:// URIs.
  • Skills: jb-revnet-deploy for the call, revnet-economics and revnet-modeler for choosing the numbers.

Draft formatEvery fieldjb-revnet-deploy skill

6

What happens when you deploy

Project buildersApp builders

One deployFor call per chain, with the same configuration and salt on each, so the revnet's ERC-20 gets the same address everywhere.

  • Creation fee: 0.0001 ETH per chain, paid as a payment into the Juicebox fee project, so you receive its tokens for it.
  • Token: an ERC-20 named after your revnet with your ticker, deployed automatically on every chain at one shared address.
  • Pool: a Uniswap V4 pool per accepted token is initialized at the issuance price with a 1% fee tier, 200 tick spacing, and a two-day TWAP window. It starts empty; liquidity is added later.
  • Chains: with two or more chains the transactions go through Relayr as one paid bundle and settle in one to two minutes. With one chain it is a normal wallet transaction, or a Safe proposal if you are connected through a Safe.
  • Afterwards: each chain has its own project ID; the page lives at /<chain>:<id>. The Terms tab shows the schedule as the contracts hold it.
Safe owners: a proposal that executes after the first stage's start has passed triggers the seven-day cash out and loan lock. Leave the buffer, or set a later start.
7

Running it: what the operator can do

Project builders

The operator has nine permissions, none of which touch the economics. The Operator tab exposes them one card at a time.

Liquidity lives under Owners → Market. Anyone can add liquidity to the pool through Uniswap V4's position manager, single-sided if you like; the site shows the pool's composition, depth by price band, and every position.

Cannot: change issuance, cuts, cash out taxes, split percentages, stage timing, or withdraw the balance. There is no owner key. The project NFT is held by REVOwner, which will never transfer it.

Operator tab

Edits
Name, description, logo, links; an ENS handle; the recipients of the current stage's split share (never its size)
Chains
Extend the revnet to a new chain with the same configuration, if the stage's metadata allows it (the wizard always allows it)
Buyback router
Point at a buyback hook, pick a router terminal, set the TWAP window (5 minutes to just under 2 days), or initialize a pool by hand if the automatic one was front-run
Shop
Add items (media pinned to IPFS), mint up to 50 free copies of an item. Removal and discount edits are contract-level powers this site does not yet expose
Account
Hand the role to another address, or to the zero address to end it
Permissions
A read-only view of what is granted, flagging grants that no longer authorize anything

Operator tab sourceREVOwner.sol

8

Money in, money out, and the fees

Project builders

Contributors pay with any accepted token, a routed swap, or a card. The balance only leaves through cash outs and loans, so there is no payout to manage.

Your team is paid in tokens: the split share of every issuance, plus any auto issuance the stage names. Recipients collect auto issuance from the Owners tab. Holders get a You card with cash out, borrow, move between chains, and claim credits. Card and bank payments go through the embedded wallet's on-ramp, which buys the accepted token first.

Fees to expect

Launch
0.0001 ETH per chain
Payments
None from the protocol. A routed swap pays the pool's 1% fee tier
Cash outs (tax above 0%)
2.5% protocol fee on the value returned, plus a 2.5% revnet fee on the tokens burned. Both fund fee revnets whose tokens the payer receives
Cash outs (0% tax)
The protocol fee only on the fee-free surplus portion, which is often zero
Loans
2.5% to your revnet, 1% to the REV revnet, and the borrower's prepaid fee of 2.5–50% that buys the extra-cost-free window; ten-year expiry
Cross-chain moves
Bridge gas, quoted at the time of the move
9

Choosing the numbers

Project builders

The numbers are permanent, so model them first. The skills library ships the rules of thumb and a simulator.

  • Issuance and cuts: the cut cadence is the revnet's clock; keep it at a day or more. A launchpad shape is 10% cuts every 7 days with a 20% split share.
  • Cash out tax: light for something people should be able to leave, heavy (around 80%) for stable commerce where the balance should stay put. Above roughly 39% a loan is cheaper for a holder than cashing out.
  • Split share: stepping down across stages (30% → 20% → 10%) pays early contributors more without a governance vote later.
  • Stages: 90 or 180 days each is a common rhythm for periodic fundraising; the last stage can run forever.
  • Concentration: aim for no holder above 50% of supply; loan collateral above half the supply is a systemic risk, below a fifth is comfortable.

revnet-economics skillrevnet-modeler skillrev-sim (simulator)

App builders

10

Set up the SDK

App builders

@bananapus/nana-sdk-core carries the ABIs, the addresses, the reads, and a pure builder for every write. 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.

/v6 exports, grouped

Reads
getAccountingContexts, resolvePaymentTerminal, getCurrentRuleset, getUpcomingRuleset, getAllRulesets, previewPay, chooseBestPayRoute, getCashOutQuote, getHookAwareCashOutQuote, getBorrowableAmount, getV6SuckerPairs, getSuckerMovements, getTokenAddress, getCreditBalance, getProjectCreationFee, getProject721Shop, hasPermissions, getCashOutDelay, isRevnetOperator
Builders
buildPayTx, buildCashOutTx, buildDeployRevnetTx, buildAutoIssueTx, buildBorrowTx, buildRepayLoanTx, buildReallocateCollateralTx, buildClaimTokensTx, buildTransferCreditsTx, buildBurnTokensTx, buildSetSplitGroupsTx, buildSetPermissionsTx, buildBridgePrepareTx, buildToRemoteTx, buildBridgeClaimTx, buildSyncAccountingDataTx, buildDirectPaySwapTx, buildPermit2ApproveTx, buildCollectUniswapV4FeesTx
Prepare helpers
prepareHookAwareCashOut, prepareBestCashOut, claimFromSuckerMovement
Config builders
buildRevnetStageConfig, buildAccountingContext, buildSplit, fillSplitPercents, build721RulesetMetadata, build721PayMetadata, buildBuybackCashOutMetadata
Constants
slippageFloor, REV_METADATA_ALLOW_SUCKER_DEPLOYMENT, RULESET_WEIGHT_INHERIT, STANDARD_FEE, MAX_FEE, RESERVED_TOKEN_SPLIT_GROUP_ID, PERMIT2_ADDRESS, the uniswapV4* math family
Sub-entries
/v6/loans, /v6/loan-math, /v6/cash-out, /v6/permit2, /v6/direct-pay, /v6/uniswap-v4, /chains, /jbcenter

Code point

Imports and address lookup

Package
@bananapus/nana-sdk-core (root) and @bananapus/nana-sdk-core/v6
Identity
{ chainId: JBChainId, projectId: bigint }
Address
jbContractAddress["6"][JBCoreContracts.JBMultiTerminal][chainId], or getJBContractAddress(contract, 6, chainId)
import {
  buildBorrowTx, buildBridgeClaimTx, buildBridgePrepareTx, buildCashOutTx,
  buildClaimTokensTx, buildDeployRevnetTx, buildPayTx, buildRepayLoanTx,
  buildRevnetStageConfig, buildToRemoteTx, getBorrowableAmount,
  prepareHookAwareCashOut, previewPay, REV_METADATA_ALLOW_SUCKER_DEPLOYMENT,
  slippageFloor,
} from "@bananapus/nana-sdk-core/v6";

import {
  getJBContractAddress, JBCoreContracts, RevnetCoreContracts,
  jbMultiTerminalAbi, revLoansAbi, type JBChainId,
} from "@bananapus/nana-sdk-core";

const terminal = getJBContractAddress(JBCoreContracts.JBMultiTerminal, 6, chainId);
const loans = getJBContractAddress(RevnetCoreContracts.REVLoans, 6, chainId);

V6 SDK packageSDK sourcejb-sdk skill

11

Read the revnet

App builders

Use the index (next section) to find and display revnets. 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 and next stage
JBController.currentRulesetOf / upcomingRulesetOf
Full schedule
JBController.allRulesetsOf(projectId, startingId, size)
Accepted tokens
JBMultiTerminal.accountingContextsOf
Supply and balances
JBTokens.totalSupplyOf / totalBalanceOf / creditBalanceOf
Splits
JBSplits.splitsOf(projectId, rulesetId, groupId)
Cash out quote
JBMultiTerminal.previewCashOutFrom
Loan capacity
REVLoans.borrowableAmountFrom / loanOf
Chains
JBSuckerRegistry.suckerPairsOf
Operator
A JBPermissions grant scoped to (REVOwner, revnetId): JBPermissions.hasPermissions, or the SDK's isRevnetOperator

Code point

One multicall for the signing-critical state

const [controller, terminals, contexts, stage] = await publicClient.multicall({
  allowFailure: false,
  contracts: [
    controllerOf(projectId),
    terminalsOf(projectId),
    accountingContextsOf(projectId),
    currentRulesetOf(projectId),
  ],
});

// Re-run the reads a quote depends on immediately before simulateContract.

Reference project readsStage reads

Show cached names, logos, and facts while the chain refreshes. Treat state you could not read as unknown, never as zero, empty, or permitted.
12

Indexed data: Bendystraw

App builders

Bendystraw is the Juicebox indexer. It is where this site's discover page, activity feeds, price charts, holder tables, and LP positions come from, and it is public.

A Ponder service watches every V6 contract on every supported chain and serves the results over GraphQL. Use it for anything a person reads. Use the chain for anything a wallet signs, and re-read that state right before building the transaction: the index can lag the chain by a few blocks, and a lagging index looks like empty data, not an error.

This site never sends GraphQL text from the browser. Each query is registered on the server under an operation id; the browser posts { operation, variables } to a same-origin route that validates the variables and forwards it. Heavier history and ranking queries are server-only. Copy that shape if you expose the index to untrusted clients.

  • Key by chain: chainId + projectId, never projectId alone — the same number exists on every chain. Filter V6 rows with version: 6.
  • Float, not Int: numeric arguments on singular queries are Float! (Ponder's choice); an Int! variable fails validation with no data.
  • Page to the end: lists take limit and offset and return totalCount; loop rather than trusting one page.
  • suckerGroupId is as-of-event: rows written before chains were linked keep the old group id; query every project in the group for full history.
  • Pool reserves without RPC: sum the indexed positions at the latest swap's sqrtPriceX96 — the recipe is in the jb-bendystraw skill and this site's price chart.

Endpoints

Mainnets
https://bendystraw.up.railway.app/graphql — Ethereum, Optimism, Base, Arbitrum; no API key
Testnets
https://testnet.bendystraw.xyz/graphql — Sepolia and the L2 Sepolias
Schema
…/schema opens a playground; POST an introspection query for codegen

Code point

What a revnet page asks for

The revnet
project(chainId, projectId, version: 6) → name, balance, owner, suckerGroupId
Its chains
projects(where: { suckerGroupId })
Activity
activityEvents / payEvents / cashOutTokensEvents by projectId or suckerGroupId
Holders
participants
Market
buybackPools → swapEvents (post-trade sqrtPriceX96) → buybackPoolPositions
Loans
loans, borrowLoanEvents
Cross-chain
suckerTransactions and their status
import { requestBendystraw, selectBendystrawEndpoint } from "@bananapus/nana-sdk-core";

const endpoint = selectBendystrawEndpoint(
  { mainnet: "https://bendystraw.up.railway.app/graphql", testnet: "https://testnet.bendystraw.xyz/graphql" },
  { chainId },
);

const { project } = await requestBendystraw(
  endpoint,
  `query Revnet($chainId: Float!, $projectId: Float!) {
     project(chainId: $chainId, projectId: $projectId, version: 6) {
       name balance owner suckerGroupId
     }
   }`,
  { chainId, projectId: Number(projectId) },
);

Bendystraw sourcePlaygroundThis site's queriesThe same-origin route

Building with an agent? The jb-bendystraw skill carries the schema, these query patterns, and the gotchas. Hand it over before asking for a feed, chart, or holder table.
13

Get paid

App builders

Quote the terminal, compare any live market route, then sign with a minimum token output. Tell the user which route they are taking.

A terminal payment issues new tokens or, if the buyback hook finds a better price, buys from the pool. A direct pool swap is a different transaction that skips the split share. Compare executable, slippage-protected minimums rather than chart prices.

ERC-20 payments approve only the request's spender for only the required amount (this site uses a plain approve for pay and repay; Permit2 is reserved for direct pool swaps). Native payments carry the amount in value. Shop purchases are payments with tier metadata; show every NFT plus the token result in the confirmation.

Code point

JBMultiTerminal.pay

Quote
previewPay → previewPayFor
Builder
buildPayTx
Bound
minReturnedTokens
Shop metadata
build721PayMetadata
Not directly accepted?
resolvePaymentTerminal finds the router terminal; chooseBestPayRoute compares
const quote = await previewPay(publicClient, {
  chainId, terminal, projectId, token, amount, beneficiary, metadata,
});

const tx = buildPayTx({
  chainId, terminal, projectId, token, amount, beneficiary, metadata,
  minReturnedTokens: slippageFloor(quote.beneficiaryTokenCount, 100n),
  memo,
});

// ERC-20: approve tx.address. Native: tx.value === amount.

Payment route and approval flowRoute preview helpers

Code point

JBMultiTerminal.addToBalanceOf

Adds funds without issuing tokens. Only for a token the terminal accepts directly.

14

Cash out

App builders

Quote through the terminal's hook-aware preview. A surplus-only calculation can disagree with the transaction that actually runs.

previewCashOutFrom runs the real data hook and buyback decision. The route decides where the minimum goes: minTokensReclaimed on the treasury path, buyback metadata on the pool path. Re-quote after any stage, supply, balance, pool, or fee change.

Credits and claimed ERC-20 tokens behave differently on the open market. Only compare a direct swap for the claimed balance the router can actually spend.

Code point

JBMultiTerminal.cashOutTokensOf

Prepare
prepareHookAwareCashOut → previewCashOutFrom + buildCashOutTx
Treasury bound
route.terminalMinimum
Pool bound
route.metadata
Token count
18-decimal bigint
const prepared = await prepareHookAwareCashOut(publicClient, {
  chainId, terminal, holder, projectId, cashOutCount, tokenToReclaim,
  beneficiary,
});

const { route, transaction: tx } = prepared;
// Pool routes are re-previewed with their slippage metadata before return.

Cash out implementationHook-aware quote

Code point

Token-account operations

Claim credits
buildClaimTokensTx → JBController.claimTokensFor
Burn
buildBurnTokensTx → JBController.burnTokensOf
Auto issue
buildAutoIssueTx → REVOwner.autoIssueFor
Fees to show: a cash out from a stage with a tax above 0% pays the 2.5% protocol fee on the value returned and a 2.5% revnet fee on the tokens burned. At 0% tax the protocol fee applies only to the fee-free surplus portion, which is often zero. Tokens arriving from another chain cash out untaxed and skip the seven-day lock by design.
15

Loans

App builders

Derive loan bounds from live collateral capacity, fees, source token, and permissions, never from a cached cash out estimate.

Before borrowing, read borrowableAmountFrom in the chosen accounting context and apply a non-zero minimum. Show all three fees: the 2.5% protocol fee, the 1% revnet fee, and the borrower's prepaid fee. A holder acting for themselves needs no permission grant; a contract or operator borrowing on a holder's behalf needs OPEN_LOAN (37) from that holder. Collateral and source token are chain-local.

Before repaying, re-read loanOf and the source fee, compute a conservative ceiling, approve or permit the source token if needed, and simulate the exact collateral being returned. Native repayment sends the ceiling as value; the excess is refunded.

Code point

REVLoans.borrowFrom

Quote
getBorrowableAmount / borrowableAmountFrom
Builder
buildBorrowTx
Permission
OPEN_LOAN = 37 (REPAY_LOAN = 39, REALLOCATE_LOAN = 38 for the other calls)
Bound
minBorrowAmount
const { borrowableNow } = await getBorrowableAmount(publicClient, {
  chainId, revnetId, collateralCount, decimals, currency,
});

const tx = buildBorrowTx({
  chainId, revnetId, token, collateralCount, beneficiary, holder,
  prepaidFeePercent,            // 25–500 (2.5%–50%, out of 1000)
  minBorrowAmount: slippageFloor(borrowableNow, 100n),
});

Protected loan buildersBorrow operation

Code point

REVLoans.repayLoan

Builder
buildRepayLoanTx
Bound
maxRepayBorrowAmount; excess is refunded
Partial repay
collateralCountToReturn; may mint a replacement loan NFT
ERC-20
prior approval, or a Permit2 allowance passed in the call

Repayment implementation

16

Operator actions

App builders

Expose only the actions the deployment granted, resolved per chain. An operator is never an owner.

Build each write from freshly resolved contracts, operator address, permission IDs, and project state, and simulate each chain on its own. A multisig proposal is pending until its Safe transaction executes; do not show success or invalidate state at proposal time.

Shop tiers sit outside stage economics. Whether transfers are paused is a per-stage flag fixed at launch (build721RulesetMetadata); whether the operator can add tiers, update metadata, change discounts, or mint depends on the hook's flags and the permissions the 721 overload granted.

Operator write map

Metadata
JBController.setUriOf
Split redirect
JBController.setSplitGroupsOf
Transfer role
REVOwner.setOperatorOf
Add shop tiers
JB721TiersHook.adjustTiers
Operator mint
JB721TiersHook.mintFor
Buyback hook
JBBuybackHookRegistry.setHookFor
Router terminal
JBRouterTerminalRegistry.setTerminalFor
TWAP window
JBBuybackHook.setTwapWindowOf
Initialize pool
JBBuybackHookRegistry.initializePoolFor
Add chains
REVDeployer.deploySuckersFor

Code point

Simulate with the operator, then write

const { request } = await publicClient.simulateContract({
  account: operator,
  address: hook,
  abi: jb721TiersHookAbi,
  functionName: "adjustTiers",
  args: [tierConfigurations, tiersToRemove],
});

const hash = await walletClient.writeContract(request);

Operator transfer implementationShop tier writes

For a revnet with no operator ever, pass address(0): REVOwner treats it as the explicit no-operator value and writes no permissions. An address nobody controls, like the 0xdead one this site uses, has the same effect.
17

Move across chains

App builders

A cross-chain move is a state machine with several transactions: prepare, send, prove, claim, and separately sync accounting.

A prepared move is not delivered value. Track its source sucker, peer sucker, token mapping, leaf index, beneficiary, proof, transport, fees, and status. CCIP and native bridges need different value and take different times; discover the payable value by simulating the exact call.

Accounting sync changes the displayed group backing without moving any local balance. Keep queued, in transit, claimable, claimed, failed, and retriable distinct.

Sucker sequence

1. prepare (source)
burn the tokens and queue a leaf
2. toRemote (source)
send the tree root through the bridge
3. claim (destination)
prove the leaf and mint the tokens
syncAccountingData
push the local balance snapshot to the peer, separately

Code point

Builders

1. Prepare
buildBridgePrepareTx → sucker.prepare
2. Send
buildToRemoteTx → sucker.toRemote
3. Claim
buildBridgeClaimTx → peerSucker.claim, or claimFromSuckerMovement
Accounting
buildSyncAccountingDataTx → sucker.syncAccountingData
Peers
getV6SuckerPairs; movements via getSuckerMovements
const prepare = buildBridgePrepareTx({
  chainId, sucker, projectTokenCount, beneficiary,
  minTokensReclaimed, token, metadata,
});

const send = buildToRemoteTx({ chainId, sucker, token, value: bridgeFee });
const claim = buildBridgeClaimTx({ chainId: peerChainId, sucker: peer, claim: proof });
const sync = buildSyncAccountingDataTx({ chainId, sucker, value: syncFee });

Protected prepare builderProof and claim flowSettlement state machine

18

Wallets, Safes, Relayr, and Permit2

App builders

Four kinds of signing show up in a revnet app: a plain wallet write, a Safe proposal, a Relayr bundle for many chains at once, and a Permit2 signature for pool swaps. Each has its own reviewed path here.

RPC goes through Juicebox Center, an origin-allowlisted provider with no client-side key; wallets connect through injected, WalletConnect, Coinbase, Safe, and an embedded Para wallet that also provides the card on-ramp. Every write hook switches the wallet to the target chain before it does anything else.

Cross-chain authority is a real constraint: a Safe's L1 and L2 singletons differ, so a Safe that operates a revnet on one chain may not exist at the same address on another. Check before offering an operator action there.

Signing paths in this site

Wallet write
useReviewedWriteContract: review → switch chain → simulate as the connected account → send with gas headroom → wait for the receipt
Safe
The Safe App connector plus the transaction service: proposals are polled, never reported as done; a same-address Safe can be deployed on a new chain from src/lib/safeDeployment.ts
Relayr
ERC-2771 forward requests signed once and posted as a prepaid bundle; used for multichain launch, metadata edits, and operator writes so the user pays gas on one chain
Permit2
Only for direct Uniswap V4 swaps; pay and repay use a plain approve for the exact amount

Reviewed writeRelayr bundleSafe queueWallet configjb-safe-and-relayr-execution skill

19

Metadata and IPFS

App builders

A revnet's name, logo, description, and links are one JSON file on IPFS, referenced by the project's uri. Read it through the SDK; pin new versions through Juicebox Center.

  • Shape: JBProjectMetadata: name, description, projectTagline, logoUri, coverImageUri, infoUri, payButton, payDisclosure, tags, twitter, telegram, discord, archived.
  • Read: getProjectMetadata(publicClient, { jbControllerAddress, projectId }) resolves the uri and fetches it; ipfsUri / cidFromIpfsUri handle the encoding.
  • Pin: this site pins JSON, images (25 MB), and media (500 MB) straight from the browser to Juicebox Center, which guards against empty files. The gateway is juicebox.center/ipfs/.
  • Write: the operator updates the pointer with JBController.setUriOf (SET_PROJECT_URI).

Pinning at launchJuicebox Center IPFS client

20

One transaction boundary

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. Only confirmed success invalidates reads.

This site enforces it mechanically: requireTransactionReview re-encodes the request after the user has seen it and throws if anything changed, and a build-time script rejects any wallet write outside the four reviewed hooks.

Build → simulate → decode → review → write → confirm
  fresh reads
    → pure builder
      → simulateContract with the real account
        → encode and decode the calldata, show it to the user
          → writeContract
            → waitForTransactionReceipt
              → success only on receipt.status === "success"

Code point

Reference boundary

const tx = buildOperation(freshState, userInput);

const { request } = await publicClient.simulateContract({ ...tx, account });

const calldata = encodeFunctionData(tx);
const decoded = decodeFunctionData({ abi: tx.abi, data: calldata });
await review({ ...tx, calldata, decoded });

const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Transaction reverted");

Review decoderWrite-site inventory checkjb-tx-safety skill

21

Run this site locally

App builders

revnet.money is the reference implementation of everything above. Fork it, run it, and copy the flow closest to yours.

  • 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: PR tests use fixtures; nothing in them reaches a wallet, an RPC, the index, or Relayr.

Commands

npm run dev
Next.js on port 3002
npm run check
The release-equivalent gate: dependencies, types, lint, format, protocol checks, unit tests, build, bundle, and browser tests
npm test / npm run test:browser
Vitest, then Playwright against the built app
npm run wallet-writes:check
Fails if any wallet write bypasses the reviewed hooks

RepositoryTESTING.md

Contract builders

22

What you can plug in, and what you cannot

Contract builders

A revnet's pay and cash out paths are fixed by REVOwner. You extend a revnet around those paths, not inside them.

REVOwner is hard-wired as the project's ruleset data hook. On a payment it asks the revnet's 721 hook for its one tier split, then asks the buyback hook whether the pool is the better route, and returns at most those two specifications. On a cash out it returns the buyback specification, if any, and its own fee specification. deployFor has no field for a third-party hook, and hasMintPermissionFor answers yes only to REVLoans, the buyback hook, and registered suckers.

Extension points

You can

You cannot

Route the split share to a split hook contract (IJBSplitHook)

Install a custom data hook, pay hook, or cash out hook

Ship shop tiers with a custom IJB721TokenUriResolver

Mint revnet tokens from your own contract

Allow Croptop posting categories at launch (allowedPosts)

Add ruleset flags beyond the extraMetadata bits

Choose which sucker deployers link the chains

Change stage economics after launch

Build anything that pays into, reads, or wraps the revnet: project payers, routers, terminal wrappers, keepers

Take the project NFT back from REVOwner

REVOwner.solREVDeployer.sol

23

Split hooks: the main extension point

Contract builders

A stage's split recipients can be contracts. When the split share is distributed, the controller hands your contract its tokens and calls processSplitWith. This is how the LP split hook seeds pool liquidity from revenue.

Splits live in group 1 (RESERVED_TOKENS). Set the split's hook field to your contract's address. If the revnet's token is an ERC-20, the controller approves your contract for the split amount and you must transferFrom during the call; any allowance you leave unused is revoked and burned. If the token is still credits, the controller transfers the credits to you directly. A revert in your hook is caught and emitted as SplitHookReverted rather than blocking distribution.

Code point

IJBSplitHook

Function
processSplitWith(JBSplitHookContext calldata) external payable
Context
token, amount, decimals (18), projectId, groupId, split (percent, projectId, beneficiary, preferAddToBalance, lockedUntil, hook)
Caller
JBController; the reserved-token path does not check ERC-165, but implement it anyway
function processSplitWith(JBSplitHookContext calldata context) external payable nonReentrant {
    if (msg.sender != address(CONTROLLER)) revert Unauthorized();
    if (context.groupId != 1 || address(context.split.hook) != address(this)) revert BadSplit();

    // ERC-20: pull it now; the allowance is revoked (and the remainder burned) after this call.
    IERC20(context.token).transferFrom(msg.sender, address(this), context.amount);
    // ... do the thing: LP, vest, distribute, forward to another project.
}

IJBSplitHook.solJBSplitHookContext.solLP split hook (worked example)jb-split-hook skill

sendReservedTokensToSplitsOf has no reentrancy guard. Add nonReentrant, and treat the burn-on-unconsumed-allowance rule as a real hazard: a hook that under-pulls loses those tokens permanently.
24

Hooks on the projects around a revnet

Contract builders

A revnet will not run your pay or cash out hook, but the Juicebox projects that pay into it, sit beside it, or wrap it will. These are the four interfaces, and how metadata reaches them.

  • Funds: native value arrives as msg.value; ERC-20 arrives as an allowance you must transferFrom during the call, revoked afterwards. A hook that is not feeless receives the amount net of the 2.5% fee.
  • Two metadatas: hookMetadata is authored by the data hook and is yours to trust; payerMetadata / cashOutMetadata is whatever the caller sent and must be treated as hostile.
  • 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))).
  • noop: a specification with noop = true is informational; the terminal never calls it. That is how a hook reports a decision without receiving funds.
  • Reentrancy: pay and cashOutTokensOf have no guard and call hooks after state is recorded; add nonReentrant. Use override(ERC165, IERC165) for supportsInterface.

Interfaces (all extend IERC165)

IJBRulesetDataHook
beforePayRecordedWith(ctx) → (weight, JBPayHookSpecification[]); beforeCashOutRecordedWith(ctx) → (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, JBCashOutHookSpecification[]); hasMintPermissionFor(projectId, ruleset, addr)
IJBPayHook
afterPayRecordedWith(JBAfterPayRecordedContext) payable — payer, projectId, rulesetId, amount, forwardedAmount, weight, newlyIssuedTokenCount, beneficiary, hookMetadata, payerMetadata
IJBCashOutHook
afterCashOutRecordedWith(JBAfterCashOutRecordedContext) payable — holder, projectId, rulesetId, cashOutCount, reclaimedAmount, forwardedAmount, cashOutTaxRate, beneficiary, hookMetadata, cashOutMetadata
IJBSplitHook
processSplitWith(JBSplitHookContext) payable — see the previous section

IJBRulesetDataHook.solIJBPayHook.solIJBCashOutHook.solJBMetadataResolver.soljb-pay-hook skilljb-cash-out-hook skill

25

Install the code and deploy from Solidity

Contract builders

The V6 repos ship as npm packages and import by package path. deployFor takes the whole revnet as structs; every field is listed here with its unit.

npm i the packages you need and import by path (import "@rev-net/core-v6/src/REVDeployer.sol"). remappings.txt in every repo only maps forge-std; node_modules resolves the rest. That is also how the deploy artifacts record source names.

Packages (Solidity 0.8.28)

@rev-net/core-v6
REVDeployer, REVOwner, REVLoans
@bananapus/core-v6
Terminals, controller, tokens, splits, permissions, the hook interfaces
@bananapus/721-hook-v6
Shop tiers
@bananapus/buyback-hook-v6
Pool routing
@bananapus/suckers-v6
Cross-chain
@bananapus/router-terminal-v6
Token routing
@croptop/core-v6, @bananapus/permission-ids-v6
Posting, permission constants

Code point

REVDeployer.deployFor, field by field

Signature
deployFor(uint256 revnetId, REVConfig, JBAccountingContext[], REVSuckerDeploymentConfig, REVDeploy721TiersHookConfig, REVCroptopAllowedPost[]) payable → (revnetId, IJB721TiersHook)
Short form
the 4-argument overload deploys an empty shop with the base currency and 18 decimals
revnetId
0 creates a project; msg.value must equal JBProjects.creationFee(). A non-zero id must be owned by the caller, sends no value, and hands the NFT to REVOwner for good
REVConfig
description { name, ticker, uri, salt }, baseCurrency (uint32), operator, scopeCashOutsToLocalBalances, stageConfigurations[]
REVStageConfig
startsAtOrAfter (uint48, strictly increasing; 0 = now for the first), autoIssuances[] { chainId, count (uint104), beneficiary }, splitPercent (uint16, of 10,000), splits[] (JBSplit), initialIssuance (uint112, 18-dec tokens per base unit; 1 = inherit), issuanceCutFrequency (uint32 seconds), issuanceCutPercent (uint32, of 1e9), cashOutTaxRate (uint16, of 10,000, below 10,000), extraMetadata (uint16; bit 2 allows adding chains)
JBAccountingContext
token, decimals (uint8), currency (uint32; by convention uint32(uint160(token)))
REVSuckerDeploymentConfig
deployerConfigurations[], salt (0 skips sucker deployment)
REVDeploy721TiersHookConfig
baseline721HookConfiguration { name, symbol, baseUri, tokenUriResolver, contractUri, tiersConfig, flags }, salt, preventOperatorAdjustingTiers / UpdatingMetadata / Minting / IncreasingDiscountPercent
REVCroptopAllowedPost
category (uint24), minimumPrice (uint104), minimumTotalSupply, maximumTotalSupply, maximumSplitPercent (uint32), allowedAddresses[]
// Same config + same salt + same sender on every chain → same token and sucker addresses.
(uint256 revnetId, IJB721TiersHook hook) = REV_DEPLOYER.deployFor{value: PROJECTS.creationFee()}({
    revnetId: 0,
    configuration: config,
    terminalConfigurations: contexts,
    suckerDeploymentConfiguration: suckers,
    tiered721HookConfiguration: shop,
    allowedPosts: new REVCroptopAllowedPost[](0)
});

REVDeployer.solREVStageConfig.soljb-revnet-deploy skill

The configuration hash covers each stage's start, split percent, issuance, cut, tax, extraMetadata, and the auto issuances; it deliberately excludes split recipients, which is why the operator may redirect them. Adding a chain later needs the same hash, so keep the exact configuration you launched with.
26

Loans from a contract

Contract builders

REVLoans is callable by any contract that holds the right permission from the token holder. Liquidation is permissionless, which makes it a keeper job.

A holder acting for themselves needs no grant. A contract acting for a holder needs JBPermissions.setPermissionsFor from that holder, scoped to the revnet (ROOT works but is far too broad). Every mutating call runs under a loan-specific reentrancy guard; the collateral is burned when the loan opens and minted again when it closes.

REVLoans surface

borrowFrom(revnetId, token, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent, holder)
OPEN_LOAN (37) from holder; returns (loanId, REVLoan)
repayLoan(loanId, maxRepayBorrowAmount, collateralCountToReturn, beneficiary, allowance) payable
REPAY_LOAN (39) from the loan NFT owner; excess value refunded
reallocateCollateralFromLoan(loanId, collateralCountToTransfer, token, minBorrowAmount, collateralCountToAdd, beneficiary, prepaidFeePercent)
REALLOCATE_LOAN (38), plus OPEN_LOAN when adding collateral
liquidateExpiredLoansFrom(revnetId, startingLoanId, count)
Anyone, after the ten-year expiry
borrowableAmountFrom, loanOf, loanSourceTokensOf, determineSourceFeeAmount
Views for quoting

REVLoans.soljb-revloans skill

27

Test against the real thing

Contract builders

The revnet repo ships a local workflow base and a mainnet fork base. Use both: the first for logic, the second against the deployed contracts.

  • Sizes: REVDeployer and REVOwner were split to fit EIP-170; the buyback hook dropped getters for the same reason. Check forge build --sizes early, and plan for library extraction if you are close.
  • Bytecode parity: verify your deployment against the deploy-all-v6 artifacts rather than trusting a source match; linked libraries change the hash.

Foundry bases

TestBaseWorkflow
From @bananapus/core-v6/test/helpers; deploys a full local protocol. Most revnet tests extend it
ForkTestBase
revnet-core-v6/test/fork; forks Ethereum at a pinned block via the RPC_ETHEREUM_MAINNET env var and foundry.toml's rpc_endpoints
Helpers
_buildMinimalConfig(cashOutTaxRate), _build721Config(), _deployRevnet, a Uniswap V4 LiquidityHelper, and MaliciousContracts for reentrancy cases

revnet-core-v6 testsTestBaseWorkflow

28

Sharp edges

Contract buildersApp builders

The things that bite people who read the interfaces but not the implementations.

  • 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 terminal registry probes. Expose it, or beneficiaries and refunds land on your intermediary.
  • Buyback metadata is three words: the pay metadata under getId("pay") for the buyback hook decodes as (amountToSwapWith, minimumSwapAmountOut, skipSplits). Always encode all three; older hook versions tolerate a trailing third word, the current one requires it.
  • hookMetadata is not yours: for the buyback hook it is a large internal tuple. Treat payerMetadata as untrusted and hookMetadata as the data hook's, never as something a payer controls.
  • Sucker cash outs: tokens arriving from another chain cash out untaxed and skip the seven-day lock, against the local chain's backing only. Only registry-deployed suckers get that treatment.
  • 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.
  • The 7-day lock: a first stage whose start is already past at execution locks cash outs and loans for seven days on that chain. Relevant to Safe proposals and to scripted multichain launches.
  • Burned allowance: a split hook that leaves allowance unspent loses those tokens; the controller burns the remainder.

jb-buyback-hook skilljb-terminal-selection skilljb-suckers skill

Ship it safely

29

Test what can surprise you

Test the builders you ship, at the edges where fixed economics, route changes, permissions, and asynchronous settlement bite.

Fork-test against the current deployments. Then publish the addresses, source, transaction map, and a human-readable stage 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.

  • Launch: the encoded configuration round-trips through the ABI on every overload; a stale first-stage start is rejected; the same salt yields the same 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.
  • Loans: only the loan permissions (37, 38, 39) are granted, scoped to the revnet; repayment ceilings cannot underpay the live obligation; partial repay mints the replacement NFT.
  • Operator: no exposed call can change committed issuance, cuts, taxes, or split percentages; a Safe proposal is not success.
  • Multichain: one chain's project, token, decimals, operator, or proof is never reused on another; delayed claims stay claimable.
  • Hooks: reentrancy from a hostile hook or token, an under-pulling split hook, hostile payer metadata.

Audit prompts and source indexWhat users will checkReference web client

Start from the smallest operation your product needs, copy its reference pattern, and keep the live read, pure builder, simulation, review, and confirmation on one path. The complete working implementation is the Revnet Money repository.