Opening launches on Ripples
Opening launches on Ripples
Documentation
Start with how Ripples works, then the reference for the chain you are building on. If you are here to launch something rather than integrate, the creator guide is the shorter road.
Every address
Listed in one place
What a launch freezes lists every address we deployed or hold. One you were sent as ours that is not on it did not come from us.
Every key
Its limits in writing
The same list states both funding targets, the fees, what a creator earns, the pool lock, and what each of our keys can and cannot do.
Feeds and indexers
One generated document
Integrate carries every address, event topic and call, plus the market feed that answers DEX Screener and GeckoTerminal.
Start here
Ripples creates a token with a market anyone can trade from the moment it exists, an NFT collection people buy from directly, or both together. This is the whole product in four steps. Everything after it is reference.
Supply, the funding target, fees, what the creator earns and the vesting of reserved tokens are written when a launch is created. What a launch freezes lists each of those numbers, every address Ripples deployed, and every key it holds with exactly what that key can and cannot do. The same walk-through, at more length, is on About.
Buying
Open a market and choose Buy. A market priced in ETH on Robinhood can be paid for in any of the ways below, and whichever you pick, the tokens arrive in the wallet you connected on Robinhood.
| Pay from | With | Your wallet asks | Tokens arrive |
|---|---|---|---|
| Robinhood | ETH | Once | When the buy confirms |
| Robinhood | USDG, RIPPLES, or PENGU, swapped to ETH first | Two or three times | When the buy after the swap confirms |
| Base, Ethereum, Arbitrum, or Optimism | ETH or USDC | Once, or twice for USDC | Usually within a minute |
| Solana | SOL or USDC | Once | Usually within a minute |
A market priced in ETH takes it straight from your wallet in one transaction, and the network fee is paid in ETH too.
Choose Pay with another token under the amount. The list shows what your wallet holds of USDG, RIPPLES, and PENGU, and never a tokenized stock. Your token is swapped to ETH through 0x first. The ETH it brings is filled in as the amount, and you approve the buy. If your wallet asks for an approval before the swap, it covers exactly the amount you swap.
Choose Buy from another chain under the amount and pick where you pay from. 0x prices the purchase and Relay carries it to Robinhood, where the tokens land in the wallet you connected.
A market priced in RIPPLES, PENGU, or a tokenized stock is bought with that token on Robinhood, and the two extra ways to pay are not offered on it. A market on Elysium is bought with HYPE on Elysium.
Protections
The cases a launch or a trade can run into, and what happens in each. Every answer links to the section with the detail.
Every trade carries a slippage limit you choose: 0.5%, 1% or 2%. If the price moves further than that before the trade lands, the trade is refused and the amount you offered stays in your wallet.
By default neither market opens unless both sides fill. Everyone on a side that does not open takes their money back from that side's contract, and if a full side is not opened within 15 minutes of its deadline, anyone on it can take their money back without asking anyone. While a side is filling you can leave at any time, except in the last five minutes of its 30-minute joining window. A creator can choose to let a full side open on its own, and it then opens once the other side can no longer fill. How a Crosscurrent fills
The tokens reserved for NFT buyers stay locked until the market reaches its target and its liquidity goes live. After that, each buyer's share stays locked for the launch's cliff period and then unlocks gradually. Each launch sets one of two rules when it is created, and the rule can never be changed: either the claim stays with the wallet that minted the NFT, or it moves with the NFT, so whoever holds the NFT can claim any tokens that have not yet been claimed. The collection's page shows which rule applies. The creator is paid for NFT sales without waiting for the market to reach its target. What happens when the market fills
Settling takes no key of ours. Any wallet can make the call, and Ripples's own bot makes it with no more power than any other wallet. Whoever sends it pays the network fee.
The wake's contract reads who holds the NFT at the moment it pays, so the next payout goes to the new holder, including income that built up before the sale. A seller who wants that income collects it first. Nobody can point a wake anywhere else, anyone can trigger a payout, and a payout only ever goes to the holder. When the agent a launch starts from has its NFT on another chain, the creator income stays with the wallet that launches, and the review says so before you sign. Leave a wake
An agent can buy its own token, sell it inside the ceiling you set, mint from its own collection, pay the addresses you named and write to its run log. It can do nothing else. Its contract has no owner, no setter and no withdrawal, so its limits hold for the life of the launch and nobody can raise them, you included. Pieces it mints stay with it, because it cannot transfer an NFT. Launching an agent
The reply under it says which part to change, such as a missing picture or a ticker that is missing or too long. Each account can launch ten a day, and the reply says when the next one is due. A launch made from a post is held until its creator claims it with one signature. Launch from an X post
Some assets let their issuer pause transfers or block an address. While that hold is on, every buy, sell, mint and move into the pool on a market priced in that asset stops, and it resumes when the issuer lifts the hold. The control is the issuer's alone. What a launch costs
For creators
You need a browser wallet, a name, a ticker, and enough of the chain's own asset to pay for the launch. Any launch that creates a token asks for a picture and a description as well. On Robinhood the token keeps them: the image link, the description and your links are written into the token when it is created, and nothing can change them afterwards. An NFT-only launch on Robinhood asks for neither: it carries its own artwork. Everything else is decided in the launch form, which adds up everything the transaction will spend and names each part before your wallet is asked to approve it.
Optional creator fees
A creator can route a share of their future creator-fee income, from 0.01% to 100%, to the people holding the token, or on a launch with a collection to whoever holds its pieces. The decision is permanent: once fees are routed, no one can reverse it or point it somewhere else, so treat it as final before you sign. On Robinhood the wallet currently receiving creator fees sets the share and who it reaches when it prepares the launch's distributor; the rest of the fee keeps going to the creator, or to a payee they name. A launch with a collection started there opens with its pieces sharing the fee: the review states the share, and the switch beside the line turns it off. On a launch with Foam, the piece a qualifying buy mints counts like any other. Preparing the distributor changes nothing on its own, and the second transaction is what moves the fees.
This is separate from pricing a market in a stock token or recording a StockLink. NFT sale proceeds and creator balances earned before routing remain separate. Rewards contain actual earned assets: the paired asset, the launch token, or both. Eligible balances are weighted by how long they were held during each period. There is no fixed yield; a period with no distributable fees pays nothing.
Ripples reads public holding history and publishes a funded allocation for each period. What Ripples can and cannot do with it is set out in the Terms.
Once a period is published, Ripples sends each holder's share to their wallet. Ripples pays the network fee. Nothing is required from the holder. A share too small to be worth its network fee is left in Account to claim.
Account shows each published period and both amounts in each asset's own units, and a period that has not arrived can be claimed there. Ripples checks the claim against the chain before a wallet is asked to sign, and refuses one that has already been paid. Both assets are claimed together; an issuer hold on a required transfer leaves the claim unspent until it lifts. A claim you make yourself pays network fees in the network's own asset. A display multiplier changes a wallet's display, not the amount transferred.
Public allocations are available from GET /v1/holder-rewards with rail, account and an optional cursor. Full epoch proofs use GET /v1/holder-rewards/epochs/{distributor}/{epoch}?rail=.... Rewards are offered only where the chain's own factory or program supports them. A setting on this site cannot turn them on.
Launch types
A token launch creates one fixed supply of a token and a market where anyone can buy and sell it. An NFT collection launch creates a drop people buy from directly. A combined launch creates both and connects them, which is the launch type Ripples was built for.
In a combined launch, part of every NFT payment goes into the token's market instead of to you, and the wallet that bought the NFT is recorded for a share of the token supply. That share is not paid out immediately: it waits for the market to reach its funding target, then releases gradually. The rest of each payment is yours, and you take it out from your account page whenever you want.
The launch form offers three settings for that. They differ in how much of each NFT payment the market takes, how much of the token supply the buyers are reserved, and how long they wait for it:
Every collection keeps its own copy of those four numbers from the moment it is created, so they describe the launch you make today rather than a rule the network applies to everyone. They cannot be changed afterwards.
Yes, on Robinhood. Any wallet can launch a token or an NFT collection on either chain, and any wallet on Robinhood can create a combined launch: a token with its own NFT collection.
You can change the launch type while you are still filling the form. Once the launch is created, it is fixed.
On Robinhood, paying in ETH, your wallet asks to wrap what the launch costs, to let the factory spend it, then to create the launch, and a launch priced in another asset adds a permission for that asset. A combined launch there is one transaction with no opening buy, followed by a signature for the artwork upload and one transaction that links the artwork to the collection.
Wake
Whoever holds an NFT on Robinhood can launch a market from it, and the NFT leaves a wake: the market's creator income, paid to whoever holds it. When the NFT is sold, the wake goes with it. Neither the seller nor the buyer can point it anywhere else.
Along the way the piece is registered as an ERC-8004 agent, with the piece as its face and links to its pages on 8004scan and OpenSea. Its holder can rename it and point it at an endpoint of their own. Feedback recorded for the agent stays with the agent, so it stays with the piece.
A wallet that holds an agent can also start a launch from it, and its name and face fill in the launch. When the agent's NFT is on Robinhood, the wake follows that NFT. When it is on another chain, the creator income stays with the wallet that launches.
One market can also share its wake across up to 256 pieces of the collection it starts from. Turn on Share the wake under Advanced on the review and list the pieces by token id, such as 1-20, 25; each gets an equal part. Every piece collects its own part, the part changes hands with the piece, and anyone can pay every piece its part at once.
The wake is paid by a contract with no owner and no settings, which reads the NFT's holder at the moment it pays. Anyone can trigger a payout, and a payout only ever goes to the holder. Every piece that has left a wake is listed on Wake, with what it is owed and what it has paid.
Upwell
An Upwell is an NFT collection for a coin already trading on Pons, on Robinhood. Every mint buys the coin for the person minting, in the same transaction, so the piece and the coins arrive in their wallet together.
Of each mint price, 80% buys the coin on its Pons curve, or in its Uniswap v4 pool once the coin has graduated. The collection's creator earns 15% and Ripples 5%. Pons's 1% fee and any tax the coin's creator set come out of the buy, so the mint shows how many coins it buys before it is signed. The minter signs a minimum, and if the price moves past it, nothing is minted and nothing is spent.
Anyone can give a coin an Upwell, and a coin can have any number of them. Start from Upwell: paste the coin's address, add the art, then set a price of at least 0.001 ETH and a supply. Launching costs the same 0.0005 ETH fee as any collection, and the creator withdraws their share from the collection's page whenever they like. The Upwell board on the same page ranks coins by what their Upwells have bought.
Upwells buy coins that Pons launched and prices in ETH. A mint waits while Pons is still taxing a brand-new coin's first buys, and a coin between its curve and its pool has the pool opened as part of the mint.
The factory is 0x48Fb81eEaDC1A8Af9037abD7B23DCc2aF8554e33 and the contract that buys the coin is 0x61E8c408750FB6b608f58F6AF8Df30DE2F792b32, both source-verified on Sourcify. The buyer has no owner and holds nothing between mints.
Any Virtuals agent token on Robinhood can take an Upwell too, on the same terms. Paste the agent token's address on Upwell, or find the agent there by name. The token is read from Virtuals' own launch contract before the Upwell is offered, so only tokens Virtuals launched are accepted.
Each mint swaps its 80% from ETH to VIRTUAL in the pool of the two, then spends the VIRTUAL on the agent token: on its Virtuals curve, or in its pool against VIRTUAL once the agent has graduated. A mint that takes the curve to its graduation target graduates the agent as part of the mint. The pools' 0.3% fee and the token's trading tax come out of the buy, and the mint shows how many tokens it buys before it is signed.
A new agent launch taxes buys heavily for its first minutes (up to 98 minutes for some launches). Its Upwell can be made, and can mint, once that window has closed, and the Upwell page says when that is. An Upwell made by the wallet that launched the agent says so on its page.
The factory is 0x9D8EfE29fE1e72601425489075d797555F873301. The contract that buys the token is the one its VENUE() names; it has no owner and holds nothing between mints.
Agents
Virtuals agents hire each other through ACP, Virtuals' Agent Commerce Protocol. The buying agent funds a job in an escrow contract on Robinhood, and the escrow pays the seller when the work is delivered. Ripples sells token launches on Robinhood this way, and the agent pays in USDG, the stablecoin ACP uses there.
The job's request carries what the launch form asks for: a name, a ticker, a logo link, a description and any links. It also sets the launch's creator income, which has two parts, both fixed for the market's life: a share of the trade fee, and an optional extra charge on every trade. And it names the address on Robinhood that receives that income. Ripples checks the request and the logo before it prices the job, and turns down a request that fails before anything is paid. An ACP job can name an evaluator, a third party who approves the work before the escrow pays. Ripples takes only jobs without one, so the escrow pays as soon as the launch is delivered.
Ripples makes the launch from its agent wallet, 0x85F5AdeE5D6Bb0d6e36C444E637B8A6d44171266, and pays the launch fee and the network fee itself. The chain records that wallet as the token's creator, and explorers and trackers show it that way. The launch has no opening buy, so no wallet holds any of the token until someone buys it. Ripples delivers the token's address, the pool id, the address of the market's own contract, which holds its liquidity, and a link to its page. Once the payment is released, Ripples hands the creator income over to the address in the request with setCreatorFeeRecipient and posts that transaction in the job. From then on only that address can point the income elsewhere. Income from trades before the handover stays with Ripples. If the launch cannot be made, Ripples rejects the job and the escrow refunds the agent. If the agent rejects the job, or the job expires, after the launch is made and before it is delivered, the agent takes its payment back and the launch stays with Ripples.
The market page then names that address as the one the launch was made for, and the launch appears on that address's account, with the income waiting to collect and the controls to share it with holders or point it elsewhere.
Ripples also sells a live read of any Ripples market on Robinhood: its price, liquidity, holder count, progress toward its funding target and the income waiting for its recipient. A token that does not trade on a Ripples market is turned down before anything is paid.
Virtuals agent tokens can also take an Upwell, a collection whose every mint buys the agent's token for the minter.
An agent with a wallet of its own can also launch directly, through the SDK or the MCP server. See Use Ripples from your own agent.
Launch types
An agent launch is a combined launch that the agent opens for itself. A contract is created for it and opens the launch in the same transaction, which makes that contract the creator of record: a 70% share of every trade fee is paid into it and becomes the agent's working capital rather than your income.
From there the agent works its own launch. It can buy its own token on its own market, sell it back inside a ceiling you set, mint from its own collection, pay addresses you named, and write to its public run log. It cannot do anything else. There is no owner, no setter and no withdrawal, so the limits you set are the limits for the life of the launch, and you cannot raise them afterwards any more than anyone else can.
You set seven things: the wallet that works the agent, the runway that goes into the treasury at launch, what it can spend in a day, what it can spend in one transaction, how much of its own token it can sell in a day, how many pieces it can mint in a day, and the addresses it can pay. The daily spending limit covers everything together, so buying, minting and paying all draw on the same budget. A sell limit of zero means the agent can never sell its own token, which is what the form opens on.
An agent launch takes the same two wallet signatures any other launch on Robinhood takes: one spending permission for the ETH, then the launch itself. The contract, its opening funds and the launch all land in that one transaction, so there is no state where the agent holds money and has no market.
Two things follow from the contract holding everything. Pieces the agent mints from its own collection stay with it, because it can pay published addresses and cannot transfer an NFT. And the artwork settings stay yours: you point the collection at its pictures and freeze them through the agent, the same way you would on any other launch.
Launch types
Give one coin a token on each chain with the same name and ticker, and one image and description shown on both markets. In the launch form, choose a pair of chains. A token opens both markets straight away. A coin launched with an NFT collection or an agent opens through Fill first.
Fill first creates a contract on each chain that holds an opening buy anyone can add to. For the first 30 minutes any wallet can join with up to a quarter of a side's target. After that, a side takes only what it still needs. Once both sides are full, Ripples opens the Robinhood market and then the Solana market, and each side's target becomes its market's first buy. That buy pays no opening tax, the tax on buys in a new market's first seconds.
Everyone on a side pays the same average price and gets tokens in proportion to what they put in. If more came in than the side needed, every share is scaled down by the same fraction and the rest comes back with the tokens.
Open now launches both markets from your own two wallets, one after the other, with an equal opening buy on each. Launch the second within 10 minutes of the first to pair them as one Crosscurrent; after that, the first stays a launch of its own.
You need a Solana wallet and a Robinhood wallet connected at once, a name, a ticker and an image. Your share of the trade fee on each chain is paid to the wallet you created that side with.
A Fill first deadline is at least 31 minutes and at most 24 hours after you create it. Each side's opening buy is capped at 40% of that side's funding target, so both markets open as live markets below their targets. An opening buy at the cap holds about 47% of the Robinhood token's supply and 52% of the Solana token's, and the review shows the exact share before you create.
A Crosscurrent's two funding targets are set to the same value when it is created. Every other launch keeps its own chain's target.
Choose Crosscurrent with a collection to open a token on each chain with a collection. Publish the artwork before you create: it is fixed, and minting opens when the markets open. Until a market reaches its funding target, part of every mint on its chain goes into that market. People who mint before then share part of that chain's token, released from when the market reaches its target: with Spark, 2% over 90 days, starting 14 days later; with Wave, 5% over a year, starting 30 days later. Tokens from a mint stay with the wallet that minted, and a Crosscurrent's collection cannot take a later drop.
The Robinhood token opens with a supply of 1,121,052,631 and the Solana token with 1,065,000,000, at the same price, about three times a Crosscurrent's without a collection. When a market reaches its funding target, tokens nobody bought are burned, and the share for people who mint is kept only where someone minted.
Launching from a post
Tag @ripplesdotrun in a post that says launch, give it a ticker and attach a picture. That is the whole form. A token and a collection are created on Robinhood, the picture becomes the artwork, and a reply under your post carries the links. Everything else below is optional and can be said in any order, in the words you would use anyway. A post that only talks about a launch, with no ticker in it, is left alone.
The launch is made by Ripples and held for you until you claim it. Claiming is one signature on the claim page the thread links to, and it hands you the creator fee and piece #1. A post that says for @someone launches it for them instead, and it is their claim to make.
$KOI. Up to ten letters and digits with at least one letter, so a ticker may begin with a digit. Digits alone are read as a number rather than a ticker, which is how a price written in the post stays a price. The first ticker in the post is the launch's own.for @alice. The reply is addressed to them and the claim is theirs.33 pieces, and equally 7 editions, 12 nfts, 5 items, 9 pcs or x1000. Any number from one upwards. Say nothing and it is 100. A bare number is left in the name, so 50 Cent is a name and not a size.paired with GLD, or pair with, pair it with, pair it up with, with or without a dollar sign. The market then prices, trades and charges its fees in that stock instead of in ETH. It has to be a stock Ripples prices launches against; a ticker outside that set is answered with the reason. A stock with no market at that moment is answered instead of launched, so the post can be made again when it trades.silly mode or dark mode, anywhere in the tweet. Silly generates every piece absurd, surprising and far out; dark generates it eerie and ominous, scary without gore. Say nothing and the pieces are generated the usual way. The word mode is what makes it an instruction, so a launch can still be called Dark Pepe.2% creator fee or creator fee of 2%. Anywhere from 1% to 10%, charged on every trade on top of the 1% everyone pays. Say nothing and it is 2%.half of that to holders, or all, everything, or a figure like 30% of that to holders. Anywhere from 1% to 100% of your own fee, paid to holders in what the launch trades against: the asset it is paired with, or ETH.hey @ripplesdotrun launch $JPMEME JPEG Morgan for @alice, 33 pieces,
paired with GLD, 2% creator fee, with half of that going to holdersThat makes a 33-piece collection called JPEG Morgan, trading against gold, charging 2% to the creator on every trade, half of which goes back to whoever is holding, paid in gold. Alice claims it.
A launch that promised a share gets two contracts of its own at the moment it is made. One divides every creator fee at the share the post named and sends the creator's part straight on. The other holds what is owed to holders and pays it out. The share is set when the launch is made and there is no function anywhere for changing it afterwards.
What each wallet is owed is worked out from how long it held rather than from what it held at one moment, so buying just before a payout earns close to nothing. Periods close about once a day. Nothing expires, and what is owed can be taken in Account under paired stock rewards, or by anyone pushing it on a holder's behalf.
A post that cannot be launched gets a reply saying which part to change: a missing picture, a missing or over-long ticker, a stock nobody here prices against, a fee or a share outside its range, a share with no pairing to be paid in, or a collection of no pieces. Ten launches a day per account, for yourself or for someone else; the reply says when the next one is due.
Cost
The launch fee is set on chain and read when the form opens, so the form shows you the fee that will actually be charged rather than one written down here. Below it, “Total at approval” adds up everything else the transaction will spend, with the parts named underneath, and the form refuses to submit if your wallet cannot cover it. On Robinhood and Elysium the network fee is not part of that total. It comes on top, in ETH, and the line says so.
Launches, mints and trades are charged in ETH, and every transaction also costs a small network fee in ETH. Keep a little ETH beyond what you spend. The 1% trade fee is charged on every trade, before the target and after it. 30% of it goes to Ripples, the creator takes the share they chose at launch, up to 70%, and the rest builds the market's own locked liquidity. A market here also pays you through the creator fee you set at launch, charged in ETH on every trade and paid to you in full, and through your NFT proceeds. A market keeps the schedule it launched under, so an older market can carry a different fee, split, or target.
A market can be priced in another approved asset instead, and the form lists the assets it may name. Today that list is ETH, RIPPLES, and PENGU, plus the tokenised stocks the registry approves, on Robinhood. What a launch freezes carries the current list. That choice is fixed when the launch is created. The launch fee stays in ETH on Robinhood whatever the market is priced in, so a creator who picks a different asset approves two spending permissions rather than one, each captioned for what it covers. The funding target and the opening reserve then come from that asset's own row, in that asset's own decimal places, and the target above describes the default asset alone. An asset whose issuer can pause it, or block an address, stops every buy, sell, mint and move into the pool on a market priced in it while the hold is on. Neither control is Ripples' and neither can be lifted here.
The 1% trade fee goes to Ripples before the target and after it. A market here pays you through the creator fee you set at launch, charged in HYPE on every trade and paid to you in full, and through your NFT proceeds. A market keeps the schedule it launched under, so an older market can carry a different fee, split, or target.
Artwork
An NFT points at a small text file that names it and links its image. Somebody has to publish those files, and for a launch made here that is Ripples.
Choose the image files on the Art step and Ripples publishes them, one numbered file per NFT, then points the collection at them. Large collections upload in batches; the form reports progress and tells you what is left if a batch fails.
The launch's own picture follows what you are launching. A token launched on its own takes its image on the Token step; a launch with a collection shows its first piece. Whenever the launch creates a token, Ripples publishes the image, the description and the links you entered and serves them at a public address.
Keep the square version of your picture sharp: a marketplace uses it as the collection's avatar and crops anything that is not square.
You can freeze the artwork links later. That settles the address every NFT reads its details from and gives up your own ability to move it. Pieces hosted with Ripples can no longer be replaced or removed by you, and Ripples can remove hosted pieces that break its terms.
Liquidity
Your market has a funding target. While it is below that target, buyers trade against the market itself and the price rises as tokens are sold. What the market holds moves both ways: a buy adds to it and a sell takes from it, so the figure on the market page falls as well as rises. Anyone can settle a market that reaches its target. Ripples runs a bot that does it automatically, with no more power than any other wallet.
What settling does depends on the chain. On Solana a launch made today opens on Meteora's bonding curve, and when the curve holds 85 SOL its reserve and remaining supply move into a public Meteora pool at the price the curve reached, with the liquidity locked for good. Ripples moves it within moments of the curve filling, and trading continues in the pool. A launch made on the Ripples program moves into a public Raydium pool instead, under the same lock: its funding target includes the cost of creating that pool, which is why the target you see is a little above the market threshold, and trading pauses for a moment while the pool is prepared.
On Robinhood a launch is a public Uniswap v4 pool from its first block, so there is nowhere for it to move. Settling retires the position that priced the filling market and opens the permanent one in the same pool, in one transaction, under the lock the launch declared at create. Whoever triggers it pays the network fee.
When the market reaches its target and its liquidity goes live, the clock also starts on the tokens a combined launch reserves for its NFT buyers. Nothing is released before then. After that, each buyer's share stays locked for the launch's cliff period and then unlocks gradually until all of it can be claimed. Each launch sets one of two rules when it is created, and the rule can never be changed: either the claim stays with the wallet that minted the NFT, or it moves with the NFT, so whoever holds the NFT can claim any tokens that have not yet been claimed. The collection's page shows which rule applies.
Liquidity going live does not change what the market pays you. On Solana your share of the trade fee is yours from the first trade, and after the target the same share of what the locked pool collects is yours. Your account page shows what is waiting and collects it. On Robinhood nothing changes at the target: the 1% trade fee keeps the split it launched with and your creator fee keeps running. The pool there charges no fee of its own to share. A market keeps the schedule it launched under, so an older market can carry a different fee, split, or target.
Your NFT proceeds are separate from all of this. They do not wait for the target and they do not stop at it. As soon as people buy, the amount is yours, and your account page has it ready to collect.
Earnings
Everything a launch earns you is collected from one place, your account page. Connect the wallet that made the launch and the first card shows what is ready on that chain, with one button that collects all of it.
Creator fees build with every trade, and NFT proceeds are yours as soon as an NFT sells. Each has its own line on the card, in the asset it was earned in. Nothing there expires, so collect when it suits you.
On Robinhood, an agent collects the same set with collect in the SDK or ripples_collect through the MCP server.
After you launch
Two links matter. The collection page is where people buy an NFT. The market page is where the token trades. Both work without a wallet connected, so anyone can read the price, the supply and how much of the target the market holds before deciding anything.
Say these four things plainly, because a buyer cannot work them out from a picture:
Do not promise a price, a return, or that anyone will be able to sell. You control none of those. What you can promise is what the contracts do, and every figure on both pages is read from the chain, so anyone can check you.
After the market reaches its target, the pages link to the pool. That is a good moment to post again: the liquidity is locked by then, and it is the first thing a careful buyer looks for.
Collections on Robinhood use the standard ERC-721 and ERC-7572 interfaces, so marketplaces that index this chain can list them. Check OpenSea for the current state of that support.
Robinhood
Every number below is set when a launch is created and enforced by the contracts on Robinhood. None of it is a policy. Where Ripples holds a key, the line says exactly what that key can and cannot do.
Addresses
Every address Ripples deployed or holds, in full. Ripples announces one of its own here and nowhere else, so an address you were sent as ours that is not on this list did not come from us. A launch made on Ripples has its own address, which its own page shows. A trade on Robinhood also settles through Uniswap's pool manager and its universal router, which Ripples did not deploy. The Robinhood reference below prints the router's address, and the graduation hook above answers the pool manager's. Finishing a market that has reached its target takes no key of ours, so no wallet is listed for it: anyone can make that call.
Integrate carries the same addresses in machine-readable form, with the event topics and the calls that read a market.
Approved assets
A launch settles in one asset, chosen when it is created and frozen with everything else in this section. The owner keeps a list of the assets a new launch may name. That list is a list of assets: it says nothing about who may trade, hold or see a market, and Ripples builds no eligibility or jurisdiction gate anywhere. The rows below are copied from the deployment record rather than read from a chain, so the registry named first is the authority and each row carries the block it was approved in.
A launch that names no asset takes the chain's own and the figures the form fills in, which include the funding target below. A launch that names another asset must declare that asset's row as the registry holds it, and the factory refuses the create otherwise. Those figures are not restated here: the form reads the row when you pick the asset, and it will not let a launch declare a row it could not read. Two rows are never comparable as plain numbers either, since each is in its own asset's decimal places.
The stock link registry beside it records an association and nothing more. It stores an address, verifies nothing about the issuer or the instrument, moves no money, and cannot be changed or removed once written. An association and a settlement asset are separate choices: a launch can carry one, the other, both, or neither.
Combined launch
A combined launch is one token and one NFT collection that need each other. The share of a mint that buys the token market while it is filling, the slice of supply those minters vest, and the vest schedule are written into the launch and cannot be redirected after create, by the creator or by Ripples.
Market
Below the target, buyers trade against the market itself. At the target the position goes under the lock the launch declared at create, at the price the market last traded at. On Robinhood the launch is already a public Uniswap v4 pool, so settling retires the position that priced the filling market and opens the permanent one in the same pool. The figures below are what the launch form sets today. A market created earlier keeps the target, the fee and the shares it was created under, and its own page states them. Anyone can settle a finished market. Ripples runs a bot that does it automatically, with no more power than any other wallet.
totalBurned() counts only tokens retired through the burner, including every buyback it made. These are different totals. Transfers to the burn address do not reduce the token's totalSupply()Creator earnings
A creator is paid from mints, from trades, or from both. Trading pays from the launch's first trade, and reaching the target does not stop it. The rows below are the whole answer. Each launch fixes its own figures at create, so an older launch carries what it was created with and its own page shows it.
Keys
Ripples never receives a wallet key and never signs a mint. The only launches and trades it signs are Fill first openings: when a side is full, the Crosscurrent opening key below creates that side's market and makes its opening buy with the money its contributors put in. Every key it does hold is listed here, with what that key can do and what it cannot.
Reading the owner from Robinhood.
Today's limits
These are the limits of what is deployed today, stated so nobody reads a locker and finds a promise this section did not make.
The contract names behind all of this are in the Robinhood reference below. The genesis launches are on the genesis page.
Chain one · Solana Mainnet
The browser uses a checked-in description of the Ripples program, its Anchor interface definition (IDL), to build transactions with Solana's @solana/kit library. Compatible wallets use Solana Wallet Standard to review and sign those transactions. The public API indexes program accounts, publishes launch images and details for the launch form, and stores signed NFT reveal settings. A separate Virtuals agent integration supports one dedicated agent wallet. It can read, nothing more. Agent transactions, paid offerings, and Mainnet access are off.
Deployment
engine is dbc for a launch on Meteora's curve and ripples, or absent, for one on the Ripples program. Read it off the record rather than the date: each kind keeps its own reads, trades and claimsQuoteAsset account per mint, written by the config authority. A launch records the mint, its token program, and its decimals when it is created, so read the launch rather than a listNEXT_PUBLIC_LAUNCHPAD_CHAIN=solana
NEXT_PUBLIC_LAUNCHPAD_CLUSTER=mainnet-beta
NEXT_PUBLIC_LAUNCHPAD_PROGRAM_ID=RippcKjgg9pkjHY7RKp6R1K8jPGwq2BRSJcgbErufjF
NEXT_PUBLIC_LAUNCHPAD_API_BASE=https://api.ripples.run
NEXT_PUBLIC_ALLOW_MAINNET=1The browser reads Solana Mainnet through the registry relay at https://api.ripples.run/v1/rpc, which holds the provider keys, and confirms each transaction by polling the same relay. The page opens no websocket to a Solana node. Without an API base, a local build reads the cluster's public endpoint, api.mainnet-beta.solana.com.
NEXT_PUBLIC_LAUNCHPAD_RPC_URL names an endpoint for the page to read first, with the relay behind it. Every NEXT_PUBLIC_ value is readable in the page, so the build refuses an endpoint that carries a key in its query string, its path, or a user name and password. The relay takes one JSON-RPC call per request, allows the read methods a page needs plus sendTransaction, and answers getProgramAccounts for the Ripples programs only. NEXT_PUBLIC_LAUNCHPAD_RPC_FALLBACK_URL names a different second endpoint; set it to off to read from the first alone.
Transactions
The connected wallet pays the SOL cost. Before asking for a signature, the app checks the amount being transferred, the minimum SOL needed to keep new accounts active (rent exemption), and a network-fee buffer. The @solana/kit planner fetches a recent blockhash, simulates the transaction, and adds a compute-unit safety margin. After signing, the app shows the transaction signature and checks its status until it confirms or its lastValidBlockHeight expires.
const message = await client.planTransaction(instructions);
const transaction = await signTransactionMessageWithSigners(message);
const signature = getSignatureFromTransaction(transaction);
await sendWithoutConfirming(transaction, {
commitment: "confirmed",
preflightCommitment: "confirmed",
skipPreflight: false,
});
// Poll signature status and block height using
// message.lifetimeConstraint.lastValidBlockHeight.Before the pool is created, token prices are quoted in SOL. A purchase creates the buyer's standard token holding account (associated token account) only when one does not already exist. Launch tokens on the Ripples program use nine decimal places, those on Meteora's curve six, and the wallet signs the complete trade.
Agent integration
Virtuals' Agent Commerce Protocol (ACP) lets agents buy and sell services. The private Ripples service connects one registered Virtuals agent and one dedicated Solana wallet to the selected network, the configured Ripples program, and the committed Anchor IDL. It accepts only a defined list of Ripples actions. It rejects arbitrary transfers, instructions for other programs, Virtuals token-launch tools, general-purpose trading, and program-authority or maintenance actions.
Before sending a transaction, the service confirms that both Solana endpoints are on the expected network. It decodes each instruction from the Anchor IDL and checks the program address, account order, read/write permissions, required signer, current Ripples account state, and spending limits for one action and one day. Each request receives a stable request fingerprint, a digest used to recognize the same request, and a Solana memo. If the result is unclear, the service pauses and checks the existing transaction instead of sending it again.
The optional seller listing (ACP offering) is a fixed-price USDC service named ripples-action-v1. After a buyer funds a job, the service reads and checks the latest request again immediately before execution. The agent wallet pays any SOL sent into Ripples, Solana network fees, and new-account funding. Virtuals sponsorship does not pay for Ripples program transactions.
The integration is read-only. Agent transactions, the paid seller listing, and Mainnet access are off. This section will say so when that changes.
Your own agent
To create or join a Crosscurrent on both chains from your own agent, read Run a Crosscurrent from an agent.
An agent that already holds a wallet does not go through the Virtuals service above. It reads launches from the public API, reads chain state through the registry relay or a node of its own, builds the same instructions the browser builds, and signs them with its own key. That wallet pays every network fee and every trade. Nothing on this page signs, pays, or approves on its behalf.
GET https://api.ripples.run/v1/launches lists Solana Mainnet token launches, newest first, as count, nextOffset and a launches array. Each row carries launch (the market address), mint, creator, name, symbol, and collection for a combined launch. /v1/collections does the same for NFT collections, and /v1/launches/<address> returns one launch with its current market state. The list answers Solana Mainnet by default and Robinhood for ?chain=robinhood, and the single-launch route reads whichever address form it was given, so it answers on either chain. /v1/collections indexes Solana Mainnet alone. Robinhood launches can also be read straight from the factories, as the board does.
const base = process.env.RIPPLES_API ?? "https://api.ripples.run";
const page = await fetch(`${base}/v1/launches?offset=0`).then((r) => r.json());
for (const row of page.launches) {
// row.launch, row.mint, row.creator, row.name, row.symbol, row.collection
}
// An index with nothing in it is a valid answer, so read the first row only if there is one.
const [first] = page.launches;
const one = first && (await fetch(`${base}/v1/launches/${first.launch}`).then((r) => r.json()));Each chain is counted on its own, and either answers count: 0 until the first launch on it exists, so write the loop to survive an empty page. limit defaults to 50 and caps at 200 on /v1/launches, and is 25 either way on /v1/collections; offset counts rows from the newest. Reads are limited to 120 a minute per client address, and every answer carries what is left in x-ratelimit-remaining.
Point RIPPLES_API at your own build's API when you want to run the same calls against a test network.
For chain state, POST https://api.ripples.run/v1/rpc relays one JSON-RPC call per request to the Solana Mainnet endpoints the site reads through. It accepts the read methods a client needs (getAccountInfo, getMultipleAccounts, getLatestBlockhash, simulateTransaction, getSignatureStatuses and the rest) plus sendTransaction. getProgramAccounts is answered for the Ripples program only, and a batched request is refused. The relay is limited to 600 calls a minute per client address, so an agent that polls harder than that should bring its own endpoint.
On Solana Mainnet, build the buy from the published IDL, solana/idl/ripples.json, with the program address from the table above. Generating a client from it with Codama gives the same getBuyInstructionAsync the browser uses. The launch account names the mint, the vault, and the address the launch pays its creator at, the settings account names the treasury, and the price is a constant-product curve on the launch's virtual reserves, so the floor can be sized before anything is signed. The buyback vault is a program address the client derives; every other account is on one of those two records.
A launch whose holders take a share of its creator income records it as holderShareBps. Where that is above zero and the launch pays its creator anywhere other than its own distributor, a buy or a sell also names the distributor after the instruction's own accounts, writable, and on a launch priced in a token the distributor's escrow after that. Both are program addresses derived from the launch: ["holder_rewards", launch] and ["escrow", quote_mint, distributor]. The program refuses a trade on such a launch without them.
import {
appendTransactionMessageInstructions, createKeyPairSignerFromBytes, createSolanaRpc,
createTransactionMessage, getBase64EncodedWireTransaction, pipe,
setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import { fetchConfig, fetchLaunch, findConfigPda, getBuyInstructionAsync } from "./generated/ripples";
const rpc = createSolanaRpc("https://api.ripples.run/v1/rpc");
const agent = await createKeyPairSignerFromBytes(agentSecretKey);
const launch = await fetchLaunch(rpc, launchAddress);
const [configAddress] = await findConfigPda();
const settings = await fetchConfig(rpc, configAddress);
const [buyerToken] = await findAssociatedTokenPda({
owner: agent.address,
mint: launch.data.mint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
// The platform's fee and the creator's own charge come off the same base, then the
// constant-product output. In the first 3 seconds the opening tax comes off
// before both, so a floor sized here fails until that window closes.
const quoteIn = 100_000_000n;
const fee = (quoteIn * BigInt(launch.data.tradeFeeBps)) / 10_000n;
const creatorTax = (quoteIn * BigInt(launch.data.creatorTaxBps)) / 10_000n;
const net = quoteIn - fee - creatorTax;
const expected = (launch.data.virtualToken * net) / (launch.data.virtualQuote + net);
const minTokensOut = (expected * 9_900n) / 10_000n;
// Where this launch pays its creator. The account is pinned to that address, so it
// cannot be derived, and a launch created before the field existed carries the default
// address there and is paid at launch.creator instead.
const creatorPayout =
launch.data.creatorPayout === "11111111111111111111111111111111"
? launch.data.creator
: launch.data.creatorPayout;
const buy = await getBuyInstructionAsync({
launch: launchAddress,
mint: launch.data.mint,
tokenVault: launch.data.tokenVault,
buyerToken,
buyer: agent,
treasury: settings.data.treasury,
creatorPayout,
quoteIn,
minTokensOut,
});
const { value: latest } = await rpc.getLatestBlockhash().send();
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayerSigner(agent, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latest, m),
(m) => appendTransactionMessageInstructions([buy], m),
);
const signed = await signTransactionMessageWithSigners(message);
const signature = await rpc
.sendTransaction(getBase64EncodedWireTransaction(signed), { encoding: "base64" })
.send();On Robinhood a market is a Uniswap v4 pool, so the agent prices it from the hook with viem and swaps through the Universal Router, using the addresses from the Robinhood deployment table. The router spends through Permit2, so the quote token is approved to Permit2 and Permit2 to the router. The network fee is paid in ETH from the same wallet.
import { createPublicClient, createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const chain = {
id: 4663,
name: "Robinhood",
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
};
const account = privateKeyToAccount(agentPrivateKey);
const client = createPublicClient({ chain, transport: http() });
const wallet = createWalletClient({ account, chain, transport: http() });
const QUOTE = parseAbi(["function approve(address spender,uint256 value) returns (bool)"]);
const PERMIT2_ABI = parseAbi([
"function approve(address token,address spender,uint160 amount,uint48 expiration)",
]);
const ROUTER = parseAbi([
"function execute(bytes commands,bytes[] inputs,uint256 deadline) payable",
]);
// Price the market off LaunchLens.launchesOf and take the hook's cut off the output
// before signing a floor. The Robinhood "Token trading" section shows that
// arithmetic in full, and where the lens is deployed.
const quoteIn = 10n ** 16n;
const amountOutMinimum = floorFor(quoteIn);
const expiry = Math.floor(Date.now() / 1000) + 3_600;
await wallet.writeContract({ address: quoteToken, abi: QUOTE, functionName: "approve", args: [permit2, quoteIn] });
await wallet.writeContract({ address: permit2, abi: PERMIT2_ABI, functionName: "approve", args: [quoteToken, universalRouter, quoteIn, expiry] });
const hash = await wallet.writeContract({
address: universalRouter,
abi: ROUTER,
functionName: "execute",
args: [
"0x10",
[encodeV4Swap({ poolKey, zeroForOne: quoteIsCurrency0, amountIn: quoteIn, amountOutMinimum })],
BigInt(expiry),
],
});
await client.waitForTransactionReceipt({ hash });Token launch
In the standard web flow, the creator supplies a name, ticker, description, image, and optional Website, X, Telegram, and custom links. Ripples publishes them together, then fills the instruction's uri field automatically. Developers building their own interface may provide a compatible public metadata URI directly.
The createLaunch instruction creates three core records: the market state (launch program-derived address, or PDA), the standard Solana token (SPL Token mint), and the trading inventory controlled by Ripples (program vault). The full token supply is issued once, then the powers to mint or freeze more tokens are removed. Metaplex Token Metadata stores the token name, symbol, and URI.
import { getCreateLaunchInstructionAsync } from "./generated/ripples";
const instruction = await getCreateLaunchInstructionAsync({
launch,
tokenVault,
creatorToken,
creator: walletSigner,
treasury,
args: {
nonce,
name: "Ripple",
symbol: "RPL",
uri: "ipfs://…",
curveSupply: 800_000_000n * 1_000_000_000n,
lpSupply: 321_052_631n * 1_000_000_000n,
virtualQuote: 36_250_000_000n,
virtualToken: 1_073_000_000n * 1_000_000_000n,
graduationQuote: 29_000_000_000n,
lpUnlockAt: 9_223_372_036_854_775_807n,
linked: false,
nftAllocationBps: 0,
mintToCurveBps: 0,
vestingCliff: 0n,
vestingDuration: 0n,
initialBuy: 0n,
minTokensOut: 0n,
snipeExemptions: [],
creatorTaxBps: 0, // the creator's own charge, 0 to 1000 bps
},
});Token-only launches open with a virtual quote reserve of 0.4 times their funding target and cannot accept NFT contributions. Linked launches use 1.25 times the same target and retain the larger liquidity allocation needed for NFT-funded graduation. For an approved token quote, read its current QuoteAsset row: the stored reserve is the linked reserve. The standalone reserve is two fifths of its target, available only when both ratios are exactly representable in raw quote units. The program checks these terms at creation. Later approval changes apply to future launches; they do not change an existing market's reserves or target.
The market's fee is not an argument. The program writes 1% into every launch it creates, so a caller cannot open a market on other terms. creatorTaxBps is the creator fee on top of it, paid to the creator in full and capped by the program. It sits last in the arguments rather than in the slot the fee left, so a client built against the older list fails to decode instead of sending a tax nobody asked for.
The move to a Raydium liquidity pool becomes available when the actual SOL reserve covers the funding target (graduationQuote) plus the saved setup cost (graduationCostLamports). An optional first purchase (initialBuy) completes in the same transaction as creation.
A token linked to an NFT collection begins in the waiting state (LinkPending). Trading stays disabled until createLinkedCollection succeeds. If that second transaction never completes, the creator can call cancelPendingLaunch.
NFT collections
Every NFT is a Metaplex Core asset in its collection's own Core collection, so any wallet or marketplace that reads Metaplex Core shows it. Solana marketplaces list collections one at a time, and a collection page here shows its NFTs from the moment they are minted.
createCollection creates the Ripples collection state and its Metaplex Core collection. mintAsset creates one NFT per transaction, records the SOL payment, updates the buyer's mint limit, and records any contribution to the token market of a combined launch for the original minter.
| Instruction | Authority | Purpose |
|---|---|---|
mintAsset | Minter | Pay SOL and create one NFT. |
revealAsset | Reveal authority | Replace a live placeholder exactly once. |
setBaseUri | Creator | Move the link a prepared collection reads its artwork from, until it is locked. |
setPlaceholderUri | Creator | Change the live placeholder before freezing. |
freezeMetadata | Creator | Permanently disable URI controls. |
creatorWithdraw | Creator | Withdraw only attributed creator proceeds. |
SOL sent directly to the collection's program-derived address (PDA) is not counted as an NFT payment. A separate recovery instruction, callable by anyone, can send only that extra SOL to the recorded creator.
Stock association
A token launch, an NFT collection, or a combined launch may optionally create an immutable StockLink sidecar. It records the subject, exact mint address, and SPL Token or Token-2022 program. Creation verifies that the mint account exists, is initialized, and is owned by the recorded token program. The sidecar leaves its subject account and launch economics unchanged.
| Launch type | StockLink subject |
|---|---|
| Token launch | Launch PDA |
| NFT collection | Collection PDA |
| Combined launch | Launch PDA |
The program verifies the mint-account relationship, not the issuer or what the mint tracks. The first-party Mainnet picker resolves tokenized tracker mints from the issuer catalog. If that catalog is unavailable, link creation is disabled and the bundled snapshot remains explicitly unverified. Public pages repeat the exact mint lookup and required Token-2022 program check before calling a link an issuer-catalog match. Direct program callers can link another valid SPL Token or Token-2022 mint, but the first-party UI labels it unverified.
A stock link records a reference. It moves no assets and creates no entitlement. The link does not purchase or swap for the tracker token, pair it with the launch, take custody, reward holders, confer a dividend entitlement, or distribute any asset. It changes no economics: a market that records one keeps the asset, the target, the fee and the vesting terms it was created with, and the sidecar can be read without touching any of them. The first-party picker is disabled on test networks.
A launch settles in the asset it was created against, which is SOL unless the creator named an approved mint at create. That choice is made in the same form and is otherwise unrelated to a StockLink: a launch may reference a tracker and settle in SOL, settle in an approved mint and reference nothing, do both, or do neither. Recording an association never moves the asset a trade settles in, and pricing a market in a mint records no association with its issuer. The market page says which of the two, if either, a launch carries.
Access to a referenced tokenized tracker, or to an asset a launch settles in, depends on the issuer's terms and the user's jurisdiction. That is a disclosure about the asset and its issuer. Ripples builds no eligibility or jurisdiction gate: nothing in the program, the site or this reference checks where a reader is or who they are, and the approved list is a list of assets. The issuer's own pause and account freeze are the only holds, they are read rather than applied, and a market priced in a paused mint stops until the issuer lifts it.
Token trading
A launch on Meteora's curve (engine: "dbc") trades through Meteora's own program and SDK, @meteora-ag/dynamic-bonding-curve-sdk. The pool address is the market: quote against the pool and its config, then send the swap with the quote's floor. Every trade pays its fee in SOL, and the fee eases over the market's first moments, so quote each trade rather than assuming a rate. Once the curve holds 85 SOL the program refuses further swaps until the pool opens, and from then on Jupiter routes the token.
const client = new DynamicBondingCurveClient(connection, "confirmed");
const virtualPool = await client.state.getPool(pool);
const config = await client.state.getPoolConfig(virtualPool.poolState.config);
const quote = client.pool.swapQuote2({
virtualPool, config,
swapBaseForQuote: false, // true to sell
hasReferral: false, eligibleForFirstSwapWithMinFee: false,
currentPoint: await getCurrentPoint(connection, config.activationType),
slippageBps: 100, swapMode: SwapMode.ExactIn, amountIn: lamports,
});
const transaction = await client.pool.swap2({
owner, pool, swapBaseForQuote: false, referralTokenAccount: null,
swapMode: SwapMode.ExactIn, amountIn: lamports, minimumAmountOut: quote.minimumAmountOut,
});A launch on the Ripples program trades against its launch account with the instructions below.
// Amounts are base units: the quote's own smallest unit, and 9-decimal SPL units.
// A launch quoted in SOL takes lamports here; one quoted in a mint takes that mint's.
// creatorPayout is where this launch pays its creator, pinned by the launch record:
// launch.creatorPayout, or launch.creator where that field is the default address.
// A launch with launch.holderShareBps above zero that pays its creator anywhere other than
// its distributor also takes the distributor, writable, after the instruction's own
// accounts, and on a token-priced launch the distributor's escrow after that.
await getBuyInstructionAsync({
launch,
mint,
tokenVault,
buyerToken,
buyer: walletSigner,
treasury,
creatorPayout,
quoteIn: 100_000_000n,
minTokensOut,
});
await getSellInstructionAsync({
launch,
launchAuthority,
mint,
tokenVault,
sellerToken,
seller: walletSigner,
treasury,
creatorPayout,
tokensIn,
minQuoteOut,
});A launch fixes tradeFeeBps when it is created and keeps it for the life of the market, so read it from the launch account rather than assuming a rate. It fixes quoteMint, quoteTokenProgram and quoteDecimals the same way, so read the asset off the launch too. A launch created before per-launch assets existed reports its quote as absent, which means SOL. The web form creates Solana markets at 1%, split as fixed shares of the fee itself: 0.7% of volume to the creator, 0.24% set aside for the $RIPPLES buyback, and the remainder to the protocol. The creator's share is paid from the launch's first trade, and after graduation collectLockedFees divides what the locked pool position collects on the same recorded share. Read that share as creatorFeeShareBps on the launch account: a market created under an earlier schedule reports its own. A launch can also fix a creator fee,creatorTaxBps, which comes off the same amount the fee does, on a buy and on a sell, and reaches the creator whole with no share to the protocol. Read it off the launch and size the floor after it, as the agent buy above does. During the first 3 seconds, non-exempt buyers also pay an opening tax that falls from 99% to zero. That tax buys no tokens. It is held by the launch as snipeTaxCollected and seeded into the pool at graduation, and it is deliberately outside realQuote(), so it does not count toward the funding target. The signer always supplies a price limit: minTokensOut for purchases and minQuoteOut for sales.
A market pays its creator to creatorPayout on the launch account. It starts as the creator, and only the address named there can move it, with setCreatorPayout, which takes effect on the next trade. Income that address cannot receive directly waits in escrow rather than becoming protocol revenue: claimEscrow pays out one balance in one asset, and claimNativeEscrow pays out SOL. Both are permissionless and neither can redirect the payment. A SOL claim too small to lift the recipient over its own rent floor is refused, because the runtime would reap the balance it landed in.
Combined launches
Before the Raydium pool is created, the configured share of each NFT payment moves into the token market reserve without changing its virtual pricing reserves. A minter's share is fixed when the launch graduates: it is pro-rata to what their mints routed into the market. After the pool is complete, claimVested releases tokens according to the configured waiting period and vesting schedule. finalizeBeneficiary closes a minter out once they have claimed in full, which is what lets the last of the rounding dust be accounted for. Selling or transferring the NFT does not transfer this token claim.
A combined launch on Meteora's Dynamic Bonding Curve (DBC) works differently, and the instructions and endpoint for it have dbc in their names. The pool is created from one of three Ripples configs, one for each setting listed below. Each setting fixes how much of the token supply is set aside for the collection and how it unlocks. The collection is then linked to the pool with create_dbc_linked_collection, which opens its sale 31 seconds after the pool is created, when the curve's higher fee for its opening seconds has ended. Mints sent before then are refused. Until the curve completes, each mint (mint_asset_dbc) spends the configured portion of its price buying the token on the curve through the Ripples program. After the curve completes, the whole mint price is the collection's revenue. Once the curve has migrated to its Meteora pool, anyone can call settle_dbc_link, which combines the tokens those mints bought with the supply set aside. Ripples runs a service that calls it, so integrators normally don't need to. Each NFT, called a piece here, is entitled to the tokens bought when it was minted, plus a share of the collection's set-aside supply in proportion to the SOL its mint spent on the curve. Pieces minted after the curve completes spend nothing on the curve, so they receive no tokens and no share. A piece's share unlocks on the setting's schedule, counted from the moment the curve completed, just before its Meteora pool opens. The share belongs to the piece, not to the wallet that minted it: whoever holds the piece claims with claim_dbc_piece, and GET /v1/dbc/pieces/<collection>/<serial> reports how much of that piece's share has unlocked and how much has been claimed.
Liquidity pools
A launch on Meteora's curve completes when its SOL reserve reaches the threshold its config fixes, 85 SOL on the standard config. Ripples then runs migrate_damm_v2, which opens the launch's Meteora pool (DAMM v2) at the price the curve reached and locks the liquidity for good; anyone may run it, and a pool somebody else migrated is left as it is. The creator's locked position in that pool collects fees from then on, claimed with claimPositionFee from the cp-amm SDK, and the curve's own creator fees with claimCreatorTradingFee2 at any time. Terminals list the pool from its first trade.
A launch on the Ripples program moves into a Raydium pool (CPMM) in these steps:
beginGraduation pauses token trading and records the amounts that will start the liquidity pool.createCpmmPool prepares the required SOL and creates the launch-bound Raydium constant-product pool (CPMM). The pool pairs the launch token against the launch's own quote mint, under that mint's own token program. A mint carrying extensions Raydium screens needs its SupportMintAssociated account passed as a remaining account, and a mint with no such account cannot enter a pool.lockLp secures the liquidity-provider position. Permanent locks use Raydium Burn & Earn.finalizeGraduation marks the move complete after the program verifies where the liquidity position is held.Before the pool is created, the launch creator or current Ripples program authority can call resumeTrading to clear the prepared amounts and reopen token trading. This recovery path closes once a pool position has been recorded.
A time-locked liquidity-provider position (LP position) remains in a program-owned account until lpUnlockAt. Permanent locks use collectLockedFees, which sweeps both sides of the pool into the launch's own accounts and divides what arrived on the launch's creatorFeeShareBps: the creator's share goes to creatorPayout and the remainder to the treasury. It pays the same creatorPayout a trade pays, so moving that address moves both. Time-locked positions use withdrawUnlockedLp only after the lock expires.
Public registry API
This API supports the public web app. The routes below index the Solana chain; /v1/launches also answers Robinhood for ?chain=robinhood, and /v1/launches/<address> answers on either chain. The private Virtuals agent integration is a separate service; this API never receives its wallet credentials or transaction requests.
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/launches | Indexed launch PDAs and current public state. |
| GET | /v1/collections | Indexed collection PDAs and supply. |
| POST | /v1/auth/solana/challenge | Create a one-time message for a wallet to sign. |
| POST | /v1/auth/solana/verify | Verify the Ed25519 wallet signature and issue a private session token. |
| POST | /v1/launch-media | Validate and publish a launch image and its public token or collection details. Requires a Solana wallet session. |
| POST | /v1/collections/:collection/config/challenge | Bind provider config to creator and collection. |
| PUT | /v1/collections/:collection/config | Save the freshly signed provider config. |
/v1/launch-media accepts name, symbol, description, and an image object containing a MIME type and base64 data. Optional socials fields cover Website, X, and Telegram; customLinks accepts up to five labeled HTTPS links. Images may be PNG, JPEG, GIF, WebP, or AVIF and may not exceed 1 MB. The service verifies the actual file type, stores the image and JSON at public links addressed by their own content, and returns metadataUri and imageUrl. The same address answers with the same bytes for as long as it is hosted, and Ripples can remove content that breaks its terms. The browser uses metadataUri automatically; creators using the standard form never need to enter or host it themselves.
Addresses are base58 and case-sensitive. Sign the exact UTF-8 challenge bytes and submit the raw 64-byte Ed25519 signature encoded as base58. This proves wallet control without a password or a Solana transaction. Each challenge works once and expires.
Reference · Robinhood
Ripples is a set of public contracts, and their source is published at github.com/ripples-dot-run/contracts under MIT. Two factories deploy every launch, so a token, its market, its collection, and its vesting contract all come from code you can read before you use it. The browser calls them with viem, and any wallet that supports Robinhood 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
approvedQuote(address) and quoteEconomics(address), paged by quoteCount() and quotes(offset, limit)linkOf(curve). It stores an address and verifies nothing about the issuer or the instrumentcontractURI.A market is read and traded through the contracts of the group it was created under; its page names them.
NEXT_PUBLIC_LAUNCHPAD_CHAIN=rh-mainnet
NEXT_PUBLIC_LAUNCHPAD_RH_RPC_URL=https://rpc.mainnet.chain.robinhood.comThe endpoint is optional and defaults to the address above. Set it only to front the public endpoint with your own origin. A build whose chain and endpoint name different networks is refused at startup rather than left reading one network while labeling itself the other.
A 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 pulled by allowance, and the approval names the launch's own quote rather than the factory's default: a buy or a mint on a market priced in an approved asset is approved against that asset, read with QUOTE() off the market or the collection. A trade is the one thing that does not spend a plain allowance: it goes through a router that spends through Permit2, so a first trade in an asset approves the token to Permit2 once and then Permit2 to the router for an amount and a window. The second grant names whichever router is placing the trade, and a permission granted to one router does not cover a trade through the other. The launch fee is the exception and stays in ETH, which feeToken() answers, so creating a launch priced in another asset takes two approvals: one for the fee and one for the opening buy. Launch fees and mints each need an approve before the call that spends it, and neither takes a native payment, so paying either in ETH wraps exactly what it costs first, in the same action. A buy on a market priced in ETH needs no approval at all: it sends the ETH with the swap, as Token trading below sets out. 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.
The swap parameters are the one place this network departs from current v4-periphery, and it fails quietly. Its SWAP_EXACT_IN_SINGLE struct carries an extra head word between amountOutMinimum and the offset to hookData. Encode the documented five-field struct and the router reads the hook-data offset out of that unused slot. Where the pool's first currency is the chain's own asset the misread lands on a zero word and the swap goes through. Where it is an ERC-20 the same slot holds a token address, the calldata bounds check fails, and the call reverts with no return data at all, before the router reaches the PoolManager. Every market priced in ETH is ERC-20 quoted, so encodeV4Swap writes the sixth field.
// One read prices the market: config, position, state, raise, price, opening tax.
// launchesOf lives on the lens; the hook has no such function and reverts.
const [view] = await client.readContract({
address: launchLens,
abi: LAUNCH_LENS_ABI,
functionName: "launchesOf",
args: [[poolId]],
});
// Which side the hook charges depends on the hook the pool is keyed to, so read
// view.config.hook before sizing the floor.
//
// On QuoteFeeHook every leg comes off the WETH side whichever side the swap
// specifies: the opening tax first, then the trade fee and the creator's own
// charge on what is left. An exact-input buy is charged on its input and the
// curve sees the net, so the floor is the constant-product move on that net.
const taxQ = (quoteIn * view.snipeTaxBps) / 10_000n;
const feeQ = ((quoteIn - taxQ) * view.config.tradeFeeBps) / 10_000n;
const creatorQ = ((quoteIn - taxQ) * view.config.creatorTaxBps) / 10_000n;
const netOnQuoteFee = tokensOut(view, quoteIn - taxQ - feeQ - creatorQ);
// On LaunchHook the cut comes out of the swap's unspecified side, which on an
// exact-input buy is the launch token: the opening tax first, then the trade fee
// and the creator's own charge on what is left.
const gross = tokensOut(view, quoteIn);
const tax = (gross * view.snipeTaxBps) / 10_000n;
const fee = ((gross - tax) * view.config.tradeFeeBps) / 10_000n;
const creatorTax = ((gross - tax) * view.config.creatorTaxBps) / 10_000n;
const net = gross - tax - fee - creatorTax;
// Size the floor on what reaches the buyer, at the tolerance the app signs.
// quoteFeeHook is the address the deployment table above lists.
const isQuoteFeeHook = (hook) => hook.toLowerCase() === quoteFeeHook.toLowerCase();
const reaches = isQuoteFeeHook(view.config.hook) ? netOnQuoteFee : net;
const amountOutMinimum = (reaches * 9_900n) / 10_000n;
// V4_SWAP is the router's one command; the actions inside it are
// SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL. The deadline is a wall-clock
// second: the router compares it to block.timestamp.
const { request } = await client.simulateContract({
account: buyer,
address: universalRouter,
abi: UNIVERSAL_ROUTER_ABI,
functionName: "execute",
args: [
"0x10",
[encodeV4Swap({ poolKey, zeroForOne: quoteIsCurrency0, amountIn: quoteIn, amountOutMinimum })],
BigInt(Math.floor(Date.now() / 1000) + 600),
],
});
const hash = await wallet.writeContract(request);
await client.waitForTransactionReceipt({ hash });Token launch
createLaunch deploys the ERC-20 and the launch's locker, opens the pool and seeds the curve position, 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 is one transaction and leaves no half-created state to repair. Both calls take the launch fee from the caller's allowance, so the wallet approves that spend before either one.
Both calls take an optional opening buy. The factory routes it through the launch's own LPLocker, which it registers as exempt, so only the trade fee applies to that buy. Three writes fill the exempt set: the creator at registration, every address passed in snipeExempt, and any address the fee recipient is later moved to with setCreatorFeeRecipient. The set is narrower than it reads. The hook matches its exempt set against the address that called the PoolManager, and every ordinary trade reaches the PoolManager through a router, so a creator buying later pays the opening tax like any other buyer. An address passed in snipeExempt is only ever exempt for a buy it sends to the PoolManager itself.
The two calls are two launch shapes. A standalone launch opens against a smaller pricing reserve, because its raise can only arrive one trade at a time and every trade moves the price. A combined launch opens against a larger one, because an NFT collection can route a whole raise in without the price moving at all. Both reach the same target, and the registry row carries a reserve for each shape.
const params = {
name: "Ripple",
symbol: "RPL",
curveSupply: 800_000_000n * 10n ** 18n,
lpTokenSupply: 219_000_000n * 10n ** 18n,
vQuoteInit: 275_880_000_000_000_000n,
vTokenInit: 1_073_000_000n * 10n ** 18n,
graduationQuote: 689_700_000_000_000_000n,
lpUnlockAt: 0n,
// On quoteFeeFactory the charge below is the whole of what trading pays the
// creator: that factory stamps no creator share into the trade fee.
creatorTaxBps: 0n, // the creator's own charge, 0 to 1000 bps
// The asset this market settles in. The zero address means the factory's default.
// vQuoteInit and graduationQuote above are in THIS asset's own decimals and must
// equal the row in the registry that belongs to the factory you create on, so
// read them from quoteFeeRegistry.quoteEconomics(quote) rather than typing them.
// The two factories keep different rows for the same asset and each refuses the
// other's.
quote: approvedQuote,
};
// The launch fee is charged in feeToken(), so it is approved, not sent as value. A
// launch priced in another approved asset approves that asset for the opening buy as
// well, which is two permissions rather than one.
await wallet.writeContract({
address: feeToken,
abi: QUOTE_ABI,
functionName: "approve",
args: [quoteFeeFactory, launchFee],
});
const { request } = await client.simulateContract({
account: creator,
address: quoteFeeFactory,
abi: TOKEN_FACTORY_ABI,
functionName: "createLaunch",
args: [params],
});The trade fee is not a launch parameter. Every market this factory opens charges TRADE_FEE_BPS, and the pool fee and tick spacing are the hook's own constants. The reserves above are the standalone pair. createLinkedLaunch declares the linked pair instead, and the factory refuses either shape declared against the other's reserve.
An lpUnlockAt of zero is the factory's way of saying the liquidity never unlocks. The factory rejects a configuration whose supply cannot seed its own graduation, whose seed price falls outside its band, or whose creator tax exceeds MAX_CREATOR_TAX_BPS, so an unlaunchable market never reaches the chain.
NFT collections
Collections on Robinhood use the standard ERC-721 and ERC-7572 interfaces, with ERC-2981 royalties, so marketplaces that index this chain can list them. Check OpenSea for the current state of that support. The collection page and every NFT row here link to the marketplace address for the collection where the chain has one.
A collection deployed by the factories above answers contractURI() (ERC-7572). The factory points it at https://api.ripples.run/v1/collection-assets/<collection address>/contract.json at creation, and that document carries the collection's name, a square logo, its page on this site, and the royalty percentage and payout address read from the contract, so a marketplace page fills itself in without anyone claiming it. The description a marketplace shows comes from the artwork the creator published rather than from this document. A collection reads the origin once and keeps it, so a drop created before its factory published one answers nothing here; its collection page says which of the two it is. A creator hosting their own document can repoint it with setContractURI at any time, including after the artwork is frozen.
createCollection deploys a collection at a fixed price in the collection's own quote, which is ETH unless the creator named another approved asset at create. Naming an asset makes that asset's registry row bind: the price has to clear the row's floor, minPriceQuote, and an asset the registry has not approved is refused. A collection that names none takes the factory's default and is held to no row. The other half of the price rule, that a mint's routed share must not round to nothing at the asset's decimals, belongs where both numbers exist: createLinkedLaunch checks it at create and Collection721.linkToCurve checks it again when the two are joined. A combined launch shares one asset across the collection and the curve, so the routed share of a mint reaches the market in the same asset the minter paid in; linkToCurve reverts QuoteMismatch rather than joining two that disagree. 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 the collection's quote and create the NFTs, with a floor on the routed share. |
setTokenURI | Platform signer | Replace a live placeholder exactly once. |
setBaseURI | Creator | Move the link a prepared collection reads its artwork from, until it is frozen. |
setPlaceholderURI | Creator | Change the live placeholder before it is frozen. |
setContractURI | Creator | Repoint the collection document marketplaces read. Outside the freeze, which covers the artwork links. |
freezeMetadata | Creator | Permanently stop the artwork links from moving. |
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
A Robinhood market is an ordinary Uniswap v4 pool with the Ripples hook on it. A buy or a sell is a v4 swap through the Universal Router at 0x8876789976dEcBfCbBbe364623C63652db8C0904, spending through Permit2 at 0x000000000022D473030F116dDEE9F6B43aC78BA3, so any aggregator that already routes v4 can trade a Ripples market without integrating anything of ours. That path takes any order the market can fill whole, which is every ordinary size. The address the app calls a market is the launch's LPLocker. It holds the position and answers the launch's own facts, and it takes no orders.
An order larger than what the market has left to sell needs the other path. A launch's liquidity is one position over one bounded price range, and LaunchHook refuses any swap that would end outside it. The Universal Router places its swap with a wide price limit, so an oversized buy walks past the range end, meets that refusal and reverts whole. Ripples' own router at 0x22DB9B2c6C6DB56ABC3fc196E4bC1FBb9c4338a6 places the same swap with the range end as the price limit instead. The pool fills to the end of the curve and stops, and the quote it never reached is never pulled out of the wallet. It is the path the app trades through when a buy is paid out of WETH or a market is priced in another asset, and the one to use for the last buy before a market reaches its target. A market keyed to QuoteFeeHook takes the same call at 0x0e558e1e65BD1850730A3B4a89654c066fC035B7: a v4 pool's hook is part of its key, so a router built against one hook refuses every pool on the other, and the market's page names the one to use.
swapExactIn(key, zeroForOne, amountIn, minAmountOut, recipient, deadline) returns (spent, amountOut) and spends through the same Permit2 allowance the Universal Router path uses, granted to this router rather than to that one. spent is what the pool consumed and is at most amountIn. minAmountOut is a price rather than a quantity: the router holds spent * minAmountOut <= amountIn * amountOut, which on an order that spends in full is exactly amountOut >= minAmountOut and on one that fills partly asks the same rate of the part that filled. So a floor signed against the whole order keeps its meaning whichever way the order lands.
Read the outcome from Filled(poolId, sender, recipient, zeroForOne, amountIn, spent, amountOut). spent below amountIn is a partial fill, and the difference is money that stayed with the sender. The PoolManager's own Swap reports the part that filled and says nothing about the part that did not, so amountIn beside spent is the only place a partial fill is readable at all. CurveEndReached is the revert on a market with nothing left to sell.
A market priced in ETH settles in WETH, and the app trades it in ETH through the Universal Router, one call each way. A buy carries the ETH as the call's value: WRAP_ETH into the router's own balance, then V4_SWAP with SETTLE paid by the router and TAKE_ALL to the buyer, then UNWRAP_WETH to hand back anything the swap left, with no approval. A sale is V4_SWAP with the WETH taken to the router (TAKE to the router itself), then UNWRAP_WETH to the seller with the floor as its minimum, so the floor binds the ETH that arrives. Neither leaves WETH with the router or the trader.
The pool key is LaunchHook.poolKeyFor(token, quote), and its id is the same hash the PoolManager takes, so a market can be addressed before anything has been read from it. poolIdOfToken(token) goes the other way.
Everything a price needs comes from one read: LaunchLens.launchesOf(bytes32[] poolIds), at the market reader in the deployment table above. The hook carries no launchesOf of its own, and a call sent there reverts. Each LaunchView carries the config, the seed plan, the live state, the raise so far, the pool price, and the opening tax in force at that block. Read the fee as config.tradeFeeBps, the target as config.graduationQuote, the asset as config.quote and the opening tax as snipeTaxBps. The tax decays from config.launchedAt, which is when trading opened rather than when the launch was registered. The quote's decimal places come from the quote token itself: no contract in a launch republishes them, and a target read at the wrong decimals is out by orders of magnitude. The lens holds no state and no permission, and everything it returns it reads back out of the hook's own views and the PoolManager, so a caller who does not trust it can reproduce every field from those two addresses.
Price a market from the position and the pool price, never from the pool's active liquidity, which reads zero while the curve position is intact. Read view.config.hook before sizing a floor: which side of the swap the hook charges is a property of the hook, and the two below differ. Reconstruct a fill from the PoolManager's Swap together with TradeFeeCharged, CreatorTaxCharged and SnipeTaxCharged: Swap alone overstates what the trader received by everything the hook took.
The web form creates Robinhood markets at 1%, which is the whole fee, and a market keeps the fee, the share and the hook it was registered with. For the first 3 seconds after trading opens, a buy pays an opening tax on top that starts at 99% and falls to zero; the tax is charged first, then the trade fee and the launch's own config.creatorTaxBps on what is left. The creator's tax is credited to the creator whole and reaches no split, so a market that declared one takes three legs out of a buy rather than two.
Markets created by QuoteFeeFactory 0x66112C2c987eD7A366715bD28CCc0DF629813206 are keyed to QuoteFeeHook 0x6172fD469E900F74eD04b1763daB4D2c2BFB6AcC, which takes every leg out of the ETH side of the swap, in beforeSwap where that side is specified and in afterSwap where it is not, so a buy and a sell both pay in ETH and the opening tax comes out of the quote a buyer pays rather than the tokens they receive. The hook credits the fee and the tax to the treasury the factory names, read live through treasury(poolId). Since 2 November 2026 that is the buyback fee router 0xbc3940Ef143fb3C064a238805A31c18945be1C60, which sends 70% to BuybackLiquidity 0x4B2B547688a7D39512937aD9583315619c3Ce711 and 30% to the Ripples treasury; the buyback buys $RIPPLES and locks it as dead-owned liquidity. Until 1 November 2026 it was QuoteFeeRouter 0xC8842e8196E96Cef0988Cffb144722fDe8a8a684, which sent 70% to the buyback burner. config.creatorFeeBps is 0 on every launch this factory creates. A market created in the Founders Window is the exception, by design: its factory (0x1C4032F557621D3b77E11748706BEfd26F3b6ef4) stamps 30% into config.creatorFeeBps, paid to the creator from the first trade for the life of the market, and the rest of its fee goes to the same buyback fee router. Launch on Ripples during the Founders Window and reach a $30K market cap within seven days. We'll cover your DEX Screener Enhanced Token Info. Available to the first 10 qualifying projects, alongside the existing Founders Window benefits. config.creatorTaxBps is charged in ETH on the same base and credited to the creator in full.
Markets created by TokenLaunchFactory 0xE89C1b33B6C2505414667bE7E5084774a72150e1, including $RIPPLES and $KOI, are keyed to LaunchHook 0xaF59944A7d03B914567cb0272b7E588A7aE7AAC4, which takes its cut out of the swap's unspecified side, which on an exact-input buy is the launch token, so compute the floor you sign on the net rather than on what the pool pays out. It splits the fee in afterSwap from the launch's first trade, on config.creatorFeeBps, and credits both sides to its pull-escrow ledger in the currency the fee landed in: a buy's fee is in the launch token and a sell's is in the quote. Read the share off the launch rather than assuming it. On an ordinary buy the taxed amount is in the launch token and the hook credits it to BURN_SINK, and the quote the buyer paid stays in the market.
Nothing leaves the hook during a swap. What it owes an address is owed(token, account), and claim(token) takes it. claimFor(token, account) is the same payment made by anyone, and it never pays anyone but account. A creator sends that income somewhere else with setCreatorFeeRecipient(poolId, recipient), callable by the address receiving it today, which starts as the creator.
| Call | Purpose |
|---|---|
LaunchLens.launchesOf(poolIds) | Config, position, state, raise, price, and opening tax, per pool id. |
LaunchHook.snipeTaxBps(poolId) | The opening tax in force at this block, in basis points. |
LaunchHook.snipeExempt(poolId, account) | Whether an address pays no opening tax. Matched against the caller of the PoolManager, so a router hides the trader behind it. |
LPLocker.QUOTE() · LPLocker.TOKEN() | The two assets this market pairs. |
LPLocker.realQuote() · GRADUATION_QUOTE() | The raise so far and the raise that graduates it, in the quote's own units. |
LPLocker.graduated() | Whether the raise has closed. |
LaunchHook.owed(token, account) | What the hook still owes an address in one asset, in that asset's own units. |
Markets created before this generation are bonding curves rather than pools. Those carry buy, buyFor, sell, sellWithPermit, quoteBuyFor, TRADE_FEE_BPS, QUOTE_DECIMALS() and LAUNCH_START. None of those selectors exists on a market the current factory creates, and calling one reverts. Call poolId() on the market address to tell the two apart: a pool-backed market answers it, a curve does not.
Combined 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.
A minter's share is fixed when the launch graduates and the reserved slice is delivered: allocationOf(minter) is pro-rata to what they routed in. 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. finalizeBeneficiary(minter) closes a beneficiary out once they have claimed in full, so burnSurplus can burn the rounding dust left above the reserved balance.
Agent launches
AgentTreasuryFactory.createAndLaunch(charter, tp, np, lp, d, runway) deploys an AgentTreasury, moves runway into it from the caller, and has it call TokenLaunchFactory.createLinkedLaunch with the same tuples a combined launch takes. The treasury is therefore the launch's creator, and the creator's share of every trade fee is credited to it. Approve runway of RUNWAY_ASSET() to the factory first; it is the launch factory's fee token, and a launch settling in anything else is refused.
The charter is (address operator, uint128 dailySpend, uint128 perCallSpend, uint128 dailySell, uint32 dailyMint, address[] payees), fixed in the constructor. The three quote figures are in RUNWAY_ASSET and dailySell is in the launch token. perCallSpend has to be non-zero and at most dailySpend; at most eight payees, each once.
The operator key calls buy(amountIn, minAmountOut, deadline), sell(amountIn, minAmountOut, deadline), mint(quantity, minRoutedTotal), pay(to, amount) and note(digest, uri). Trading goes through this launch's own pool; the treasury reads the key off its locker, so there is no venue argument. Quote leaving the contract is metered against dailySpend and perCallSpend together across buys, mints and payouts; selling is metered on the order rather than the fill. The window is the UTC day. remainingToday() answers what is left of each ceiling.
claimFees() and claimVested() are permissionless and can only pay the treasury: the first pulls the creator's share out of the hook, the locker and the collection, the second the slice the treasury's own mints earned. The creator keeps setCollectionBaseURI, setCollectionPlaceholderURI and freezeCollectionMetadata, and has no other call. There is no owner, no pause and no withdrawal.
Every call emits: Launched, Bought, Sold, Minted, Paid, FeesClaimed, VestedClaimed and Noted. AgentTreasuryFactory.isFromFactory(address) is what tells a treasury this factory deployed from any other contract answering the same reads.
@ripplesdotrun/agent-sdk signs these calls from your agent's own key, and @ripplesdotrun/mcp serves them to a model over stdio. Neither runs the agent: the model loop, memory and policy stay yours. The runway, the limits and the piece price are counted in RUNWAY_ASSET(), as above.
import { createRipplesAgent } from "@ripplesdotrun/agent-sdk";
import { privateKeyToAccount } from "viem/accounts";
const agent = createRipplesAgent({
network: "mainnet",
account: privateKeyToAccount(process.env.RIPPLES_PRIVATE_KEY),
allowMainnet: true,
});
const { treasury, token, market } = await agent.launch({
runway: "0.05",
charter: {
operator: process.env.OPERATOR_ADDRESS, // a key of its own, apart from this wallet
dailySpend: "0.03",
perCallSpend: "0.01",
dailySell: "100000",
dailyMint: 3,
payees: [process.env.PAYEE_ADDRESS],
},
token: { name: "Field Notes", symbol: "NOTES", image: "https://example.com/notes.png" },
collection: { name: "Field Notes Pieces", symbol: "NOTESP", price: "0.01", supply: 88, baseUri: "https://example.com/notes/" },
});
console.log(treasury, token, market);{
"mcpServers": {
"ripples": {
"command": "npx",
"args": ["-y", "@ripplesdotrun/mcp"],
"env": {
"RIPPLES_NETWORK": "mainnet",
"RIPPLES_ALLOW_MAINNET": "1",
"RIPPLES_MAX_PER_CALL": "0.02",
"RIPPLES_PRIVATE_KEY": "…"
}
}
}
}Wake
A wake is a market whose creator income is routed to a contract made for one NFT. AgentPayeeFactory.payeeOf(nft, tokenId) answers that contract's address before it exists, and create(nft, tokenId) deploys it. Anyone can call either; a second create returns the same address, and a create reverts NoHolder() while nothing on the chain holds the token. The market's current creator fee recipient then calls the hook's setCreatorFeeRecipient(poolId, payee). Only the current recipient can move it and the payee has no call that moves it again, so the route is permanent.
AgentPayee.release(hook, currency) pays out, and anyone can call it. It pulls what the hook holds for the payee in currency, the zero address for ETH, and sends the payee's whole balance to holder(), which reads ownerOf at that moment. Nothing about the payee changes when the NFT is sold: the next payout finds the new holder. Each payout emits Released(currency, to, amount).
A wake shared by several pieces is routed to a WakeSplit instead. WakeSplitFactory.splitOf(pieces) and create(pieces) take from 1 to 256 (address nft, uint256 tokenId, uint32 weight) entries, ordered by NFT and then by token id, with no repeats and no zero weight, and every piece needs a holder at create. Each piece is paid weight / totalWeight of what arrives, rounded down. release(hook, currency, index) pays one piece; releaseMany(hook, currency, indexes) pays several after one pull and skips a piece it cannot pay with ReleaseFailed rather than reverting. due(currency, index), holderOf(index) and pieces() read it. The launch form gives every piece an equal weight; the contracts take any.
On ripples.run, leaving a wake also registers the piece as an ERC-8004 agent. On chain that is a call of its own: the holder registers the piece through the ERC-8217 binding adapter with register(0, nft, tokenId, agentURI), where 0 is ERC-721, and the agent answers to whoever holds the NFT from then on. None of these contracts has an owner or a setting.
The API answers the same reads, open to every origin. GET /v1/wake?chain=robinhood lists up to 500 wakes with the latest 50 payouts. GET /v1/wake/{rail}/{collection}/{tokenId} answers one piece, GET /v1/wake/by-market/{rail}/{token} the wake a market pays, and GET /v1/wake/split/{rail}/{split} a shared wake with every part. A wake carries its payee, its piece, the current holder, the markets paying it, owed (what a payout would send now), paid, lastPayout, and share on a part of a shared wake. Amounts are decimal strings in base units, with the zero address for ETH.
Agents read the same way: GET /v1/agents?wallet=0x…, GET /v1/agents/{chainId}/{agentId}, GET /v1/agents/resolve?link= for an 8004scan or OpenSea link, and GET /v1/pieces/{rail}/{collection}/{tokenId}/agent.json for a piece's ERC-8004 registration file. rail is robinhood.
import { createWakeClient } from "@ripplesdotrun/agent-sdk";
import { privateKeyToAccount } from "viem/accounts";
const wake = createWakeClient({
chain: "robinhood",
account: privateKeyToAccount(process.env.RIPPLES_PRIVATE_KEY),
allowMainnet: true,
});
// Route a market you created to one piece, read it, and pay whoever holds it.
await wake.pinWake({ token, nft, tokenId });
console.log(await wake.wake({ nft, tokenId }));
await wake.releaseWake({ nft, tokenId });
// Or share it across pieces, then pay every part.
const { split } = await wake.shareWake({ token, pieces: [{ nft, tokenId: 1 }, { nft, tokenId: 2 }] });
await wake.releaseSplit({ split });bindToken registers a piece you hold as an agent, and splitOf and wakeOfSplit read a shared wake. The MCP server carries the same calls as ripples_bind_token, ripples_pin_wake, ripples_wake, ripples_release_wake, ripples_share_wake, ripples_wake_split and ripples_release_split; the ones that sign need RIPPLES_PRIVATE_KEY and RIPPLES_ALLOW_MAINNET=1, and routing asks for acknowledge: "permanent" because it cannot be undone.
Uniswap v4 liquidity pool
LaunchHook.graduationReady(poolId) turns true once LPLocker.realQuote() clears GRADUATION_QUOTE() and the opening tax window has closed. From there LPLocker.settleGraduation() is callable by anyone and runs once. It closes the raise on the hook, retires the curve position whole, opens a full-range position out of the proceeds and anything a linked collection routed in, hands a linked launch its reserved slice, and burns whatever supply is left. Swaps are refused between the close and the permanent position, which is why it is one transaction rather than two. Quote beyond what the token inventory can pair goes to the treasury rather than moving the opening price.
There is no graduate() on a market address. The hook's graduate(poolId) takes only the launch's own locker, so settleGraduation() is the call to make: it is permissionless, anyone can finish a market that has reached its target, and it closes the raise and settles the position in one transaction or reverts whole.
The locker holds the position under UNLOCK_AT. A permanent lock never reaches unlock. The pool charges no fee of its own, so collectFees normally collects nothing. It stays because the position is real, and a pool that ever earned a fee would have no other way to divide it: the call divides the position's fees on the share the factory stamped into the locker: nothing to the creator on a locker from QuoteFeeFactory 0x66112C2c987eD7A366715bD28CCc0DF629813206, which sends the position's fees to the treasury the factory recorded, and a 70% share to the creator on one from TokenLaunchFactory 0xE89C1b33B6C2505414667bE7E5084774a72150e1, with the rest to the treasury. What a finished market pays its creator is the trade fee the hook credits, taken with LaunchHook.claim. 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, and two token launch factories have made markets on this chain, so a complete board pages both and asks each one isFromFactory in turn. launchCount() and launches(offset, limit) page the token launches; collectionCount() and collections(offset, limit) page the drops. isFromFactory(address) is the check that an address came from Ripples rather than from a lookalike deployment: pass the launch's locker on the token rail, the collection on the NFT rail. Multicall3 sits at its canonical address on Robinhood, 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, locker, hook, creator }[]A token answers for itself. name() and symbol() are the ERC-20 pair; logo() and description() return strings; and socials() returns five links as twitter, telegram, discord, website and farcaster, any of which may be empty. All seven are written in the token's constructor and have no setter, so what you read once is what the launch froze. Nothing about a launch depends on an index this app controls.
Your own agent
An agent that already holds a wallet reads launches from the public API, reads chain state from the endpoint in the deployment table or from a node of its own, builds the same calls the browser builds, and signs them with its own key. That wallet pays every network fee and every trade. Nothing on this page signs, pays, or approves on its behalf.
GET https://api.ripples.run/v1/launches?chain=robinhood lists Robinhood token launches, newest first, as count, nextOffset and a launches array. Each row carries address (the token), pair (the pool id its market trades under), creator, token with its name and symbol, and status. /v1/launches/<address> returns one launch with its current market state, by its token address or its pool id. Launches can also be read straight from the factories, as the board does.
The list answers count: 0 until the first launch exists, so write the loop to survive an empty page. limit defaults to 50 and takes up to 200, and offset counts rows from the newest. Reads are limited to 120 a minute per client address, and every answer carries what is left in x-ratelimit-remaining. Point RIPPLES_API at your own build's API when you want to run the same calls against a test network.
@ripplesdotrun/agent-sdk reads these routes without a key: createMarketClient() gives markets({ chain, offset, limit }), market(address, { chain }) and stats(). @ripplesdotrun/mcp serves the same reads to a model as ripples_markets, ripples_market and ripples_stats. Both answer with the API's own rows, so a market the board leaves out is left out there too.
On Robinhood a market is a Uniswap v4 pool, so the agent prices it from the hook with viem and swaps through the Universal Router, using the addresses from the Robinhood deployment table. The router spends through Permit2, so the quote token is approved to Permit2 and Permit2 to the router. The network fee is paid in ETH from the same wallet.
import { createPublicClient, createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const chain = {
id: 4663,
name: "Robinhood",
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
};
const account = privateKeyToAccount(agentPrivateKey);
const client = createPublicClient({ chain, transport: http() });
const wallet = createWalletClient({ account, chain, transport: http() });
const QUOTE = parseAbi(["function approve(address spender,uint256 value) returns (bool)"]);
const PERMIT2_ABI = parseAbi([
"function approve(address token,address spender,uint160 amount,uint48 expiration)",
]);
const ROUTER = parseAbi([
"function execute(bytes commands,bytes[] inputs,uint256 deadline) payable",
]);
// Price the market off LaunchLens.launchesOf and take the hook's cut off the output
// before signing a floor. The Robinhood "Token trading" section shows that
// arithmetic in full, and where the lens is deployed.
const quoteIn = 10n ** 16n;
const amountOutMinimum = floorFor(quoteIn);
const expiry = Math.floor(Date.now() / 1000) + 3_600;
await wallet.writeContract({ address: quoteToken, abi: QUOTE, functionName: "approve", args: [permit2, quoteIn] });
await wallet.writeContract({ address: permit2, abi: PERMIT2_ABI, functionName: "approve", args: [quoteToken, universalRouter, quoteIn, expiry] });
const hash = await wallet.writeContract({
address: universalRouter,
abi: ROUTER,
functionName: "execute",
args: [
"0x10",
[encodeV4Swap({ poolKey, zeroForOne: quoteIsCurrency0, amountIn: quoteIn, amountOutMinimum })],
BigInt(expiry),
],
});
await client.waitForTransactionReceipt({ hash });Crosscurrents
With Fill first, each side of a Crosscurrent is held in an escrow: a contract on its own chain that holds the side's opening buy until the side opens or refunds, and pays out to its market, to the wallets that paid in, and to the creator (the launch deposit, and on Robinhood the creator's trade fees). On Solana the upgrade authority listed below can replace the program and change this. The earlier dual_fill program listed below still runs the Crosscurrents created before the current one.
Both escrows store the same dualFillKey, the sha256 of the bytes ripples-dual-fill-v1, a zero byte and the canonical JSON of the record's core. The core names both creators, both factories or programs, the identity and terms hashes, the deadline and the nonce, so anyone holding the record recomputes the key and finds both escrows from it. The record id is the base64url of the key's first 16 bytes.
A creator calls createFill on the factory with the launch parameters, the dualFillKey, the target, the deadline, the public window's end, the open-alone choice and a fee budget. Contributors call depositEth where the side is held in ETH or the factory wraps ETH, deposit with an allowance where it takes a token, and withdraw while the side is filling. The opener, a key Ripples holds that the factory and each fill's terms name as KEEPER, calls open once the side is full, or abort to return a side that must open with the other to its contributors at once. Anyone calls claimFor and refundFor on anyone's behalf, returnFeeBudget once a side is refundable, and claimFees and handOverFees after it opens. Events: FillCreated, Deposited, Withdrawn, Filled, Cancelled, Aborted, Opened, Claimed, Refunded, FeeBudgetReturned, CreatorFeesForwarded and FeesHandedOver.
A creator calls create_fill, which moves the launch reserve into the vault. Contributors call contribute and withdraw; the creator can cancel a filling side before its deadline. The fill's opener, the Ripples key each fill names, calls open or abort. Anyone pays for claim, refund, return_reserve and close_fill. close_fill never closes the fill account itself, so no fill can be created twice at one address. Events: FillCreated, Contributed, Withdrawn, FillFull, FillCancelled, FillAborted, FillOpened, Claimed, Refunded, ReserveReturned and FillClosed.
The opener is the Crosscurrent opening key, a key Ripples holds: KEEPER on the factory and opener on each Solana fill. It can open a full side, abort a side that must open with the other into refunds, return launch deposits and forward creator fees. It cannot move contributions anywhere else or open a side that is not full.
A Crosscurrent with a collection is created through LinkedDualFillFactory.createFill, which takes the launch, collection and coupling parameters beside the fill's own terms and stores keccak256(abi.encode(tp, np, lp)) as PARAMS_HASH. open(tp, np, lp) creates the linked launch, its collection and the minters' vesting in one transaction, freezes the collection's metadata, and emits Opened then CollectionOpened. collection() and vesting() read zero until then. Every other call and event is DualFill's, with the same selectors. The fill stays the collection's owner and royalty receiver, and claimFees forwards what the collection pays it to the creator.
On Solana, create_linked_fill writes the fill with the Linked profile and a LinkedTerms account at the seeds linked and the fill. open_linked(collection_nonce) creates the launch, its collection and the opening buy in one instruction, and forward_income sends collection revenue and royalties that reached the fill's vault on to the creator. Events: LinkedFillCreated, LinkedOpened and IncomeForwarded.
Agents
An agent can create a Crosscurrent, join one, and claim or take back its money without a browser. It signs with its own keys, a Robinhood Chain wallet and a Solana keypair, and pays every network fee itself. Ripples never signs for it or holds its keys.
Every answer starts with statusLabel and summary. Quote them instead of describing status yourself.
import { readFile } from "node:fs/promises";
import { createDualFillClient, solanaSignerFromFile } from "@ripplesdotrun/agent-sdk";
import { privateKeyToAccount } from "viem/accounts";
const client = createDualFillClient({
network: "testnet",
evm: privateKeyToAccount(process.env.RIPPLES_PRIVATE_KEY),
solana: await solanaSignerFromFile(process.env.RIPPLES_SOLANA_KEYPAIR),
apiBase: process.env.RIPPLES_API_BASE,
site: process.env.RIPPLES_SITE,
});
const created = await client.create({
identity: {
name: "Example",
symbol: "EXMPL",
description: "What this coin is for.",
image: { bytes: await readFile("coin.png"), mime: "image/png" },
},
openingBuy: "0.01",
deadline: { minutes: 360 },
ifOneSideFills: "refund-both",
});
console.log(created.statusLabel, created.summary, created.url);{
"mcpServers": {
"ripples": {
"command": "npx",
"args": ["-y", "@ripplesdotrun/mcp"],
"env": {
"RIPPLES_NETWORK": "robinhood-testnet",
"RIPPLES_PRIVATE_KEY": "…",
"RIPPLES_SOLANA_KEYPAIR": "/path/to/keypair.json",
"RIPPLES_API_BASE": "…",
"RIPPLES_SITE": "…"
}
}
}
}