Skip to main content
In this guide, you’ll launch a token on Robinhood Chain by calling BagsFactory directly. A single transaction deploys the token, its bonding curve, and its fee-share contract, mints the fixed supply to the curve, and registers everything on-chain. You can optionally include an atomic initial buy in the same transaction.

Prerequisites

Before starting, make sure you have:
  • Completed the Environment Setup (chain.ts, addresses.ts, clients.ts, and the ABIs).
  • Read the Overview for the token lifecycle and fee model.
  • ETH in your wallet to cover the creation fee (currently 0 ETH), any initial buy, and gas.

1. How Launching Works

BagsFactory exposes two launch entry points:
FunctionUse whenvalue to send
createNo initial buycreationFee (surplus refunded)
createAndBuyYou want an atomic first buycreationFee + initialBuyWei
Both take the same arguments:
function create(
    string name,
    string symbol,
    string metadataURI,
    address partner,       // address(0) for none
    uint16 partnerBps,     // 0 when no partner
    address[] claimers,    // fee-share recipients
    uint16[] bps           // parallel to claimers; must sum to 10000
) external payable returns (address token, address curve);
With createAndBuy, the factory spends the entire surplus above creationFee on an atomic buyFor(msg.sender, 1) — there is no front-run window, and if the buy would cross the graduation threshold the excess is auto-refunded.
The returned token and curve addresses, plus the feeShare and poolId, must be read from the TokenCreated event in the receipt. Clone addresses are not predictable before the launch.

2. Fee Claimers Rules

The claimers / bps arrays configure who earns the creator side (1%) of trading fees. The factory enforces:
  1. 1 to 100 claimers, with claimers.length == bps.length.
  2. Each claimer address is valid, non-zero, and unique, and not equal to the partner.
  3. Each bps is a uint16, and all bps sum to exactly 10000 (100%). The factory enforces only the sum — individual shares of 0 are accepted on-chain. The client validator below additionally requires each share to be >= 1 as a sensible default (a 0-bps claimer would never earn).
The simplest setup is a single claimer (the creator) at 10000 bps. Validate client-side before signing — an on-chain revert still costs gas.
When splitting a percentage across claimers, have the last claimer absorb any rounding remainder so the total lands on exactly 10000.

3. Prepare Metadata

metadataURI should point to a JSON document describing the token (name, symbol, description, image, socials), typically hosted on IPFS. Upload it however you like and pass the resulting URI (e.g. ipfs://...). This guide assumes you already have a metadataURI.

4. The Launch Script

Save this as launch-token.ts. It validates claimers, reads the live creation fee, simulates, sends, and parses the receipt.
launch-token.ts
import "dotenv/config";
import {
  getAddress,
  isAddress,
  parseEther,
  parseEventLogs,
  zeroAddress,
  type Address,
} from "viem";
import { publicClient, getWalletClient } from "./clients";
import { ROBINHOOD_LAUNCHPAD } from "./addresses";
import { bagsFactoryAbi, bagsBondingCurveAbi } from "./abi";

const TOTAL_BPS = 10_000;
const MAX_CLAIMERS = 100;

interface ClaimerAllocation {
  address: Address;
  bps: number;
}

/** Client-side mirror of the factory's claimer rules. Returns an error string, or null when valid. */
function validateClaimers(claimers: ClaimerAllocation[], partner?: Address): string | null {
  if (claimers.length === 0) return "Add at least one fee claimer.";
  if (claimers.length > MAX_CLAIMERS) return `At most ${MAX_CLAIMERS} claimers.`;

  // Only reject claimers matching a *real* partner — zeroAddress means "no partner".
  const partnerKey =
    partner && partner.toLowerCase() !== zeroAddress ? partner.toLowerCase() : null;

  const seen = new Set<string>();
  for (const c of claimers) {
    if (!isAddress(c.address)) return `"${c.address}" is not a valid address.`;
    const key = c.address.toLowerCase();
    if (key === zeroAddress) return "The zero address cannot be a claimer.";
    if (partnerKey && key === partnerKey) return "The partner cannot also be a claimer.";
    if (seen.has(key)) return "Claimer addresses must be unique.";
    seen.add(key);
    if (!Number.isInteger(c.bps) || c.bps < 1 || c.bps > TOTAL_BPS)
      return "Each claimer needs a whole bps share between 1 and 10000.";
  }
  const total = claimers.reduce((sum, c) => sum + c.bps, 0);
  if (total !== TOTAL_BPS)
    return `bps must sum to exactly 10000 (currently ${total}).`;
  return null;
}

async function launchToken(params: {
  name: string;
  symbol: string;
  metadataURI: string;
  claimers: ClaimerAllocation[];
  /** Optional partner that earns `partnerBps` of the creator side before claimers split the rest. */
  partner?: Address;
  partnerBps?: number;
  /** ETH spent on an atomic initial buy. Use 0n for no buy. */
  initialBuyEth?: bigint;
}) {
  const { name, symbol, metadataURI, claimers } = params;
  const initialBuyWei = params.initialBuyEth ?? 0n;
  const partner = params.partner ?? zeroAddress;
  const partnerBps = params.partnerBps ?? 0;

  const claimersError = validateClaimers(claimers, partner);
  if (claimersError) throw new Error(claimersError);

  const walletClient = getWalletClient();
  const account = walletClient.account;

  // The creation fee is owner-settable — read it live right before signing.
  const creationFee = await publicClient.readContract({
    address: ROBINHOOD_LAUNCHPAD.factory,
    abi: bagsFactoryAbi,
    functionName: "creationFee",
  });

  // partner defaults to address(0) (no partner) unless one is passed in.
  const args = [
    name,
    symbol,
    metadataURI,
    partner,
    partnerBps,
    claimers.map((c) => getAddress(c.address)),
    claimers.map((c) => c.bps),
  ] as const;

  const value = creationFee + initialBuyWei;
  const functionName = initialBuyWei > 0n ? "createAndBuy" : "create";

  console.log(`Launching $${symbol} via ${functionName} (fee ${creationFee} wei)...`);

  // Simulate first so reverts surface as decoded custom errors before signing.
  const { request } = await publicClient.simulateContract({
    account,
    address: ROBINHOOD_LAUNCHPAD.factory,
    abi: bagsFactoryAbi,
    functionName,
    args,
    value,
  });

  const txHash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
  if (receipt.status !== "success") {
    throw new Error("Launch transaction reverted on-chain. No token was created.");
  }

  // Clone addresses only exist in the TokenCreated event.
  const [created] = parseEventLogs({
    abi: bagsFactoryAbi,
    logs: receipt.logs,
    eventName: "TokenCreated",
  });
  if (!created) throw new Error("Launch confirmed but no TokenCreated event found.");

  // A large enough initial buy can graduate the token in the same tx.
  const migratedLogs = parseEventLogs({
    abi: bagsBondingCurveAbi,
    logs: receipt.logs,
    eventName: "Migrated",
  });

  const result = {
    txHash,
    token: created.args.token as Address,
    curve: created.args.curve as Address,
    feeShare: created.args.feeShare as Address,
    poolId: created.args.poolId as `0x${string}`,
    migrated: migratedLogs.length > 0,
  };

  console.log("Token launched!");
  console.log("  token   :", result.token);
  console.log("  curve   :", result.curve);
  console.log("  feeShare:", result.feeShare);
  console.log("  poolId  :", result.poolId);
  console.log("  migrated:", result.migrated);
  console.log(`  explorer: https://robinhoodchain.blockscout.com/tx/${txHash}`);
  return result;
}

// Example: launch with all fees to the creator and a small initial buy.
launchToken({
  name: "My Token",
  symbol: "MTK",
  metadataURI: "ipfs://YOUR_METADATA_CID",
  claimers: [
    { address: "0xYOUR_WALLET_ADDRESS", bps: 10000 }, // creator gets 100%
  ],
  initialBuyEth: parseEther("0.01"),
}).catch(console.error);

5. Run Your Script

npx ts-node launch-token.ts
On success you’ll see the token, curve, feeShare, and poolId addresses. Save the token address — it’s the identifier you’ll use everywhere else.

6. Splitting Fees Among Multiple Claimers

To share the creator side with additional wallets, list each with its bps. The example below gives the creator 50%, and two collaborators 30% and 20%:
launchToken({
  name: "Shared Token",
  symbol: "SHARE",
  metadataURI: "ipfs://YOUR_METADATA_CID",
  claimers: [
    { address: "0xCREATOR", bps: 5000 },
    { address: "0xCOLLAB_ONE", bps: 3000 },
    { address: "0xCOLLAB_TWO", bps: 2000 }, // absorbs rounding; total = 10000
  ],
});
To include a partner, pass a non-zero partner address and a partnerBps (the partner’s cut of the creator side, base 10000). The partner is paid off the top; the claimers then split whatever remains, so their bps still sum to 10000. The partner must not also appear in claimers.
launchToken({
  name: "Partnered Token",
  symbol: "PART",
  metadataURI: "ipfs://YOUR_METADATA_CID",
  partner: "0xPARTNER_ADDRESS",
  partnerBps: 1000, // partner takes 10% of the creator side
  claimers: [
    { address: "0xCREATOR", bps: 10000 }, // creator gets the full claimer pot (the other 90%)
  ],
});

7. What Happens Next

After launch, the token trades on its bonding curve. Buyers send ETH to the curve to receive tokens; the creator side of every trade fee accrues to the feeShare contract. When enough ETH is raised, the token graduates to a Uniswap v4 pool.

Troubleshooting

  • BagsFactory_InsufficientCreationFee — you sent less than creationFee(). Read the fee live and set value accordingly.
  • BagsFactory_NoClaimers — the claimers array is empty. Pass at least one claimer.
  • BagsFactory_InvalidClaimers — the config failed a rule in section 2: claimers.length != bps.length, a duplicate or zero address, a claimer equal to the partner, more than 100 claimers, or bps that don’t sum to 10000.
  • BagsFactory_InvalidPartnerBpspartnerBps exceeds 10000.
  • BagsFactory_BuyFailed(curve, value) — the atomic initial buy in createAndBuy reverted (e.g. slippage or curve state). Lower the buy value, or call create and buy separately.
  • BagsFactory_NoBuyValue — you called createAndBuy with value == creationFee (nothing left to buy with). Either add buy value or call create.
  • Simulation reverts before sending — the decoded custom error tells you exactly which rule failed; fix the inputs and retry. Simulating first means you never waste gas on a doomed launch.
For the full function, event, and error catalog, see the Contracts Reference.