Developers

Create tokens on Livo programmatically

Telegram

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).

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 V2

Both 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
}
namerequired
Token name. Non-empty.
symbolrequired
Token symbol. Non-empty, ≤ 96 bytes on-chain.
saltrequired
Vanity salt — the deployed address must end in 0x1110. See the Vanity Salt section for mining a compatible salt.
feeSharesrequired
Trading-fee recipients. Non-zero, unique accounts; every shares> 0; the array must sum to exactly 10000. At most one entry may set directFeesEnabled (fees forwarded on each accrual instead of pull-claimed).
liquidityTierrequired
Post-graduation pool depth + graduation marketcap. 0 = THIN (1.75 ETH liq / 6.125 ETH mcap), 1 = DEFAULT (3.5 / 12.25), 2 = THICK (7.0 / 24.5). Set it explicitly — a zero-initialised field resolves to THIN, not DEFAULT.

taxConfigs — 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 / sellTaxBps
Long-term static tax per direction. Capped by the total-fee rule lpFeeBps + 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.
taxDurationSeconds
Static-tax window length. 0 disables (then both bps must be 0). Max ~120 years. Anchored by startTaxFromLaunch.
startTaxFromLaunch
Window anchor for both the static tax and the decay leg. true → [launch, launch+duration] (taxed pre-graduation too); false → [graduation, graduation+duration] (no tax before graduation).
buyTaxDecayStartBps / sellTaxDecayStartBps
Optional linear launch-tax decay — a higher rate at the anchor decaying linearly down to the static rate (to 0 if untaxed). If set, must be strictly greater than the direction's static rate; combined (buy + sell) ≤ 2000 bps. 0 = no decay for that direction.
taxDecayDuration
Decay window length. 0 disables (then both decay-start bps must be 0). Max 20 minutes. If a static tax is also set, taxDurationSeconds ≥ taxDecayDuration.
Static tax and decay are independent — set either, both, or neither. The effective rate a trade pays per direction is 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
}
renounceOwnership
true → tokenOwner = zero address. false → tokenOwner = msg.sender.
lpFeeBps
Post-graduation Uniswap V4 hook fee selector. Must be 100 (1%) or 50 (0.5%) — anything else reverts.
V2 has no equivalent: V2 tokens are always deployed ownerless and carry no post-graduation LP fee.

buyOnDeployShares — SupplyShare[]

struct SupplyShare {
    address account;
    uint256 shares;   // bps, > 0; array sums to exactly 10000
}
buyOnDeployShares
Optional deployer buy. The SupplyShare[] 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
}
antiSniperConfigs
Opt-in via a non-zero protectionWindowSeconds. 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
}
creatorVaults
Optional vesting vaults that lock part of the supply at deploy. Empty array = none. Max 5 vaults; the sum of supplyBps≤ 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

referral
Reserved for future relayer payouts. Nothing is wired to it on-chain yet — a non-zero value only emits TokenReferral(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-data
txHashrequired
0x-prefixed, 66-character hex string (precomputed in Step 3)
namerequired
Token name, max 96 characters
symbolrequired
Max 96 characters
chainIdrequired
1 (mainnet), 11155111 (sepolia), 4663 (Robinhood), or 46630 (Robinhood testnet)
description
Max 250 characters
socials
JSON array of up to 5 http(s) URL strings (e.g. ["https://x.com/foo","https://t.me/bar"]). Icons are derived from each URL; invalid entries are dropped.
image
JPEG, PNG, GIF, or WebP, max 5MB. Optional — uploading a file is the slow path. Omit it and either pass imageUrl instead, or set the image later (see Step 6).
imageUrl
An already-pinned IPFS reference — ipfs://<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 4

Step 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>" }
txHash
0x-prefixed, 66-char hex. Preferred identifier. Provide this or tokenAddress.
tokenAddress
0x-prefixed token address. Alternative to txHash.
imageUrlrequired
IPFS reference only — ipfs://<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:

name
Metadata: 1-96 chars. On-chain: non-empty.
symbol
Metadata: 1-96 chars. On-chain: non-empty, ≤ 96 bytes.
feeShares / buyOnDeployShares
Every share > 0; unique non-zero accounts; must sum to exactly 10000.
buyTaxBps / sellTaxBps
lpFeeBps + tax ≤ 500 (V2: ≤ 500; V4: ≤ 400 or 450).
taxDurationSeconds
0 to disable, else up to ~120 years; if non-zero, a buy or sell tax must be set.
taxDecayDuration
0 to disable, else ≤ 20 min; combined decay start ≤ 2000 bps.
liquidityTier
0, 1, or 2 — set explicitly.
lpFeeBps (V4)
Must be 100 or 50.
antiSniper (when window enabled)
maxBuyPerTxBps 10-300; maxWalletBps 10-300 and ≥ maxBuyPerTxBps; window 60-86400; whitelist ≤ 20.
creatorVaults
supplyBps a multiple of 500, sum ≤ 3000; ≤ 5 vaults.

Key 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 image field is a raw file upload, which the API pins to IPFS via Pinata. To skip the upload, pass an already-pinned imageUrl (IPFS only) instead, or attach the image after creation via PATCH /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