Create
Documentation
Get started

Quickstart

Launch a token with an opening buy, in one transaction, with viem. Every address below is Arc mainnet (chain 5042); see Addresses.

The snippets are written against a QUOTE — any approved pair token — rather than USDC in particular. Pick one from GET /v1/protocolapprovedPairTokens, and take its decimals from the same record; see Decimals for the five and their scales.

ts
const QUOTE = USDC;                                  // or WETH, cirBTC, EURC, XAUM
const QUOTE_DECIMALS = 6;                            // 18 for WETH/XAUM, 8 for cirBTC

1 · Check you are allowed to launch#

Launching is gated and disabled by default on a fresh deployment — on Arc mainnet it stays closed until the owner opens it. Always check first:

ts
const allowed = await client.readContract({
  address: FACTORY, abi: factoryAbi, functionName: "canLaunch", args: [account],
});
if (!allowed) throw new Error("launching is not open for this address");

2 · Pin the economics#

expectedEconomics freezes the terms your user was shown. If the owner re-pegs the curve or changes the launch fee between your quote and your signature, the launch reverts instead of landing on terms nobody agreed to.

ts
const expectedEconomics = await client.readContract({
  address: FACTORY, abi: factoryAbi,
  functionName: "previewLaunchEconomics", args: [0n, QUOTE],
});

Fetch it in the same flow as the submit. Do not cache it. Passing bytes32(0) waives the check, which means accepting whatever terms are live when the transaction lands.

3 · Approve the router#

The atomic path pulls the fee and the opening buy, and the spender is the router, not the factory. They are two different assets unless the launch quotes in USDC: the fee is always launchFee() of launchFeeToken() (5 USDC), the opening buy is quoteIn of QUOTE.

ts
const fee      = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFee" });
const feeToken = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFeeToken" });
const openingBuy = parseUnits("1", QUOTE_DECIMALS);  // 1 unit of the quote, in ITS decimals

if (feeToken.toLowerCase() === QUOTE.toLowerCase()) {
  // Same token: one approval for the sum covers both pulls.
  await walletClient.writeContract({
    address: QUOTE, abi: erc20Abi, functionName: "approve",
    args: [LAUNCH_AND_BUY, fee + openingBuy],
  });
} else {
  // Different tokens: the fee in USDC, the buy in the quote — two approvals, same spender.
  await walletClient.writeContract({
    address: feeToken, abi: erc20Abi, functionName: "approve", args: [LAUNCH_AND_BUY, fee],
  });
  await walletClient.writeContract({
    address: QUOTE, abi: erc20Abi, functionName: "approve", args: [LAUNCH_AND_BUY, openingBuy],
  });
}

4 · Launch#

ts
const { result } = await client.simulateContract({
  address: LAUNCH_AND_BUY, abi: launchAndBuyAbi, functionName: "launchAndBuy",
  args: [
    {
      name: "My Token", symbol: "MINE", logo: "", description: "",
      socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" },
      creatorFeeRecipient: account,                  // must NOT be zero on this path
      creatorTaxBps: 0,
      expectedEconomics,
      salt: crypto.getRandomValues(new Uint8Array(32)),
    },
    0n,            // launchConfigId
    QUOTE,         // pairToken
    openingBuy,    // quoteIn — must be non-zero here, in the quote's decimals
    0n,            // minTokensOut
    account,       // recipient of the bought tokens
  ],
  account,
});
const [token, curve, tokensOut] = result;

Then send it with an explicit gas limit — see the warning below.

Warning
If the opening buy is large enough to cross the graduation threshold, the curve tries to seed the Uniswap pool inside this same transaction. eth_estimateGas cannot size that: the seed is a best-effort try/catch, so a simulation in which it fails still succeeds overall and returns a limit too small for it to work — every time, deterministically. Send something generous (8,000,000 is ample). EIP-1559 charges for gas used, not the limit, so over-providing costs nothing.

5 · Launch without an opening buy#

launchAndBuy reverts on a zero quoteIn — it exists to buy and refuses to act as a plain deployer. Use the factory directly, and note the approval target moves with it:

ts
await walletClient.writeContract({
  address: feeToken, abi: erc20Abi, functionName: "approve", // the fee token (USDC), whatever the quote
  args: [FACTORY, fee],                                      // the FACTORY, not the router
});

const { result } = await client.simulateContract({
  address: FACTORY, abi: factoryAbi, functionName: "launchToken",
  args: [params, 0n, QUOTE], account,
});

A token launched this way starts with the creator holding none of its supply.

6 · Buy on the curve#

The curve has no fixed address — read it from the launch record.

ts
const launch = await client.readContract({
  address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token],
});

await walletClient.writeContract({
  address: launch.pairToken, abi: erc20Abi, functionName: "approve", // the launch's OWN quote
  args: [launch.curve, amountIn],                    // approve the CURVE, amount in the quote's decimals
});

await walletClient.writeContract({
  address: launch.curve, abi: curveAbi, functionName: "buy",
  args: [amountIn, minTokensOut, account],
});