Skip to main content
In this guide, you’ll read on-chain state for Bags tokens on Robinhood Chain: a single-call state snapshot via BagsLens, token discovery through the factory registry and events, pool ID derivation, and post-migration price reads. All of these are read-only — no wallet or gas required.

Prerequisites

Before starting, make sure you have:
The publicClient from Setup batches parallel readContract calls into single multicall3 requests. Prefer many small reads with Promise.all over hand-rolled multicall — batching handles it and keeps you under the public-RPC rate limit.

1. Read a Token’s State with BagsLens

BagsLens.getTokenState returns everything you need about a token in one eth_call. It’s the recommended entry point for any token view.
read-state.ts
import { publicClient } from "./clients";
import { ROBINHOOD_LAUNCHPAD } from "./addresses";
import { bagsLensAbi } from "./abi";
import type { Address } from "viem";

export async function readTokenState(token: Address) {
  const state = await publicClient.readContract({
    address: ROBINHOOD_LAUNCHPAD.lens,
    abi: bagsLensAbi,
    functionName: "getTokenState",
    args: [token],
  });
  if (!state.exists) return null; // not a Bags token
  return state;
}
The returned TokenState struct:
FieldTypeMeaning
existsboolfalse ⇒ not a Bags token
migratedbooltrue ⇒ graduated to a Uniswap v4 pool
curveaddressThe token’s BagsBondingCurve
feeShareaddressThe token’s BagsFeeShare
poolIdbytes32Uniswap v4 pool ID
thresholdQuoteuint256ETH (wei) needed to graduate
realQuoteReservesuint256Real ETH reserves the curve holds right now (wei)
realTokenReservesuint256Tokens left on the curve
virtualTokenReservesuint256Virtual token reserve (curve math)
virtualQuoteReservesuint256Virtual ETH reserve (curve math)
priceQuotePerTokenuint256Spot price, ETH wei per whole token
bondingProgressPctuint256Graduation progress: 099 while bonding, reaching 100 only once migrated is true
totalRaiseduint256Cumulative ETH raised into the curve over its lifetime, measured against thresholdQuote (wei)
priceQuotePerToken freezes at migration — it is only valid while migrated is false. For migrated tokens, read the live price from the pool (see section 5).

Batch multiple tokens

For lists, use getTokenStates to fetch many tokens in one call:
const states = await publicClient.readContract({
  address: ROBINHOOD_LAUNCHPAD.lens,
  abi: bagsLensAbi,
  functionName: "getTokenStates",
  args: [tokens], // Address[]
});

2. Discover Tokens via the Factory Registry

BagsFactory maintains an append-only registry of every launch. Because it’s append-only, the tail of the list is the newest launches.
list-tokens.ts
import { publicClient } from "./clients";
import { ROBINHOOD_LAUNCHPAD } from "./addresses";
import { bagsFactoryAbi, bagsLensAbi } from "./abi";

/** The newest `limit` launches with live state, in one batched round-trip. */
export async function listNewestTokens(limit: number) {
  const total = Number(
    await publicClient.readContract({
      address: ROBINHOOD_LAUNCHPAD.factory,
      abi: bagsFactoryAbi,
      functionName: "allTokensLength",
    })
  );
  if (total === 0) return { items: [], total: 0 };

  const count = Math.min(limit, total);
  const offset = BigInt(total - count); // tail = newest

  const tokens = await publicClient.readContract({
    address: ROBINHOOD_LAUNCHPAD.factory,
    abi: bagsFactoryAbi,
    functionName: "getTokens",
    args: [offset, BigInt(count)],
  });

  const states = await publicClient.readContract({
    address: ROBINHOOD_LAUNCHPAD.lens,
    abi: bagsLensAbi,
    functionName: "getTokenStates",
    args: [tokens],
  });

  // Newest first.
  const items = tokens
    .map((token, i) => ({ token, state: states[i] }))
    .filter((row) => row.state.exists)
    .reverse();
  return { items, total };
}
Other registry lookups:
CallReturns
factory.allTokensLength()Total number of launches
factory.getTokens(offset, limit)A page of token addresses
factory.allTokens(index)Token address at an index
factory.curveForToken(token)The token’s bonding curve
factory.feeShareForToken(token)The token’s fee-share contract
factory.tokenForPoolId(poolId)The token for a given pool ID
factory.creationFee()Current launch fee (wei)
factory.graduationThreshold()Default graduation threshold (wei)

3. Read Immutable Token Info

Name, symbol, and metadata URI never change, so read them once and cache. Non-Bags ERC-20s revert on metadataURI, so treat that as absent.
token-info.ts
import { publicClient } from "./clients";
import { bagsTokenAbi } from "./abi";
import type { Address } from "viem";

export async function readTokenInfo(token: Address) {
  const [name, symbol, metadataURI, totalSupply] = await Promise.all([
    publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "name" }),
    publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "symbol" }),
    publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "metadataURI" }).catch(() => ""),
    publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "totalSupply" }),
  ]);
  return { name, symbol, metadataURI, totalSupply };
}

4. Discover via TokenCreated Events

You can also index launches directly from TokenCreated logs (useful for backfilling history or building a stream). Start no earlier than the protocol deploy block.
index-launches.ts
import { parseAbiItem } from "viem";
import { publicClient } from "./clients";
import { ROBINHOOD_LAUNCHPAD, ROBINHOOD_DEPLOY_BLOCK } from "./addresses";

const tokenCreatedEvent = parseAbiItem(
  "event TokenCreated(address indexed token, address indexed curve, address indexed creator, address feeShare, bytes32 poolId, string name, string symbol, string metadataURI)"
);

export async function getLaunches(fromBlock: bigint = ROBINHOOD_DEPLOY_BLOCK) {
  const logs = await publicClient.getLogs({
    address: ROBINHOOD_LAUNCHPAD.factory,
    event: tokenCreatedEvent,
    fromBlock,
    toBlock: "latest",
  });
  return logs.map((log) => ({
    token: log.args.token,
    curve: log.args.curve,
    creator: log.args.creator,
    feeShare: log.args.feeShare,
    poolId: log.args.poolId,
    name: log.args.name,
    symbol: log.args.symbol,
    metadataURI: log.args.metadataURI,
    blockNumber: log.blockNumber,
    txHash: log.transactionHash,
  }));
}
For a live feed, checkpoint the last scanned block and only query [lastScanned + 1, latest] on each poll rather than re-scanning from the deploy block every time.

5. Post-Migration Price from the Pool

After migration, read the live spot price from the pool’s slot0 via StateView. Never fall back to the frozen lens price for migrated tokens. Add the StateView fragment to abi/periphery.ts:
export const stateViewAbi = [
  {
    type: "function",
    name: "getSlot0",
    inputs: [{ name: "poolId", type: "bytes32" }],
    outputs: [
      { name: "sqrtPriceX96", type: "uint160" },
      { name: "tick", type: "int24" },
      { name: "protocolFee", type: "uint24" },
      { name: "lpFee", type: "uint24" },
    ],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "getLiquidity",
    inputs: [{ name: "poolId", type: "bytes32" }],
    outputs: [{ name: "liquidity", type: "uint128" }],
    stateMutability: "view",
  },
] as const;
Then read slot0 and convert sqrtPriceX96 to ETH per whole token:
pool-price.ts
import { publicClient } from "./clients";
import { ROBINHOOD_LAUNCHPAD } from "./addresses";
import { stateViewAbi } from "./abi/periphery";
import { isTokenCurrency0 } from "./poolKey";
import type { Address, Hex } from "viem";

const Q96 = 2 ** 96;

/** ETH per whole token from a v4 sqrtPriceX96 price. Returns null on failure. */
export async function readPoolPriceEth(token: Address, poolId: Hex): Promise<number | null> {
  try {
    const [sqrtPriceX96] = await publicClient.readContract({
      address: ROBINHOOD_LAUNCHPAD.stateView,
      abi: stateViewAbi,
      functionName: "getSlot0",
      args: [poolId],
    });

    // price(currency1 per currency0) = (sqrtPriceX96 / 2^96)^2
    const ratio = (Number(sqrtPriceX96) / Q96) ** 2;
    // Orient so the result is ETH (WETH) per token.
    const ethPerToken = isTokenCurrency0(token) ? ratio : 1 / ratio;
    return ethPerToken > 0 ? ethPerToken : null;
  } catch {
    return null;
  }
}
sqrtPriceX96 is a Q64.96 fixed-point number. The Number()-based conversion above is fine for display; for high-precision accounting, compute the square in BigInt before converting.

6. Pool ID Derivation

Prefer the poolId from getTokenState or the TokenCreated event. When you must derive it yourself, it’s keccak256(abi.encode(poolKey)) over the sorted key:
pool-id.ts
import { encodeAbiParameters, keccak256, type Address, type Hex } from "viem";
import { bagsPoolKey } from "./poolKey";

export function poolIdForToken(token: Address): Hex {
  const key = bagsPoolKey(token);
  return keccak256(
    encodeAbiParameters(
      [{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }],
      [key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks]
    )
  );
}
The derivation must match the factory byte-for-byte or every derived poolId is wrong. Cross-check your derived value against the on-chain poolId from getTokenState for a known token, and prefer the on-chain value whenever it’s available.

Next steps