stroid.fun

Construct Launch TX

Build, sign, and send a stroid launchStroidDotFun transaction directly against LaunchpadV3.

Everything you need to construct a token-launch transaction by hand. The frontend at stroid.fun/create does exactly this — read creationFee(), build a LaunchOpts struct, send.

Network and target

  • Chain: Ethereum Mainnet (chainId = 1)
  • LaunchpadV3: 0x75D9ef48E30bCB0B658c1d387233fB52EBf9a4fE
  • FeeHookV3: 0x0d62529346ac2c61f5c0582210D01214687bc0CC
  • Metadata host: https://metadata.stroid.fun

All new launches must target LaunchpadV3 — that's the only deployment with a maintained pool initializer.

Metadata URI

metadataURI is an arbitrary string the launchpad records on-chain. The indexer fetches it after TokenLaunched and pulls name, symbol, description, image, website, twitter out of the JSON document.

The recommended flow is to POST your form to api.stroid.fun/metadata — the API stores the image and JSON document under random keys in the metadata bucket and returns the public URL you should record on-chain:

const form = new FormData();
form.append('name', 'My Token');
form.append('symbol', 'MYTK');
form.append('description', 'a short bio');
form.append('website', 'https://mytoken.example');
form.append('twitter', '@mytoken');
form.append('image', file); // PNG/JPEG/GIF/WebP, ≤ 15 MiB

const res  = await fetch('https://api.stroid.fun/metadata', { method: 'POST', body: form });
const json = await res.json() as { metadataURI: string; imageUrl: string | null };

// json.metadataURI is e.g. "https://metadata.stroid.fun/metadata/<uuid>.json"
// pass that string into launchStroidDotFun(...) below.

You can also self-host: any HTTPS URL pointing at a JSON document with name / symbol / description / image / website / twitter fields will work. The indexer also accepts ipfs://CID/....

The single entrypoint

LaunchpadV3 exposes one canonical launch function that takes a LaunchOpts struct. Every field on the struct is required by Solidity — you cannot omit any of them. Each field has a "do nothing" zero value that the contract treats as the default.

struct CreatorRecipient { address wallet; uint16 bps; }

struct LaunchOpts {
    CreatorRecipient[] creatorRecipients; // [] → 100% to msg.sender
    uint24             customFeeBps;      //  MUST BE 0 — custom fees disabled at protocol level
    address            partner;           // 0x0 → no partner attribution
    uint8              sqrtPriceTier;     //  0 → tier 1 (~1 ETH virtual LP)
}

function launchStroidDotFun(
    string  name,
    string  symbol,
    string  metadataURI,
    bytes32 salt,
    LaunchOpts calldata opts
) external payable returns (address token, uint256 devBuyTokens);

For the simplest "100% to me, global tier schedule, no partner, ~1 ETH virtual LP" launch you must still pass every field — just zero them out:

const opts = {
  creatorRecipients: [],
  customFeeBps:      0,
  partner:           '0x0000000000000000000000000000000000000000',
  sqrtPriceTier:     0,
};

There is no shorter overload, no "partial struct," and no way to skip a field. If your encoder doesn't include all four keys the call won't ABI-encode and the wallet will reject it before broadcast.

The function returns the deployed token address and the dev-buy token amount delivered to the creator.

Value math

msg.value must satisfy:

msg.value >= creationFee() + devBuyEth
  • creationFee() is a public state var on LaunchpadV3. Always read it fresh — the multisig admin can retune it (capped at a hard MAX_CREATION_FEE = 0.01 ETH).
  • Anything above creationFee() is spent as the dev-buy on the freshly opened pool, and the resulting tokens are forwarded to the creator. Send exactly creationFee() to skip the dev-buy.

Salt behavior (CREATE2)

You pass any bytes32 salt. The contract derives the actual CREATE2 salt as:

keccak256(abi.encodePacked(msg.sender, salt))

Same user-salt from a different sender produces a different token address — squatting the salt of someone else's 0xdead token isn't possible.

A random 32-byte salt is fine; the address is just used as a token identifier on-chain.

Predicting the token address (pre-launch)

The launchpad deploys the token via CREATE2, so the address is fully deterministic from (launcher, salt, name, symbol, metadataURI) — you can compute it before sending the launch tx and use it for vanity addresses, frontrunning protection, or pre-publishing your CA on socials.

There's a public view helper on the launchpad that does this for you:

function predictTokenAddress(
    address caller,        // who will sign the launch tx (== msg.sender)
    bytes32 salt,          // the user-provided salt you'll pass to launchStroidDotFun
    string  memory name,
    string  memory symbol,
    string  memory metadataURI
) external view returns (address);

It uses the same keccak256(abi.encodePacked(caller, salt)) derivation internally, hashes the token's init code, and returns the CREATE2 address — exactly what launchStroidDotFun will deploy.

const PREDICT_ABI = [
  {
    name: 'predictTokenAddress',
    type: 'function',
    stateMutability: 'view',
    inputs: [
      { name: 'caller',      type: 'address' },
      { name: 'salt',        type: 'bytes32' },
      { name: 'name',        type: 'string'  },
      { name: 'symbol',      type: 'string'  },
      { name: 'metadataURI', type: 'string'  },
    ],
    outputs: [{ name: '', type: 'address' }],
  },
] as const;

const predicted = await publicClient.readContract({
  address: LAUNCHPAD,
  abi: PREDICT_ABI,
  functionName: 'predictTokenAddress',
  args: [account.address, salt, 'My Token', 'MYTK', metadataURI],
});

console.log('token will deploy at', predicted);
// → 0xAbC… — same address you'll get back from launchStroidDotFun

Vanity-mining is just a loop: pick a random salt, call predictTokenAddress, check the resulting address against your pattern (e.g. starts with 0xc0ffee…), repeat until you hit it. For pure-offchain mining you can replicate the formula yourself:

actualSalt   = keccak256(abi.encodePacked(caller, salt))
initCode     = LaunchTokenV3.creationCode
            ++ abi.encode(name, symbol, 1_000_000_000e18, LAUNCHPAD, metadataURI)
initCodeHash = keccak256(initCode)
address      = last 20 bytes of keccak256(0xff ++ LAUNCHPAD ++ actualSalt ++ initCodeHash)

The init-code hash is the same for every launch with identical (name, symbol, metadataURI) triples, so you can cache it across millions of salt attempts and only re-hash the cheap final step. Note: the launchpad bakes TOTAL_SUPPLY = 1_000_000_000e18 and address(this) = LAUNCHPAD into the constructor args itself — you don't pass either.

Predicted addresses are guaranteed to match the deployed address as long as (caller, salt, name, symbol, metadataURI) are identical between the prediction call and the launch tx. Change anything (even one character in name) and the address shifts.

creatorRecipients

Splits the creator side of swap fees across up to 5 wallets. Set at launch and admin-tunable afterwards (creator cannot rewrite their own splits, but they could ask the protocol).

Validation rules — any of these revert:

RuleReason
creatorRecipients.length between 0 and 5hard cap of MAX_RECIPIENTS = 5
Each wallet != address(0)zero-address recipients rejected
Each bps in [1, 10000]zero / overflow recipients rejected
Sum of all bps exactly 10000every basis point of the creator side has to be assigned
No duplicate walletseach address may appear only once

Empty array ([]) is the canonical "send 100% of the creator side to msg.sender" shorthand. A single recipient [{ wallet: someAddress, bps: 10000 }] is equivalent to passing the empty array when someAddress == msg.sender.

Recipients accrue independently in FeeHookV3.ethOwed(addr) and each recipient claims their own share by calling claim() from their own wallet — see Construct Fee Claim TX.

customFeeBps

Currently disabled at the protocol level. FeeHookV3.customFeesEnabled is false, and the launchpad calls setCustomFeeAtLaunch whenever you pass a non-zero value — that call reverts with CustomFeesDisabled() and the entire launch tx fails.

You must pass customFeeBps: 0. Any other value bricks the launch. Every token currently uses FeeHookV3's market-cap-based global tier schedule. The field exists on the struct because Solidity requires it, and it's reserved for a future protocol upgrade — but today it's a hard zero.

partner

Optional partner attribution wallet, baked into the launch and emitted in the TokenLaunched event. The contract just records this address — it only earns a share of fees if a Stroid admin has registered it on FeeHookV3 with setPartnerBps(partner, bps).

Pass address(0) to skip partner attribution. There's no on-chain check that msg.sender actually has any relationship to partner; the partner program's economics are administered off-chain.

sqrtPriceTier

Selects the starting virtual liquidity for the token's Uniswap V4 pool. The tier index IS the ETH amount:

TierVirtual ETH
0aliased to tier 1
1~1 ETH (default)
2~2 ETH
......
10~10 ETH

Higher tiers start with a flatter price-impact curve (more virtual ETH on the buy side at launch). The available tier set is admin-tunable via LaunchpadV3.setTickUpperTiers(...). Picking a tier outside the active set reverts with TierUnset().

ABI fragment

[
  {
    "name": "creationFee",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [{ "name": "", "type": "uint256" }]
  },
  {
    "name": "launchStroidDotFun",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "name",        "type": "string"  },
      { "name": "symbol",      "type": "string"  },
      { "name": "metadataURI", "type": "string"  },
      { "name": "salt",        "type": "bytes32" },
      {
        "name": "opts",
        "type": "tuple",
        "components": [
          {
            "name": "creatorRecipients",
            "type": "tuple[]",
            "components": [
              { "name": "wallet", "type": "address" },
              { "name": "bps",    "type": "uint16"  }
            ]
          },
          { "name": "customFeeBps",  "type": "uint24"  },
          { "name": "partner",       "type": "address" },
          { "name": "sqrtPriceTier", "type": "uint8"   }
        ]
      }
    ],
    "outputs": [
      { "name": "token",        "type": "address" },
      { "name": "devBuyTokens", "type": "uint256" }
    ]
  }
]

Worked example (viem)

import {
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
  parseEther,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const mainnet = defineChain({
  id: 1,
  name: 'Ethereum',
  nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
  rpcUrls: { default: { http: ['https://eth.llamarpc.com'] } },
});

const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });

const LAUNCHPAD = '0x75D9ef48E30bCB0B658c1d387233fB52EBf9a4fE' as const;

const ABI = [
  {
    name: 'creationFee',
    type: 'function',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ name: '', type: 'uint256' }],
  },
  {
    name: 'launchStroidDotFun',
    type: 'function',
    stateMutability: 'payable',
    inputs: [
      { name: 'name',        type: 'string'  },
      { name: 'symbol',      type: 'string'  },
      { name: 'metadataURI', type: 'string'  },
      { name: 'salt',        type: 'bytes32' },
      {
        name: 'opts',
        type: 'tuple',
        components: [
          {
            name: 'creatorRecipients',
            type: 'tuple[]',
            components: [
              { name: 'wallet', type: 'address' },
              { name: 'bps',    type: 'uint16'  },
            ],
          },
          { name: 'customFeeBps',  type: 'uint24'  },
          { name: 'partner',       type: 'address' },
          { name: 'sqrtPriceTier', type: 'uint8'   },
        ],
      },
    ],
    outputs: [
      { name: 'token',        type: 'address' },
      { name: 'devBuyTokens', type: 'uint256' },
    ],
  },
] as const;

// Always read the live creationFee — the protocol owner can retune it.
const creationFee = await publicClient.readContract({
  address: LAUNCHPAD,
  abi: ABI,
  functionName: 'creationFee',
});

const devBuy = parseEther('0.05'); // optional; pass 0n to skip

const salt = `0x${[...crypto.getRandomValues(new Uint8Array(32))]
  .map((b) => b.toString(16).padStart(2, '0'))
  .join('')}` as `0x${string}`;

const recipients = [
  { wallet: account.address,                              bps: 7000 }, // 70% to launcher
  { wallet: '0xAbC0000000000000000000000000000000000001', bps: 2000 }, // 20% to a partner
  { wallet: '0xAbC0000000000000000000000000000000000002', bps: 1000 }, // 10% to another
];
// bps must sum to exactly 10000

const opts = {
  creatorRecipients: recipients,
  customFeeBps:  0,                                                       // MUST be 0 — custom fees disabled at protocol level
  partner:       '0x0000000000000000000000000000000000000000' as const,   // no partner
  sqrtPriceTier: 1,                                                       // ~1 ETH virtual LP (default)
};

// Upload the metadata first (see "Metadata URI" above) — `metadataURI` is
// the URL the API hands back, e.g.
//   https://metadata.stroid.fun/metadata/<uuid>.json
const metadataURI = 'https://metadata.stroid.fun/metadata/<uuid>.json';

const hash = await walletClient.writeContract({
  address: LAUNCHPAD,
  abi: ABI,
  functionName: 'launchStroidDotFun',
  args: ['My Token', 'MYTK', metadataURI, salt, opts],
  value: creationFee + devBuy,
});

const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('launched in block', receipt.blockNumber);

For the simple "100% to me, no splits, no partner, default tier" path just send an empty creatorRecipients array and zero out the rest:

const opts = {
  creatorRecipients: [],
  customFeeBps: 0,                                                  // MUST be 0 — custom fees disabled
  partner: '0x0000000000000000000000000000000000000000' as const,   // no partner
  sqrtPriceTier: 0,                                                 // contract aliases 0 → tier 1
};

Common revert reasons

Revert / errorWhy
InsufficientEth()msg.value < creationFee()
BadRecipients()> 5 entries, zero address, bps == 0, or duplicate wallet
BpsSumMismatch()recipient shares didn't sum to exactly 10000
CustomFeeBpsTooHigh()customFeeBps > 400
TierUnset()sqrtPriceTier is not in the admin-configured tier table
Generic CREATE2 failureanother launcher already used this exact (msg.sender, salt) pair — pick a fresh random salt

On this page