Developers
Create tokens on Livo programmatically
Are you a dev integrating with Livo? Stay up to date with factories & ABI updates by joining the Livo dev integration updates Telegram group
Overview
Token creation on Livo follows a strict order: build the create-token transaction and precompute its hash, submit the token metadata (name, image, socials) to the Livo API keyed by that precomputed hash, and finally broadcast the signed transaction on-chain.
Order matters. The metadata submission and the on-chain broadcast must be performed one after the other with no delay between them.
Tip: uploading an image file is the slowest part of the metadata call. To launch instantly, omit the image — pass an already-pinned imageUrl (IPFS) in Step 4, or skip it entirely and attach the image within 10 minutes via PATCH /api/tokens/image (Step 6).
Deployed contracts
Step 1: Authenticate
All API calls require a JWT Bearer token. Obtain one by signing a message with your wallet. Tokens expire after 7 days.
POST /api/auth/wallet
Content-Type: application/json
{
"address": "0xYourWalletAddress",
"signature": "<signature>",
"message": "Sign in to Livo\nTimestamp: <unix_ms>"
}
// Response:
{ "token": "eyJhbGci..." }The message must contain Timestamp: <unix_ms> where the timestamp is within the last 5 minutes.
Step 2: Build the Create Token Call
The protocol uses two token factories. Pick one by whether the token should carry a post-graduation Uniswap LP fee for the creator:
if (creator takes a post-graduation LP fee):
use LivoFactoryUniV4Unified // graduates to Uniswap V4
else:
use LivoFactoryUniV2Unified // graduates to Uniswap V2Both factories expose the same struct-based createToken, differing only in one extra UniV4Configs struct on the V4 factory. Every knob (fee split, liquidity tier, tax, anti-sniper, creator vaults, buy-on-deploy) is a struct argument; disabled features are passed as zeroed structs or empty arrays. See Function Signatures below.
Grab the factory addresses — LivoFactoryUniV2Unified (proxy) and LivoFactoryUniV4Unified (proxy) — from the per-chain deployment files: Ethereum and Robinhood. All contracts are verified on Etherscan — fetch ABIs from there.
Function Signatures
createToken takes structured inputs. The V2 and V4 factories take the same arguments; the V4 factory inserts one extra univ4Configs struct in position 3.
// V2 — LivoFactoryUniV2Unified (graduates to Uniswap V2)
function createToken(
TokenSetupTiered tokenSetup,
TaxConfigs taxConfigs,
SupplyShare[] buyOnDeployShares,
AntiSniperConfigs antiSniperConfigs,
CreatorVault[] creatorVaults,
address referral
) payable returns (address token);
// V4 — LivoFactoryUniV4Unified (graduates to Uniswap V4; adds univ4Configs)
function createToken(
TokenSetupTiered tokenSetup,
TaxConfigs taxConfigs,
UniV4Configs univ4Configs, // V4 only, position 3
SupplyShare[] buyOnDeployShares,
AntiSniperConfigs antiSniperConfigs,
CreatorVault[] creatorVaults,
address referral
) payable returns (address token);Total supply is always 1,000,000,000e18. All bps values are basis points (10000 = 100%). To disable a feature, pass a zeroed struct (tax, anti-sniper) or an empty array (buy-on-deploy, creator vaults). Pass address(0) for referral for now (see below).
tokenSetup — TokenSetupTiered
struct TokenSetupTiered {
string name;
string symbol;
bytes32 salt; // mined so the address ends in 0x1110 (see Vanity Salt)
FeeShare[] feeShares; // trading-fee recipients; shares sum to 10000
LiquidityTier liquidityTier; // 0 = THIN, 1 = DEFAULT, 2 = THICK
}
struct FeeShare {
address account;
uint256 shares; // bps, > 0; array sums to exactly 10000
bool directFeesEnabled; // at most ONE entry may be true
}namerequiredsymbolrequiredsaltrequired0x1110. See the Vanity Salt section for mining a compatible salt.feeSharesrequiredshares> 0; the array must sum to exactly 10000. At most one entry may set directFeesEnabled (fees forwarded on each accrual instead of pull-claimed).liquidityTierrequiredtaxConfigs — TaxConfigs
struct TaxConfigs {
uint16 buyTaxBps; // long-term buy tax; 0 disables static tax
uint16 sellTaxBps; // long-term sell tax; 0 disables static tax
uint32 taxDurationSeconds; // static-tax window; 0 disables (then bps must be 0)
bool startTaxFromLaunch; // true: window from launch; false: from graduation
uint16 buyTaxDecayStartBps; // optional launch-tax decay start (buy); 0 = none
uint16 sellTaxDecayStartBps; // optional launch-tax decay start (sell); 0 = none
uint32 taxDecayDuration; // decay window; 0 disables; max 20 min
}buyTaxBps / sellTaxBpslpFeeBps + tax ≤ 500: V2 has no LP fee, so up to 500 bps (5%); V4 leaves 400 bps (100-bps hook) or 450 bps (50-bps hook). 0 disables static tax.taxDurationSecondsstartTaxFromLaunch.startTaxFromLaunchtrue → [launch, launch+duration] (taxed pre-graduation too); false → [graduation, graduation+duration] (no tax before graduation).buyTaxDecayStartBps / sellTaxDecayStartBpstaxDecayDurationtaxDurationSeconds ≥ taxDecayDuration.—max(decay, static).univ4Configs — UniV4Configs (V4 factory only)
struct UniV4Configs {
bool renounceOwnership; // true → deployed ownerless; false → owner = msg.sender
uint16 lpFeeBps; // post-graduation hook fee: must be 100 or 50
}renounceOwnershiptrue → tokenOwner = zero address. false → tokenOwner = msg.sender.lpFeeBps100 (1%) or 50 (0.5%) — anything else reverts.—buyOnDeployShares — SupplyShare[]
struct SupplyShare {
address account;
uint256 shares; // bps, > 0; array sums to exactly 10000
}buyOnDeploySharesSupplyShare[] only splits the bought tokens across recipients (bps summing to 10000); the amount bought is set by the ETH you send (msg.value). value > 0 ⇔ array non-empty (one without the other reverts); pass []when not buying. The buy is bounded by the bonding curve, not a fixed percentage: its ceiling is the token's graduation amount, which shrinks with the liquidityTier threshold and with any creator-vault reserved supply. Read the exact ceiling from maxBuyOnDeploy(tier, totalLockedInVaultsBps) and size value with quoteBuyOnDeploy(...); overshooting reverts MaxEthReservesExceeded.Sizing the creator-buy (and the max-buy special case). maxBuyOnDeploy(tier, totalLockedInVaultsBps) returns the largest token amount a deploy-buy can take — the amount that reaches graduation. totalLockedInVaultsBps is the sum of creatorVaults[].supplyBps (0 when there are no vaults), so reserving vault supply lowers the ceiling. Feed any amount up to that into quoteBuyOnDeploy(...) to get the exact value to send — it inflates the raw curve cost by the launch buy fee (tax / trading fee). Passing the max amount is a special case: the deploy-buy graduates the token in the same transaction.
// tier = tokenSetup.liquidityTier
// totalLockedInVaultsBps = sum of creatorVaults[].supplyBps (0 if no vaults)
uint256 maxTokens = factory.maxBuyOnDeploy(tier, totalLockedInVaultsBps);
// Buy any amount up to maxTokens. Passing maxTokens is the max-buy special
// case: the deploy-buy reaches graduation, so the token graduates on deploy.
uint256 tokenAmount = maxTokens;
// quoteBuyOnDeploy inflates the raw curve cost by the launch buy fee (tax /
// trading fee). The V4 factory appends univ4Configs as a 5th arg.
uint256 value = factory.quoteBuyOnDeploy(tier, tokenAmount, totalLockedInVaultsBps, taxConfigs);
// Send value as msg.value; the bought tokenAmount is split across recipients.
SupplyShare[] buyOnDeployShares = [SupplyShare(recipient, 10000)];antiSniperConfigs — AntiSniperConfigs
struct AntiSniperConfigs {
uint16 maxBuyPerTxBps; // 10..300 (0.1%..3% of supply)
uint16 maxWalletBps; // 10..300, and >= maxBuyPerTxBps
uint40 protectionWindowSeconds; // 0 disables; else 60..86400 (1min..24h)
address[] whitelist; // <= 20 addresses; bypass caps in the window
}antiSniperConfigsprotectionWindowSeconds. To disable, pass all zeros / empty array (sentinel: if the window is 0, every other field must be 0/empty).creatorVaults — CreatorVault[]
struct CreatorVault {
address owner;
uint256 supplyBps; // non-zero multiple of 500 (5%); sum across vaults <= 3000 (30%)
uint256 cliffSeconds; // pure lock-up before vesting
uint256 vestingSeconds; // linear vesting after the cliff
}creatorVaultssupplyBps≤ 3000 (30%). Locked supply selects an allocation-specific bonding curve for the tier (relaxed starting mcap; the tier's graduation invariants still hold).referral — address
referralTokenReferral(token, referral), with no storage or payout. Pass address(0) for now.Vanity Salt
The factory uses CREATE2. Livo requires the deployed token address to end in 1110. You must brute-force a salt that produces a matching address. The factory namespaces the salt by the deployer: the effective CREATE2 salt is keccak256(abi.encodePacked(msg.sender, salt)), so the resulting address depends on the account that sends the createToken transaction.
Mine against the exact token implementation the factory will clone. It clones a taxable or a base impl depending on whether tax is configured, so read the impl for your specific inputs from previewTokenImplementation(feeShares, buyOnDeployShares, taxConfigs, antiSniperConfigs) before searching. If the config changes the dispatch path (tax vs base) between preview and submit, the mined address won't match and the call reverts with InvalidTokenAddress.
import { keccak256, concat, toBytes } from "viem";
const PROXY_PREFIX = "0x3d602d80600a3d3981f3363d3d373d3d3d363d73";
const PROXY_SUFFIX = "0x5af43d82803e903d91602b57fd5bf3";
// deployer = the account that will send the createToken tx
function findVanitySalt(factoryAddress, tokenImplementation, deployer) {
const initcodeHash = keccak256(
concat([PROXY_PREFIX, tokenImplementation, PROXY_SUFFIX])
);
const buffer = new Uint8Array(85);
buffer[0] = 0xff;
buffer.set(toBytes(factoryAddress), 1);
buffer.set(toBytes(initcodeHash), 53);
// effective CREATE2 salt = keccak256(deployer ++ salt)
const saltPreimage = new Uint8Array(52);
saltPreimage.set(toBytes(deployer), 0);
const salt = crypto.getRandomValues(new Uint8Array(32));
for (;;) {
saltPreimage.set(salt, 20);
buffer.set(keccak256(saltPreimage, "bytes"), 21);
const hash = keccak256(buffer);
if (hash.endsWith("1110")) {
const saltHex = "0x" + Array.from(salt,
(b) => b.toString(16).padStart(2, "0")).join("");
return { salt: saltHex, tokenAddress: "0x" + hash.slice(26) };
}
for (let i = 31; i >= 0; i--) {
if (salt[i] < 255) { salt[i]++; break; }
salt[i] = 0;
}
}
}Step 3: Precompute the Transaction Hash
Encode the createToken call, build an EIP-1559 transaction (chainId, nonce, gas, fee fields, data, value=0), sign it offline, and take the keccak256 of the signed RLP payload. That digest is the txHash you submit to the API in Step 4 — and the same hash the network will assign once you broadcast the transaction in Step 5.
import { keccak256, encodeFunctionData } from "viem";
// tokenSetup, taxConfigs, antiSniperConfigs built as in Step 2; salt from the Vanity Salt section.
const data = encodeFunctionData({
abi: factoryAbi,
functionName: "createToken",
// V2 factory args — the V4 factory inserts univ4Configs ({ renounceOwnership, lpFeeBps })
// in position 3, right after taxConfigs.
args: [tokenSetup, taxConfigs, buyOnDeployShares, antiSniperConfigs, creatorVaults, referral],
});
const fees = await publicClient.estimateFeesPerGas();
const tx = {
type: "eip1559",
chainId,
nonce: await publicClient.getTransactionCount({ address: account.address }),
to: factoryAddress,
data,
value: 0n, // > 0 only for a deployer buy (buyOnDeployShares non-empty)
gas: await publicClient.estimateGas({ account, to: factoryAddress, data }),
maxFeePerGas: fees.maxFeePerGas,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
};
const signedTx = await walletClient.signTransaction(tx);
const txHash = keccak256(signedTx);Step 4: Submit Metadata
Submit token metadata to the API before broadcasting the transaction, using the precomputed txHash from Step 3.
POST /api/tokens/create
Authorization: Bearer <jwt_token>
Content-Type: multipart/form-datatxHashrequirednamerequiredsymbolrequiredchainIdrequireddescriptionsocialsimageimageUrl instead, or set the image later (see Step 6).imageUrlipfs://<cid> or an /ipfs/<cid> gateway URL. IPFS only; other URLs are rejected. Skips the upload entirely. If both image and imageUrl are sent, the uploaded file wins.Response:
{
"success": true,
"txHash": "0x...",
"imageUrl": "https://..."
}Step 5: Broadcast the Transaction
As soon as the metadata POST returns successfully, broadcast the signed transaction from Step 3. Steps 4 and 5 must be performed back-to-back with no delay between them.
const hash = await publicClient.sendRawTransaction({
serializedTransaction: signedTx,
});
// hash === txHash submitted in Step 4Step 6 (optional): Set the Image Later
To launch as fast as possible — e.g. reacting to breaking news — create the token with metadata only (no image / imageUrl in Step 4) and attach the image within 10 minutesof the on-chain creation. The window is measured from the token's on-chain creation timestamp, so the token must already be indexed (a few seconds after broadcast); call again if it returns 409.
PATCH /api/tokens/image
Authorization: Bearer <jwt_token>
Content-Type: application/json
{
"txHash": "0x...", // or "tokenAddress": "0x..."
"imageUrl": "ipfs://<cid>" // IPFS only (ipfs:// or /ipfs/<cid> gateway URL)
}
// Response:
{ "success": true, "txHash": "0x...", "imageUrl": "https://...mypinata.cloud/ipfs/<cid>" }txHashtokenAddress.tokenAddresstxHash.imageUrlrequiredipfs://<cid> or an /ipfs/<cid> gateway URL.Only the token's original creator (the authenticated wallet that submitted the metadata) may set the image. Returns 403 after the 10-minute window closes. The image can only be set once — a token that already has an image (set at creation or by a prior call) returns 409 and cannot be changed.
Validation Rules
Metadata (API) limits are stricter than the on-chain ones; the factory enforces the rest. Per-field constraints are in Function Signatures; the ones most likely to revert:
namesymbolfeeShares / buyOnDeploySharesbuyTaxBps / sellTaxBpstaxDurationSecondstaxDecayDurationliquidityTierlpFeeBps (V4)antiSniper (when window enabled)creatorVaultsKey Events
After token creation, the factory emits:
event TokenCreated(
address indexed token,
string name,
string symbol,
address tokenOwner,
address launchpad,
address graduator,
address feeHandler
)The launchpad emits:
event TokenLaunched(
address indexed token,
uint256 graduationThreshold,
uint256 maxExcessOverThreshold
)Parse the TokenLaunched event from the transaction receipt to get the created token address.
Notes
- Token creation is free (no ETH cost beyond gas) — unless you buy supply on deploy (
value> 0). - Tokens start on a bonding curve and automatically graduate to Uniswap once enough ETH is raised. The threshold scales with the chosen
liquidityTier— DEFAULT graduates at the original depth (~3.5 ETH of liquidity), THIN at half, THICK at double. - The
imagefield is a raw file upload, which the API pins to IPFS via Pinata. To skip the upload, pass an already-pinnedimageUrl(IPFS only) instead, or attach the image after creation viaPATCH /api/tokens/image(see Step 6).
Complete Example
End-to-end token creation using viem (no tax, single fee receiver). Assumes findVanitySalt from the Vanity Salt section and a factoryAbi fetched from Etherscan.
import {
createPublicClient,
createWalletClient,
http,
keccak256,
encodeFunctionData,
zeroAddress,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const API_BASE = "https://livo.trade";
const FACTORY_ADDRESS = "0x..."; // LivoFactoryUniV2Unified — see contracts repo
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });
// 1. Authenticate
const timestamp = Date.now();
const message = `Sign in to Livo\nTimestamp: ${timestamp}`;
const signature = await walletClient.signMessage({ account, message });
const { token: jwt } = await fetch(`${API_BASE}/api/auth/wallet`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: account.address, signature, message }),
}).then((r) => r.json());
// 2. Build the createToken structs (minimal token: single fee receiver, no tax,
// no anti-sniper, no vaults, no buy-on-deploy, DEFAULT liquidity tier).
const name = "My Token";
const symbol = "MTK";
const feeShares = [
{ account: account.address, shares: 10000n, directFeesEnabled: false },
];
const taxConfigs = {
buyTaxBps: 0, sellTaxBps: 0, taxDurationSeconds: 0, startTaxFromLaunch: false,
buyTaxDecayStartBps: 0, sellTaxDecayStartBps: 0, taxDecayDuration: 0,
};
const antiSniperConfigs = {
maxBuyPerTxBps: 0, maxWalletBps: 0, protectionWindowSeconds: 0, whitelist: [],
};
const buyOnDeployShares = []; // empty ⇒ no deployer buy (value must be 0)
const creatorVaults = [];
// The factory clones a taxable or base impl depending on the tax config; mine the
// vanity salt against the impl this view returns for THESE exact inputs.
const tokenImplementation = await publicClient.readContract({
address: FACTORY_ADDRESS,
abi: factoryAbi,
functionName: "previewTokenImplementation",
args: [feeShares, buyOnDeployShares, taxConfigs, antiSniperConfigs],
});
const { salt } = findVanitySalt(FACTORY_ADDRESS, tokenImplementation, account.address);
const tokenSetup = { name, symbol, salt, feeShares, liquidityTier: 1 /* DEFAULT */ };
const data = encodeFunctionData({
abi: factoryAbi,
functionName: "createToken",
// V2 args; the V4 factory inserts { renounceOwnership, lpFeeBps } in position 3.
args: [tokenSetup, taxConfigs, buyOnDeployShares, antiSniperConfigs, creatorVaults, zeroAddress],
});
// 3. Sign the transaction offline and precompute the txHash
const fees = await publicClient.estimateFeesPerGas();
const tx = {
type: "eip1559",
chainId: mainnet.id,
nonce: await publicClient.getTransactionCount({ address: account.address }),
to: FACTORY_ADDRESS,
data,
value: 0n,
gas: await publicClient.estimateGas({ account, to: FACTORY_ADDRESS, data }),
maxFeePerGas: fees.maxFeePerGas,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
};
const signedTx = await walletClient.signTransaction(tx);
const txHash = keccak256(signedTx);
// 4. Submit metadata BEFORE broadcasting (must be immediately followed by step 5)
const form = new FormData();
form.append("txHash", txHash);
form.append("name", name);
form.append("symbol", symbol);
form.append("chainId", String(mainnet.id));
form.append("description", "An example token");
// Socials: JSON array of up to 5 http(s) URLs. The icon is derived from each
// URL; invalid entries are dropped server-side.
form.append("socials", JSON.stringify([
"https://x.com/yourproject",
"https://t.me/yourproject",
]));
// form.append("image", imageFile); // optional File / Blob
const metaRes = await fetch(`${API_BASE}/api/tokens/create`, {
method: "POST",
headers: { Authorization: `Bearer ${jwt}` },
body: form,
});
if (!metaRes.ok) throw new Error(`metadata POST failed: ${await metaRes.text()}`);
// 5. Broadcast immediately — no delay allowed between steps 4 and 5
const broadcastedHash = await publicClient.sendRawTransaction({
serializedTransaction: signedTx,
});
console.log("token tx:", broadcastedHash); // === txHash