---
title: "Launch a token"
description: "Deploy a launch token and its bonding curve through the Clank factory."
---

> Documentation Index
> Fetch the complete documentation index at: https://developer.clank.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Launch a token

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](https://viem.sh/) 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`.

```ts title="launch-token.ts"
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.

```ts title="launch-token.ts"
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.

```ts title="launch-token.ts"
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.

```ts title="launch-token.ts"
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](/guides/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.

```ts title="launch-token.ts"
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.

```ts title="launch-token.ts"
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,
});
```

> **Refresh before signing**
>
> Configuration can change between the displayed quote and wallet confirmation.
> Re-run `prepareLaunch` and the quote when the account, chain, pair token,
> launch config, metadata, salt, amount, or slippage changes. The pinned
> economics and final simulation make a stale transaction revert rather than
> launch with different terms.

## 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.

```ts title="launch-token.ts"
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](/concepts/token-lifecycle) for how the remaining curve supply
progresses into permanently locked Uniswap V4 liquidity.

Source: https://developer.clank.trade/guides/launch-a-token/index.mdx
