@ripplesdotrun/agent-sdk
v0.18.0
Published
Signs Ripples actions on Robinhood Chain from an agent's own keys: launch a token and its collection with a spending charter, trade it, collect what it earns. Does not run an agent: bring your own model loop, memory and policy.
Downloads
2,903
Maintainers
Readme
@ripplesdotrun/agent-sdk
This signs Ripples actions on Robinhood Chain from an agent's own keys: launch a token and its collection with a spending charter, trade it, collect what it earns, and leave its income to whoever holds an NFT. It does not run an agent. Bring your own model loop, memory and policy, and call a method here once that loop decides to act.
An agent launch is a token, an NFT collection, and a contract that holds the agent's money. The contract is the launch's creator, so the creator's share of every trade fee, and any creator charge the launch sets, is the agent's working capital rather than a founder's income. On mainnet the creator's share is 30% of the 1% trade fee, read from the launch factory this release pins. What the agent may do with its money is fixed before the first mint and nobody, including the person who created it, can raise those limits afterward.
npm install @ripplesdotrun/agent-sdk viemESM only, Node 20.18 or newer. Import it from an .mjs file, or from a package whose package.json sets
"type": "module": the examples here use top-level await, which runs only in ESM. The package brings its own
@solana/kit 8, so a project on another kit version keeps its own.
Launch one
import { privateKeyToAccount } from "viem/accounts";
import { createRipplesAgent } from "@ripplesdotrun/agent-sdk";
const agent = createRipplesAgent({
network: "mainnet",
account: privateKeyToAccount(process.env.KEY as `0x${string}`),
allowMainnet: true,
});
// The agent's own operator key, and an address it may pay. Both are yours to set.
const operatorAddress = privateKeyToAccount(process.env.OPERATOR_KEY as `0x${string}`).address;
const host = "0x1111111111111111111111111111111111111111";
const { treasury, token, market, collection } = await agent.launch({
runway: "0.05",
creatorChargePercent: 2, // optional: 2% of every trade, on top of the 1% trade fee, paid to the treasury
charter: {
operator: operatorAddress, // a key of its own, apart from the wallet creating the agent
dailySpend: "0.03",
perCallSpend: "0.01",
dailySell: "100000", // its own token, per day
dailyMint: 3,
payees: [host], // the addresses it may pay
},
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/",
},
});creatorChargePercent is an extra charge on every trade in the agent's own market, from 0 to 10 percent in steps of
0.01, paid in full to the treasury and claimed with the rest of its income. Traders pay it on top of the platform's 1%
trade fee, so a higher charge earns more per trade and invites fewer trades. It is fixed at launch, like the charter,
and is 0 when left out. A charge outside that range is refused as plan_invalid.
Two wallet requests: a spending permission for the runway, then the launch. The treasury, its runway and the launch all land in one transaction, so there is no state where the agent holds money and has no market.
The runway, the charter's spending limits and the piece price are in WETH (DEPLOYMENTS[network].quote), not ETH. On
mainnet, wrap ETH into WETH before the launch by sending it to that contract's deposit(); on the testnet, the same
contract's faucet() gives a wallet 1 WETH a day. Each launch is a new, permanent agent with a runway of its own, so a
client that already works a treasury, one made with treasury or one that has launched, refuses another launch as
already_launched. Make a new client, without treasury, for each launch.
launch(plan, { signal, onStep }) reads signal before each signature and stops there as cancelled once it aborts.
After the approval landed, that refusal carries it in completed, and a launch sent again uses it. Once the launch
itself is signed, it is followed to its receipt. onStep is told { step: "approve" }, where the wallet still has to
approve the runway, then { step: "launch" }, as each starts; a callback that throws does not stop the launch.
The whole plan is checked on this machine before the chain is read: the token's name is 1 to 32 bytes, its ticker 1 to
10 letters or digits, its image link at most 512 bytes, its description at most 2,048 bytes and each of its links at
most 256 bytes, each measured after trimming, and every amount a decimal string within its asset's 18 decimal places. An
agent's collection has a royalty of 0 and opens no later waves, and a collection.endTime, when given, is at least 10
minutes away, since the chain refuses a mint window that has closed by the time the launch lands. A plan that breaks a
rule is refused as plan_invalid.
The client then checks the runway covers the launch fee (runway_too_small; the pinned launch factory charges none, so
any runway above zero covers it) and the wallet holds it
(insufficient_funds, naming the chain), and simulates the launch as if the permission were already granted, all before
the first request is signed. Where the node takes no state override, or the token's allowance cannot be located, that
simulation is skipped and the launch is simulated after the permission instead; any other failure stops the launch
before the permission is asked for.
Run it
Only the operator key spends, so the agent runs from a client of its own, made with that key and the treasury.
const run = createRipplesAgent({
network: "mainnet",
account: privateKeyToAccount(process.env.OPERATOR_KEY as `0x${string}`),
treasury,
allowMainnet: true,
});
const bought = await run.buy("0.005"); // its own token, on its own market
await run.sell("1000"); // up to today's sell ceiling; a charter with none never sells
await run.mint(1); // from its own collection; the piece stays with the agent
await run.pay(host, "0.002"); // only an address the charter published
await run.note("shipped v2", "https://example.com/changelog"); // a dated entry in its public run log
await run.claim(); // the trade fees and the collection's share of every mint; any key may send it
await run.charter(); // the limits, which never change
await run.budget(); // what is left of today's, on the UTC day, and what the treasury holds
await run.owed(); // what claim() would take in now, read without signing
await run.activity(); // what it has done, newest first, over a block range
await run.outcome(bought); // what one transaction did to the treasury, read from its receiptA buy or sell with no minReceived, and a mint with no minRouted, gets a floor 1% under what
the market answers at that moment. Pass your own to set it yourself. A trade's deadlineSeconds, 300 by default, is
counted from the latest block's time, not this machine's clock. A buy in the market's first
seconds is refused as snipe_window, because the opening tax takes up to 99% of it then; the refusal says about how
many seconds are left.
activity reads back from toBlock 10,000 blocks at a time until it has limit entries, 50 by default, reaches
fromBlock, the last 50,000 blocks when none is given, or reads the treasury's launch where its factory made and
launched it in one transaction, since nothing is older. So fromBlock: 0n reads the whole history and stops at the
launch. signal stops the read before its next window, as cancelled. Through an endpoint that will not read 10,000
blocks at once, activity is refused as argument_invalid, naming the span.
outcome(hash) answers what one transaction did to the treasury, as the entries activity answers, read from its
receipt alone: an entry for each record it wrote there, an empty list for a transaction that wrote nothing on this
treasury, transaction_unconfirmed while it has no receipt, and transaction_reverted for one that reverted. Right
after a write, an endpoint whose nodes are a block apart may answer before it holds the receipt.
budget() answers what is left of each daily limit, balance, what the treasury holds in WETH, and tokens, the agent's
own tokens it holds. Every spend comes out of the balance and every sell out of the tokens, whatever is left of the
day's limits. owed() answers what claim() would take in now, as { quote, tokens }, without signing anything.
note publishes a hash of its text and the link beside it. The text itself stays with you, so
link to where it can be read. The text cannot be empty, and the link, when given, starts with https://, http://,
ipfs:// or ar:// and is at most 200 bytes; anything else is argument_invalid before anything is asked.
claimVested() takes in the token slice the agent's own mints earned it, once the market has graduated. A launch with
linked.objectClaim: true ties that slice to the pieces instead, and claimVestedObjects(tokenIds) claims it for the
pieces the treasury holds. capabilities() answers whether the treasury, or the factory before a launch, supports that
(objectVesting; where it does not, both are refused as capability_unsupported), and how this launch keys its vest
(vestKeyedToPieces: true for claimVestedObjects, false for claimVested, null when the treasury or its collection
cannot say). The other claim is refused in words that name the right one. Any key may send either claim.
setCollectionBaseUri(uri) points the collection at its artwork, with a trailing slash, and
freezeCollectionMetadata() fixes it for good. Both are the creator's calls: send them from the client that launched
the agent, or from one made with the creating wallet's key and treasury. From any other key, the operator's included,
the chain refuses them with NotCreator, as transaction_refused. launched() answers the launch's token, market,
collection and vesting.
A call only the operator may make (buy, sell, mint, pay, note) from another key is refused as not_operator
before the opening window or the market is asked anything. A zero amount is argument_invalid. Then, before anything is
simulated or signed:
- a buy or a payment is held to the charter's per-transaction limit (
over_call_spend) and to what is left of the day (over_daily_spend), both in WETH, and to what the treasury holds (insufficient_funds, naming the balance); a payment also to the charter's payees (not_payee, naming the address); - a sell is held to the charter (with no
dailySellthe agent can never sell, and above what is left of today's ceiling isover_daily_sell) and to the tokens the treasury holds (more_than_held).
A mint's limits are the contract's to hold, and a mint past one is refused when it is simulated, before anything is
signed: over_daily_mint in pieces, or insufficient_funds when the treasury holds too little to pay for it. A trade the
market would fill below its floor is transaction_refused, saying what the market would give against the floor.
claim() reads what the treasury is owed first. With nothing owed it sends nothing and is refused as
nothing_to_claim, and so are claimVested() and claimVestedObjects() with nothing vested.
Every failure is a RipplesAgentError with a stable code, typed as RipplesAgentErrorCode. Before the first read or
transaction on a treasury, the client checks that its reader is on the right chain (wrong_chain), that the Ripples
factory is the one it expects (deployment_mismatch) and that the treasury came from it (not_a_ripples_agent).
isRipplesAgent(treasury, { network, rpcUrl }) answers whether a pinned agent factory made an address, without a
client, and refuses an endpoint on another chain as wrong_chain. A treasury that is not an address, or whose capitals
do not match its checksum, is refused as address_invalid when the client is created; an all-lowercase address is
taken. A transaction sent and not seen to confirm is refused as transaction_unconfirmed, with its hash in
transaction: check it on the explorer before sending it again.
A call that sends several transactions (launch with its approval, claimFees, pushCreatorShare, collect,
pinWake, releaseWake, shareWake and releaseSplit) and stops after one of them has landed carries the ones that
landed in completed, in order, each with its hash and its kind: approve, a fee payout's quote or token, a
collect item's kind or a wake's step. The message names them and says what asking again does.
| Code | When |
|---|---|
| network_invalid | The network is not mainnet, testnet, robinhood or robinhood-testnet; a blank name is refused too, and only a missing one means mainnet |
| address_invalid | A treasury, or the address given to pay, is malformed or fails its checksum |
| argument_invalid | A method argument is wrong on its own, such as buy("-1"), buy("0"), mint(21) or an amount past what the chain can carry, a value the contract cannot take, a signal that is not an AbortSignal or an onStep that is not a function, a malformed outcome hash, a note with no text or a link it does not take, confirmations outside 1 to 64, an activity span the endpoint will not read or a block past the chain's head; or a client made with an option it does not take, or an rpcUrl that is not an http or https address |
| plan_invalid | Something in a launch plan breaks a rule |
| already_launched | launch on a client that already works a treasury |
| cancelled | A launch whose signal aborted before its next signature, or an activity read whose signal aborted |
| signer_missing, account_not_local | A write from a client with no account; an account that is not a local account (a key given as text, a wallet client), refused when the client is made |
| mainnet_not_allowed | A write on mainnet without allowMainnet: true |
| treasury_missing | A call that needs a treasury, before launch or without treasury |
| wrong_chain, deployment_mismatch, not_a_ripples_agent | The reader, the write endpoint, the factory or the treasury is not the one expected, or a node prepared a transaction for another chain; a pinned factory with no code, or another contract's, is deployment_mismatch |
| not_operator | A call only the operator may make, from another key |
| capability_unsupported | The factory or the treasury does not support claims by piece (linked.objectClaim, claimVestedObjects) |
| runway_too_small, insufficient_funds | The runway does not cover the launch fee, the wallet does not hold the runway in WETH, or the treasury does not hold what a buy, a payment or a mint spends |
| more_than_held | A sell above the tokens the treasury holds |
| snipe_window, over_daily_sell, nothing_to_receive | A trade in the market's first seconds, above today's sell ceiling, or too small to receive anything |
| over_call_spend, over_daily_spend, over_daily_mint, not_payee | Above the charter's per-transaction or daily spending limit, said in WETH, or its daily mint limit, or a payment to an address the charter does not name |
| nothing_to_claim | A claim with nothing owed, or a vest claim with nothing vested |
| transaction_refused | The chain or the node refused the call, another transaction from the same wallet took its nonce, or the node turned it away for another one waiting at its nonce, so it did not land; transaction carries its hash when the node turned it away or another transaction replaced it |
| transaction_reverted, receipt_unexpected | The transaction landed and failed, or landed without doing what was asked |
| transaction_unconfirmed | The transaction was sent and not seen to confirm within 180 seconds, or outcome found no receipt for it |
| endpoint_refused | The endpoint answered HTTP 401 or 403: check its key if it has one, or pass your own rpcUrl |
| chain_unavailable | The chain did not answer, or the endpoint is limiting requests (HTTP 429) |
The agent client, a launch link and a wake client write the same way. Writes go through transport or rpcUrl when
either is given, and through publicClient when only that is given, so a client handed a fork's reader prepares and
sends on the fork. A transaction is sent once, to one endpoint (a fallback transport's first), whatever retries the
transport carries. One client sends one transaction at a time, so calls made at once go out in turn with their own
nonces; give each key one client that writes. A wallet's speed-up of the same call counts as that call landing. Within
30 seconds of an approval it sent confirming, a client asks a refused call again, up to five times a second apart,
before it reports the refusal, since its node may still lack that approval; any other refusal is answered at once.
confirmations, one by default, is a whole number of blocks from 1 to 64, and anything else is refused as
argument_invalid when the client is made. A write is waited for up to 180 seconds, and one not seen to confirm by then
is transaction_unconfirmed. An endpoint that answers HTTP 401 or 403 turned the client away, and a call through it is
refused as endpoint_refused with the status, even when the body is a JSON-RPC error: check its key if it has one, or
pass your own rpcUrl. One that answers 429 is limiting requests, which is chain_unavailable saying so. A transport
of your own keeps its own HTTP handling. The public endpoints are shared and limit how often one client may ask, so an
agent in production passes its own rpcUrl.
Each client refuses, when it is made, an option it does not take (naming the one it takes where the spelling is close,
such as rpcUrl for rpc), an rpcUrl that is not an http or https address, and an account that is not a local
account, without repeating the value. An option set to undefined is not counted.
The charter
| Field | What it bounds |
|---|---|
| operator | The only key that can spend, and only through buy, sell, mint, pay and note |
| dailySpend | Buying, minting and paying together, in the market's asset |
| perCallSpend | One transaction. At most the daily figure, and zero only when that is zero |
| dailySell | Its own token, per day. Omit it and the agent can never sell |
| dailyMint | Pieces per day. Omit it and it never mints |
| payees | Up to eight addresses. Omit them and it can pay nobody |
A launch is permanent. The charter's limits and payees are fixed in the same transaction as the launch and cannot change afterward, by anyone, for any reason. There is no owner, no pause and no withdrawal on the contract. An operator can spend at most a day's limits, only on the agent's own market and the published payees. Treat the operator key like the funds it controls. Losing the operator key does not free the funds it controlled: it ends the agent.
The client takes any wallet as operator, the creating wallet included. Give the agent a key of its own, as the example
above does, and run it from that key.
Linking your own contracts to a launch
A launch's creator trading fee is paid by its hook to one address, and that address can be a contract of your own: a
game's treasury, a vault, a splitter. createLaunchLink works on any Ripples launch, whether or not this SDK created
it, and has no treasury of its own. Reads need no key.
import { createLaunchLink } from "@ripplesdotrun/agent-sdk";
import { privateKeyToAccount } from "viem/accounts";
const link = createLaunchLink({
network: "mainnet",
account: privateKeyToAccount(process.env.CREATOR_KEY as `0x${string}`),
allowMainnet: true,
});
const launchToken = "0x1111111111111111111111111111111111111111"; // the launch's token
const gameTreasury = "0x2222222222222222222222222222222222222222"; // your contract
const fees = await link.fees(launchToken); // who is paid, what they are owed, the holders' side if any
await link.setFeeRecipient(launchToken, gameTreasury); // one way: only the current recipient can move it
await link.claimFees(launchToken); // pays the recipient; anyone may send this
console.log(fees.recipient, fees.owed.quote);The fee is paid in the asset the market settles in. A market that settles in the chain's own ETH (nativeQuote on
what fees returns) pays a contract recipient by plain transfer, so the contract has to accept ETH, or its claims
revert and its balance stays owed on the hook until it does. Fees already earned stay owed to the address that earned
them: after a move, claimFees(token, earlier) still pays the earlier recipient. claimFees and pushCreatorShare
answer each payout's amount from the record the contract writes as it pays. Both are writes, so a client with no
account, or one on mainnet without allowMainnet, is refused before anything is read. A launch hook the record pins that
has no code at its address, or another contract's, is deployment_mismatch, as isRipplesAgent is for its factories.
setFeeRecipient takes a wallet or a contract of your own. It refuses as argument_invalid a distributor the shared
holder factory made, which routeHolderShare points the fee at once it has checked its terms, and one from a factory in
WITHDRAWN_HOLDER_FACTORIES; a distributor of the first-generation holder factory,
0x1CF47470213C2b22d1e72aD24aA977342FB85178, it takes.
Keeping a share of the creator fee for holders goes through the shared holder factory:
prepareHolderShare(token, { holderBps, creatorPayee, audience }) prepares a distributor that keeps holderBps of the
fee for the token's holders, or for the pieces of the launch's collection with audience: "pieces", and
routeHolderShare(token, distributor, terms) points the fee at it, once, for good. Only the launch's creator or its
current fee recipient can prepare one, and anyone else is refused as not_fee_recipient before anything is signed; a
launch already routed is refused as already_routed, whoever asks. A share for pieces on a launch made without a
collection is argument_invalid before anything is signed. The distributor is read again right before the route is
signed and refused when it is not the one prepared for this launch with the terms given.
Mainnet's record names the shared holder factory, 0x48eBd2b9d44663BfbBd5eD869ac577e802F75B7d, so both calls run there;
the testnet's names none, so there both are refused as holder_share_unavailable before anything is signed. A factory
in WITHDRAWN_HOLDER_FACTORIES is refused by address whatever a record says. A launch keeps its audience: "coin" and
audience: "pieces" preparations apart: with no distributor, routeHolderShare routes the newest preparation for
terms.audience, or the launch's only one when terms.audience is left out, and a launch that has both is refused as
argument_invalid until one is named. Where a launch already routes its fee to holders, fees names its distributor
under holders.
collect sends every claim the account page sends for the signing wallet, on the chain it names: the creator fee per
asset the hook owes it, a collection's proceeds where it is the creator, its vest where it minted, holder periods the
publisher could not push, and a paired launch's stock rewards. Each is read first and sent only with something to pay.
An item's amount, where it has one, is what the contract's record in the receipt says it paid.
Every page the API answers is read before the first claim is sent. A holder period is claimed only from a distributor
that a holder factory this package pins made; any other distributor is listed in skipped with the reason, and so is a
claim whose figures cannot be read or that a contract refuses to answer. Past 1,000 launches, or ten pages of holder
periods, the rest are noted in skipped; name further launches in launches. A launch whose hook or collection cannot
be read stops collect, with what it already sent in completed.
const collected = await link.collect({ chain: "robinhood" });
for (const item of collected.sent) console.log(item.kind, item.contract, item.hash);
for (const skip of collected.skipped) console.log(skip.kind, skip.reason);collect is also a top-level call, collect({ chain: "robinhood", account, allowMainnet: true }), which makes the link
for the chain named. The wallet's launches and the rewards published for it come from the Ripples API, the public one on
mainnet; the testnet has none, so collect({ chain: "robinhood-testnet", launches: [token] }) names the launches and
skips the rewards, saying so. launches works on mainnet too, for a minter collecting a vest on a launch that is not
theirs. A link's own collect refuses a chain that names the other network as wrong_chain, and one it does not know
as argument_invalid. collect called with no options is refused as argument_invalid. Its answer names the network
in chain: robinhood on mainnet, robinhood-testnet on the testnet.
| Code | When |
|---|---|
| network_invalid | The network is not mainnet, testnet, robinhood or robinhood-testnet, a blank name included |
| address_invalid, argument_invalid | An address is malformed or fails its checksum, or an argument breaks a rule: a holder distributor or the zero address as a fee recipient, a share's holderBps or audience, a distributor that is not the one prepared for this launch with these terms, or a chain or launches that collect cannot take, or no options at all |
| signer_missing, account_not_local, mainnet_not_allowed | As for the agent client |
| wrong_chain | The reader or the write endpoint is not on the link's chain, or a link's collect names the other network |
| launch_unknown | The token has no pool on any hook the network's record names |
| not_fee_recipient | A write from an account that is not the launch's current creator-fee recipient; prepareHolderShare also takes the launch's creator |
| already_routed | A share prepared or routed for a launch whose fee already goes to holders, said before who is asking |
| holder_share_unavailable | A holder share on a network that does not offer one (the testnet), or whose record names a factory in WITHDRAWN_HOLDER_FACTORIES or one that does not answer liveDistributorOf |
| api_unavailable | The Ripples API did not answer collect |
| receipt_unexpected, transaction_refused, transaction_reverted, transaction_unconfirmed, endpoint_refused, chain_unavailable | As for the agent client |
Wakes
A piece leaves a wake when a market is launched from it: the market's creator income goes to whoever holds the piece,
and when the piece is sold, the income goes with it. The income is paid by a contract with no owner that reads the
piece's holder at the moment it pays. createWakeClient works with pieces and launches on Robinhood Chain, and reads
need no key. A client made with chain: "base" reads and releases a wake on Base, and refuses bindToken, pinWake and
shareWake as robinhood_only, since new launches run on Robinhood Chain.
import { createWakeClient, wakeLaunchUrl } from "@ripplesdotrun/agent-sdk";
import { privateKeyToAccount } from "viem/accounts";
const wakes = createWakeClient({
chain: "robinhood",
account: privateKeyToAccount(process.env.HOLDER_KEY as `0x${string}`),
allowMainnet: true,
});
const piece = { nft: "0x2222222222222222222222222222222222222222", tokenId: 1n }; // an ERC-721 you hold
const { agentLink } = await wakes.bindToken(piece); // the piece becomes an agent its holder controls
console.log(wakeLaunchUrl(agentLink)); // launch a market from it on ripples.run
const launchToken = "0x1111111111111111111111111111111111111111";
await wakes.pinWake({ ...piece, token: launchToken }); // a launch you made pays the piece from now on
const wake = await wakes.wake(piece); // owed now, paid so far, the last payout
await wakes.releaseWake(piece); // pays everything owed to the holder; anyone may send it
console.log(wake?.owed, wake?.paid, wake?.lastPayout);This SDK does not launch a plain token from a wallet, so a new market that leaves a wake starts on ripples.run from
wakeLaunchUrl, which opens the launch on Robinhood Chain; a link to an agent on Base is refused as robinhood_only. A launch you already made is pointed at a piece with
pinWake, which creates the piece's payee where it does not exist yet. That cannot be undone: the hook lets only the
current recipient move a launch's income, and the payee never moves it.
wake reads what is owed from the hook and the payee when it is called, and the markets, payouts and the piece's name
and picture from the Ripples API. Without the API, paid and lastPayout are null and launches names the markets to
count. What the hook owes the payee in ETH and WETH on Robinhood Chain, or WETH on Base, is always read and released; a
holder share's creator part, and income in any other currency, are counted and released only for a market the API lists
or launches names. One call reads up to 256 listed markets, 16 at a time, and marketsOmitted says how many more were
listed.
A piece paid through a split names its part in split (the split, the piece's place in it, its weight and the total
weight) once the chain confirms it, and wakeOfSplit(wake.split.split) reads what that part is owed. wake finds the
split through the Ripples API: where the API does not list it, split is null, and a piece paid only through a split
answers null. wakeOfSplit(split) reads a split you know from the chain.
Before anything is signed, a token the signer does not hold is refused as not_holder, a piece with no holder on the
client's chain as nft_not_on_chain, a piece held by its own payee as argument_invalid, and a launch the signer
cannot pin as not_fee_recipient, already_routed or launch_unknown. releaseWake brings any holder share that owes
the payee up to date, then releases each currency. A step the chain refuses before anything is signed for it, such as a
currency the holder cannot take or a share's push, is listed in skipped with the reason, and the rest of the release
goes ahead. A sync or push reports the amount the share's own record says it moved.
bindToken builds the token's default registration file on apiBase, which has to be an https address; pass
agentURI for any other.
A launch can also leave its wake to many pieces at once. shareWake creates a split for up to 256 pieces and points
the launch's creator income at it, for good. Each piece is owed its part, one part unless you give it a weight, and
its part is paid to whoever holds the piece when it is paid out.
const shared = await wakes.shareWake({
token: "0x3333333333333333333333333333333333333333", // another launch you made
pieces: [
{ nft: "0x2222222222222222222222222222222222222222", tokenId: 1n },
{ nft: "0x2222222222222222222222222222222222222222", tokenId: 2n, weight: 2 },
],
});
const split = await wakes.wakeOfSplit(shared.split); // each piece: owed now, paid so far, holder
const { released, failed } = await wakes.releaseSplit({ split: shared.split }); // anyone may send it
console.log(split.pieces, released, failed);Pieces can be named in any order, and splitOf answers where a set's split is before it exists. shareWake refuses a
piece the split itself would hold as argument_invalid. releaseSplit pays at most 64 pieces a transaction. A piece it
cannot pay, because nobody holds it or its holder turns the payment away, is listed in failed and keeps its part
until it can be paid; a holder share's sync or push the chain refuses is listed in skipped. A release that stops
part way carries each part that landed in completed, with the pieces it paid and could not.
| Code | When |
|---|---|
| network_invalid | chain is not robinhood, base, 4663 or 8453 |
| robinhood_only | bindToken, pinWake or shareWake on Base, or wakeLaunchUrl for an agent on Base |
| address_invalid, argument_invalid | An address, token id, weight, index or link is malformed, a piece is named twice or held by its own payee or split, more than 256 pieces or 50 launches are named, or apiBase is not https where it builds the registration file |
| signer_missing, account_not_local, mainnet_not_allowed | As for the agent client; both chains are mainnets, so every write needs allowMainnet: true |
| wrong_chain | The reader or the write endpoint is not on the client's chain |
| not_holder, nft_not_on_chain | A token the signer does not hold, or a piece with no holder on the client's chain |
| launch_unknown, not_fee_recipient, already_routed | A token that is not a launch on the chain's quote-fee hook, a signer that is not its creator-fee recipient, or a launch whose income already follows a piece, pieces or holders |
| split_unknown | An address that is not a split the chain's wake split factory made |
| receipt_unexpected, transaction_refused, transaction_reverted, transaction_unconfirmed, endpoint_refused, chain_unavailable | As for the agent client |
Crosscurrents already open
New launches on Ripples run on Robinhood Chain, so the Crosscurrent clients start nothing new: create and
createAgent on createDualFillClient, and launch on createBridgedClient, are refused as robinhood_only before
anything is asked or signed. A Crosscurrent already open is read, joined and settled here as before, on every chain it
runs on.
import { privateKeyToAccount } from "viem/accounts";
import { createDualFillClient, solanaSignerFromFile } from "@ripplesdotrun/agent-sdk";
const client = createDualFillClient({
network: "mainnet",
allowMainnet: true,
evm: privateKeyToAccount(process.env.RIPPLES_PRIVATE_KEY as `0x${string}`),
solana: await solanaSignerFromFile("/path/to/keypair.json"),
});
const id = "4xP2gcyOH_D6jDfg3eebmg"; // the last part of its ripples.run/crosscurrent link
await client.status({ id }); // the page's words, what was verified on each chain, and this wallet's positions
await client.join({ id, side: "solana", amount: "0.05" });
await client.withdraw({ id, side: "solana" });
await client.claim({ id }); // every side, once it is open
await client.refund({ id }); // every side, once it cannot open
await client.takeBackDeposit({ id }); // each side's launch deposit, once it opens or can no longer open
await client.collectFees({ id }); // the creator's trade income on the EVM side, once it opens
await client.positions(); // every Crosscurrent either wallet has money inA client made without wallets reads and cannot sign. resume(receipt) finishes a create that stopped part way, from the
receipt its onReceipt saved, and cancelSetup({ id }) lets go of one still being set up. An agent Crosscurrent's
operator buys, sells, pays and writes notes through client.agent({ id }), and its creator stops it with stopAgent and
takes back a side that did not open with returnAgentMoney. On createBridgedClient, resume(assetId) finishes a
launch that was reserved and not sent, and progress, waitUntilLive and nameSolanaPayout work on any launch already
made.
Every refusal is a RipplesDualFillError with a stable code, typed as DualFillErrorCode, and a summary to show as
it is; the one-wallet client's is a BridgedRefusedError. Every answer starts with statusLabel and summary, the
words ripples.run shows for the same Crosscurrent.
Networks for single-chain agents
createRipplesAgent takes network: "mainnet" (Robinhood Chain, 4663), its default, or "testnet" (Robinhood Chain
testnet, 46630). Both settle in WETH. Any other name, a blank one included, is refused as network_invalid when the
client is created; only a missing name means mainnet.
Signing on mainnet needs allowMainnet: true, and without it a transaction is refused as mainnet_not_allowed. Reads
need nothing. account has to be a local account, such as one from privateKeyToAccount, so the client knows each
transaction's hash before it is sent. No refusal carries the path or query of a configured RPC address.
Read the contract-level reference at https://ripples.run/docs#rh-agents.
Pinned addresses
DEPLOYMENTS[network] holds the single-chain agent factory, token launch factory and settlement asset this client pins
for "mainnet" and "testnet", with the launch hooks a launch link asks for a token's pool and the holder reward
factories it reads. DUAL_FILL_NETWORKS[network] holds the Crosscurrent contracts and Solana programs for the same two,
including each Solana program's code hash and upgrade authority, and each EVM chain's earlier fill factories in
legacyFills, whose Crosscurrents are still read, joined, claimed and refunded. WAKE_CHAINS[chain] holds the adapter,
payee factory, split factory, hook and holder factory a wake client uses on Robinhood Chain and Base.
All three are frozen and exported, so a host application, or anything auditing one, can read the exact addresses this
build signs against without digging through source. LINKED_DEFAULTS and LINKED_RESERVES, what a launch sends where
its plan names nothing else, are frozen too. DEPLOYMENTS also answers to robinhood and robinhood-testnet;
DUAL_FILL_NETWORKS is keyed mainnet and testnet alone. SECURITY.md lists every one of these addresses, the
devnet programs' code hashes aside, next to how to verify a pin yourself, and states what this package can and cannot
protect you from.
Building from a checkout
Run npm install, then npm test, which builds dist/ first. The repository commits no lock file, so use
npm install rather than npm ci. Outside the monorepo, the tests that read its contract and program records are
skipped.
Licence
MIT
