Documentation

Integration recipes

viem snippets for launching from a script, predicting the token address, reading and collecting fees, claiming, and reading token metadata.

On this page

Every function name and parameter name below is taken from the deployed sources. CONTRACTS and the ABI constants stand for your application's address and ABI modules; nothing here hardcodes an address.

ContractAddressApeScan
ArmoryLauncher0x69d3…c8C5
ArmoryLaunchLocker0x0972…Cf55
ArmoryLaunchFeeReceiver0x20bF…57c0
ArmoryTokenOwnerRegistry0xA360…5dDD
WAPE0x48b6…B557

Launch from a script

launch takes one struct. Every field is required by the ABI; empty strings are allowed for the metadata fields.

FieldTypeNotes
namestringnon-empty
symbolstringnon-empty
imagestringartwork URI, ar://, ipfs:// or https://
descriptionstringplain text
socialsstringone JSON string, e.g. keys x, telegram, discord, website
metadataURIstringERC-7572 contractURI
userSaltbytes32CREATE2 salt input; use a fresh random value per attempt
tickIfToken0IsNewTokenint24opening price tick; multiple of 500, at least -887000, below 887000
supplyuint256raw units; 0 means 1,000,000,000e18
devBuyMinOutuint256slippage floor for the dev buy; 0 is safe, see below

The tick is the price of one token in WAPE, as if the new token were token0: price = 1.0001 ^ tick. The launcher mirrors it when the token sorts above WAPE, so you never need to know the final ordering. Pick a starting market cap in APE, divide by supply, and snap down to the tier's spacing:

const TICK_SPACING = 500;
const MAX_USABLE_TICK = 887_000;
 
/** Opening tick for a target market cap, in APE. Snaps down. */
export function tickFromMarketCap(mcapApe: number, supplyTokens: number): number {
  const price = mcapApe / supplyTokens; // APE per token
  const raw = Math.log(price) / Math.log(1.0001);
  const tick = Math.floor(raw / TICK_SPACING) * TICK_SPACING;
  if (tick < -MAX_USABLE_TICK || tick >= MAX_USABLE_TICK) throw new Error("tick out of range");
  return tick;
}

Then build, check and send:

import { parseEther, toHex } from "viem";
import { CONTRACTS } from "./addresses";
import { LAUNCHER_ABI } from "./abis";
 
const params = {
  name: "Example",
  symbol: "EXMPL",
  image: "ar://…",
  description: "One line about the token.",
  socials: JSON.stringify({ x: "https://x.com/example" }),
  metadataURI: "ar://…",
  userSalt: toHex(crypto.getRandomValues(new Uint8Array(32))),
  tickIfToken0IsNewToken: tickFromMarketCap(5_000, 1_000_000_000),
  supply: 0n, // default supply
  devBuyMinOut: 0n,
};
 
// Read the fee right before sending. Everything above it is the dev buy,
// and the launcher refunds nothing.
const launchFee = await client.readContract({
  address: CONTRACTS.launcher,
  abi: LAUNCHER_ABI,
  functionName: "launchFee",
});
const devBuy = parseEther("50");
 
const predicted = await client.readContract({
  address: CONTRACTS.launcher,
  abi: LAUNCHER_ABI,
  functionName: "computeTokenAddress",
  args: [account.address, params],
});
 
const { request, result } = await client.simulateContract({
  account,
  address: CONTRACTS.launcher,
  abi: LAUNCHER_ABI,
  functionName: "launch",
  args: [params],
  value: launchFee + devBuy,
});
const [token, pool, positionId] = result; // token === predicted
 
const hash = await wallet.writeContract(request);

devBuyMinOut can stay zero. The pool is created and bought from in the same transaction, at a price this call sets, so nobody can move the price in between.

If the simulation reverts PoolPriceMismatch(), someone created the pool for the predicted address first. Generate a new userSalt and try again.

A Solidity caller does the same thing with ArmoryLauncher(launcher).launch{value: fee + devBuy}(params). The creator of record is msg.sender, so a contract that launches becomes the original launcher, the first token owner and the first fee recipient, and receives the dev buy. Make sure it can call transferTokenOwnership and updateCreatorRecipient, or hand those off in the same transaction.

Find a launch

const launch = await client.readContract({
  address: CONTRACTS.launcher,
  abi: LAUNCHER_ABI,
  functionName: "getLaunch",
  args: [token],
});
// launch.creator, launch.pool, launch.positionId, launch.createdAt
// A zero creator means the token was not launched here.

To list every launch, index TokenLaunched from the launcher's deploy block, or page through allTokens(i) up to allTokensLength().

Read pending fees

There is no pending-fees getter. Simulate collect instead: the position manager updates the position before paying, so the result is exactly what a collect in this block would pay. Any account works.

import { CONTRACTS, WAPE } from "./addresses";
import { LAUNCH_LOCKER_ABI } from "./abis";
 
const { result } = await client.simulateContract({
  account: anyAddress,
  address: CONTRACTS.launchLocker,
  abi: LAUNCH_LOCKER_ABI,
  functionName: "collect",
  args: [positionId],
});
const [amount0, amount1] = result;
 
// token0 is the lower address.
const tokenIsToken0 = token.toLowerCase() < WAPE.toLowerCase();
const pending = tokenIsToken0
  ? { token: amount0, wape: amount1 }
  : { token: amount1, wape: amount0 };
 
// The creator's part, rounded down like the fee receiver.
const { creatorBps } = await client.readContract({
  address: CONTRACTS.launchLocker,
  abi: LAUNCH_LOCKER_ABI,
  functionName: "getConfig",
  args: [positionId],
});
const creatorWape = (pending.wape * BigInt(creatorBps)) / 10_000n;
const creatorToken = (pending.token * BigInt(creatorBps)) / 10_000n;

These are gross amounts. The creator receives creatorBps of each side; on the token side the rest is burned while burnTokenFees() is true.

Collect

await wallet.writeContract({
  address: CONTRACTS.launchLocker,
  abi: LAUNCH_LOCKER_ABI,
  functionName: "collect",
  args: [positionId],
});

No access control, and the caller receives nothing. Payouts go to the recorded fee recipient and the protocol. For several positions, collectMany(ids) does them in one transaction, but one unknown id reverts all of them, so check each id with getConfig (a zero creator means not locked) before batching.

Claim an unclaimed balance

A payout that failed to transfer waits in the fee receiver. Check both sides:

import { LAUNCH_FEE_RECEIVER_ABI } from "./abis";
 
for (const asset of [WAPE, token]) {
  const owed = await client.readContract({
    address: CONTRACTS.launchFeeReceiver,
    abi: LAUNCH_FEE_RECEIVER_ABI,
    functionName: "claimable",
    args: [recipient, asset],
  });
  if (owed > 0n) {
    await wallet.writeContract({
      address: CONTRACTS.launchFeeReceiver,
      abi: LAUNCH_FEE_RECEIVER_ABI,
      functionName: "claim",
      args: [asset],
    });
  }
}

claim pays msg.sender, so it must be sent by the recipient the balance was credited to.

Read token metadata

import { ARMORY_TOKEN_ABI } from "./abis";
 
const read = (functionName: string) => ({
  address: token,
  abi: ARMORY_TOKEN_ABI,
  functionName,
});
 
const [image, description, socials, contractURI, metadataLocked, tokenOwner] =
  await client.multicall({
    contracts: [
      read("image"),
      read("description"),
      read("socials"),
      read("contractURI"),
      read("metadataLocked"),
      read("tokenOwner"),
    ],
    allowFailure: false,
  });
 
let links: Record<string, string> = {};
try {
  const parsed = socials ? JSON.parse(socials) : {};
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) links = parsed;
} catch {
  // Not JSON. Treat as no socials rather than failing the page.
}

image and contractURI may be ar:// or ipfs:// URIs; resolve them through a gateway before putting them in an img tag. A zero tokenOwner means the token was renounced, and metadataLocked tells you whether the one edit is spent.

To follow changes, watch MetadataUpdated on the token and TokenOwnershipTransferred on the registry.