This is the part people get wrong, so it is worth being precise: the API never signs anything. You POST what the token should be, and you get back unsigned calldata plus a list of things that must happen first. Your user's wallet does the rest.
Send an Idempotency-Key — retrying with the same key returns the same prepared transaction instead of preparing a second one.
POST /v1/pads/{slug}/tokensRequest
{
"name": "Hood Cat",
"ticker": "HOODCAT",
"description": "the cat that bought the dip",
"image": "ipfs://bafkreihq2s7x…",
"creator": "0x4a1d90Ff23cB8e07A1502dEc7719b6Aa3C0f88b0",
"devBuy": { "amount": "100000000", "currency": "USDG" },
"links": { "x": "https://x.com/hoodcat" }
}
responseJSON
{
"token": {
"predictedAddress": "0x91b2e7Ac03d5F1882bB4fD0a7cE6109a54cB4cE1",
"ticker": "HOODCAT"
},
"transaction": {
"chainId": 4663,
"to": "0x8f3C1b9a44Ee02c7B1d5aA90fE31c7742Bd41d17",
"data": "0x1f2a3b4c000000000000000000000000…",
"value": "500000000000000", // your launch fee, in wei
"gasLimit": "2450000"
},
"requires": [
{
"type": "erc20-approval",
"token": "0x51aB6f8cB2e7419b0dA3E8Cc2FbD1904Ee7803F7",
"spender": "0x8f3C1b9a44Ee02c7B1d5aA90fE31c7742Bd41d17",
"amount": "100000000"
}
],
"expiresAt": "2026-08-30T12:04:00Z"
}
launch.tsviem
import { createWalletClient, custom, erc20Abi } from 'viem'
const wallet = createWalletClient({ transport: custom(window.ethereum) })
const [account] = await wallet.getAddresses()
const res = await pad.tokens.create({ name: 'Hood Cat', ticker: 'HOODCAT' })
// 1. clear everything the API says must happen first
for (const req of res.requires) {
if (req.type === 'erc20-approval') {
await wallet.writeContract({
account, abi: erc20Abi, address: req.token,
functionName: 'approve',
args: [req.spender, BigInt(req.amount)],
})
}
}
// 2. send exactly what you were handed. never rebuild the calldata —
// the predicted address depends on it byte for byte.
const hash = await wallet.sendTransaction({
account,
chainId: res.transaction.chainId,
to: res.transaction.to,
data: res.transaction.data,
value: BigInt(res.transaction.value),
gas: BigInt(res.transaction.gasLimit),
})
// 3. the token is indexed the moment the block lands
const token = await pad.tokens.waitFor(hash) // resolves in ~2s