Build

Build with revnets

Launch a revnet, connect an app, or write a contract. Start with your task and open technical details as you need them.

New to building payments? Read a project, preview a payment, and try it on a test network, then return here for revnet schedules, loans, and operator controls.

Revnets are built on Juicebox. The Juicebox build guide covers shared contract actions and permissions.

Build with an AI assistant

Give your assistant the Juicebox V6 skills to help it explain anything Juicebox.

Relevant skills: jb-deploy-ui, revnet-economics, jb-revloans, jb-suckers, jb-bendystraw, jb-interact-ui.

Read or manually copy the prompt

Start here

When to use a revnet

Use a revnet to share revenue under token rules fixed at launch. Use a Juicebox project when a team needs direct payouts or the ability to change those rules.

The contract that owns a revnet enforces its fixed token schedule. A manager, called the operator, may change details such as contributor addresses, payment paths, names, bridges, and shops. Check the launch terms and current permissions.

Project builders can launch from this site without code. App builders use a software kit, called the SDK, and a service that organizes blockchain records, called an indexer. Contract builders write integrations in Solidity, the contracts' programming language. Follow the sections for your role.

Pick the model
You need toUse
Set aside some newly created tokens for contributorsRevnet
Give holders a configured way to cash out or borrowRevnet (under its terms and available funds)
Run a token with a published scheduleRevnet
Pay out a budget to a team each monthJuicebox project
Change the rules after launchJuicebox project

How a revnet worksLook up a termLaunch without writing codeConnect an app with the SDKBuild a contract integrationJuicebox build guide

Back to contents

Project builders

Launch from the wizard

Project builders

The create page has six sections. Save a draft, review the full schedule, then connect a wallet to launch. Check the quoted launch cost on every selected network before signing.

The tokens created per unit paid are the issuance rate; later stages can inherit it. A recurring issuance cut reduces that rate (default 10% every 30 days). The cash out tax controls how much stays for other holders (the form offers 0–80%, in steps of 5, default 20%; contracts allow anything below 100%). The split share sets aside a percentage for contributors. Auto issuance sets aside fixed token amounts to claim without paying. Read the Learn definitions before choosing these values.

The six sections

1. Look
Name (up to 50 characters), token symbol (2–10), logo, description, and optional links. At launch these details are stored as a file on IPFS, a system that identifies files by their contents.
2. Money and chains
Choose what the revnet holds: ETH, USDC, both, or another compatible token checked on each network. Choose the currency used to price new tokens: ETH or USD. Then choose the networks.
3. Terms
Set the token creation rate, recurring reductions, contributor share, cash out terms, fixed token allocations, and start times. This schedule cannot be edited after launch; allowed recipient changes remain possible.
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 assigns a dead address intended to be inaccessible. On: one address for all chains, or one per chain. Verify control of each address, especially a multisig on a new chain.
6. Deploy
Refresh the launch quote, review, then sign. On one network, submit a transaction or propose it to a shared Safe wallet. For several networks, the Relayr service submits a group of transactions funded from one network. Confirm each destination separately.
Stage starts must increase, a contributor share above 0% needs a recipient, and rate cuts should be at least a day apart. The form sets the first start ten minutes after preparation; you can choose a later time. If execution happens after that start, cash outs and loans lock for seven days.

Open the wizardUnderstand the stage settingsHow the form becomes a deployFor call

Back to contents

Draft files, and launching with an agent

Project builders

Save the launch form as a .jb draft to reopen, share, or review before deployment. An AI agent can help prepare a draft; you still need to check the configuration and any transaction you 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-deploy-ui for launch interfaces, revnet-economics and revnet-modeler for exploring configurations. Check generated drafts against the current schema and live deployment contracts.

Draft formatEvery fieldLaunch interface skill

Back to contents

What happens when you deploy

Project buildersApp builders

The wizard prepares a deployFor call for each selected chain using matching configuration and salts. Check every destination result: chain-local project IDs and completion states are separate, even when the token address matches.

  • Creation fee: read JBProjects.creationFee() on each chain immediately before preparing the deployment. Show the quoted amount separately from network and relay costs.
  • Token: an ERC-20 named after your revnet with your ticker. Matching deployments can share a token address across chains when the deployer, sender, configuration hash, and salts match; verify the resulting address on each chain.
  • 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, Relayr submits a paid bundle. Completion depends on each chain and the relay service; do not promise a fixed settlement time. With one chain, use a wallet transaction or Safe proposal and wait for execution.
  • 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.
Back to contents

Running it: what the operator can do

Project builders

The operator has nine default permissions plus any integration permissions granted at launch. The Operator tab exposes available actions; inspect grants per chain because routing, recipients, and shop decisions can affect users.

Collect fixed token allocations from Owners. Holders use the You card to cash out, borrow, move between networks, or claim tokens for other apps. Card and bank payment services buy an accepted asset first.

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.solWhere payments and withdrawals goCosts and the tokens payers receive

Back to contents

Choosing the numbers

Project builders

Model the complete schedule before launch. There is no universal safe tax, cut cadence, or allocation: compare outcomes under realistic revenue, holder concentration, and withdrawal scenarios.

  • Issuance and cuts: simulate both low and high revenue as issuance falls. Show tokens reaching the payer after the split, and consider every stage transition. A shorter cadence makes timing more important; the contract documentation recommends at least a day.
  • Cash out tax: compare exits at several shares of total supply. A higher tax retains more of a proportional withdrawal. Loans also use the current cash out tax, so borrowing is not a shortcut around it; compare net proceeds and repayment obligations.
  • Split share: stepping down across stages (30% → 20% → 10%) pays early contributors more without a governance vote later.
  • Stages: align starts with actual product needs and model late payments around transitions. The final stage continues, including any recurring issuance cuts. Zero issuance prevents further minting from payments, but does not cancel other token allocations.
  • Concentration: test large-holder exits, auto issuance claims, heavy borrowing, and empty local balances. Outstanding loans affect both effective supply and backing; arbitrary concentration thresholds do not establish safety.

revnet-economics skillrevnet-modeler skillrev-sim (simulator)First: distinguish issuance, market, and cash out pricesTry a payment and inspect its result on testnet

Back to contents

App builders

Every operation, in one table

App buildersContract builders

Find the contract call behind each action. Some actions need spending permission, a chosen payment path, or several transactions. The following sections explain what to read and the smallest amount the user agrees to receive.

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
Back to contents

The contracts, and where their addresses live

App buildersContract builders

A revnet combines shared protocol contracts with project-specific tokens, shops, and bridge pairs. Start from the deployment artifacts, then resolve the project's active addresses on its chain.

  • 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, enforces its rules, and holds the operator's permissions
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

Back to contents

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.

Start with a TypeScript app and a viem public client for the selected chain; add a wallet client only for signing. Install @bananapus/nana-sdk-core and viem, and pin versions in your lockfile. The examples are focused integration fragments: provide the named clients, chain ID, addresses, user inputs, and error handling in your app.

/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 sourceV6 API skill

Back to contents

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.totalBalanceOf / creditBalanceOf; JBController.totalTokenSupplyWithReservedTokensOf; REVLoans.totalCollateralOf / totalBorrowedFrom for effective loan accounting
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

Resolve the live controller, then read the stage

import {
  getJBContractAddress, JBCoreContracts, jbDirectoryAbi, jbControllerAbi,
} from "@bananapus/nana-sdk-core";

const directory = getJBContractAddress(JBCoreContracts.JBDirectory, 6, chainId);
const controller = await publicClient.readContract({
  address: directory, abi: jbDirectoryAbi,
  functionName: "controllerOf", args: [projectId],
});
const stage = await publicClient.readContract({
  address: controller, abi: jbControllerAbi,
  functionName: "currentRulesetOf", args: [projectId],
});

// Resolve the accepted terminal for your payment token separately.
// Re-read all quote dependencies 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.
Back to contents

Indexed data: Bendystraw

App builders

Bendystraw indexes on-chain events into searchable data for discovery, activity, charts, holders, and liquidity positions. Use it to display history; use fresh contract reads to authorize a transaction.

A Ponder service watches supported contracts and serves indexed results over GraphQL, an API where you request named fields. The index can lag, return incomplete history, or be unavailable. Distinguish loading, unavailable, stale, and empty results; a missing row is not proof that a revnet or transaction does not exist.

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 },
);

type RevnetResult = {
  project: { name: string | null; balance: string; owner: string; suckerGroupId: string | null } | null;
};
type RevnetVariables = { chainId: number; projectId: number };
if (projectId > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Project ID exceeds the indexer numeric range");

const { project } = await requestBendystraw<RevnetResult, RevnetVariables>(
  endpoint,
  `query Revnet($chainId: Float!, $projectId: Float!) {
     project(chainId: $chainId, projectId: $projectId, version: 6) {
       name balance owner suckerGroupId
     }
   }`,
  { chainId, projectId: Number(projectId) },
);
// Treat project === null as not indexed; do not turn its balances into zero.
// Generate response types from the current endpoint schema in production.

Bendystraw sourcePlaygroundThis site's queriesThe same-origin route

The jb-bendystraw skill gives an assistant the data schema and query examples for feeds, charts, and holder tables.
Back to contents

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,
});
if (quote.beneficiaryTokenCount <= 0n) throw new Error("No token output; review the payment terms");

const tx = buildPayTx({
  chainId, terminal, projectId, token, amount, beneficiary, metadata,
  minReturnedTokens: slippageFloor(quote.beneficiaryTokenCount, 100n), // 1% tolerance; 100 basis points
  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.

Back to contents

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
Use the final quote from the actual payment path. The Learn fee reference explains costs and recipients. A registered bridge contract cashes out without the cash out tax or launch lock; the receiving holder's later cash out does not inherit those exemptions.

Cash out costs and recipients

Back to contents

Loans

App builders

Calculate loan limits from current funds, collateral, costs, the borrowed asset, and permissions. A saved cash out estimate is not enough.

Before borrowing, read borrowableAmountFrom for the selected asset. borrowableNow limits the recorded debt to available funds; borrowableCapacity measures what the collateral supports and can be larger. minBorrowAmount protects the debt amount before costs, not the money received. Show both the debt and the amount the borrower receives, with each quoted cost.

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. Loan permissions can redirect funds or recovered collateral, so explain the specific authority before requesting a grant.

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 protects gross principal; calculate net proceeds separately
const { borrowableNow } = await getBorrowableAmount(publicClient, {
  chainId, revnetId, collateralCount, decimals, currency,
});
if (borrowableNow <= 0n) throw new Error("No funds available to borrow for this collateral");

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

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

Back to contents

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 the explicit no-operator setting, pass address(0). Relinquishing to it is permanent. The site's dead-address default is intended to be inaccessible, but it is not the zero-address mechanism; display the actual value and inspect permissions instead of inferring authority from a label.
Back to contents

Move across chains

App builders

A cross-chain move has separate stages: prepare on the source chain, send through a bridge, obtain a proof, then claim on the destination. Accounting sync is a separate operation; a source receipt does not mean destination funds are ready.

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

Back to contents

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
One exact ERC-2771 authorization per destination chain, posted as a prepaid bundle; choose a funding chain and pay once for multichain launch, metadata edits, and operator writes
Permit2
Only for direct Uniswap V4 swaps; pay and repay use a plain approve for the exact amount

Reviewed writeRelayr bundleSafe queueWallet configRelayr integration skill

Back to contents

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

Back to contents

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

Refresh the state, build one transaction request, simulate it as the actual account, decode the call for user review, submit that request, and wait for a successful receipt. A wallet signature or Safe proposal alone is not confirmation.

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 checkTransaction interface skill

Back to contents

Run this site locally

App builders

Run the reference web client to inspect a complete payment, launch, or holder flow. Start with its README and environment example; use the repository's pinned Node version.

  • 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
Run unit and integration tests with Vitest
npm run build:browser && npm run test:browser
Build with deterministic fixtures, then run Playwright; a normal production build uses different endpoints
npm run wallet-writes:check
Fails if any wallet write bypasses the reviewed hooks

Code point

Start a local development server

git clone https://github.com/mejango/revnet-money.git
cd revnet-money
# Install the Node version from .nvmrc with your Node version manager.
cp .env.example .env.local
npm ci
npm run dev
# Open http://localhost:3002

RepositoryTESTING.md

Back to contents

Contract builders

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 canYou 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 IJB721TokenUriResolverMint 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 chainsChange stage economics after launch
Build anything that pays into, reads, or wraps the revnet: project payers, routers, terminal wrappers, keepersTake the project NFT back from REVOwner

REVOwner.solREVDeployer.sol

Back to contents

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. For an ERC-20 token, the controller grants an allowance: pull the tokens during the callback or the unconsumed amount is revoked and burned. For credits, it transfers them to the hook before the callback. A callback revert emits SplitHookReverted without blocking distribution; unpulled ERC-20 tokens are burned, while transferred credits remain at the hook.

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
// ERC-20-only fragment. Define CONTROLLER, PROJECT_ID, and PROJECT_TOKEN
// in your contract; inherit ReentrancyGuard and implement IERC165.
using SafeERC20 for IERC20;

function processSplitWith(JBSplitHookContext calldata context) external payable nonReentrant {
    if (msg.sender != address(CONTROLLER)) revert Unauthorized();
    if (context.projectId != PROJECT_ID) revert BadProject();
    if (context.groupId != 1 || address(context.split.hook) != address(this)) revert BadSplit();
    if (context.token == address(0) || context.token != PROJECT_TOKEN) revert BadToken();

    // ERC-20: pull it now; the allowance is revoked (and the remainder burned) after this call.
    IERC20(context.token).safeTransferFrom(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.
Back to contents

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 forwarding uses an allowance to pull during the call, revoked afterwards. Validate the actual forwarded amount and use SafeERC20 for transfers.
  • Two metadatas: hookMetadata is authored by the ruleset data hook; payerMetadata and cashOutMetadata originate with the caller. Authenticate the terminal, project, and expected data hook before relying on that origin, and validate both payloads. A legitimate shared terminal can call your hook for a different project.
  • 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 call hooks after recording state without a global reentrancy guard. Protect your hook's state and external calls; nonReentrant alone does not validate who called or which project is involved. Use override(ERC165, IERC165) for supportsInterface when inheriting both.

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

Back to contents

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[]
// Resolve the deployed contracts and construct the structs before this fragment.
// For matching chains, use the same explicit first-stage start and configuration hash.
(uint256 revnetId, IJB721TiersHook hook) = REV_DEPLOYER.deployFor{value: PROJECTS.creationFee()}({
    revnetId: 0,
    configuration: config,
    accountingContextsToAccept: contexts,
    suckerDeploymentConfiguration: suckers,
    tiered721HookConfiguration: shop,
    allowedPosts: new REVCroptopAllowedPost[](0)
});

REVDeployer.solREVStageConfig.solLaunch interface skill

The configuration hash covers the description's name, ticker, and salt; base currency; balance scope; and each stage's effective start, split percent, issuance, cuts, tax, extraMetadata, and auto issuances. It excludes split recipients. A first-stage start of 0 becomes the execution timestamp, so sending 0 independently on several chains can produce different hashes. Keep the origin's effective configuration, sender, salts, and deployment addresses when adding a chain; verify every resulting token and bridge pair.
Back to contents

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

Back to contents

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

Back to contents

Sharp edges

Contract buildersApp builders

Implementation details that can change the meaning or safety of an otherwise valid call.

  • Who is the payer: JBMultiTerminal records its ERC-2771-resolved caller as the payer. A wrapper can therefore be the payer even when a human funded it. Router integrations may probe originalPayer() for fee or refund attribution; that does not rewrite the terminal's payer context or authenticate a human. Set the token beneficiary explicitly.
  • 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: the buyback hook uses an internal tuple that is not an application input format. Caller metadata is untrusted, and hook metadata is only as trustworthy as the authenticated caller, project, and configured data hook. Do not decode it as a user-controlled request.
  • Sucker cash outs: the registered sucker's source-chain cash out is untaxed and skips the launch lock, using local backing only. This permits bridge accounting. Tokens received by an ordinary holder on the destination do not acquire an exemption for a later cash out.
  • 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.

Buyback hook sourcejb-terminal-selection skilljb-suckers skill

Back to contents

Ship it safely

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 configuration round-trips through every supported ABI overload; a past nonzero first-stage start produces the documented seven-day lock; matching effective configuration, sender, salts, and deployment addresses produce the intended cross-chain token addresses.
  • 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

Back to contents

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.