based alpha
Create token

Developer docs

Automate launches and trades

Every Based Alpha token is a bonding curve you can drive directly on chain. This page has the contract addresses, the ABI, and copy-paste examples in viem and Foundry cast for launching a token, buying, and selling. No API key and no SDK are required.

Overview

A launch mints a fixed 1,000,000,000 supply. 793.1M sells along a constant-product bonding curve; the rest is reserved for liquidity. Buys and sells run against the curve until it sells out, at which point the token graduates: the reserved tokens and the raised ETH are paired as concentrated liquidity on the Based DEX, and trading continues there. Every trade pays a flat 1.25% fee (0.95% protocol, 0.30% creator). Creation itself is free.

All of this is plain contract calls. You interact with one launchpad address for creation and curve trading, and with the DEX router once a token has graduated.

Network and addresses

ChainRobinhood Chain, ID 4663
RPChttps://rpc.mainnet.chain.robinhood.com
Explorerhttps://robinhoodchain.blockscout.com
Launchpad0x5640c62Fe43a64f9Ae0811114874E95A819dB744
WETH0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73
DEX SmartRouter0x6841F504d5eBF6595388eFD80a91b0388686DF30
DEX QuoterV20xF97a3bD1EFa5A38bca072DEd6E30c6C2d86BEc1f

The launchpad address is a proxy and is stable across upgrades. Always read live values (fees, quotes) from the contract rather than hardcoding them.

ABI

You only need a handful of functions. Here is a minimal human-readable ABI for viem. For other stacks, the same signatures work with ethers or any ABI-encoding library.

ts
import { parseAbi } from "viem";

export const launchpadAbi = parseAbi([
  "struct Curve { uint128 virtualEth; uint128 virtualToken; uint128 realEth; uint128 tokensSold; bool graduated; bool migrated; address creator; }",

  // write
  "function createToken(string name, string symbol, string metadataURI) payable returns (address token)",
  "function buy(address token, uint256 minTokensOut) payable returns (uint256 tokensOut)",
  "function sell(address token, uint256 tokenAmount, uint256 minEthOut) returns (uint256 ethOut)",

  // read
  "function quoteBuy(address token, uint256 ethAmount) view returns (uint256 tokensOut)",
  "function quoteSell(address token, uint256 tokenAmount) view returns (uint256 ethOut)",
  "function getPrice(address token) view returns (uint256 weiPerToken)",
  "function getCurve(address token) view returns (Curve)",
  "function creationFee() view returns (uint96)",
  "function POOL_SENTINEL_WETH_AMOUNT() view returns (uint256)",
  "function tokenCount() view returns (uint256)",
  "function allTokens(uint256 index) view returns (address)",

  // events
  "event TokenCreated(address indexed token, address indexed creator, string name, string symbol, string metadataURI, uint256 virtualEth, uint256 virtualToken)",
  "event Trade(address indexed token, address indexed trader, bool isBuy, uint256 ethAmount, uint256 tokenAmount, uint256 protocolFee, uint256 creatorFee, uint256 virtualEth, uint256 virtualToken, uint256 tokensSold)",
  "event Graduated(address indexed token, uint256 ethLiquidity, uint256 tokenLiquidity)",
  "event Migrated(address indexed token, address indexed pool, uint256 ethAdded, uint256 tokensAdded, uint256 migrationFee)",
]);

Client setup used by every example below.

ts
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { launchpadAbi } from "./abi";

const LAUNCHPAD = "0x5640c62Fe43a64f9Ae0811114874E95A819dB744";

export const chain = {
  id: 4663,
  name: "Robinhood Chain",
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
} as const;

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
export const publicClient = createPublicClient({ chain, transport: http() });
export const wallet = createWalletClient({ account, chain, transport: http() });

Launch a token

createToken takes a name, a symbol, and a metadata URI, and returns the new token address. The name and symbol you pass become the on-chain ERC20 name and symbol. The metadata URI points at a JSON file, hosted by you, that carries the display details (logo, description, links).

Token metadata

The URI stored on chain is just a pointer. It can be any https:// URL or an ipfs:// URI, which the app resolves through a public gateway. The file is fetched once and cached, so host it somewhere durable. It is a JSON object with these fields:

FieldTypeRequiredNotes
namestringyesDisplay name. Match the on-chain name.
symbolstringyesTicker. Match the on-chain symbol.
descriptionstringnoShort blurb shown on the token page.
imagestring (url)noLogo URL. png, jpg, gif, or webp. ipfs:// is resolved via a gateway.
websitestring (url)noMust be http(s) to render.
twitterstring (url)noMust be http(s) to render.
telegramstring (url)noMust be http(s) to render.

A complete file looks like this:

json
{
  "name": "My Token",
  "symbol": "MYT",
  "description": "One or two sentences on what the token is.",
  "image": "https://your-host.example/logo.png",
  "website": "https://mytoken.example",
  "twitter": "https://x.com/mytoken",
  "telegram": "https://t.me/mytoken"
}

The image is a URL to a separately hosted file, not embedded bytes, so host the image first and reference its URL. For metadata that can never change, pin the JSON to IPFS and pass the ipfs:// URI. Building and hosting it:

ts
import { readFile } from "node:fs/promises";

// Bring your own hosting. Anything that returns a public https:// or ipfs://
// URL works: an IPFS pinning service (Pinata, web3.storage), S3, or your own
// server. Two uploads: the image first, then the JSON that references it.
async function upload(data: Uint8Array | string, contentType: string): Promise<string> {
  // ... send to your host, return the resulting public URL
  throw new Error("implement with your storage provider");
}

// 1. Host the image, get its URL. png, jpg, gif, or webp.
const image = await upload(await readFile("./logo.png"), "image/png");

// 2. Build the metadata and host it. The returned URL is your metadataURI.
const metadata = {
  name: "My Token",     // keep in sync with the on-chain name below
  symbol: "MYT",        // keep in sync with the on-chain symbol below
  description: "One or two sentences on what the token is.",
  image,                // url from step 1
  website: "https://mytoken.example",
  twitter: "https://x.com/mytoken",
  telegram: "https://t.me/mytoken",
};
const metadataURI = await upload(JSON.stringify(metadata), "application/json");

// 3. Pass metadataURI to createToken (next example).

Or skip hosting: use ours

If you would rather not run your own storage, Based Alpha hosts launch assets for you. It takes a one-time wallet signature (no gas, moves no funds) that authorizes the uploads for ten minutes; reuse it across the image and metadata calls. This is a convenience, rate-limited per wallet, and not a guaranteed-permanent store. For full control or immutability, self-host or pin to IPFS as above.

ts
import { readFile } from "node:fs/promises";
import { privateKeyToAccount } from "viem/accounts";

const BASE = "https://alpha.based.one";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

// Sign the upload authorization once (no gas). The message must match exactly.
const issued = Date.now();
const message = [
  "Based Alpha — authorize launch asset upload",
  "",
  "This lets alpha.based.one store your token's image and metadata.",
  "It does not move funds or approve transactions.",
  `Address: ${account.address.toLowerCase()}`,
  `Issued: ${new Date(issued).toISOString()}`,
].join("\n");
const signature = await account.signMessage({ message });
const auth = {
  "x-upload-address": account.address,
  "x-upload-issued": String(issued),
  "x-upload-signature": signature,
};

// 1. Upload the image (png/jpg/gif/webp, <= 4MB) -> { url }
const imageForm = new FormData();
imageForm.set("file", new Blob([await readFile("./logo.png")], { type: "image/png" }));
imageForm.set("symbol", "MYT");
const { url: image } = await fetch(`${BASE}/api/launch-assets?asset=image`, {
  method: "POST", headers: auth, body: imageForm,
}).then((r) => r.json());

// 2. Upload the metadata JSON -> { metadataUri } for createToken.
const { metadataUri } = await fetch(`${BASE}/api/launch-assets?asset=metadata`, {
  method: "POST",
  headers: { "content-type": "application/json", ...auth },
  body: JSON.stringify({
    name: "My Token", symbol: "MYT", description: "One or two sentences.",
    image, website: "https://mytoken.example", twitter: "https://x.com/mytoken",
  }),
}).then((r) => r.json());

Create the token

One detail that trips people up: the call reserves and seeds the token's graduation pool in the same transaction, so msg.value must cover the creation fee plus a 1 wei pool sentinel. Anything above that is treated as a dev buy (you buying your own token on launch). A bare zero-value create reverts.

ts
import { parseEther, parseEventLogs } from "viem";
import { launchpadAbi } from "./abi";
import { publicClient, wallet, chain } from "./client";

const LAUNCHPAD = "0x5640c62Fe43a64f9Ae0811114874E95A819dB744";

// 1. Host a metadata JSON anywhere reachable (IPFS or HTTPS). Shape:
//    { name, symbol, description, image, website, twitter, telegram }
const metadataURI = "https://your-host.example/my-token.json";

// 2. msg.value must cover creationFee + a 1 wei pool sentinel, plus any
//    optional dev buy (ETH spent buying your own token in the same tx).
const [creationFee, sentinel] = await Promise.all([
  publicClient.readContract({ address: LAUNCHPAD, abi: launchpadAbi, functionName: "creationFee" }),
  publicClient.readContract({ address: LAUNCHPAD, abi: launchpadAbi, functionName: "POOL_SENTINEL_WETH_AMOUNT" }),
]);
const devBuy = parseEther("0.01"); // 0n for no dev buy
const value = creationFee + sentinel + devBuy;

const hash = await wallet.writeContract({
  address: LAUNCHPAD,
  abi: launchpadAbi,
  functionName: "createToken",
  args: ["My Token", "MYT", metadataURI],
  value,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });

// 3. The new token address comes from the TokenCreated event.
const [created] = parseEventLogs({ abi: launchpadAbi, eventName: "TokenCreated", logs: receipt.logs });
const token = created.args.token;
console.log("launched", token);

Buy

Send ETH as the transaction value and pass a minTokensOut floor for slippage protection. Quote first with quoteBuy, which accounts for fees, then apply your tolerance.

ts
import { parseEther } from "viem";
import { launchpadAbi } from "./abi";
import { publicClient, wallet } from "./client";

const LAUNCHPAD = "0x5640c62Fe43a64f9Ae0811114874E95A819dB744";
const token = "0xYourToken";

// Decide how much ETH to spend. The 1.25% trade fee comes out of this amount.
const spend = parseEther("0.05");

// Quote, then set a floor with your slippage tolerance.
const expected = await publicClient.readContract({
  address: LAUNCHPAD, abi: launchpadAbi, functionName: "quoteBuy", args: [token, spend],
});
const minTokensOut = (expected * 99n) / 100n; // 1% slippage

await wallet.writeContract({
  address: LAUNCHPAD, abi: launchpadAbi, functionName: "buy",
  args: [token, minTokensOut], value: spend,
});

Sell

Selling is two steps: approve the launchpad to move your tokens, then call sell with a minEthOut floor. Use quoteSell for the expected return.

ts
import { parseAbi } from "viem";
import { launchpadAbi } from "./abi";
import { publicClient, wallet } from "./client";

const LAUNCHPAD = "0x5640c62Fe43a64f9Ae0811114874E95A819dB744";
const token = "0xYourToken";

// The token is a standard ERC20. Approve the launchpad to pull it, then sell.
const erc20 = parseAbi([
  "function approve(address spender, uint256 amount) returns (bool)",
  "function balanceOf(address owner) view returns (uint256)",
]);

const account = wallet.account.address;
const amount = await publicClient.readContract({
  address: token, abi: erc20, functionName: "balanceOf", args: [account],
});

await wallet.writeContract({ address: token, abi: erc20, functionName: "approve", args: [LAUNCHPAD, amount] });

const expectedEth = await publicClient.readContract({
  address: LAUNCHPAD, abi: launchpadAbi, functionName: "quoteSell", args: [token, amount],
});
const minEthOut = (expectedEth * 99n) / 100n; // 1% slippage

await wallet.writeContract({
  address: LAUNCHPAD, abi: launchpadAbi, functionName: "sell", args: [token, amount, minEthOut],
});

Read state and events

Read the curve to check price and whether a token has graduated, and watch Trade or TokenCreated to build feeds.

ts
import { launchpadAbi } from "./abi";
import { publicClient } from "./client";

const LAUNCHPAD = "0x5640c62Fe43a64f9Ae0811114874E95A819dB744";
const token = "0xYourToken";

// Curve state. Once graduated is true, buy/sell on the curve revert and
// trading moves to the DEX pool (see below).
const c = await publicClient.readContract({
  address: LAUNCHPAD, abi: launchpadAbi, functionName: "getCurve", args: [token],
});
console.log({ graduated: c.graduated, migrated: c.migrated, tokensSold: c.tokensSold });

// Spot price in wei per whole token (1e18 base units).
const price = await publicClient.readContract({
  address: LAUNCHPAD, abi: launchpadAbi, functionName: "getPrice", args: [token],
});

// Stream trades in real time.
const unwatch = publicClient.watchContractEvent({
  address: LAUNCHPAD, abi: launchpadAbi, eventName: "Trade",
  onLogs: (logs) => logs.forEach((l) => console.log(l.args)),
});

After graduation

Once getCurve(token).graduated is true, the curve buy and sell calls revert. The token now trades as a standard V3 pool against WETH at the 1% fee tier. Route swaps through the DEX SmartRouter and price them with the QuoterV2 (both addresses above). The pool pairs the token with WETH, so wrap ETH first or let the router handle it. The Migrated event carries the pool address.

Parameters

Total supply1,000,000,000 (18 decimals)
Sold on curve793,100,000
Reserved for liquidity206,900,000
Trade fee1.25% (0.95% protocol + 0.30% creator)
Creation fee0
GraduationWhen the curve sells out, then LP on the Based DEX at 1% fee
Decimals18

Prefer the command line? The same flow with Foundry cast:

bash
LAUNCHPAD=0x5640c62Fe43a64f9Ae0811114874E95A819dB744
RPC=https://rpc.mainnet.chain.robinhood.com

# launch a token (creation fee is 0; 1 wei sentinel + a 0.01 ETH dev buy)
cast send $LAUNCHPAD "createToken(string,string,string)" \
  "My Token" "MYT" "https://your-host.example/my-token.json" \
  --value 10000000000000001 --private-key $PK --rpc-url $RPC

# quote and buy 0.05 ETH (set MIN_OUT to the quote minus your slippage)
cast call $LAUNCHPAD "quoteBuy(address,uint256)(uint256)" $TOKEN 50000000000000000 --rpc-url $RPC
cast send $LAUNCHPAD "buy(address,uint256)" $TOKEN $MIN_OUT \
  --value 0.05ether --private-key $PK --rpc-url $RPC

# sell: approve first, then sell (MIN_ETH = quoteSell minus slippage)
cast send $TOKEN "approve(address,uint256)" $LAUNCHPAD $AMOUNT --private-key $PK --rpc-url $RPC
cast send $LAUNCHPAD "sell(address,uint256,uint256)" $TOKEN $AMOUNT $MIN_ETH \
  --private-key $PK --rpc-url $RPC

Ready to launch from the app instead? Create a token.