Skip to main content

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 needWhere
A Bankr account and its wallet addressbankr.bot
An API key with Wallet API accessbankr.bot/api-keys. A read-only key can't create
A little over $100 of USDC in that walleton 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.amountRaw is in the token's base units: 105000000 is 105 USDC. Exactly 100 USDC can price a few cents under $100 and be refused (SEED_BELOW_FLOOR), so leave a little room.
  • rebalance.mode decides who moves the basket later. manual means your code proposes new weights, which is what this guide uses. auto with rule: "monthly" restores the targets every month without you; off never rebalances.
  • fees.feeBps is the index fee on every trade, 25 to 100. holderShareBps is 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:

FieldMeaning
navUsd, navPerShareUsdwhat the basket is worth, in total and per index token
components[].targetBps, currentBpseach token's target weight and where it has drifted to
components[].state, statuswhether a token, or the whole index, is paused
strengthUsd, thinnesthow large a trade the index takes at 2 % impact, and the token that limits it
rebalancethe mode, and any mode change that is pending
planthe rebalance in progress, or null
nextProposalAtwhen the next proposal is allowed
vol24hUsd, change24hPctthe 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:

  1. It waits a 48-hour notice, so holders can see it coming.
  2. The basket then moves through public auctions that anyone can fill, a slice at a time, under daily limits.
  3. 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-create is free and returns the exact reason a create would fail. Have the agent read blockers instead 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​

AnswerWhat to do
400 with detailsa field breaks a rule; each entry names the field and the reason
401the key is missing or wrong
403 on a writethe key is read-only or has no Wallet API access, or the wallet is not this index's rebalancer
409 with blockers on a createfix what the blockers name (too small a first buy, a thin basket, a token still warming) and quote again
429slow down: 5 creates and 10 rebalancing calls a minute per wallet
503a price or a read wasn't fresh; try again in a few seconds

Next​