Klik is a permissionless token launchpad on Ethereum mainnet and Base. Anyone can deploy a fully on-chain ERC-20 token with bootstrapped Uniswap V4 liquidity in a single transaction - no seed rounds, no VCs, no presales.
Every token launched through Klik:
69 (Ethereum) or 420 (Base), mined via CREATE2Note: In some cases, a token address might not start with 0x69 (or 0x420 on Base), and that is not an issue.
Klik charges a dynamic swap fee that decreases as the token's market cap grows. This rewards early believers and reduces friction for larger tokens. Fees are split between the platform and the token creator.
| Market Cap (ETH) | Total Fee | Platform | Creator |
|---|---|---|---|
| 0 - 15 ETH | 1.00% | 0.60% | 0.40% |
| 15 - 25 ETH | 0.95% | 0.55% | 0.40% |
| 25 - 50 ETH | 0.90% | 0.45% | 0.45% |
| 50 - 100 ETH | 0.85% | 0.42% | 0.43% |
| 100 - 200 ETH | 0.80% | 0.38% | 0.42% |
| 200 - 350 ETH | 0.75% | 0.35% | 0.40% |
| 350 - 550 ETH | 0.70% | 0.32% | 0.38% |
| 550 - 800 ETH | 0.65% | 0.29% | 0.36% |
| 800 - 1200 ETH | 0.62% | 0.26% | 0.36% |
| 1200 - 1600 ETH | 0.58% | 0.24% | 0.34% |
| 1600 - 2000 ETH | 0.54% | 0.22% | 0.32% |
| 2000 - 3000 ETH | 0.50% | 0.20% | 0.30% |
| 3000 - 4500 ETH | 0.35% | 0.13% | 0.22% |
| 4500 - 6000 ETH | 0.22% | 0.07% | 0.15% |
| >= 6000 ETH | 0.10% | 0.03% | 0.07% |
Pass the correct factory address for your target chain when calling deployCoin or generating a salt.
| Chain | Chain ID | Factory Address |
|---|---|---|
| Ethereum | 1 | 0x254Bf550657040f78608476cE9AaD820aB2266ad |
| Base | 8453 | 0xB6CB1c049ee8942683Fd3172f7EBA63B6e8a6835 |
deployCoin is the only on-chain step. Everything before it - hosting metadata, picking a salt - is up to you. The Klik APIs exist for convenience but are not required: any URL-hosted JSON and any random bytes32 work. Each step below explains the API path the in-app launch flow uses, plus the self-hosted alternative.
Your token needs an image URL inside its metadata JSON. You do not need to call our API for this - host the image on your own CDN, S3, R2, IPFS pin, anywhere reachable over HTTP(S), and reference that URL in the metadata JSON. The Klik in-app flow uses /api/uploadImage only as a convenience for users who don't have their own hosting; it's not part of the deploy contract.
deployCoin takes a string metadataUri. The contract stores it verbatim and never fetches it. You do not need to use our metadata API - any URL pointing at JSON in the structure below will work: an HTTPS URL on your own server (e.g. https://example.com/metadata.json), an ipfs://... URI, an Arweave link, anything. Klik and any third-party indexer that follows our schema will read it.
Required JSON structure:
{
"name": "My Token",
"symbol": "MYT",
"description": "Your token description",
"image": "https://example.com/logo.png",
"website": "https://mytoken.com",
"twitter": "https://x.com/mytoken",
"telegram": "https://t.me/mytoken"
}The Klik in-app flow uses /api/uploadMetadata to pin the JSON to IPFS for users who don't want to run their own host. It's a convenience layer - the contract treats your self-hosted URL identically.
deployCoin takes a bytes32 salt that determines the CREATE2 address of your token. You do not need to call our API for this - any random 32-byte value works, generated client-side in your frontend or backend:
// Random bytes32 (browser)
const salt = '0x' + Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// Random bytes32 (Node / ethers)
const salt = ethers.hexlify(ethers.randomBytes(32));Use /api/generate-salt only if you want a vanity address starting with 0x69 (Ethereum) or 0x420 (Base). The API mines salts on a private RPC pool until it finds one that produces the desired prefix. Skipping the call costs nothing - your token still deploys, just at a non-vanity address.
deployCoinfunction deployCoin(
string calldata name,
string calldata symbol,
string calldata metadataUri,
bytes32 salt,
uint256 configId
) external payable;metadataUri - any URL pointing at JSON in the structure above (the in-app flow passes the IPFS CID returned by /api/uploadMetadata, but https://example.com/metadata.json works just as well)salt - any 0x-prefixed bytes32. Use /api/generate-salt only if you want a vanity prefix; otherwise random bytes are fineconfigId - Ethereum uses 0 to 4; Base uses 0msg.value - optional creator buy amountEthereum liquidity tiers:
| configId | Starting Liquidity |
|---|---|
0 | 0.69 ETH |
1 | 1 ETH |
2 | 2 ETH |
3 | 5 ETH |
4 | 10 ETH |
Base: only configId = 0 is available.
If you pass a non-zero msg.value to deployCoin, the contract applies a penalty to discourage disproportionate supply accumulation by the creator at launch. The penalty is not a flat percentage — each liquidity config carries its ownpenaltyMultiplier, so the same creator buy amount results in a different penalty depending on which configId you picked.
The effective penalty is derived on-chain from two values:
getPenalty(ethAmount) — the base penalty for your buy size (in bps)getLiquidityConfig(configId).penaltyMultiplier — a per-config scaling factorFinal penalty ≈ getPenalty(msg.value) × penaltyMultiplier. Higher-liquidity tiers tend to carry a lower multiplier, so a creator buy on a deeper pool is penalized less than the same buy on a shallower pool. Always simulate deployCoin with your intended msg.value before sending the transaction — the simulation returns the exact token amount you would receive, inclusive of penalty, so you can confirm the outcome instead of deploying blind.
Every swap on a Klik V4 pool routes a slice of ETH through the hook. The hook sends the platform share straight to the platform treasury and parks the creator share inside the token contract itself. To withdraw that creator share to your wallet, the creator (or the platform admin) makes a single on-chain call:
function collectFees(address tokenAddress)
external
returns (uint256 ethCollected);creator on the token contract) or the platform admin. Anyone else gets "Not authorized".This applies to the new V4 factory only. V3-legacy tokens use a different flow (Uniswap NFT positions); not covered here.
ethers)import { ethers } from 'ethers';
const FACTORY_ADDRESS = '0x254Bf550657040f78608476cE9AaD820aB2266ad'; // Ethereum
const TOKEN_ADDRESS = '0xYourKlikTokenAddress';
const FACTORY_ABI = [
'function collectFees(address tokenAddress) returns (uint256 ethCollected)'
];
const provider = new ethers.JsonRpcProvider('https://rpc.mevblocker.io/fast');
const wallet = new ethers.Wallet('0xyour_creator_private_key', provider);
const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, wallet);
const tx = await factory.collectFees(TOKEN_ADDRESS);
console.log('Broadcasted:', tx.hash);
const receipt = await tx.wait();
console.log('Confirmed in block:', receipt.blockNumber);web3.py)from web3 import Web3
FACTORY_ADDRESS = '0x254Bf550657040f78608476cE9AaD820aB2266ad'
TOKEN_ADDRESS = '0xYourKlikTokenAddress'
FACTORY_ABI = [
{
'type': 'function',
'name': 'collectFees',
'stateMutability': 'nonpayable',
'inputs': [{'name': 'tokenAddress', 'type': 'address'}],
'outputs': [{'name': 'ethCollected', 'type': 'uint256'}],
}
]
w3 = Web3(Web3.HTTPProvider('https://rpc.mevblocker.io/fast'))
account = w3.eth.account.from_key('0xyour_creator_private_key')
factory = w3.eth.contract(
address=Web3.to_checksum_address(FACTORY_ADDRESS),
abi=FACTORY_ABI,
)
tx = factory.functions.collectFees(
Web3.to_checksum_address(TOKEN_ADDRESS)
).build_transaction({
'from': account.address,
'nonce': w3.eth.get_transaction_count(account.address),
})
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
print('Broadcasted:', tx_hash.hex())
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print('Confirmed in block:', receipt.blockNumber)The token contract holds the unclaimed creator share as plain ETH balance. Read it without sending a tx:
const claimable = await provider.getBalance(TOKEN_ADDRESS);
console.log('Claimable creator ETH:', ethers.formatEther(claimable));That value is exactly what collectFees would forward to the creator if called right now.
Base URL: https://klik.finance
/api/uploadTokenMetadata is the one-call developer flow. It is not the path used by the in-app launch modal, but it is useful for external integrations.
Content-Type: multipart/form-data or application/json
Use multipart when sending an image. JSON mode is text-only.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Token name |
symbol | string | Yes | Token ticker symbol |
description | string | No | Token description |
website | string | No | Project website URL |
twitter | string | No | Twitter/X handle or URL |
telegram | string | No | Telegram link |
image | file | No | Token image, multipart only, max 1 MB |
creator | string | No | Wallet address, required if you want salt generation |
factory | string | No | Factory address, defaults to Ethereum |
chainId | string | No | "1" or "8453" |
testnet | boolean | No | Use testnet RPC for salt generation |
{
"name": "My Token",
"symbol": "MYT",
"description": "Your token description",
"creator": "0xYourWalletAddress",
"chainId": "1",
"image": "<binary file>"
}{
"name": "My Token",
"symbol": "MYT",
"description": "Your token description",
"creator": "0xYourWalletAddress",
"chainId": "1"
}Response:
{
"cid": "QmXyz...",
"url": "QmXyz...",
"imageCid": "QmAbc...",
"salt": "0xdeadbeef...",
"saltAddress": "0x69ab12...",
"targetPrefix": "69",
"hasTargetPrefix": true
}Endpoint: POST /api/uploadImage
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Image file, max 1 MB |
{
"file": "<binary file>"
}Response:
{ "cid": "QmAbc...", "url": "QmAbc..." }Endpoint: POST /api/uploadMetadata
Content-Type: application/json
The frontend sends this payload before launch.
{
"name": "My Token",
"symbol": "MYT",
"description": "Your token description",
"image": "ipfs://QmImageCid...",
"website": "https://mytoken.com",
"twitter": "https://x.com/mytoken",
"telegram": "https://t.me/mytoken"
}Example payload:
{
"name": "My Token",
"symbol": "MYT",
"description": "Your token description",
"image": "ipfs://QmAbc...",
"website": "https://mytoken.com",
"twitter": "https://x.com/mytoken",
"telegram": "https://t.me/mytoken"
}Response:
{ "cid": "QmXyz...", "url": "QmXyz..." }Endpoint: POST /api/generate-salt
The frontend sends the chain-specific factory address along with the wallet address.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Token name |
symbol | string | Yes | Token symbol |
creator | string | Yes | Creator wallet address |
factory | string | No | Factory address, defaults to Ethereum factory |
chainId | string | No | "1" or "8453" |
testnet | boolean | No | Use testnet RPC |
num_matches | number | No | Defaults to 1 |
max_attempts | number | No | Defaults to 25000 |
{
"name": "My Token",
"symbol": "MYT",
"creator": "0xYourWalletAddress",
"factory": "0x254Bf550657040f78608476cE9AaD820aB2266ad",
"chainId": "1"
}Response:
{
"factory": "0x254Bf550657040f78608476cE9AaD820aB2266ad",
"target_prefix": "69",
"total_attempts": 4821,
"has_target_prefix": true,
"results": [
{ "salt": "0xdeadbeef...", "address": "0x69ab12..." }
]
}If has_target_prefix is false, the endpoint still returns a valid fallback salt.
All endpoints enforce per-IP rate limits.
| Endpoint | Per Second | Per Minute |
|---|---|---|
/api/uploadImage | 30 | 120 |
/api/uploadMetadata | 30 | 120 |
/api/uploadTokenMetadata | 10 | 60 |
/api/generate-salt | - | 60 |
/api/search | 6 | 100 |
/api/sort-mcap | 6 | 40 |
/api/sort-trending | 4 | 30 |
/api/deployed-tokens | 4 | 20 |
/api/ecosystem-stats | 4 | 20 |
{
"error": "Rate limit exceeded",
"message": "Too many requests per second. Please slow down.",
"resetIn": 1,
"type": "second"
}These examples match the current app flow more closely than the previous version: metadata upload, salt generation, then deployCoin.
ethers)Install: npm install ethers
import { ethers } from 'ethers';
const FACTORY_ABI = [
'function deployCoin(string name, string symbol, string metadataUri, bytes32 salt, uint256 configId) payable'
];
const RPC_URL = 'https://rpc.mevblocker.io/fast';
const PRIVATE_KEY = '0xyour_private_key_here';
const CHAIN_ID = '1';
const FACTORY_ADDRESS = '0x254Bf550657040f78608476cE9AaD820aB2266ad';
const TOKEN_NAME = 'My Token';
const TOKEN_SYMBOL = 'MYT';
const TOKEN_DESCRIPTION = 'Your token description';
const CONFIG_ID = 1n;
const CREATOR_BUY_ETH = '0';
async function main() {
const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const creator = await wallet.getAddress();
const metadataRes = await fetch('https://klik.finance/api/uploadMetadata', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: TOKEN_NAME,
symbol: TOKEN_SYMBOL,
description: TOKEN_DESCRIPTION,
image: '',
website: '',
twitter: '',
telegram: '',
}),
});
if (!metadataRes.ok) throw new Error(await metadataRes.text());
const { cid } = await metadataRes.json();
const saltRes = await fetch('https://klik.finance/api/generate-salt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: TOKEN_NAME,
symbol: TOKEN_SYMBOL,
creator,
factory: FACTORY_ADDRESS,
chainId: CHAIN_ID,
}),
});
if (!saltRes.ok) throw new Error(await saltRes.text());
const saltPayload = await saltRes.json();
const salt = saltPayload.results[0].salt;
const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, wallet);
const tx = await factory.deployCoin(
TOKEN_NAME,
TOKEN_SYMBOL,
cid,
salt,
CONFIG_ID,
{ value: ethers.parseEther(CREATOR_BUY_ETH) }
);
console.log('Broadcasted tx:', tx.hash);
const receipt = await tx.wait();
console.log('Confirmed in block:', receipt.blockNumber);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});web3.py)Install: pip install web3 requests
import requests
from web3 import Web3
FACTORY_ABI = [
{
"type": "function",
"name": "deployCoin",
"stateMutability": "payable",
"inputs": [
{"name": "name", "type": "string"},
{"name": "symbol", "type": "string"},
{"name": "metadataUri", "type": "string"},
{"name": "salt", "type": "bytes32"},
{"name": "configId", "type": "uint256"}
],
"outputs": []
}
]
RPC_URL = "https://rpc.mevblocker.io/fast"
PRIVATE_KEY = "0xyour_private_key_here"
CHAIN_ID = "1"
FACTORY_ADDRESS = "0x254Bf550657040f78608476cE9AaD820aB2266ad"
TOKEN_NAME = "My Token"
TOKEN_SYMBOL = "MYT"
TOKEN_DESCRIPTION = "Your token description"
CONFIG_ID = 1
CREATOR_BUY_ETH = "0"
w3 = Web3(Web3.HTTPProvider(RPC_URL))
account = w3.eth.account.from_key(PRIVATE_KEY)
creator = account.address
metadata_res = requests.post(
"https://klik.finance/api/uploadMetadata",
json={
"name": TOKEN_NAME,
"symbol": TOKEN_SYMBOL,
"description": TOKEN_DESCRIPTION,
"image": "",
"website": "",
"twitter": "",
"telegram": "",
},
timeout=60,
)
metadata_res.raise_for_status()
cid = metadata_res.json()["cid"]
salt_res = requests.post(
"https://klik.finance/api/generate-salt",
json={
"name": TOKEN_NAME,
"symbol": TOKEN_SYMBOL,
"creator": creator,
"factory": FACTORY_ADDRESS,
"chainId": CHAIN_ID,
},
timeout=60,
)
salt_res.raise_for_status()
salt = salt_res.json()["results"][0]["salt"]
factory = w3.eth.contract(
address=Web3.to_checksum_address(FACTORY_ADDRESS),
abi=FACTORY_ABI,
)
deploy_call = factory.functions.deployCoin(
TOKEN_NAME,
TOKEN_SYMBOL,
cid,
bytes.fromhex(salt[2:]),
CONFIG_ID,
)
value_wei = w3.to_wei(CREATOR_BUY_ETH, "ether")
tx = deploy_call.build_transaction(
{
"from": creator,
"chainId": int(CHAIN_ID),
"nonce": w3.eth.get_transaction_count(creator),
"value": value_wei,
}
)
tx["gas"] = deploy_call.estimate_gas({"from": creator, "value": value_wei})
latest_block = w3.eth.get_block("latest")
if latest_block.get("baseFeePerGas") is not None:
tx["maxPriorityFeePerGas"] = w3.to_wei(1, "gwei")
tx["maxFeePerGas"] = latest_block["baseFeePerGas"] * 2 + tx["maxPriorityFeePerGas"]
else:
tx["gasPrice"] = w3.eth.gas_price
signed_tx = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print("Broadcasted tx:", tx_hash.hex())
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print("Confirmed in block:", receipt.blockNumber)