> For the complete documentation index, see [llms.txt](https://docs.kumbaya.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kumbaya.xyz/developers/launchpad/launching.md).

# Launching a token (ignite)

A launchpad token is created by a single call to **`FireLaunch.ignite(IgniteParams)`**. That one transaction:

1. Deploys a fresh `FireToken` ERC-20 (CREATE2, deterministic from `salt`)
2. Creates the corresponding Uniswap V3 pool at the starting tick if it doesn't exist
3. Transfers the bonding-curve allocation to `FireGraduator`
4. Calls `FireGraduator.createPositions()` to seed the curve with overlapping concentrated positions plus a tail
5. Splits the creator allocation across `FuelVault` (liquid bucket) and the on-token vesting schedule (which doesn't unlock until graduation)

The function returns the deployed token address.

## Function

```solidity
function ignite(IgniteParams calldata params)
    external
    nonReentrant
    whenNotPaused
    returns (address token);
```

> ⚠️ **`ignite()` does not accept `msg.value`.** It cannot bundle an ETH purchase. To buy tokens at launch, send a second transaction immediately after - see [**Buying at launch**](#buying-at-launch) below.

## `IgniteParams`

```solidity
struct IgniteParams {
    string  name;                  // Token name (must be non-empty)
    string  symbol;                // Token symbol (must be non-empty)
    uint256 totalSupply;           // Total fixed supply, must fit in uint128
    address numeraire;             // Quote token (must be approved in registry)
    int24   tickLower;             // Bonding-curve start tick (lower price)
    int24   tickUpper;             // Graduation tick (target price)
    uint24  feeTier;               // V3 pool fee tier (must be approved)
    uint16  skimBps;               // % of buys redirected to FuelVault as buyer credits
    uint16  creatorAllocationBps;  // Creator's % of total supply
    uint16  maxShareToBeSoldBps;   // % of supply allocated to bonding curve
    uint16  numPositions;          // Bonding-curve positions; must be in [minPositions, maxPositions]
    uint256 vestingDuration;       // Creator vesting duration in seconds
    bytes32 salt;                  // CREATE2 salt for deterministic token address
}
```

> **Don't pass `0` for `numPositions` or `vestingDuration` expecting a "registry default."** There is no fallback - `numPositions == 0` reverts with `ZeroPositions`, and `vestingDuration == 0` only passes when `creatorAllocationBps == 0` (because the bounds check is skipped) **and** the registry's `requiredVestingDuration` is also 0. Always pass the values you want.

## Validation: two phases

`_validateParams` runs two checks in sequence:

### Phase 1 - bounds

| Constraint                                                                                                                                       | Implementation                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `name`, `symbol` non-empty                                                                                                                       | `EmptyName` / `EmptySymbol`                                                        |
| `0 < totalSupply ≤ type(uint128).max`                                                                                                            | `ZeroSupply` / `SupplyExceedsUint128`                                              |
| Numeraire on the approved set                                                                                                                    | `registry.approvedNumeraires(numeraire)` (public mapping) → `NumeraireNotApproved` |
| Fee tier on the approved set                                                                                                                     | `registry.approvedFeeTiers(feeTier)` (public mapping) → `FeeTierNotApproved`       |
| Fee tier has tick spacing in V3 factory                                                                                                          | `factory.feeAmountTickSpacing(feeTier) != 0` → `InvalidFeeTier`                    |
| `tickLower < tickUpper`, both aligned to fee tier spacing                                                                                        | `InvalidTickRange` / `TickNotAligned`                                              |
| `skimBps ≤ registry.maxSkimBps()`                                                                                                                | `SkimTooHigh` (max 1500 = 15%)                                                     |
| `creatorAllocationBps ≤ registry.maxCreatorAllocationBps()`                                                                                      | `CreatorAllocTooHigh` (max 2000 = 20%)                                             |
| `creatorAllocationBps + maxShareToBeSoldBps ≤ 9000`                                                                                              | `AllocationsAbove90Percent` (≥10% tail)                                            |
| `maxShareToBeSoldBps ≥ registry.minMaxShareToBeSoldBps()`                                                                                        | `MaxShareToBeSoldTooLow`                                                           |
| `numPositions ≠ 0`                                                                                                                               | `ZeroPositions`                                                                    |
| `numPositions ∈ [registry.minPositions(), registry.maxPositions()]`                                                                              | `TooFewPositions` / `TooManyPositions` (5 … 50)                                    |
| `vestingDuration ∈ [minVestingDuration(), maxVestingDuration()]` *only if* `creatorAllocationBps > 0` and not all of it is captured to FuelVault | `VestingDurationTooShort` / `VestingDurationTooLong` (90 … 730 days)               |
| If `creatorAllocationBps > 0` and registry hasn't set `requiredVestingDuration` and not all is captured to FuelVault                             | `RequiredCreatorVestingUnset`                                                      |

### Phase 2 - registry pin matching

If the registry has set any `required*` value to a non-zero value, the corresponding parameter **must match exactly**. The registry-required tick range *also* accepts the inverse (negated and swapped):

| If registry has set…                                           | …user param must equal                     | Mismatch revert                                          |
| -------------------------------------------------------------- | ------------------------------------------ | -------------------------------------------------------- |
| `requiredTotalSupply`                                          | itself                                     | `TotalSupplyMismatch`                                    |
| `requiredTickLower` / `requiredTickUpper`                      | itself OR `(-reqTickUpper, -reqTickLower)` | `TickRangeMismatch`                                      |
| `requiredFeeTier`                                              | itself                                     | `FeeTierMismatch`                                        |
| `requiredSkimBps`                                              | itself                                     | `SkimBpsMismatch`                                        |
| `requiredCreatorAllocationBps` / `requiredMaxShareToBeSoldBps` | both must match (set atomically)           | `CreatorAllocationMismatch` / `MaxShareToBeSoldMismatch` |
| `requiredNumPositions`                                         | itself                                     | `NumPositionsMismatch`                                   |
| `requiredVestingDuration`                                      | itself                                     | `VestingDurationMismatch`                                |

> The registry pinning means production mainnet may force values tighter than the absolute caps. **Always read the registry's `required*` views before constructing `IgniteParams`** - see [Live registry config](/developers/building-agents/registry-config.md).
>
> The contract-level inverse-tick acceptance (line `bool reverseMatch = (params.tickLower == -reqTickUpper && params.tickUpper == -reqTickLower)` in `_validateRegistryRequirements`) is *separate* from the indexer's `(ticks, fireTokenPosition)` check - see [**Token ordering and tick math**](#-token-ordering-and-tick-math) below.

## Production defaults (Kumbaya frontend)

The Kumbaya frontend uses these values for every standard launch on mainnet:

```ts
{
  defaultTotalSupply:    1_000_000_000n * 10n ** 18n,  // 1B tokens
  tokenDecimals:         18,
  feeTier:               10000,                         // 1%
  tickSpacing:           200,
  tickLower:             -219400,
  tickUpper:             -174800,
  skimBps:               300,                           // 3%
  creatorAllocationBps:  0,                             // no on-token creator allocation
  maxShareToBeSoldBps:   7200,                          // 72% to bonding curve
  numPositions:          50,
  vestingDuration:       90n * 24n * 60n * 60n,         // 90 days
}
```

A few things worth calling out for integrators replicating the defaults:

* **`creatorAllocationBps = 0`.** The standard frontend launch grants **no on-token creator allocation**. The remaining `10000 - 7200 = 2800` bps (28%) goes to the **tail position**. Creators earn from post-grad `FireStream` fees and FuelVault skim/gifts - there's no vesting schedule to claim because there's nothing vesting.
* You *can* set `creatorAllocationBps` up to the registry cap (`MAX_CREATOR_ALLOCATION_BPS = 2000`, i.e. 20%) when calling `ignite` yourself - but make sure `creatorAllocationBps + maxShareToBeSoldBps ≤ 9000` so there's still tail liquidity.
* Always read `LaunchState` (returned by `FireLaunch.getLaunchState(token)`) to know the actual split for a *deployed* token - defaults can change.

## ⚠ Token ordering and tick math

**This is the easiest way to break a launch.** Read this section before you call `ignite`.

> 🚨 **The Kumbaya indexer only indexes launches that match an allowed `(tickLower, tickUpper, fireTokenPosition)` configuration for the chain.** A token launched with anything else still exists on-chain (the contract is permissionless), but it won't be picked up by the indexer - so it won't appear in the launchpad feed, search, or curated APIs, and Kumbaya pricing won't track it. Direct V3 trading against the pool still works for anyone with the contract address.

In Uniswap V3, every pool has a **`token0`** and a **`token1`**, sorted by address: the lower-address token is `token0`. Ticks measure the price of `token0` denominated in `token1` - so swapping which side your token is on **inverts the meaning of every tick value**.

### What the indexer actually accepts

The validator (`isValidFirePoolConfig`) accepts **both** the canonical config and its inverse:

| Side                                      | `fireTokenPosition` | `tickLower` | `tickUpper` |
| ----------------------------------------- | ------------------- | ----------- | ----------- |
| **Canonical** (Kumbaya frontend defaults) | `token0`            | `-219400`   | `-174800`   |
| **Inverse** (token is `token1`)           | `token1`            | `174800`    | `219400`    |

The inverse is the same curve from the pool's other side: ticks negated **and** swapped, position flipped. Fee tier, supply, skim, allocation, vesting duration, and position count are **not** checked by the indexer - only this `(tickLower, tickUpper, fireTokenPosition)` triple.

### Recommended path: mine for `token0`

The Kumbaya frontend always launches with token as `token0` and the canonical ticks above. Two reasons to follow that path:

1. It matches every screenshot and creator-facing UI in `kumbaya.xyz` - no surprises for users.
2. `FireLaunch.ignite()` deploys via CREATE2, so the token address is determined by the salt. Salt mining for `tokenAddr < numeraire` (i.e. token is `token0`) typically takes only a few thousand attempts.

The Kumbaya defaults (`tickLower = -219400`, `tickUpper = -174800`) **assume the new token is `token0`**:

* The negative ticks place the launched token at a *low* price relative to the numeraire - i.e. early buyers get cheap tokens.
* The graduation tick (`tickUpper = -174800`) is the price target the curve climbs *up* toward.
* The tail position extends from `tickUpper` outward toward `MAX_TICK`.

If your launched token ends up as `token1` (higher address than the numeraire), every one of those statements flips. Buys would walk price the wrong way, the "graduation" tick becomes a floor instead of a ceiling, and the tail position points the wrong direction.

### Always mine the salt for token0

`FireLaunch.ignite()` deploys the new token via CREATE2, so the resulting address is fully determined by `(deployer, salt, init code, constructor args)`. Pick salts in a loop until the resulting address is **strictly less than** the numeraire (WETH) address:

```ts
import { keccak256, encodePacked, getCreate2Address } from 'viem'

function mineToken0Salt({
  deployer,        // FireLaunch address
  initCodeHash,    // FireToken creation code hash
  numeraire,       // WETH address - the token your new token must beat
  vanitySuffix,    // optional: '069' | '420' | '888' from VANITY_SUFFIXES
  maxAttempts = vanitySuffix ? 1_000_000 : 10_000,
}) {
  for (let i = 0; i < maxAttempts; i++) {
    const salt = randomBytes32()
    const tokenAddr = getCreate2Address({
      from: deployer,
      salt,
      bytecodeHash: initCodeHash,
    })
    // Required: token must be token0 (strictly less than numeraire).
    if (BigInt(tokenAddr) >= BigInt(numeraire)) continue
    // Optional: filter for vanity ending.
    if (vanitySuffix && !tokenAddr.toLowerCase().endsWith(vanitySuffix)) continue
    return { salt, tokenAddr }
  }
  throw new Error('salt mining exhausted')
}
```

The Kumbaya frontend uses these defaults:

| Setting                                  | Value               |
| ---------------------------------------- | ------------------- |
| Max attempts (no vanity)                 | `10,000`            |
| Max attempts (with vanity, 3-hex suffix) | `1,000,000`         |
| Vanity suffixes available                | `069`, `420`, `888` |

Mining without a vanity is fast - it's a 50/50 coin flip per salt, so you typically find one in a handful of tries.

### Launching as `token1` (inverse config)

If you can't (or don't want to) mine for `token0`, you can launch with the new token as `token1` and the **inverse ticks** above. This *is* indexed correctly and will appear on the launchpad - the indexer treats canonical and inverse as equivalent.

To do it:

1. Mine a salt where `tokenAddr > numeraire`.
2. Pass `tickLower = 174800` and `tickUpper = 219400` (the negated and swapped values of the canonical config).
3. Everything else stays the same.

In practice, mining for `token0` is faster than the salt search needed to land on a memorable address while satisfying `tokenAddr > numeraire`, so the canonical config is what you'll see in production.

### What the indexer rejects

Any tuple `(tickLower, tickUpper, fireTokenPosition)` that doesn't match either row above. **The contract may still accept the call** (because `_validateRegistryRequirements` only checks tick *values*, not the resulting `token0`/`token1` ordering) - but the indexer skips the launch.

Examples and their outcomes:

| Ticks passed                            | Token side after deploy | Contract `ignite`                                    | Indexer               |
| --------------------------------------- | ----------------------- | ---------------------------------------------------- | --------------------- |
| `(-219400, -174800)` (canonical)        | `token0`                | accepts                                              | ✓ indexed             |
| `(-219400, -174800)` (canonical)        | `token1`                | accepts (matches required)                           | ✗ skipped (unindexed) |
| `(174800, 219400)` (inverse)            | `token1`                | accepts                                              | ✓ indexed             |
| `(174800, 219400)` (inverse)            | `token0`                | accepts (matches required inverse)                   | ✗ skipped             |
| Custom range, e.g. `(-150000, -100000)` | either                  | reverts (`TickRangeMismatch`) if registry pins ticks | n/a                   |

If the launch is unindexed, the token is permanent and tradeable but **invisible to Kumbaya**. Re-deploying with the correct ordering means a fresh address.

### Sanity check before submitting `ignite`

```ts
// Belt-and-braces assertion - run this client-side before calling writeContract.
if (BigInt(predictedTokenAddr) >= BigInt(numeraireAddr)) {
  throw new Error(
    'predicted token address is not less than numeraire - '
    + 'salt mining failed; do not submit ignite()',
  )
}
```

A failed launch costs gas and produces a stranded token. The 5-line check above prevents both.

## Example: deploy with viem

```ts
import { encodeFunctionData, parseUnits } from 'viem'
import { fireLaunchAbi } from './abis/fireLaunch'

const FIRE_LAUNCH = '0x69FE0908F1211dE66F7067021998f28A5693ABbD' // mainnet

const params = {
  name: 'My Token',
  symbol: 'MTK',
  totalSupply: parseUnits('1000000000', 18),    // 1B tokens
  numeraire: '0x4200000000000000000000000000000000000006', // WETH
  tickLower: -219400,                           // production frontend default
  tickUpper: -174800,                           // production frontend default
  feeTier:   10000,                             // 1%
  skimBps:    300,                              // 3% skim → FuelVault
  creatorAllocationBps: 0,                      // no on-token alloc (default)
  maxShareToBeSoldBps:  7200,                   // 72% bonding curve, 28% tail
  numPositions: 50,                             // production default
  vestingDuration: BigInt(90 * 24 * 60 * 60),   // 90 days
  salt: pickSaltSoTokenIsToken0(),              // see "Salt mining" below
}

const txHash = await wallet.writeContract({
  address: FIRE_LAUNCH,
  abi: fireLaunchAbi,
  functionName: 'ignite',
  args: [params],
})
```

After confirmation, parse the `TokenIgnited(address indexed token, address indexed creator, address indexed pool, uint256 totalSupply, uint16 skimBps, int24 tickLower, int24 tickUpper)` event to read the deployed token address, or compute it yourself from the salt + init code hash.

## Salt mining

The CREATE2 salt is significant: many integrations want **the new token to be `token0` of the V3 pool** (i.e. lower address than the numeraire). The Kumbaya frontend mines salts to satisfy that ordering, and additionally tries vanity suffixes like `069` and `420`. Mining is fast - a few thousand attempts is typical.

Reference logic from the frontend (simplified):

```ts
function mineSalt(deployer, ctorArgs, numeraire, vanitySuffix?) {
  for (let i = 0; i < 100_000; i++) {
    const salt = randomBytes(32)
    const tokenAddr = computeCreate2Address(deployer, salt, ctorArgs)
    if (BigInt(tokenAddr) >= BigInt(numeraire)) continue           // need token0
    if (vanitySuffix && !tokenAddr.endsWith(vanitySuffix)) continue
    return salt
  }
}
```

## Buying at launch

The Kumbaya frontend lets a creator buy from their own bonding curve in the same UX flow. The contract supports this only as a **two-transaction batch**, not a single call:

```
tx 1: FireLaunch.ignite(params)            → returns tokenAddress
tx 2: SwapRouter02.exactInputSingle({
        tokenIn:  numeraire (WETH),
        tokenOut: tokenAddress,
        fee:      params.feeTier,
        recipient: creator,
        amountIn: ethAmount,
        ...
      })
```

In practice both txs are signed up-front with sequential nonces and broadcast together via `eth_sendRawTransactionBatch` (or sequentially). The second tx executes against the fresh pool created by the first.

> ⚠️ **Not atomic.** If the second tx fails (e.g. slippage), the token is still deployed. Validate the bonding-curve price math client-side before submitting the buy.

## Errors to handle

Most validation failures revert with these custom errors:

| Error                                                                 | Cause                                                                                        |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `FeeTierNotApproved` / `InvalidFeeTier`                               | `feeTier` not in registry's accepted set / not a valid V3 fee tier                           |
| `NumeraireNotApproved`                                                | `numeraire` not in registry's accepted set                                                   |
| `ZeroSupply` / `SupplyExceedsUint128`                                 | Bad `totalSupply`                                                                            |
| `CreatorAllocTooHigh`                                                 | `creatorAllocationBps > MAX_CREATOR_ALLOCATION_BPS` (2000)                                   |
| `SkimTooHigh`                                                         | `skimBps > MAX_SKIM_BPS` (1500)                                                              |
| `AllocationsAbove90Percent`                                           | `creatorAllocationBps + maxShareToBeSoldBps > 9000`                                          |
| `MaxShareToBeSoldTooLow`                                              | `maxShareToBeSoldBps` below registry minimum                                                 |
| `InvalidTickRange` / `TickNotAligned`                                 | `tickLower`, `tickUpper` invalid or not aligned to fee tier's tick spacing                   |
| `TooFewPositions` / `TooManyPositions` / `ZeroPositions`              | `numPositions` outside `[5, 50]`                                                             |
| `VestingDurationTooShort` / `VestingDurationTooLong`                  | `vestingDuration` outside registry-allowed range                                             |
| `EmptyName` / `EmptySymbol`                                           | Missing metadata fields                                                                      |
| `*Mismatch` family (`TotalSupplyMismatch`, `TickRangeMismatch`, etc.) | An existing pool already exists for this pair but with different params than what you passed |
| `PoolAlreadyInitialized`                                              | Pool exists and is already at a non-zero price                                               |

## Where to next

* [**Bonding curve and graduation**](/developers/launchpad/graduation.md) - what happens after a launch
* [**Fees and credits**](/developers/launchpad/fees-and-credits.md) - how creators earn from launches
* [**Contracts at a glance**](/developers/launchpad/contracts.md) - function index across all launchpad contracts
