Skip to content

Reading token state

Everything about a CAST token can be read straight from the chain. These examples use viem and run in order, top to bottom: each one uses what the one before it defined.

Tested against the live chain
Every snippet on this page runs against the deployed contracts in our test suite, so you can copy them as they are.

Set up a client

Install viem (npm i viem), then define the chain and the CAST addresses. Addresses come from Network and contracts.

cast.ts
import { createPublicClient, defineChain, http, parseAbi, zeroAddress } from "viem";

export const robinhood = defineChain({
  id: 4663,
  name: "Robinhood Chain",
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
  contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" } },
});

export const client = createPublicClient({ chain: robinhood, transport: http() });

export const CAST = {
  launcher: "0xFc7931816e1b9ddc87B6C284765B5AFD593F0404",
  router: "0xbDDC1Bc49902C4afe3396ED0e2A11Cf8747275E4",
  quoter: "0xAD9CFf7FdcD136C92365C8409A41760a9736Dec9",
  hook: "0x7F6AdEeF151b0079e10EDf9C96647Bd84567aAcc",
  rulebook: "0xC1e9104F6015679c17f2E27C28D3B7910eFf181A",
  pairs: "0x5a1Ad5EFD9bbd34Bc9a73c595Ec42D1B196Af9fC",
  poolManager: "0x8366a39CC670B4001A1121B8F6A443A643e40951",
} as const;

List every token

The Launcher keeps a list of every token it created. To follow new launches as they happen, index its Launched event instead: see Events and indexing.

tokens.ts
const launcherAbi = parseAbi([
  "function tokenCount() view returns (uint256)",
  "function allTokens(uint256 index) view returns (address)",
]);

const count = await client.readContract({ address: CAST.launcher, abi: launcherAbi, functionName: "tokenCount" });

const tokens = await client.multicall({
  allowFailure: false,
  contracts: Array.from({ length: Number(count) }, (_, i) => ({
    address: CAST.launcher,
    abi: launcherAbi,
    functionName: "allTokens",
    args: [BigInt(i)],
  }) as const),
});

console.log(`${tokens.length} CAST tokens`, tokens);

Read the sealed rules

These are the token functions used on this page. getRuleSet() returns every schedule exactly as it was sealed; stage() returns 0 to 3.

token-abi.ts
export const tokenAbi = parseAbi([
  "struct Schedule { uint8 driver; uint8 mode; uint8 len; uint32[8] offsets; uint16[8] values; }",
  "struct RuleSet { uint32 shieldDuration; uint32 endOffset; uint16 botTaxHolderBps; uint16 creatorAllocationBps; uint32 creatorLockDuration; Schedule[6] schedules; }",
  "struct TradeRates { uint256 botTaxBps; uint256 tradeFeeBps; uint256 holderShareBps; uint256 burnShareBps; uint256 botTaxHolderBps; }",
  "struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; }",
  "function owner() pure returns (address)",
  "function symbol() view returns (string)",
  "function pairAsset() view returns (address)",
  "function poolKey() view returns (PoolKey)",
  "function getRuleSet() view returns (RuleSet)",
  "function launchTime() view returns (uint256)",
  "function stage() view returns (uint8)",
  "function shieldEnd() view returns (uint256)",
  "function endTime() view returns (uint256)",
  "function ruleValue(uint8 rule, address wallet, uint256 t) view returns (uint256)",
  "function nextChange(uint8 rule, address wallet) view returns (uint256)",
  "function tradeRates(bool isBuy, address trader, uint256 t) view returns (TradeRates)",
  "function maxWalletAmount() view returns (uint256)",
  "function balanceOf(address wallet) view returns (uint256)",
  "function holdAge(address wallet, uint256 t) view returns (uint256)",
  "function claimable(address holder) view returns (uint256)",
]);

// Indexes into RuleSet.schedules, and the stage numbers stage() returns.
export const Rule = { BOT_TAX: 0, BUY_FEE: 1, SELL_FEE: 2, MAX_WALLET: 3, HOLDER_SHARE: 4, BURN_SHARE: 5 } as const;
export const STAGES = ["Launch", "Shield", "Growth", "Locked In"] as const;
rules.ts
const token = tokens[0];

const [rules, stage, owner] = await Promise.all([
  client.readContract({ address: token, abi: tokenAbi, functionName: "getRuleSet" }),
  client.readContract({ address: token, abi: tokenAbi, functionName: "stage" }),
  client.readContract({ address: token, abi: tokenAbi, functionName: "owner" }),
]);

console.log(STAGES[stage]); // e.g. "Growth"
console.log(owner === zeroAddress); // true, always
console.log(rules.shieldDuration); // seconds, e.g. 3600

// Each schedule is up to 8 (offset, value) breakpoints. Values are basis points: 100 = 1%.
const botTax = rules.schedules[Rule.BOT_TAX];
for (let i = 0; i < botTax.len; i++) {
  console.log(`at +${botTax.offsets[i]}s: ${botTax.values[i] / 100}%`);
}

ruleValue(rule, wallet, t) gives any rule's value at any timestamp from launch on, and nextChange(rule, wallet) says when it next moves (0 if never).

Paired asset and price

Every token trades against one asset. pairAsset() returns address(0) for native ETH, or the address of an ERC-20 such as USDG or a stock token. Fees, rewards and volume are in that asset's smallest unit, so read its decimals: 18 for ETH and the test stocks, 6 for USDG.

Where the price comes from depends on whether the token has graduated. Read curves(token) on the Fee Hook first:

  • On the bonding curve (graduated is false), the price of one whole token in whole units of the pair is (virtualPair + raised) / (virtualToken - sold) * 10 ** (18 - decimals).
  • After graduation, it comes from the pool. The paired asset is always currency0, so the pool's price is tokens per unit of the pair, and one whole token is 10 ** (18 - decimals) / s ** 2 of the pair, where s = sqrtPriceX96 / 2 ** 96.

The pool exists from launch, but until graduation it holds no liquidity and its sqrtPriceX96 is the price the token will graduate at, not today's price. Don't read a curve token's price from the pool.

pair.ts
import { encodeAbiParameters, keccak256 } from "viem";

const erc20Meta = parseAbi([
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
]);

// What a token trades against: address(0) is native ETH, anything else an ERC-20 from the CastPairs list.
export async function pairOf(token: `0x${string}`) {
  const asset = await client.readContract({ address: token, abi: tokenAbi, functionName: "pairAsset" });
  if (asset === zeroAddress) return { asset, symbol: "ETH", decimals: 18 };
  const [symbol, decimals] = await Promise.all([
    client.readContract({ address: asset, abi: erc20Meta, functionName: "symbol" }),
    client.readContract({ address: asset, abi: erc20Meta, functionName: "decimals" }),
  ]);
  return { asset, symbol, decimals };
}

// The token's bonding curve, kept by the Fee Hook. See "Curve progress" below for every field.
const hookAbi = parseAbi([
  "function curves(address token) view returns (uint128 virtualPair, uint128 virtualToken, uint128 raised, uint128 sold, uint128 target, uint128 curveTokens, uint128 poolTokens, bool graduated)",
]);

export async function curveOf(token: `0x${string}`) {
  const [virtualPair, virtualToken, raised, sold, target, curveTokens, poolTokens, graduated] = await client.readContract({
    address: CAST.hook,
    abi: hookAbi,
    functionName: "curves",
    args: [token],
  });
  return { virtualPair, virtualToken, raised, sold, target, curveTokens, poolTokens, graduated };
}

// The pool's current price, read from the PoolManager's storage (Uniswap v4's StateLibrary layout).
const extsloadAbi = parseAbi(["function extsload(bytes32 slot) view returns (bytes32)"]);

export async function poolPrice(token: `0x${string}`, decimals: number) {
  const key = await client.readContract({ address: token, abi: tokenAbi, functionName: "poolKey" });
  const poolId = keccak256(
    encodeAbiParameters(
      [{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }],
      [key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks],
    ),
  );
  const slot = keccak256(encodeAbiParameters([{ type: "bytes32" }, { type: "uint256" }], [poolId, 6n]));
  const slot0 = await client.readContract({ address: CAST.poolManager, abi: extsloadAbi, functionName: "extsload", args: [slot] });
  const sqrtPriceX96 = BigInt(slot0) & ((1n << 160n) - 1n);

  // The paired asset is currency0, so the pool prices tokens per unit of the pair. Flip it and allow for decimals.
  const s = Number(sqrtPriceX96) / 2 ** 96;
  return 10 ** (18 - decimals) / (s * s);
}

// Paired asset per whole token right now: from the curve until the token graduates, then from the pool.
// (Before graduation the pool already exists, but its price is the graduation price, not today's.)
export async function tokenPrice(token: `0x${string}`, decimals: number) {
  const curve = await curveOf(token);
  if (curve.graduated) return poolPrice(token, decimals);
  return (Number(curve.virtualPair + curve.raised) / Number(curve.virtualToken - curve.sold)) * 10 ** (18 - decimals);
}

const pair = await pairOf(token);
const symbol = await client.readContract({ address: token, abi: tokenAbi, functionName: "symbol" });
const priceNow = await tokenPrice(token, pair.decimals);
console.log(`1 ${symbol} = ${priceNow} ${pair.symbol}, so 1 billion = ${priceNow * 1e9} ${pair.symbol}`);

The pair list itself is in the Pairs contract (CastPairs). assets() returns every asset ever listed, and pairs(asset) returns (listed, open, virtualReserve, graduationTarget): whether it's open to new launches and the curve they start on. curveFor(asset) returns the same (virtualReserve, graduationTarget), but reverts with PairNotOpen for an asset that isn't open. Both are in the asset's smallest unit. A closed pair only stops new launches: tokens already paired with it keep trading, and a token's own curve never changes after launch.

pairs.ts
import { formatUnits } from "viem";

const pairsAbi = parseAbi([
  "function assets() view returns (address[])",
  "function pairs(address asset) view returns (bool listed, bool open, uint128 virtualReserve, uint128 graduationTarget)",
]);

const assets = await client.readContract({ address: CAST.pairs, abi: pairsAbi, functionName: "assets" });
for (const asset of assets) {
  const [, open, , graduationTarget] = await client.readContract({ address: CAST.pairs, abi: pairsAbi, functionName: "pairs", args: [asset] });
  const decimals = asset === zeroAddress ? 18 : await client.readContract({ address: asset, abi: erc20Meta, functionName: "decimals" });
  const name = asset === zeroAddress ? "ETH" : asset;
  console.log(name, open ? "open" : "closed to new launches", `graduation target ${formatUnits(graduationTarget, decimals)}`);
}

Curve progress

curves(token) on the Fee Hook returns everything about a token's bonding curve. Paired-asset amounts are in the asset's smallest unit and token amounts have 18 decimals.

FieldMeaning
virtualPair, virtualTokenThe curve's virtual reserves. Its live reserves are virtualPair + raised and virtualToken - sold.
raisedPaired asset the curve holds: what buys put in after fees, less what sells took out. The token graduates when it reaches target.
soldTokens the curve has sold, less what it bought back.
targetThe graduation target, fixed at launch from the pair list.
curveTokensThe most the curve sells, about 79.3% of the tokens for sale.
poolTokensThe rest, which go into the pool with everything raised at graduation.
graduatedTrue once the token has moved into its pool. raised and sold then keep their final values.

Progress to graduation is raised / target. CAST's token pages draw it as a progress bar, and Explore counts a token as almost graduated from 50%. To follow progress live, index the hook's CurveTrade and Graduated events: see Events and indexing.

curve.ts
import { formatUnits } from "viem";

// Progress to graduation is raised / target. Amounts on the pair side are in the paired asset's
// smallest unit; amounts on the token side have 18 decimals.
const curve = await curveOf(token);
const progress = curve.graduated ? 1 : Number((curve.raised * 10_000n) / curve.target) / 10_000;

if (curve.graduated) {
  console.log(`${symbol} has graduated and trades in its pool`);
} else {
  const toGo = formatUnits(curve.target - curve.raised, pair.decimals);
  const tokensLeft = formatUnits(curve.curveTokens - curve.sold, 18);
  console.log(`${symbol} is ${(progress * 100).toFixed(1)}% of the way to graduating: ${toGo} ${pair.symbol} to go, ${tokensLeft} tokens left on the curve`);
}

What a trade costs now

tradeRates returns the rates the Fee Hook would charge at a given time, for a given trader. The buy side includes Bot Tax; the sell side includes the trader's Hold Streak.

rates.ts
const now = BigInt(Math.floor(Date.now() / 1000));

// Pass a wallet to apply its Hold Streak to the sell fee, or zeroAddress for none.
const [buyRates, sellRates, nextBotTaxChange] = await Promise.all([
  client.readContract({ address: token, abi: tokenAbi, functionName: "tradeRates", args: [true, zeroAddress, now] }),
  client.readContract({ address: token, abi: tokenAbi, functionName: "tradeRates", args: [false, zeroAddress, now] }),
  client.readContract({ address: token, abi: tokenAbi, functionName: "nextChange", args: [Rule.BOT_TAX, zeroAddress] }),
]);

const buyCost = Number(buyRates.botTaxBps + buyRates.tradeFeeBps) / 100;
const sellCost = Number(sellRates.tradeFeeBps) / 100;
console.log(`A buy costs ${buyCost}%, a sell costs ${sellCost}%`);
console.log(nextBotTaxChange === 0n ? "Bot Tax won't change again" : `Bot Tax changes at ${new Date(Number(nextBotTaxChange) * 1000)}`);

For fee amounts in the paired asset and who receives them, use the Quoter: see Quote a trade.

A wallet's position

Balance, Hold Streak and the rewards a wallet can collect, in the token's paired asset.

position.ts
export async function position(token: `0x${string}`, wallet: `0x${string}`) {
  const now = BigInt(Math.floor(Date.now() / 1000));
  const [balance, holdSeconds, rewards] = await Promise.all([
    client.readContract({ address: token, abi: tokenAbi, functionName: "balanceOf", args: [wallet] }),
    client.readContract({ address: token, abi: tokenAbi, functionName: "holdAge", args: [wallet, now] }),
    client.readContract({ address: token, abi: tokenAbi, functionName: "claimable", args: [wallet] }),
  ]);
  // rewards is in the token's paired asset, in its smallest unit (wei for ETH, 6 decimals for USDG).
  return { balance, holdStreakDays: Number(holdSeconds) / 86_400, rewards };
}

Read the grade

The Rulebook recomputes the grade from the sealed rules on-chain, so it always matches what the token can actually charge.

grade.ts
const rulebookAbi = parseAbi(["function gradeOf(address token) view returns (uint8 grade, uint256 score)"]);

const [grade, score] = await client.readContract({
  address: CAST.rulebook,
  abi: rulebookAbi,
  functionName: "gradeOf",
  args: [token],
});

console.log(`Grade ${"ABCDF"[grade]}, ${score} of 100`);

Images, links and the token list

A token's image and links aren't on-chain. Creators set them on CAST, either with a signed message or while signed in with the creator wallet, and CAST serves them over plain HTTP:

  • GET /api/token/{address}/profile returns x, telegram and website (full URLs or null) and logo, a version number or null.
  • GET /api/token/{address}/logo?v={logo} returns the 256-pixel image. Each version's URL never changes, so it can be cached for good.
  • GET /tokenlist.json lists every CAST token in the Uniswap token list format, with a logoURI for tokens that have an image.
profile.ts
const site = "https://cast-pad.xyz";
const token = "0x…"; // any CAST token

const profile = await fetch(`${site}/api/token/${token}/profile`).then((r) => r.json());
// { x: "https://x.com/yourtoken", telegram: null, website: "https://yourtoken.com", logo: 1792000000 }

const image = profile.logo ? `${site}/api/token/${token.toLowerCase()}/logo?v=${profile.logo}` : null;
Creator content
Images and links are chosen by each token's creator and can change at any time. Show them as the creator's, not as anything CAST or the contract guarantees.
Popular
  • QuickstartGet started · Set up a wallet, get free test funds and buy your first CAST token.
  • Launch a tokenGet started · Pick a pair and a template, check the grade and seal your rules in one transaction.
  • Fees and Bot TaxConcepts · The three fees a trade can pay, who receives them, and a worked example.
  • Network and contractsDevelopers · Chain settings, contract addresses, pairs and protocol constants.
  • RisksTrust and safety · What sealed rules protect you from, and what they don't.

Connect wallet

New to wallets? Start here.
By connecting, you agree to the Terms and Privacy Policy.