> 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/building-agents/launching-programmatically.md).

# Auto-launch a token

End-to-end recipe for an agent that deploys a launchpad token, optionally buys at launch, and (optionally) attaches off-chain metadata so it shows up on `kumbaya.xyz`.

## Prerequisites

* An agent wallet with ETH on MegaETH mainnet for gas (and any initial buy).
* A SIWE session if you want to attach off-chain metadata. See [**Sign in with Ethereum**](/developers/building-agents/siwe.md).
* The launchpad contract addresses for your chain - see [Contract Addresses](/developers/networks-and-contracts/contract-addresses.md).

## Step 1: Mine the salt

`FireLaunch.ignite()` deploys via CREATE2, so the resulting token address is fully determined by `(deployer, salt, init code, constructor args)`. The Kumbaya indexer requires the new token to be **`token0`** (lower address than the numeraire). Mine until you find one:

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

const FIRE_LAUNCH = '0x69FE0908F1211dE66F7067021998f28A5693ABbD'  // mainnet
const WETH        = '0x4200000000000000000000000000000000000006'

// FireToken creation code hash - read once from a deployed FireToken or compute from its bytecode.
// Trust this from the launchpad repo's `forge build` output: keccak256(FireToken.creationCode + abi.encode(...constructorArgs))
function tokenAddrFor(salt: `0x${string}`): `0x${string}` {
  const initCodeHash = computeFireTokenInitCodeHash(/* args identical across launches */)
  return getCreate2Address({
    from: FIRE_LAUNCH,
    salt,
    bytecodeHash: initCodeHash,
  })
}

let salt: `0x${string}` | null = null
let tokenAddr: `0x${string}` | null = null

for (let i = 0; i < 10_000; i++) {
  const candidate = `0x${randomBytes(32).toString('hex')}` as `0x${string}`
  const addr = tokenAddrFor(candidate)
  if (BigInt(addr) < BigInt(WETH)) {
    salt = candidate
    tokenAddr = addr
    break
  }
}

if (!salt) throw new Error('salt mining exhausted - try more attempts')
```

> The constructor args (name, symbol, supply, etc.) feed into the init code hash, so technically the search has to fix those before mining. The frontend uses up to 10,000 attempts without a vanity suffix and 1,000,000 with one. See [Token ordering and tick math](/developers/launchpad/launching.md#-token-ordering-and-tick-math) for the constraints.

## Step 2: Call `ignite`

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

const txHash = await wallet.writeContract({
  address: FIRE_LAUNCH,
  abi: fireLaunchAbi,
  functionName: 'ignite',
  args: [{
    name:                 'My Agent Token',
    symbol:               'AGENT',
    totalSupply:          parseUnits('1000000000', 18),    // 1B
    numeraire:            WETH,
    tickLower:            -219400,                          // canonical, indexed
    tickUpper:            -174800,                          // canonical, indexed
    feeTier:              10000,                            // 1%
    skimBps:              300,                              // 3%
    creatorAllocationBps: 0,
    maxShareToBeSoldBps:  7200,
    numPositions:         50,
    vestingDuration:      BigInt(90 * 24 * 60 * 60),       // 90 days
    salt: salt!,
  }],
})

const receipt = await client.waitForTransactionReceipt({ hash: txHash })
// Parse `TokenIgnited(token, creator, pool, totalSupply, skimBps, tickLower, tickUpper)` from receipt.logs
// to confirm the deployed token address matches `tokenAddr` from your salt mining.
```

If the values you pass diverge from what `FireRegistry` requires (e.g. wrong `feeTier`, wrong `vestingDuration` range), the call reverts with a `*Mismatch` error. See [**Launching a token → Errors**](/developers/launchpad/launching.md#errors-to-handle).

## Step 3 (optional): Buy at launch

`ignite()` doesn't accept ETH, so the buy is a separate transaction immediately after.

```ts
import { swapRouter02Abi } from './abis/swapRouter02'
import { parseEther } from 'viem'

const SWAP_ROUTER_02 = '0xE5BbEF8De2DB447a7432A47EBa58924d94eE470e'

const buyHash = await wallet.writeContract({
  address: SWAP_ROUTER_02,
  abi:     swapRouter02Abi,
  functionName: 'exactInputSingle',
  args: [{
    tokenIn:           WETH,
    tokenOut:          tokenAddr!,
    fee:               10000,
    recipient:         account.address,
    amountIn:          parseEther('0.1'),
    amountOutMinimum:  0n,                  // tighten with a real quote in production
    sqrtPriceLimitX96: 0n,
  }],
  value: parseEther('0.1'),                 // SwapRouter02 wraps ETH for you
})
```

> ⚠️ Not atomic with `ignite`. If the buy reverts (e.g. slippage), the token still deployed. Quote first if you care about the price.

## Step 4 (optional): Attach off-chain metadata

If the agent wants its token to render with a description, image, and social links on `kumbaya.xyz`, attach metadata via the **claim** flow. If you use the on-chain MCP, the `sign_token_claim` tool builds and signs this exact proof for you (pass its output straight to `app_post_tokens_by_mint_address_claim`) - the raw viem below is for integrators not using the MCP:

```ts
import { signTypedData } from 'viem/accounts'

const CLAIM_DOMAIN = {
  name: 'Kumbaya Token Claim',
  version: '1',
  chainId: 4326,
}

const CLAIM_TYPES = {
  ClaimListing: [
    { name: 'mintAddress', type: 'address' },
    { name: 'chainId',     type: 'uint256' },
    { name: 'timestamp',   type: 'uint256' },
    { name: 'nonce',       type: 'string'  },
  ],
} as const

const signedAt = Math.floor(Date.now() / 1000)
const nonce    = crypto.randomUUID()

const signature = await account.signTypedData({
  domain: CLAIM_DOMAIN,
  types:  CLAIM_TYPES,
  primaryType: 'ClaimListing',
  message: {
    mintAddress: tokenAddr!,
    chainId:     BigInt(4326),
    timestamp:   BigInt(signedAt),
    nonce,
  },
})

await fetch(`https://clients.kumbaya.xyz/v1/tokens/${tokenAddr}/claim`, {
  method:  'POST',
  headers: {
    'content-type': 'application/json',
    'Authorization': `Bearer ${session.token}`,   // SIWE JWT from earlier
  },
  body: JSON.stringify({
    chainId:     4326,
    description: 'A token launched by my agent.',
    category:    'MEMES',         // or 'DARES'
    signature,
    signedAt,
    nonce,
    website:     'agent.example.com',
    xHandle:     'myagent',
    telegramUrl: 't.me/myagent',
  }),
})
```

You can optionally upload an image via `POST /v1/tokens/{tokenAddr}/claim/image` (multipart, JWT-authenticated).

## Watching the launch lifecycle

After deployment, the token is on-chain, indexed, and tradeable. Track its state via:

* **`FireLaunch.getLaunchState(token)`** - full per-launch state (creator, createdAt, graduationConditionMetAt, graduated, graduationFeeBps, isToken0, graduationTick, pool, etc.).
* **Hasura indexer** - subscribe to `FireToken(where: { address: { _eq: <addr-chainId> } })` for current-state changes; subscribe to `Swap(where: { pool_id: { _eq: <pool-addr-chainId> } })` for trade activity.

## Triggering graduation

When the price tick crosses the graduation threshold, `FireGraduator.canGraduate(token)` returns `true`. Anyone can then push graduation forward:

```ts
// Step 1 (one-shot, anyone): start the grace timer.
// recordGraduationCondition lives on FireLaunch, not FireGraduator.
await wallet.writeContract({
  address: FIRE_LAUNCH,
  abi: fireLaunchAbi,
  functionName: 'recordGraduationCondition',
  args: [tokenAddr],
})

// Step 2 (after gracePeriodDuration elapses): graduate (lives on FireGraduator)
const grace = await client.readContract({
  address: REGISTRY,
  abi: fireRegistryAbi,
  functionName: 'gracePeriodDuration',
})

// Wait grace seconds (or skip if you ARE the guardian - you can graduate immediately)
await wallet.writeContract({
  address: GRADUATOR,
  abi: fireGraduatorAbi,
  functionName: 'graduate',
  args: [tokenAddr],
})
```

After graduation, post-grad fees flow via `FireStream.claimFees(token)` and creators (or anyone) can call it to sweep their share.

## Common pitfalls

* **Salt mining mistake.** If your token address ≥ numeraire, the bonding curve runs the wrong way and the indexer skips your launch. Always assert `BigInt(tokenAddr) < BigInt(numeraire)` before broadcasting.
* **Wrong `feeTier` / `vestingDuration`.** `FireRegistry` enforces specific values. Read `requiredFeeTier`, `requiredSkimBps`, and the vesting bounds before constructing `IgniteParams`. See [**Live registry config**](/developers/building-agents/registry-config.md).
* **Skipping the grace period.** Only the guardian can graduate immediately. If your agent isn't the guardian, either wait `gracePeriodDuration` after `recordGraduationCondition`, or wait for `forceGraduationDelay` from `createdAt` which bypasses the grace check entirely. `forceGraduationDelay` is governance-set (currently \~180 days / 6 months; its floor is `MIN_FORCE_GRADUATION_DELAY = 90 days`) - read `FireRegistry.forceGraduationDelay()` rather than hardcoding a value.
* **Not parsing the receipt.** If `ignite()` reverts after gas was paid, you have no token but you spent gas. Check `receipt.status === 'success'` before assuming success.

## Where to next

* [**SIWE auth**](/developers/building-agents/siwe.md) - getting the JWT for the metadata-claim step
* [**Live registry config**](/developers/building-agents/registry-config.md) - reading current registry values
* [**Launching a token (full integrator reference)**](/developers/launchpad/launching.md) - every `IgniteParams` field, every error
* [**Fees and credits**](/developers/launchpad/fees-and-credits.md) - how earnings flow after launch
