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 theIndexVaultABI 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 spacing1andIndexHookas 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, andsqrtPriceLimitX96is ignored. Protect a trade withamountOutMinimum. - 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=baselists live indexes, paged withnextCursor; see Public API. - On chain: read
IndexFactory'sIndexCreatedevents 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.
| Read | Returns |
|---|---|
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:
SETTLE(0x0b):(inputCurrency, amountIn, payerIsUser = true). Your input reaches the PoolManager before the hook runs.SWAP_EXACT_IN_SINGLE(0x06):(poolKey, zeroForOne, amountIn, amountOutMinimum, hookData = 0x).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 itstokensaslistedand your minimums in the same order; if the basket changed since, the call revertsExitListChangedand you preview again. Pass empty arrays to skip the minimums. tomust 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
deferredis true for it. Collect it later withclaim(token, to). Anyone can callclaimFor(account, token), which always paysaccount.claimOf(token, account)reads it. forfeitis 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)paysaccountin several tokens at once.
Events to index
Start at the deploy block on the contracts page.
| Contract | Events |
|---|---|
IndexFactory | IndexCreated (every new index, its basket, fees, pool ids, stockBearing and passRequired) |
IndexHook | IndexMint (a buy: quote in, fee, shares out), IndexSell (a sell: shares in, grossOut; net output is grossOut − fee), BuysPausedSet, SellsPausedSet |
IndexVault | ERC-20 Transfer, Redeemed, ClaimRecorded, ClaimPaid, ComponentAdded, ComponentRetired, ComponentDropped, TargetsSet |
FeeSplitter | FeeAccrued, Claimed, CreatorRecipientChanged |
Rebalancer | PlanProposed, 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.
| Endpoint | Returns |
|---|---|
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, /events | a 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.