1. Know what a payment does
Read project 1 on Base Sepolia, preview a payment, then try it in the app. Base Sepolia is a practice network that uses test ETH.
Paying usually funds a project and creates tokens for you. Some projects can instead buy existing tokens from a market. They can also set aside a share of new tokens for other recipients. Tokens alone do not promise ownership, votes, or profit.
Before paying, check how many tokens you receive, who else receives tokens, how you can exchange them for project funds, and what the owner can change. Project 1 is a working test project, so its terms and balances can change.
2. Read the project
Use Node.js 22 or newer. In a new folder, install the packages below, download read-project.mjs, and run it. This step only reads public data.
The script finds the contract that manages the project’s rules and tokens, called its controller. It reads the active terms, called a ruleset, from one recorded block of chain history. It checks the network and block again so the result refers to one consistent observation.
mkdir juicebox-first-payment
cd juicebox-first-payment
npm init -y
npm install --save-exact @bananapus/nana-sdk-core@2.3.2 viem@2.55.19
curl --fail -O https://juicebox.money/examples/read-project.mjs
node read-project.mjs// Juicebox V6: read Base Sepolia project 1 without a wallet or private key.
// npm install --save-exact @bananapus/nana-sdk-core@2.3.2 viem@2.55.19
import { createPublicClient, http, zeroAddress } from 'viem'
import { baseSepolia } from 'viem/chains'
import {
getJBContractAddress, JBCoreContracts, jbDirectoryAbi, jbControllerAbi,
} from '@bananapus/nana-sdk-core'
const projectIdText = process.env.JB_PROJECT_ID ?? '1'
if (!/^[1-9][0-9]*$/.test(projectIdText) || BigInt(projectIdText) >= 1n << 256n) {
throw new Error('JB_PROJECT_ID must be a positive uint256 project ID on Base Sepolia.')
}
const projectId = BigInt(projectIdText)
const client = createPublicClient({
chain: baseSepolia,
transport: http(process.env.BASE_SEPOLIA_RPC_URL ?? 'https://juicebox.center/v1/rpc/84532', {
timeout: 20_000, retryCount: 1,
}),
})
if (await client.getChainId() !== baseSepolia.id) {
throw new Error('The RPC must serve Base Sepolia (84532).')
}
// Resolve the project's controller and read its terms at the same block.
const block = await client.getBlock()
const controller = await client.readContract({
address: getJBContractAddress(JBCoreContracts.JBDirectory, 6, baseSepolia.id),
abi: jbDirectoryAbi,
functionName: 'controllerOf',
args: [projectId],
blockNumber: block.number,
})
if (controller === zeroAddress) throw new Error('No controller: check the project ID and network.')
const [ruleset, metadata] = await client.readContract({
address: controller,
abi: jbControllerAbi,
functionName: 'currentRulesetOf',
args: [projectId],
blockNumber: block.number,
})
if (ruleset.id === 0) throw new Error('This project has no active ruleset.')
const canonical = await client.getBlock({ blockNumber: block.number })
if (canonical.hash !== block.hash) throw new Error('The observation was reorganized. Run the read again.')
console.log(JSON.stringify({
protocolVersion: 6,
chainId: baseSepolia.id,
projectId,
blockNumber: block.number,
blockHash: block.hash,
blockTimestamp: new Date(Number(block.timestamp) * 1000).toISOString(),
controller,
ruleset,
metadata,
projectUrl: `https://juicebox.money/basesep:${projectId}`,
inspectUrl: `https://juicebox.center/inspect/basesep/${projectId}`,
}, (_, value) => typeof value === 'bigint' ? value.toString() : value, 2))
What success looks like
- Identity
- protocolVersion is 6, chainId is 84532, and projectId is "1".
- Evidence
- blockNumber and blockHash identify when the data was read. controller is the managing contract found through JBDirectory.
- Terms
- ruleset.id is nonzero. In metadata, reservedPercent is the share of new tokens set aside for others; cashOutTaxRate shapes how much stays for remaining holders; pausePay stops payments. dataHook names any extension that can change a payment or cash out. Compare these fields with the project’s Terms view.
- Units
- Large whole numbers print as strings. reservedPercent and cashOutTaxRate use a scale of 10,000: 6200 means 62%. weight gives new tokens per unit of the project’s pricing currency, before setting aside the reserved share. Divide it by 10^18 for the displayed rate.
Set JB_PROJECT_ID to read another Base Sepolia project. Set BASE_SEPOLIA_RPC_URL to use another service for reading that network. Always compare both the network and project ID.
3. Preview a payment
This step prepares a payment of 0.000001 test ETH. It finds the project’s payment contract, called a terminal, and requests a quote that includes any configured extensions. It sets a minimum of 99% of the quoted tokens, then runs a trial of that exact request, called a simulation. Enter only your public wallet address, never a private key.
The trial does not send a payment. It shows how this request would run against current chain data. The app may find a different way to buy tokens, so review its fresh quote when you pay.
curl --fail -O https://juicebox.money/examples/preview-payment.mjs
# Replace this placeholder with your public wallet address.
JB_PAYER=0xYourPublicWalletAddress node preview-payment.mjs// Read and simulate a native test-ETH payment. This script never signs or sends.
// npm install --save-exact @bananapus/nana-sdk-core@2.3.2 viem@2.55.19
// JB_PAYER=0xYourPublicWalletAddress node preview-payment.mjs
import { createPublicClient, decodeFunctionData, encodeFunctionData, http, isAddress, parseEther, zeroAddress } from 'viem'
import { baseSepolia } from 'viem/chains'
import { NATIVE_TOKEN } from '@bananapus/nana-sdk-core'
import { buildPayTx, previewPay, resolvePaymentTerminal, slippageFloor } from '@bananapus/nana-sdk-core/v6'
const payer = process.env.JB_PAYER
if (!payer || !isAddress(payer) || payer.toLowerCase() === zeroAddress) {
throw new Error('Set JB_PAYER to your public wallet address, never a private key.')
}
const projectIdText = process.env.JB_PROJECT_ID ?? '1'
if (!/^[1-9][0-9]*$/.test(projectIdText) || BigInt(projectIdText) >= 1n << 256n) {
throw new Error('JB_PROJECT_ID must be a positive uint256 project ID on Base Sepolia.')
}
const projectId = BigInt(projectIdText)
const client = createPublicClient({
chain: baseSepolia,
transport: http(process.env.BASE_SEPOLIA_RPC_URL ?? 'https://juicebox.center/v1/rpc/84532', {
timeout: 20_000, retryCount: 1,
}),
})
if (await client.getChainId() !== baseSepolia.id) throw new Error('The RPC must serve Base Sepolia (84532).')
const amount = parseEther('0.000001')
const terminal = await resolvePaymentTerminal(client, { chainId: baseSepolia.id, projectId, token: NATIVE_TOKEN })
// Terminal preview includes the configured hooks. It is not a best-market-route quote.
const quote = await previewPay(client, {
chainId: baseSepolia.id, terminal: terminal.address, projectId,
token: NATIVE_TOKEN, amount, beneficiary: payer,
})
if (quote.beneficiaryTokenCount === 0n) throw new Error('The tutorial expects token output; this route currently quotes zero.')
const minReturnedTokens = slippageFloor(quote.beneficiaryTokenCount, 100n) // 1% below this quote.
if (minReturnedTokens === 0n) throw new Error('The minimum output rounded to zero; inspect the terms before continuing.')
const request = buildPayTx({
chainId: baseSepolia.id, terminal: terminal.address, projectId,
token: NATIVE_TOKEN, amount, beneficiary: payer, minReturnedTokens,
memo: 'Learn, build, inspect: test payment',
})
const data = encodeFunctionData(request)
const decoded = decodeFunctionData({ abi: request.abi, data })
const simulation = await client.simulateContract({ ...request, account: payer })
console.log(JSON.stringify({
state: 'simulated-only',
chainId: baseSepolia.id,
projectId,
payer,
terminal: terminal.address,
nativeValue: request.value,
quote,
minReturnedTokens,
simulatedReturnedTokens: simulation.result,
data,
decoded,
next: `Review a fresh quote in https://juicebox.money/basesep:${projectId}. This script has not submitted a transaction.`,
}, (_, value) => typeof value === 'bigint' ? value.toString() : value, 2))
Expected output: state is "simulated-only", decoded.functionName is "pay", and nativeValue is "1000000000000" wei. Wei is ETH’s smallest unit; divide by 10^18 to get ETH. The output shows the tokens for you and other recipients, the minimum you would accept, the trial result, and the encoded request (calldata). Divide token amounts by 10^18 for displayed amounts.
4. Make a small test payment in the app
To try the payment, connect a wallet with Base Sepolia test ETH. Keep some test ETH for the network’s cost of processing the transaction, called gas. Use the app’s payment form to get a fresh quote and review it before signing.
- Open Base Sepolia project 1. Check the network, project ID, and current and upcoming terms.
- Connect your test wallet on Base Sepolia. Choose ETH and enter 0.000001. If the form cannot accept it, check the reason and stay on the test network.
- Review where the money goes, who receives tokens, the expected and minimum token amounts, and the total cost. Paying with ETH needs no separate permission to spend tokens. Other assets may need that approval.
- Read the form’s fresh trial result and transaction details. Sign only the matching wallet request.
- Save the transaction’s unique ID, called its hash. Wait for its receipt to show success. If you use a shared Safe wallet, the proposal still needs the required approvals and execution.
5. Check what happened
Open Base Sepolia project 1 in Juicescan. Keep your transaction hash and the token recipient’s address handy. The receipt shows whether the payment succeeded; the transaction details show where the money and tokens went.
- Find your transaction by its hash. Check the network, success status, sender, destination, amount, and action.
- Read its recorded events to check the project, recipient, and tokens received. Buying existing tokens can send money to a market instead of adding it all to the project.
- Check the recipient’s total holdings. Include tokens tracked inside Juicebox, called credits, and any tokens in a separate ERC-20 contract. Other payments can also change balances; use your transaction’s events to explain this change.
- Compare the network and project ID with the tutorial output. Terms and balances may have changed since the earlier read. Activity lists can take time to catch up; keep the receipt while you wait.
You are done when you can explain where your payment went and show the tokens received. Exchanging tokens for project funds, called cashing out, is a separate action that depends on the project’s terms and available funds.
6. If something goes wrong
Check the existing operation first
- Wrong chain or missing project
- Check network 84532 and project 1. A failed connection does not mean the project is missing. Try another Base Sepolia read service.
- Preview or simulation failed
- Refresh the payment contract, terms, quote, and recipient. Payments may be paused, an extension may need more information, or the quote may have changed. Keep the minimum token amount protected.
- Wallet rejected
- Rejecting the wallet request does not approve a payment. Check for an earlier transaction hash before trying again.
- Safe proposal
- Wait for the shared wallet’s required approvals and execution. A proposal alone does not send funds.
- Pending or unknown result
- Check the existing transaction hash or operation ID. A timeout does not prove failure. Find the result before paying again.
- Transaction failed
- The payment did not take effect, though it may have spent gas. Check the reason, then get a fresh quote before trying again.
- Successful receipt, missing activity
- Check the transaction directly on the chain and let the activity list catch up. A delayed list is not a reason to pay again.