Launch through the canonical factory to create the ERC-20 launchToken and its
bonding curve in one transaction. You can launch without buying, or use the
factory’s current forwarder to launch and make an opening buy atomically.
Launch only
Call launchToken on the factory. No pair-token approval is needed because
the launch does not spend the pair token.
Launch and buy
Resolve the factory’s forwarder and call launchAndBuy to create the token
and execute its first curve buy in one transaction.
Current Robinhood deployment
| Setting | Current value |
|---|---|
| Chain ID | 4663 |
| Factory | 0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2 |
| Launch config ID | 0 |
| Native pair token | Zero address |
| Launch fee | Free |
Read the configuration and forwarder from the factory at runtime. Factory settings can change for future launches, while an existing curve keeps the economics snapshotted when it was created.
Prerequisites
This guide uses Viem and assumes your app already has a
Robinhood Chain publicClient and a connected walletClient.
The code blocks below are sections of the same launch-token.ts module. Choose
either Launch only or Launch and buy; do not execute both with the same
parameters and salt.
Define the launch
Use the zero address for a native-ETH pairToken, or an ERC-20 address approved
by the factory. The current factory requires creatorTaxBps to be 0 and
buybackEnabled to be false.
import {
erc20Abi,
keccak256,
parseAbi,
parseEther,
parseUnits,
toBytes,
zeroAddress,
type Address,
type PublicClient,
type WalletClient,
} from "viem";
const ROBINHOOD_CHAIN_ID = 4663;
const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2";
const LAUNCH_CONFIG_ID = 0n;
const factoryAbi = parseAbi([
"function canLaunch(address launcher) view returns (bool)",
"function getLaunchConfig(uint256 id) view returns ((uint256 supply,uint256 curveFeeBps,uint256 phantomQuote,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,bool enabled))",
"function approvedPairTokens(address pairToken) view returns (bool)",
"function previewLaunchEconomics(uint256 launchConfigId,address pairToken) view returns (bytes32)",
"function launchFee() view returns (uint256)",
"function launchForwarder() view returns (address)",
"function predictLaunch(address creator,(string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken) view returns (address launchToken,address curve,bytes32 launchId)",
"function launchToken((string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken) payable returns (address launchToken,address curve)",
"function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)",
]);
const launchAndBuyAbi = parseAbi([
"function factory() view returns (address)",
"function launchAndBuy((string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken,uint256 pairTokenIn,uint256 minLaunchTokensOut,address recipient,address[] snipeTaxExemptions) payable returns (address launchToken,address curve,uint256 launchTokensOut)",
]);
const pairToken: Address = zeroAddress;
const launchDetails = {
name: "Example Token",
symbol: "EXAMPLE",
logo: "https://example.com/token.png",
description: "An example token launched with Clank V2.",
socials: {
twitter: "",
telegram: "",
discord: "",
website: "https://example.com",
farcaster: "",
},
creatorTaxBps: 0,
buybackEnabled: false,
// Change this value before retrying an identical launch.
salt: keccak256(toBytes("example-token-v1")),
} as const;Metadata limits are measured in UTF-8 bytes, not characters.
| Field | Requirement |
|---|---|
name |
1–64 bytes |
symbol |
1–16 bytes |
logo |
1–256 bytes |
description |
Up to 1,024 bytes |
| Each social link | Up to 256 bytes |
creatorTaxBps |
Must be 0 |
buybackEnabled |
Must be false |
Validate the config and predict addresses
Read previewLaunchEconomics immediately before prediction and pass its result
back in expectedEconomics. This pins the terms so an intervening factory
configuration update makes the transaction revert instead of silently changing
the launch.
const prepareLaunch = async ({
publicClient,
walletClient,
}: {
publicClient: PublicClient;
walletClient: WalletClient;
}) => {
const [[creator], walletChainId, rpcChainId] = await Promise.all([
walletClient.getAddresses(),
walletClient.getChainId(),
publicClient.getChainId(),
]);
if (!creator) throw new Error("Connect a wallet before launching");
if (
walletChainId !== ROBINHOOD_CHAIN_ID ||
rpcChainId !== ROBINHOOD_CHAIN_ID
) {
throw new Error("Connect the wallet and RPC to Robinhood Chain");
}
const [config, canLaunch, pairTokenApproved, expectedEconomics, launchFee] =
await Promise.all([
publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "getLaunchConfig",
args: [LAUNCH_CONFIG_ID],
}),
publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "canLaunch",
args: [creator],
}),
pairToken === zeroAddress
? Promise.resolve(true)
: publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "approvedPairTokens",
args: [pairToken],
}),
publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "previewLaunchEconomics",
args: [LAUNCH_CONFIG_ID, pairToken],
}),
publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "launchFee",
}),
]);
if (!canLaunch) throw new Error("New launches are currently disabled");
if (!config.enabled) throw new Error("The launch config is disabled");
if (!pairTokenApproved) throw new Error("The pair token is not approved");
const params = {
...launchDetails,
creatorFeeRecipient: creator,
expectedEconomics,
} as const;
const [launchToken, curve, launchId] = await publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "predictLaunch",
args: [creator, params, LAUNCH_CONFIG_ID, pairToken],
});
const [launchTokenCode, curveCode] = await Promise.all([
publicClient.getCode({ address: launchToken }),
publicClient.getCode({ address: curve }),
]);
if (
(launchTokenCode && launchTokenCode !== "0x") ||
(curveCode && curveCode !== "0x")
) {
throw new Error("This launch already exists; change the salt");
}
return {
creator,
params,
pairToken,
launchFee,
launchToken,
curve,
launchId,
} as const;
};
const launchPlan = await prepareLaunch({ publicClient, walletClient });predictLaunch is deterministic for the creator, metadata, pinned economics,
and salt. You can display the addresses before asking the user to sign, but
always confirm them from the successful transaction and factory record.
Launch only
Simulate the exact call before opening the wallet. Viem returns the predicted addresses from the simulation, then reuses the validated request for the write.
const executeLaunch = async ({
publicClient,
walletClient,
}: {
publicClient: PublicClient;
walletClient: WalletClient;
}) => {
const { request, result } = await publicClient.simulateContract({
account: launchPlan.creator,
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "launchToken",
args: [launchPlan.params, LAUNCH_CONFIG_ID, launchPlan.pairToken],
value: launchPlan.launchFee,
});
const [launchToken, curve] = result;
if (
launchToken.toLowerCase() !== launchPlan.launchToken.toLowerCase() ||
curve.toLowerCase() !== launchPlan.curve.toLowerCase()
) {
throw new Error("Simulated launch addresses changed");
}
const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Launch reverted");
return { launchToken, curve, hash, receipt } as const;
};
const launched = await executeLaunch({ publicClient, walletClient });No ERC-20 approval is necessary for a launch-only transaction. The pairToken
selects the curve’s denomination; the factory does not spend it during launch.
Verify the launch
Read the canonical record after confirmation instead of trusting an address returned by a wallet or external API.
const launchRecord = await publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "getLaunchedToken",
args: [launched.launchToken],
});
if (!launchRecord.exists) throw new Error("Factory launch record not found");
if (launchRecord.curve.toLowerCase() !== launched.curve.toLowerCase()) {
throw new Error("Factory returned a different curve");
}The token begins in the curve’s Trading state. From here, use
Trade on the bonding curve to quote and
execute buys or sells.
Launch and buy in one transaction
The atomic path uses the forwarder currently registered by the factory. The
quote below is a read-only simulation with minLaunchTokensOut set to zero. It
returns the opening buy output without deploying anything. Execution then
applies slippage and simulates the protected call again before submission.
const quoteLaunchAndBuy = async ({
publicClient,
pairTokenIn,
}: {
publicClient: PublicClient;
pairTokenIn: bigint;
}) => {
if (pairTokenIn <= 0n) throw new Error("Enter an opening buy amount");
const launchForwarder = await publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "launchForwarder",
});
if (launchForwarder === zeroAddress) {
throw new Error("Launch-and-buy is not configured");
}
const forwarderFactory = await publicClient.readContract({
address: launchForwarder,
abi: launchAndBuyAbi,
functionName: "factory",
});
if (forwarderFactory.toLowerCase() !== CLANK_FACTORY.toLowerCase()) {
throw new Error("The launch forwarder belongs to another factory");
}
const value =
launchPlan.launchFee +
(launchPlan.pairToken === zeroAddress ? pairTokenIn : 0n);
const { result } = await publicClient.simulateContract({
account: launchPlan.creator,
address: launchForwarder,
abi: launchAndBuyAbi,
functionName: "launchAndBuy",
args: [
launchPlan.params,
LAUNCH_CONFIG_ID,
launchPlan.pairToken,
pairTokenIn,
0n,
launchPlan.creator,
[],
],
value,
});
const [launchToken, curve, launchTokensOut] = result;
if (
launchToken.toLowerCase() !== launchPlan.launchToken.toLowerCase() ||
curve.toLowerCase() !== launchPlan.curve.toLowerCase()
) {
throw new Error("Simulated launch addresses changed");
}
return {
launchForwarder,
launchToken,
curve,
launchTokensOut,
pairTokenIn,
value,
} as const;
};
const openingQuote = await quoteLaunchAndBuy({
publicClient,
pairTokenIn: parseEther("0.1"),
});Display openingQuote.launchTokensOut before asking for confirmation. When the
user accepts it, apply their chosen slippage tolerance and execute the exact
protected call.
const executeLaunchAndBuy = async ({
publicClient,
walletClient,
slippageBps = 100n,
}: {
publicClient: PublicClient;
walletClient: WalletClient;
slippageBps?: bigint;
}) => {
if (slippageBps < 0n || slippageBps > 10_000n) {
throw new Error("Slippage must be between 0 and 10,000 basis points");
}
const minLaunchTokensOut =
(openingQuote.launchTokensOut * (10_000n - slippageBps)) / 10_000n;
const { request, result } = await publicClient.simulateContract({
account: launchPlan.creator,
address: openingQuote.launchForwarder,
abi: launchAndBuyAbi,
functionName: "launchAndBuy",
args: [
launchPlan.params,
LAUNCH_CONFIG_ID,
launchPlan.pairToken,
openingQuote.pairTokenIn,
minLaunchTokensOut,
launchPlan.creator,
[],
],
value: openingQuote.value,
});
const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Launch and buy reverted");
const [launchToken, curve, launchTokensOut] = result;
return { launchToken, curve, launchTokensOut, hash, receipt } as const;
};
const launchedAndBought = await executeLaunchAndBuy({
publicClient,
walletClient,
});Use an ERC-20 pair token
For launch-only, set pairToken to an approved ERC-20 and follow the same flow;
no approval is needed. For an atomic opening buy, approve the resolved forwarder
for pairTokenIn before calling quoteLaunchAndBuy, because its simulation
executes the token transfer.
const pairTokenDecimals = await publicClient.readContract({
address: pairToken,
abi: erc20Abi,
functionName: "decimals",
});
const pairTokenIn = parseUnits("100", pairTokenDecimals);
const launchForwarder = await publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "launchForwarder",
});
const allowance = await publicClient.readContract({
address: pairToken,
abi: erc20Abi,
functionName: "allowance",
args: [launchPlan.creator, launchForwarder],
});
if (allowance < pairTokenIn) {
const { request } = await publicClient.simulateContract({
account: launchPlan.creator,
address: pairToken,
abi: erc20Abi,
functionName: "approve",
args: [launchForwarder, pairTokenIn],
});
const approvalHash = await walletClient.writeContract(request);
const approvalReceipt = await publicClient.waitForTransactionReceipt({
hash: approvalHash,
});
if (approvalReceipt.status !== "success") {
throw new Error("Pair-token approval reverted");
}
}Then pass pairTokenIn to quoteLaunchAndBuy. For ERC-20 launches, the atomic
call sends only launchFee as native value; the forwarder pulls the approved
pair tokens from the creator.
Common launch failures
| Error | What to check |
|---|---|
InvalidMetadata |
Required fields and UTF-8 byte limits. |
LaunchEconomicsMismatch |
Refresh the config and expectedEconomics. |
LaunchAlreadyExists |
Change the salt or other committed launch details. |
LaunchConfigDisabled |
Select an enabled factory config. |
PairTokenNotApproved |
Use native ETH or a currently approved ERC-20. |
UnsupportedCreatorTax |
Set creatorTaxBps to 0. |
UnsupportedBuyback |
Set buybackEnabled to false. |
At creation, the curve receives the full launch-token supply. An atomic opening buy immediately transfers its purchased tokens to the recipient. See Token lifecycle for how the remaining curve supply progresses into permanently locked Uniswap V4 liquidity.