Skip to content

Launch a token

Deploy a launch token and its bonding curve through the Clank factory.

Updated View as Markdown

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.

launch-token.tsts
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.

launch-token.tsts
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.

launch-token.tsts
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.

launch-token.tsts
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.

launch-token.tsts
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.

launch-token.tsts
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.

launch-token.tsts
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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close