Run an index from a script or agent
This guide takes you from nothing to a live index that your code manages: pick a basket, create the index, watch it, and rebalance it. It uses a Bankr API key, so there is no wallet library and nothing to sign yourself.
New to index tokens? Read Index tokens first. Every endpoint here is described in full on Index tokens for developers, including the route for signing with your own wallet instead.
Before you start
| You need | Where |
|---|---|
| A Bankr account and its wallet address | bankr.bot |
| An API key with Wallet API access | bankr.bot/api-keys. A read-only key can't create |
| A little over $100 of USDC in that wallet | on Base. The first buy must be worth at least $100, and gas is paid from the wallet |
Two rules to plan around: the index tokens your first buy mints are locked for 24 hours, and the index fee you set at creation can never be changed.
export BANKR_API_KEY="bk_usr_..."
export BANKR_WALLET="0xYourBankrWallet"
The base URL is https://api.bankr.bot. The examples use Base; swap chain=base for chain=robinhood and USDC for
USDG to run on Robinhood Chain.
1. Pick a basket
List the tokens an index can hold and keep the ones marked eligible:
curl "https://api.bankr.bot/indexes/components?chain=base&kind=tokens"
{
"components": [
{
"address": "0x1ac6443d7095bf26b6114edd6ebc8ec003249514",
"symbol": "$ZBCN",
"isStock": false,
"depth2PctUsd": 355,
"eligible": true,
"refusal": null
}
]
}
Use kind=stocks for tokenized stocks. depth2PctUsd is how much one trade can move through the token's pool at 2 %
impact: a thin token still fits at a low weight, because the basket as a whole needs a Strength of $5,000.
An index holds 2 to 20 tokens. Weights are in basis points, at least 100 (1 %) each, and add up to 10000.
2. Check the ticker
curl "https://api.bankr.bot/indexes/ticker?chain=base&ticker=MYIDX"
{ "available": true }
3. Quote it
Send the index you want to quote-create, with your wallet as account. Nothing is spent. Keep adjusting until
blockers comes back empty.
curl -X POST https://api.bankr.bot/indexes/quote-create \
-H "content-type: application/json" \
-d '{
"chain": "base",
"account": "'"$BANKR_WALLET"'",
"name": "My First Index",
"symbol": "MYIDX",
"components": [
{ "address": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "weightBps": 5000 },
{ "address": "0x22af33fe49fd1fa80c7149773dde5890d3c76f3b", "weightBps": 5000 }
],
"rebalance": { "mode": "manual", "rule": "none", "driftBps": 0 },
"firstBuy": { "token": "stable", "amountRaw": "105000000" },
"fees": { "feeBps": 50, "holderShareBps": 0 }
}'
{
"sharesOut": "87043713065263413979",
"blockers": [],
"warnings": [],
"fees": { "totalBps": 50, "bankrBps": 20, "creatorBps": 30, "holdersBps": 0 }
}
firstBuy.amountRawis in the token's base units:105000000is 105 USDC. Exactly 100 USDC can price a few cents under $100 and be refused (SEED_BELOW_FLOOR), so leave a little room.rebalance.modedecides who moves the basket later.manualmeans your code proposes new weights, which is what this guide uses.autowithrule: "monthly"restores the targets every month without you;offnever rebalances.fees.feeBpsis the index fee on every trade,25to100.holderShareBpsis the part of your share that goes to holders instead of you.
4. Create it
Send the same body to POST /indexes with your key and without account. Bankr signs from the key's wallet:
curl -X POST https://api.bankr.bot/indexes \
-H "X-API-Key: $BANKR_API_KEY" \
-H "content-type: application/json" \
-d '{
"chain": "base",
"name": "My First Index",
"symbol": "MYIDX",
"components": [
{ "address": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "weightBps": 5000 },
{ "address": "0x22af33fe49fd1fa80c7149773dde5890d3c76f3b", "weightBps": 5000 }
],
"rebalance": { "mode": "manual", "rule": "none", "driftBps": 0 },
"firstBuy": { "token": "stable", "amountRaw": "105000000" },
"fees": { "feeBps": 50, "holderShareBps": 0 }
}'
The call answers once the transaction is mined:
{
"indexAddress": "0x…",
"chain": "base",
"txHash": "0x…",
"sharesOut": "87043713065263413979"
}
Save indexAddress: every later call uses it. If the call times out, don't send it again straight away. Look for
the index first with GET /indexes?chain=base&creator=$BANKR_WALLET.
5. Watch it
curl "https://api.bankr.bot/indexes/0xYourIndex?chain=base"
The fields a manager cares about:
| Field | Meaning |
|---|---|
navUsd, navPerShareUsd | what the basket is worth, in total and per index token |
components[].targetBps, currentBps | each token's target weight and where it has drifted to |
components[].state, status | whether a token, or the whole index, is paused |
strengthUsd, thinnest | how large a trade the index takes at 2 % impact, and the token that limits it |
rebalance | the mode, and any mode change that is pending |
plan | the rebalance in progress, or null |
nextProposalAt | when the next proposal is allowed |
vol24hUsd, change24hPct | the last day's volume and price change |
A new index needs about an hour before it shows a Strength and before a token launch can be paired with it.
6. Rebalance it
Propose new target weights. targets follows the same rules as components:
curl -X POST https://api.bankr.bot/indexes/0xYourIndex/plan \
-H "X-API-Key: $BANKR_API_KEY" \
-H "content-type: application/json" \
-d '{
"chain": "base",
"targets": [
{ "address": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "weightBps": 6000 },
{ "address": "0x22af33fe49fd1fa80c7149773dde5890d3c76f3b", "weightBps": 4000 }
]
}'
{ "txHash": "0x…" }
A proposal is a plan, not a trade:
- It waits a 48-hour notice, so holders can see it coming.
- The basket then moves through public auctions that anyone can fill, a slice at a time, under daily limits.
- It ends after 14 days at most. Whatever has filled stays; the rest keeps the old weights.
A plan can move between 1 % and 50 % of the basket. Read its progress from the index's plan field and from
GET /indexes/0xYourIndex/auctions?chain=base. To call it off, send { "chain": "base" } to
POST /indexes/0xYourIndex/plan/cancel.
To hand the job to a rule instead, change the mode:
curl -X POST https://api.bankr.bot/indexes/0xYourIndex/mode \
-H "X-API-Key: $BANKR_API_KEY" \
-H "content-type: application/json" \
-d '{ "chain": "base", "mode": "auto", "rule": "monthly", "driftBps": 0 }'
The whole thing as one script
Save this as index.ts and run it with bun index.ts (or Node 22+). With DRY_RUN=1 it stops after the quote.
const API = "https://api.bankr.bot";
const CHAIN = "base";
const KEY = process.env.BANKR_API_KEY!;
const WALLET = process.env.BANKR_WALLET!;
async function call(path: string, body?: unknown, key = false) {
const res = await fetch(`${API}${path}`, {
method: body ? "POST" : "GET",
headers: {
"content-type": "application/json",
...(key && { "X-API-Key": KEY }),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) throw new Error(`${path} ${res.status}: ${json.error}`);
return json;
}
// 1. Two eligible tokens, the deepest first, at equal weight.
const { components } = await call(
`/indexes/components?chain=${CHAIN}&kind=tokens`,
);
const picks = components
.filter((c: any) => c.eligible && c.depth2PctUsd)
.sort((a: any, b: any) => b.depth2PctUsd - a.depth2PctUsd)
.slice(0, 2);
const index = {
chain: CHAIN,
name: "My First Index",
symbol: "MYIDX",
components: picks.map((c: any) => ({ address: c.address, weightBps: 5000 })),
rebalance: { mode: "manual", rule: "none", driftBps: 0 },
firstBuy: { token: "stable", amountRaw: "105000000" },
fees: { feeBps: 50, holderShareBps: 0 },
};
// 2. Ticker, then 3. quote. Nothing is spent yet.
const { available } = await call(
`/indexes/ticker?chain=${CHAIN}&ticker=${index.symbol}`,
);
if (!available) throw new Error(`${index.symbol} is taken`);
const quote = await call("/indexes/quote-create", {
...index,
account: WALLET,
});
if (quote.blockers.length)
throw new Error(`blocked: ${JSON.stringify(quote.blockers)}`);
console.log(
"quote ok:",
picks.map((c: any) => c.symbol).join(" + "),
quote.fees,
);
if (process.env.DRY_RUN) process.exit(0);
// 4. Create. This spends the first buy from the key's wallet.
const created = await call("/indexes", index, true);
console.log("created", created.indexAddress, created.txHash);
// 5. Watch.
const detail = await call(`/indexes/${created.indexAddress}?chain=${CHAIN}`);
console.log(
"NAV $",
detail.navUsd,
detail.components.map((c: any) => [c.symbol, c.currentBps]),
);
// 6. Later: propose 60 / 40. Starts a 48-hour notice, then auctions.
// await call(`/indexes/${created.indexAddress}/plan`, {
// chain: CHAIN,
// targets: [
// { address: picks[0].address, weightBps: 6000 },
// { address: picks[1].address, weightBps: 4000 },
// ],
// }, true);
Giving the job to an agent
An agent runs the same six calls. What changes is how you keep it on the rails:
- Give it the reference. Point it at this page and the developer page, or at llms-full.txt, before it writes any call.
- Give it its own key and wallet. Create a separate Bankr account for the agent, fund it with only what it may spend, and set a spend limit on the wallet. A key's IP allowlist stops the key working from anywhere else.
- Make it quote before it acts.
quote-createis free and returns the exact reason a create would fail. Have the agent readblockersinstead of retrying a failed create. - Have it explain each proposal. A proposal is public for 48 hours before it trades. Publishing the agent's reasoning with it is what gives holders something to follow.
- Don't let it churn. Each plan costs the basket a little in auction slippage, is capped in size, and can't be
followed by another until
nextProposalAt. An agent that proposes on every tick gains nothing. - Poll, don't stream. Read the index once a minute at most; the public reads are limited to 60 a minute per IP.
What can stop a call
| Answer | What to do |
|---|---|
400 with details | a field breaks a rule; each entry names the field and the reason |
401 | the key is missing or wrong |
403 on a write | the key is read-only or has no Wallet API access, or the wallet is not this index's rebalancer |
409 with blockers on a create | fix what the blockers name (too small a first buy, a thin basket, a token still warming) and quote again |
429 | slow down: 5 creates and 10 rebalancing calls a minute per wallet |
503 | a price or a read wasn't fresh; try again in a few seconds |
Next
- Index tokens for developers: every endpoint, contract and event.
- Index auctions for fillers: how the rebalance auctions fill.
- Index-paired launches: launching a token against your index.