API
Create tokens on Realm programmatically
Overview
Token creation on Realm follows a strict order: build the create-token transaction and precompute its hash, submit the token metadata (name, image, socials) to the Realm 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).
Venue: launch through RealmFactoryUniV4Direct — tokens trade on Uniswap V4 from the first block.
Test first: every step works end to end on Robinhood testnet against the testnet API — see Networks and the runnable scripts.
Networks
Each network has its own API: the testnet API only accepts chainId 46630, the mainnet API only 4663. Use the API base below as the host for every endpoint on this page.
Robinhood testnet
chainIdAPI baseRPCRealmFactoryUniV4DirectAll contractsRobinhood mainnet
chainIdAPI baseRPCRealmFactoryUniV4DirectAll contractsStep 1: Authenticate
All API calls require a JWT Bearer token. Obtain one by signing a message with your wallet. Tokens expire after 30 days.
POST /api/auth/wallet
Content-Type: application/json
{
"address": "0xYourWalletAddress",
"signature": "<signature>",
"message": "Sign in to Realm\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
Tokens are created by RealmFactoryUniV4Direct. It seeds the circulating supply into one Uniswap V4 pool per pair (native and/or any whitelisted ERC20 quote, up to 3) as single-sided liquidity, opening at a fixed 1.125 ETH-equivalent market cap priced at launch from each quote's own pool. The creator's LP fee share is live from the first swap.
Every knob (fee split, tax, anti-sniper, creator vaults, dev buy) is a struct argument; disabled features are passed as zeroed structs or empty arrays. See The Direct Factory and Struct Reference below.
Factory addresses are listed under Networks. All contracts are verified on Blockscout — fetch ABIs from there, or use the inline ABIs in the runnable scripts.
The Direct Factory (Uniswap V4)
There is no bonding curve: the circulating supply is seeded into one Uniswap V4 pool per pair as single-sided liquidity, and the token trades from the first block.
function createToken(
DirectTokenSetup setup,
DirectPair[] pairs,
TaxConfigsWithDirectAllocation taxAllocationConfigs,
AntiSniperConfigs antiSniperConfigs,
CreatorVault[] creatorVaults,
DevBuy devBuy,
address referral
) payable returns (address token);
struct DirectTokenSetup {
string name;
string symbol;
bytes32 salt; // any value; namespaced by msg.sender (see Salt)
FeeShare[] feeShares; // shares sum to 10000
bool renounceOwnership; // true: deployed ownerless; false: owner = msg.sender
uint16 lpFeeBps; // 100 (1%) or 50 (0.5%), else InvalidLpFeeBps
}
struct DirectPair {
address quote; // address(0) for the chain's native coin, else a whitelisted ERC20
uint16 weightBps; // this pool's share of the circulating supply
}
struct DevBuy {
uint8 pairIndex; // which pool the buy runs on
PoolKey[] route; // MUST be empty - the factory derives the route from the whitelist
uint256 minQuoteOut; // floor on the in-launch conversion; 0 to disable
uint256 quoteAmount; // raw quote the factory pulls from the caller
SupplyShare[] recipients; // who receives the bought tokens; shares sum to 10000
}
struct TaxConfigsWithDirectAllocation {
uint16 buyTaxBps;
uint16 sellTaxBps;
uint32 taxDurationSeconds;
bool startTaxFromLaunch;
uint16 buyTaxDecayStartBps;
uint16 sellTaxDecayStartBps;
uint32 taxDecayDuration;
EarningsAllocationMultiConfig earningsAllocation;
bytes[] quoteRoutes; // positional to `pairs`: each quote's native->quote V4 route, needed
// only where a dividends leg must leave that quote; "0x" otherwise,
// trailing empties may be omitted. InvalidQuoteRoutes if malformed.
}
struct EarningsAllocationMultiConfig {
uint16 burnBps;
uint16 dividendsBps;
uint16 liquidityBps;
address[] dividendTokens;
uint16[] dividendWeightsBps;
bytes[] dividendRoutes;
}AntiSniperConfigs, CreatorVault, FeeShare and SupplyShare, and the tax and earnings-allocation fields, are documented under Struct Reference.
Pairs
At most MAX_PAIRS() (3) entries, quotes distinct, every weightBps > 0 and the set summing to exactly 10000 — otherwise InvalidPairs. A quote that is not on RealmAssetsWhitelist reverts with QuoteNotSupported, and one whose decimals the factory cannot handle with UnsupportedDecimals.
Funding the dev buy
Three mutually exclusive modes, all on pairs[devBuy.pairIndex]:
pool's quote | paid with | quoteAmount | minQuoteOut | msg.value
--------------|--------------------------------|-------------|-------------|-------------------
native (0x0) | msg.value | 0 | 0 | amount to spend
ERC20 | the quote, pulled by the factory | raw amount | 0 | 0
ERC20 | native, converted in-launch | 0 | conversion floor | ETH to convertIn the third mode the factory spends the WHOLE proceeds of the conversion on the buy, so minQuoteOut (raw quote decimals) is a floor on what the buy receives, not a refundable slippage cap. It converts through the quote's own whitelist price pool(s) and reverts with DevBuyRouteUnavailable when that quote has no V4 route back to native. In the quote-funded mode, approve the factory for quoteAmount first. Leave recipients empty for no dev buy; a mismatch between the funding and the mode reverts with InvalidDevBuy.
Where a pool opens
Fixed and factory-controlled — there is no launch price in the calldata:
function LAUNCH_MARKET_CAP_X18() view returns (uint256); // 1.125e18: the opening market cap
// of EVERY pair, native-denominated
function previewLaunchTick(address quote)
view returns (int24 tick, uint256 priceX18, uint256 marketCapX18);previewLaunchTick prices LAUNCH_MARKET_CAP_X18 against quote at the whitelist's LIVE rate and returns where that pool would open; priceX18 and marketCapX18 are in WHOLE quote units scaled by 1e18. It reverts (QuoteNotSupported) under exactly the conditions createToken would, so treat a revert as "this pair cannot be launched", not as a transient read failure.
A native pair is priced 1:1 and always opens at exactly 1.125 ETH; an ERC20 pair opens at whatever 1.125 ETH is worth in that quote at its live whitelist rate.
Struct Reference
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 (creator vaults, dev-buy recipients). Pass address(0) for referral for now (see below).
setup — DirectTokenSetup
struct FeeShare {
address account;
uint256 shares; // bps, > 0; array sums to exactly 10000
bool directFeesEnabled; // at most ONE entry may be true
}namerequiredsymbolrequiredsaltrequiredbytes32, e.g. random. See the Salt section.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).renounceOwnershiptrue deploys the token ownerless; false makes the sender its owner.lpFeeBpsrequiredInvalidLpFeeBps.taxAllocationConfigs — TaxConfigsWithDirectAllocation
Struct layout in The Direct Factory.
buyTaxBps / sellTaxBpslpFeeBps + tax ≤ 500: up to 400 bps with a 1% LP fee, 450 bps with 0.5%. 0 disables static tax.taxDurationSecondsstartTaxFromLaunchbuyTaxDecayStartBps / sellTaxDecayStartBpstaxDecayDurationtaxDurationSeconds ≥ taxDecayDuration.—max(decay, static).earningsAllocationMissingQuoteRoute(quote) — supply it in quoteRoutes. All three buckets accrue on each earnings accrual and are settled out-of-band by processBurn() / processLiquidity() / processDividends() on the token.dividendTokens / dividendWeightsBpsaddress(0) = the chain's native currency, address(type(uint160).max) = the token itself, any other address = an ERC20 the token buys on each distribution. Weights are shares of the dividends slice and must each be non-zero and sum to exactly 10000. Assets must be distinct (InvalidDividendAssetSet), the self-token sentinel is only legal as the sole asset (SelfTokenDividendMustBeSole), and naming any asset while dividendsBps == 0 reverts DividendAssetWithoutShare. Every third-party ERC20 in the set needs a route, registered at creation with RealmSwapper. There is no whitelist and no review: you name the pools your token converts through, as dividendRoutes[i], and the registry checks only its shape, reverting MalformedRoute if it does not end at the asset. 0x02 alone selects the asset's WETH pair on Uniswap V2; a leading 0x04 carries an abi-encoded Hop[] for Uniswap V4 (starting from native) and 0x03 Uniswap V3's packed path from WETH. An empty route for an asset the token has to buy reverts the creation with MissingDividendRoute(asset), unless the registry already holds an admin route for it. Quote liquidity yourself before choosing: only a registry admin can repoint a route afterwards.dividends, once livebalance x time; there are no rounds, snapshots or minimum-balance rules, and one stream per configured asset. Read a holder's accrual at the current block with previewDividend(holder, assetIndex) (the one-argument form answers for asset 0); dividendAssetCount() and dividendAssets(i) enumerate the set and its per-asset stream state. Payouts are pushed per asset by a keeper calling processDividends(assetIndex, fund, amount, minOut, holders) (the two-argument (minOut, holders) form services asset 0; direct V4 tokens also take (assetIndex, quote, amount, minOut, holders) to convert an ERC20 quote's buffer), and any holder a batch skips can pull every asset at once with claimDividends(). Each extra asset costs about 5,500 more gas on every transfer of the token, so the set is a real cost to holders, not just to the deployer.devBuy.recipients — SupplyShare[]
struct SupplyShare {
address account;
uint256 shares; // bps, > 0; array sums to exactly 10000
}recipients[] when not buying.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%).referral — address
referralTokenReferral(token, referral), with no storage or payout. Pass address(0) for now.Salt
The factory deploys through CREATE2, but the token address has no required pattern: any bytes32 salt works, so pick a random one. The factory namespaces it by the deployer — the effective CREATE2 salt is keccak256(abi.encodePacked(msg.sender, salt)) — so a salt seen in the mempool can't be front-run into the same address by another sender.
import { toHex } from "viem";
const salt = toHex(crypto.getRandomValues(new Uint8Array(32)));Step 3: Precompute the Transaction Hash
Encode the createToken call, build an EIP-1559 transaction (chainId, nonce, gas, fee fields, data, value), 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";
// setup, pairs, taxAllocationConfigs, antiSniperConfigs, devBuy and value built as in Step 2;
// salt from the Salt section.
const data = encodeFunctionData({
abi: factoryAbi,
functionName: "createToken",
args: [setup, pairs, taxAllocationConfigs, antiSniperConfigs, creatorVaults, devBuy, referral],
});
const fees = await publicClient.estimateFeesPerGas();
const tx = {
type: "eip1559",
chainId,
nonce: await publicClient.getTransactionCount({ address: account.address }),
to: factoryAddress,
data,
value, // 0n unless the dev buy is paid in native (see Funding the dev buy)
gas: await publicClient.estimateGas({ account, to: factoryAddress, data, value }),
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 minutes of 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 Struct Reference; the ones most likely to revert:
namesymbolfeeShares / devBuy.recipientsbuyTaxBps / sellTaxBpstaxDurationSecondstaxDecayDurationlpFeeBpsantiSniper (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
)Parse TokenCreated from the transaction receipt to get the created token address. launchpad is address(0) (there is no curve to trade against) and graduator is the contract that owns the token's pools.
Notes
- Token creation is free (no ETH cost beyond gas) — unless you buy supply on deploy (
value> 0, or a pulledquoteAmountof an ERC20 quote). - 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).
Runnable Scripts
A self-contained Node script (viem only, ABI inlined) that runs Steps 1-5 end to end and prints the token page. It defaults to Robinhood testnet: install viem, set a funded key, run, then edit the config block at the top (name, symbol, socials, image, dev buy) and switch NETWORK to "mainnet" when ready.
npm i viem
PRIVATE_KEY=0x... node create-token-v4.mjscreate-token-v4.mjs — download
// Create a token on Realm through RealmFactoryUniV4Direct (Uniswap V4, no bonding curve).
// Setup: `npm i viem`, then `PRIVATE_KEY=0x... node create-token-v4.mjs`.
// Runs on Robinhood testnet by default; set NETWORK = "mainnet" once it works there.
import {
createPublicClient, createWalletClient, http, parseAbi, parseEventLogs,
encodeFunctionData, keccak256, toHex, zeroAddress,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
const NETWORK = "testnet";
const NETWORKS = {
mainnet: {
chainId: 4663,
rpc: "https://rpc.mainnet.chain.robinhood.com",
api: "https://realm.trade",
factory: "0xC763b1795DaBe2D2336EbA214d16969EBc5db27F",
},
testnet: {
chainId: 46630,
rpc: "https://rpc.testnet.chain.robinhood.com",
api: "https://realm-rh-dev.vercel.app",
factory: "0xF0399F67e359c08A18816E466364797b7fBc4df4",
},
};
const net = NETWORKS[NETWORK];
// ---- Token config: tweak here ----
const NAME = "My Token";
const SYMBOL = "MTK";
const DESCRIPTION = "Launched through the Realm API";
const SOCIALS = ["https://x.com/yourproject"];
const IMAGE_FILE = ""; // optional local image (jpeg/png/gif/webp, <= 5MB), uploaded with the metadata
const IMAGE_URL = ""; // or an already-pinned IPFS url (ipfs://<cid>), faster than uploading
const DEV_BUY_ETH = 0n; // wei of ETH to buy at launch on the native pool; 0n = no dev buy
const factoryAbi = parseAbi([
"struct FeeShare { address account; uint256 shares; bool directFeesEnabled; }",
"struct DirectTokenSetup { string name; string symbol; bytes32 salt; FeeShare[] feeShares; bool renounceOwnership; uint16 lpFeeBps; }",
"struct DirectPair { address quote; uint16 weightBps; }",
"struct EarningsAllocationMultiConfig { uint16 burnBps; uint16 dividendsBps; uint16 liquidityBps; address[] dividendTokens; uint16[] dividendWeightsBps; bytes[] dividendRoutes; }",
"struct TaxConfigsWithDirectAllocation { uint16 buyTaxBps; uint16 sellTaxBps; uint32 taxDurationSeconds; bool startTaxFromLaunch; uint16 buyTaxDecayStartBps; uint16 sellTaxDecayStartBps; uint32 taxDecayDuration; EarningsAllocationMultiConfig earningsAllocation; bytes[] quoteRoutes; }",
"struct AntiSniperConfigs { uint16 maxBuyPerTxBps; uint16 maxWalletBps; uint40 protectionWindowSeconds; address[] whitelist; }",
"struct CreatorVault { address owner; uint256 supplyBps; uint256 cliffSeconds; uint256 vestingSeconds; }",
"struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; }",
"struct SupplyShare { address account; uint256 shares; }",
"struct DevBuy { uint8 pairIndex; PoolKey[] route; uint256 minQuoteOut; uint256 quoteAmount; SupplyShare[] recipients; }",
"function createToken(DirectTokenSetup setup, DirectPair[] pairs, TaxConfigsWithDirectAllocation taxAllocationConfigs, AntiSniperConfigs antiSniperConfigs, CreatorVault[] creatorVaults, DevBuy devBuy, address referral) payable returns (address)",
"event TokenCreated(address indexed token, string name, string symbol, address tokenOwner, address launchpad, address graduator, address feeHandler)",
]);
const chain = {
id: net.chainId,
name: `Robinhood ${NETWORK}`,
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: [net.rpc] } },
};
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const publicClient = createPublicClient({ chain, transport: http() });
const walletClient = createWalletClient({ account, chain, transport: http() });
// Single native (ETH) pool, 1% LP fee, no tax, no anti-sniper, no vaults.
const createArgs = (salt) => [
{
name: NAME,
symbol: SYMBOL,
salt,
feeShares: [{ account: account.address, shares: 10000n, directFeesEnabled: false }],
renounceOwnership: false,
lpFeeBps: 100,
},
[{ quote: zeroAddress, weightBps: 10000 }],
{
buyTaxBps: 0, sellTaxBps: 0, taxDurationSeconds: 0, startTaxFromLaunch: false,
buyTaxDecayStartBps: 0, sellTaxDecayStartBps: 0, taxDecayDuration: 0,
earningsAllocation: {
burnBps: 0, dividendsBps: 0, liquidityBps: 0,
dividendTokens: [], dividendWeightsBps: [], dividendRoutes: [],
},
quoteRoutes: [],
},
{ maxBuyPerTxBps: 0, maxWalletBps: 0, protectionWindowSeconds: 0, whitelist: [] },
[],
{
pairIndex: 0, route: [], minQuoteOut: 0n, quoteAmount: 0n,
recipients: DEV_BUY_ETH > 0n ? [{ account: account.address, shares: 10000n }] : [],
},
zeroAddress,
];
// 1. Authenticate
const message = `Sign in to Realm\nTimestamp: ${Date.now()}`;
const authRes = await fetch(`${net.api}/api/auth/wallet`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: account.address, signature: await walletClient.signMessage({ message }), message }),
});
const { token: jwt } = await authRes.json();
if (!jwt) throw new Error(`auth failed: ${authRes.status}`);
// 2. Any salt works; the factory namespaces it by sender (CREATE2 salt = keccak256(deployer ++ salt))
const salt = toHex(crypto.getRandomValues(new Uint8Array(32)));
// 3. Sign offline and precompute the txHash
const data = encodeFunctionData({ abi: factoryAbi, functionName: "createToken", args: createArgs(salt) });
const fees = await publicClient.estimateFeesPerGas();
const signedTx = await walletClient.signTransaction({
type: "eip1559",
chainId: chain.id,
nonce: await publicClient.getTransactionCount({ address: account.address }),
to: net.factory,
data,
value: DEV_BUY_ETH,
gas: await publicClient.estimateGas({ account, to: net.factory, data, value: DEV_BUY_ETH }),
maxFeePerGas: fees.maxFeePerGas,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
});
const txHash = keccak256(signedTx);
// 4. Submit metadata, keyed by the txHash...
const form = new FormData();
form.append("txHash", txHash);
form.append("name", NAME);
form.append("symbol", SYMBOL);
form.append("chainId", String(chain.id));
form.append("description", DESCRIPTION);
form.append("socials", JSON.stringify(SOCIALS));
if (IMAGE_FILE) {
const type = `image/${IMAGE_FILE.split(".").pop().toLowerCase().replace("jpg", "jpeg")}`;
form.append("image", new Blob([await readFile(IMAGE_FILE)], { type }), basename(IMAGE_FILE));
}
if (IMAGE_URL) form.append("imageUrl", IMAGE_URL);
const metaRes = await fetch(`${net.api}/api/tokens/create`, {
method: "POST",
headers: { Authorization: `Bearer ${jwt}` },
body: form,
});
if (!metaRes.ok) throw new Error(`metadata POST failed: ${metaRes.status} ${await metaRes.text()}`);
// 5. ...and broadcast immediately after
await publicClient.sendRawTransaction({ serializedTransaction: signedTx });
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
const [created] = parseEventLogs({ abi: factoryAbi, eventName: "TokenCreated", logs: receipt.logs });
console.log(`${receipt.status}: ${net.api}/token/${created.args.token.toLowerCase()}`);
Bonding-Curve Launches (Uniswap V2)
Realm also supports launching on a bonding curve that graduates to Uniswap V2, through a separate factory with its own interface. It is not covered here — if you want to launch that way, reach out on Telegram and we'll help you integrate it.