Loading
Loading
Developer reference · Robinhood Chain
Ripples is a set of public contracts. Two factories deploy every launch, so a token, its market, its collection, and its vesting contract all come from bytecode you can read before you use it. The browser calls them with viem, and any wallet that supports Robinhood Chain can review and sign those calls. Nothing in the middle can move your funds: launches and collections are read straight from the factories, and the trade floor you see is the floor your wallet signs.
Deployment
NEXT_PUBLIC_LAUNCHPAD_CHAIN=rh-mainnet
NEXT_PUBLIC_LAUNCHPAD_RH_RPC_URL=https://rpc.mainnet.chain.robinhood.com
NEXT_PUBLIC_LAUNCHPAD_API_BASE=http://127.0.0.1:8080A build that points at a live network also needs NEXT_PUBLIC_ALLOW_MAINNET=1, which stops a mistyped environment from aiming a development build at real funds.
Transactions
Every charge is in the quote token, pulled by allowance. Launch fees, buys, and mints each need an approve before the call that spends it, and none of them take a native payment. The app plans the pair and runs it through one eth_simulateV1 before the wallet is asked for anything, so a closed mint window, a wallet cap, a sold-out drop, or a moved price surfaces while nothing has been signed and no allowance is standing. Where the node cannot simulate, the request proceeds without one.
Gas is estimated with a margin and priced from eth_feeHistory, falling back to eth_gasPrice on a node that does not answer it. The app records the transaction hash and account nonce before sending, so a reload during a pending transaction reconciles it instead of signing a second one.
const tokensOut = await client.readContract({
address: curve,
abi: CURVE_ABI,
functionName: "quoteBuyFor",
args: [buyer, quoteIn],
});
const minTokensOut = (tokensOut * 9_950n) / 10_000n;
const { request } = await client.simulateContract({
account: buyer,
address: curve,
abi: CURVE_ABI,
functionName: "buy",
args: [quoteIn, minTokensOut],
});
const hash = await wallet.writeContract(request);
await client.waitForTransactionReceipt({ hash });Token launch
createLaunch deploys the ERC-20, its bonding curve, and the curve's locker in one transaction. The full supply is minted once at creation and there is no minting role afterwards. createLinkedLaunch adds the collection and the vesting contract to the same transaction, so a combined launch has no second approval and no half-created state to repair.
Both calls take an optional opening buy. The creator is armed snipe-exempt in the same transaction, so only the trade fee applies to it. A launch pass covers the launch fee through createLaunchWithPass.
const params = {
name: "Ripple",
symbol: "RPL",
curveSupply: 800_000_000n * 10n ** 18n,
lpTokenSupply: 265_000_000n * 10n ** 18n,
vQuoteInit: (525n * 10n ** 18n) / 100n,
vTokenInit: 1_073_000_000n * 10n ** 18n,
graduationQuote: (42n * 10n ** 18n) / 10n,
tradeFeeBps: 200n,
poolFee: 3000,
tickSpacing: 60,
lpUnlockAt: 0n,
};
// The launch fee is charged in the quote token, so it is approved, not sent as value.
await wallet.writeContract({
address: quote,
abi: QUOTE_ABI,
functionName: "approve",
args: [tokenLaunchFactory, launchFee],
});
const { request } = await client.simulateContract({
account: creator,
address: tokenLaunchFactory,
abi: TOKEN_FACTORY_ABI,
functionName: "createLaunch",
args: [params],
});An lpUnlockAt of zero is the factory's way of saying the liquidity never unlocks. The factory rejects a configuration whose target cannot be reached, whose seed price falls outside its band, or whose trade fee exceeds MAX_TRADE_FEE_BPS, so an unlaunchable market never reaches the chain.
NFT collections
createCollection deploys a collection at a fixed price in WETH. A pregen collection serves artwork from baseURI immediately. A live collection serves placeholderURI until the platform signer writes each final URI once. mint creates up to twenty NFTs per transaction, inside the collection's per-wallet cap, and routes the linked share of the payment into the token market in the same call.
| Call | Authority | Purpose |
|---|---|---|
mint(qty, minRoutedTotal) | Minter | Pay WETH and create the NFTs, with a floor on the routed share. |
setTokenURI | Platform signer | Replace a live placeholder exactly once. |
setBaseURI | Creator | Change prepared artwork before it is frozen. |
setPlaceholderURI | Creator | Change the live placeholder before it is frozen. |
freezeMetadata | Creator | Permanently disable the URI controls. |
creatorWithdraw | Creator | Withdraw only attributed creator proceeds. |
Quote tokens sent to the collection outside a mint are not counted as payment. recoverUnattributed, callable by anyone, sends only that surplus to the recorded creator.
Token trading
quoteBuyFor(recipient, amountIn) returns the tokens a buy delivers after both the trade fee and the opening charge, which is why it takes the recipient: the charge depends on who is buying. quoteSell(tokensIn) does the same in the other direction. Every call carries a floor, minTokensOut or minQuoteOut, and reverts with SlippageExceeded rather than settling below it.
Launches created through Ripples carry a 2% trade fee; each curve publishes its own immutable TRADE_FEE_BPS. For the first three seconds after LAUNCH_START, a non-exempt buyer also pays an opening charge that starts at 99% and falls to zero. That charge stays in the curve reserve and counts toward the target; it does not buy tokens.
| Call | Purpose |
|---|---|
buy(quoteIn, minTokensOut) | Spend quote from the caller's allowance. |
buyFor(recipient, amountIn, minTokensOut) | Buy on behalf of another account. |
sell(tokensIn, minQuoteOut) | Sell against a standing token allowance. |
sellWithPermit(...) | Sell on one signature, with no approval transaction. |
contribute(quoteIn) | Add quote toward the target without receiving tokens. |
Linked token and NFT launches
While the curve is open, mintToCurveBps of each NFT payment moves into the curve reserve and is recorded against the minter in AllocationVesting. The routed quote counts toward the target without changing the virtual reserves that set the price.
finalizeBeneficiary(minter) fixes a minter's share once the launch graduates. claimable(minter) reports what has vested, and claim() releases it under the launch's cliff and duration. The claim belongs to the wallet that minted; selling or transferring the NFT does not move it. Any unfinalized remainder is burned by burnSurplus.
Uniswap v4 liquidity pool
graduationReady() turns true once realQuote() clears GRADUATION_QUOTE. From there graduate() is callable by anyone and runs once: it closes the curve, seeds a full-range Uniswap v4 position at the curve's live marginal price through the graduation hook, hands the position to the launch's locker, and retires whatever token supply is left. Buys and sells revert afterwards, and quote beyond what the token inventory can pair is swept to the treasury rather than allowed to move the opening price.
The locker holds the position under UNLOCK_AT. A permanent lock never reaches unlock and only ever pays out through collectFees. A time-locked position can be unwound after its timestamp, with a floor on both amounts.
Reading the board
Every launch is read from the factory that created it. launchCount() and launches(offset, limit) page the token launches; collectionCount() and collections(offset, limit) page the drops. isFromFactory(address) is the check that a curve or collection came from Ripples rather than from a lookalike deployment. Multicall3 sits at its canonical address on Robinhood Chain, so a page of launches reads in one request.
const count = await client.readContract({
address: tokenLaunchFactory,
abi: TOKEN_FACTORY_ABI,
functionName: "launchCount",
});
const page = await client.readContract({
address: tokenLaunchFactory,
abi: TOKEN_FACTORY_ABI,
functionName: "launches",
args: [0n, count],
});
// page: { token, curve, locker, creator }[]A token's name, image, and links come from its own metadata URI, which the creator publishes at launch. Nothing about a launch depends on an index this app controls.