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.
Launch from a script
launch takes one struct. Every field is required by the ABI; empty strings are
allowed for the metadata fields.
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.