Skip to main content

Index tokens for developers

How to build on Bankr index tokens on Base and Robinhood Chain: read an index, quote and trade it through Uniswap v4, redeem it for its basket, index its events, and use Bankr's public API. Every address and ABI is on Index token contracts, and contracts.json has them in one machine-readable file.

How it works on chain​

  • An index is its own ERC-20. Each index is an IndexVault, an EIP-1167 clone at its own address, with 18 decimals. It holds the basket. Use the IndexVault ABI at the index's address.
  • One hook per chain trades every index. Each index has two Uniswap v4 pools, one against the chain's stable (USDC on Base, USDG on Robinhood Chain) and one against WETH. Both have fee 0, tick spacing 1 and IndexHook as the hook, and hold no liquidity. On a buy the hook buys every basket token on its own venue and the vault mints shares in proportion to what arrived. On a sell the vault pays out its share of each token, the hook sells them, and you receive the quote token.
  • Exact input only. An exact-output swap reverts ExactOutputNotSupported, and sqrtPriceLimitX96 is ignored. Protect a trade with amountOutMinimum.
  • Redeem is open to anyone, at any time, for free. A holder can always swap shares for their slice of every basket token, directly on the vault. No role can pause it.
  • Prices come from Chainlink where a feed exists, otherwise from Bankr-signed prices pushed on chain to the PriceModule. Each leg of a buy or sell must fill within its token's price band, and all legs of one buy within a set spread of each other (currently 3 % on both chains).
  • Rebalancing runs as public Dutch auctions on the Rebalancer; see Index auctions for fillers.

Find indexes​

  • API: GET https://api.bankr.bot/indexes?chain=base lists live indexes, paged with nextCursor; see Public API.
  • On chain: read IndexFactory's IndexCreated events from the deploy block on the contracts page. IndexFactory.isVault(address) tells you whether an address is an index from that factory.

Read an index​

IndexLens never reverts, so it is the safe place to read from. All values use 18 decimals unless noted.

ReadReturns
IndexLens.navUsdE18(index)nav, circulating supply, navPerShareE18 and complete (false when a token has no fresh price)
IndexLens.composition(index)one row per token: token, held, targetBps, weightBps, priceUsdE18, valueUsdE18, priceOk, status
IndexLens.previewRedeem(index, shares)the tokens and amounts a redeem of shares pays now
IndexLens.vestingOf(index)the holder fee still being released over the next 24 hours
IndexHook.poolsOf(index)the index's stableKey and wethKey pool keys
IndexHook.buysPaused(index), sellsPaused()whether buys of this index, or sells of every index, are paused
IndexHook.passRequired(index)whether buys need a Bankr-signed pass (false by default)
IndexVault.components(), legacyComponents()the basket's active tokens, and retired tokens it still holds
Rebalancer.planOf(index), modeOf(index)the current rebalance plan and the index's rebalancing mode
import { createPublicClient, http } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });
const abi = (name: string) =>
fetch(`https://docs.bankr.bot/index-tokens/abis/${name}.json`).then((r) =>
r.json(),
);

const LENS = "0x0E19BDeB4EF2443413926b9Daa72E484f63BB72b"; // Base IndexLens
const index = "0x081e791d7d81c1e109f6df7b868ec11006764cd8"; // AI12

const [nav, circulating, navPerShareE18, complete] = await client.readContract({
address: LENS,
abi: await abi("IndexLens"),
functionName: "navUsdE18",
args: [index],
});

Read and simulate at the latest block, not pending. A pending state can make the hook revert on states no sealed block has.

Quote and trade​

An index trades only on its own two hook pools. Any Uniswap v4 router can trade it with empty hookData (0x), unless passRequired(index) is true.

Quote with Uniswap's V4Quoter.quoteExactInputSingle. It runs the real hook: every leg, price and band. It returns the exact amountOut, or reverts with the hook's error.

const HOOK = "0x2Fd0ba044Faa96A9d1B0f010C0Dac3F696aA2888"; // Base IndexHook
const V4_QUOTER = "0x0d5e0F971ED27FBfF6c2837bf31316121532048D"; // Base V4Quoter
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base stable quote

const [stableKey] = await client.readContract({
address: HOOK,
abi: await abi("IndexHook"),
functionName: "poolsOf",
args: [index],
});
const usdcIn = 100_000_000n; // 100 USDC
const zeroForOne = stableKey.currency0.toLowerCase() === USDC.toLowerCase(); // true when paying with currency0

const { result } = await client.simulateContract({
address: V4_QUOTER,
abi: [
{
type: "function",
name: "quoteExactInputSingle",
stateMutability: "nonpayable",
inputs: [
{
name: "params",
type: "tuple",
components: [
{
name: "poolKey",
type: "tuple",
components: [
{ name: "currency0", type: "address" },
{ name: "currency1", type: "address" },
{ name: "fee", type: "uint24" },
{ name: "tickSpacing", type: "int24" },
{ name: "hooks", type: "address" },
],
},
{ name: "zeroForOne", type: "bool" },
{ name: "exactAmount", type: "uint128" },
{ name: "hookData", type: "bytes" },
],
},
],
outputs: [
{ name: "amountOut", type: "uint256" },
{ name: "gasEstimate", type: "uint256" },
],
},
],
functionName: "quoteExactInputSingle",
args: [
{ poolKey: stableKey, zeroForOne, exactAmount: usdcIn, hookData: "0x" },
],
blockTag: "latest",
});
const [sharesOut] = result;

Compare the quote with navPerShareE18 before you trade. A buy costs about the index fee (0.25 % to 1.00 %) plus each basket token's pool fee and price impact above NAV. Bankr's own app refuses a trade that costs more than the fee plus 1.5 %.

Trade through the chain's Universal Router with one V4_SWAP command (0x10). Its actions, in this order:

  1. SETTLE (0x0b): (inputCurrency, amountIn, payerIsUser = true). Your input reaches the PoolManager before the hook runs.
  2. SWAP_EXACT_IN_SINGLE (0x06): (poolKey, zeroForOne, amountIn, amountOutMinimum, hookData = 0x).
  3. TAKE_ALL (0x0f): (outputCurrency, amountOutMinimum). The output goes to the caller.

The swap struct differs by chain. Base runs Universal Router 2.0. Robinhood Chain runs a modified 2.1.1 build whose struct adds uint256 minHopPriceX36 before hookData; pass 0. To pay with ETH, put WRAP_ETH (0x0b) first and settle from the router (payerIsUser = false) against the WETH pool. To receive ETH, TAKE (0x0e) to the router and finish with UNWRAP_WETH (0x0c). ERC-20 input is paid through Permit2: approve the token to Permit2, then Permit2 to the router. The router, quoter and Permit2 addresses are on the contracts page.

A trade that can't fill reverts and costs only gas. Common reasons: a leg outside its price band, legs that drifted apart, a price that isn't fresh, or a paused index or token. The error names the cause.

Redeem​

Redeem swaps shares for the basket itself, fee-free, on the index's own address:

function redeem(uint256 shares, address to, address[] listed, uint256[] minAmountsOut, address[] forfeit)
external returns (address[] tokens, uint256[] amounts, bool[] deferred);
  • Read IndexLens.previewRedeem(index, shares) first. Pass its tokens as listed and your minimums in the same order; if the basket changed since, the call reverts ExitListChanged and you preview again. Pass empty arrays to skip the minimums.
  • to must not be zero or the index itself.
  • If a token can't be sent (its issuer paused it, for example), your amount of it becomes a claim and deferred is true for it. Collect it later with claim(token, to). Anyone can call claimFor(account, token), which always pays account. claimOf(token, account) reads it.
  • forfeit is normally empty. Name a token there only to give up your share of it, for example one whose balance can't be read.
  • Inside a Uniswap v4 unlock, after a sell of the same index in the same transaction, redeem reverts. Redeem after the unlock returns.

Creator fees​

Bankr receives 40 % of each trade's index fee, and the creator's setting splits the rest between the creator and the index's holders. The holders' part stays in the basket; everything else goes to the FeeSplitter as each trade happens. Fees there pool per account and token, across every index:

  • FeeSplitter.claimable(account, token) reads what an account can claim in one token.
  • claim(token) pays the caller; claimMany(account, tokens) pays account in several tokens at once.

Events to index​

Start at the deploy block on the contracts page.

ContractEvents
IndexFactoryIndexCreated (every new index, its basket, fees, pool ids, stockBearing and passRequired)
IndexHookIndexMint (a buy: quote in, fee, shares out), IndexSell (a sell: shares in, grossOut; net output is grossOut − fee), BuysPausedSet, SellsPausedSet
IndexVaultERC-20 Transfer, Redeemed, ClaimRecorded, ClaimPaid, ComponentAdded, ComponentRetired, ComponentDropped, TargetsSet
FeeSplitterFeeAccrued, Claimed, CreatorRecipientChanged
RebalancerPlanProposed, PlanActivated, PlanFrozen, PlanResumed, PlanEnded, TrancheStarted, TrancheFilled, TrancheClosed

The IndexMint whose sender is the factory is the creation buy (the $1 every index burns plus the creator's first buy); skip it when counting volume. Use circulatingSupply() for supply, not totalSupply().

Public API​

Read-only, no key, limited per IP address (30 to 240 requests a minute, by endpoint). Every call takes chain=base or chain=robinhood.

EndpointReturns
GET /indexes?chain=live indexes: NAV, Strength, 24-hour change and volume. Filters kind, creator, q; sort=strength or newest; cursor, limit
GET /indexes/{index}?chain=one index: basket, NAV, fees, pools, rebalancing mode and plan, status
GET /indexes/{index}/claims?chain=&account=redeem claims an account can collect from an index
GET /indexes/{index}/auctions?chain=the index's auction history, newest first
GET /indexes/auctions/live?chain=every live auction with a fill quote (fillers)
GET /indexes/components?chain=&kind=tokens a new index can hold (kind=stocks or tokens)
GET /indexes/ticker?chain=&ticker=whether a ticker is free
GET /indexes/create-config?chain=creation bounds and defaults
GET /indexes/fees?chain=&account=an account's claimable index fees (404 when it has none)
GET /indexes/dex/{chain}/latest-block, /asset, /pair, /eventsa DEX Screener / GeckoTerminal adapter feed for index pools

The base URL is https://api.bankr.bot.

For AI tools​

  • llms.txt indexes every docs page; llms-full.txt is the whole site as one file.
  • Every page also has a Markdown copy under /docs/, for example index-token-contracts.md.
  • contracts.json lists every address per chain with each ABI's URL and hash, and each ABI is plain JSON at https://docs.bankr.bot/index-tokens/abis/<Contract>.json.
  • Point an agent at this page and the contracts page, or at llms-full.txt, before it writes code against index tokens.