Skip to main content

Index auctions for fillers

Bankr index tokens rebalance through public Dutch auctions (Index tokens). Each chain's Rebalancer contract runs them. Anyone can start an auction and anyone can fill one. A fill is atomic: the index sends you the token it sells, calls your contract back, and checks that the token it buys has arrived before the transaction ends. Flash loans are fine.

This page covers how an auction prices, how to find and quote one, the callback you implement, and the checks a fill must pass.

Launch

Auctions are deployed paused and switch on together on Base and Robinhood Chain. Until then startTranche and fill revert AuctionsArePaused.

How an auction works​

A rebalance plan becomes active after its 48-hour notice. From then on, the vault runs one auction at a time, called a tranche. Each tranche sells a slice of one overweight token X for one underweight token Y.

  • The contract picks the pair. startTranche(vault) takes no pair. It ranks the eligible pairs by price tier, then by size over floor, and starts the first one that passes every start check (below). Anyone may call it once the vault has no live tranche.
  • Size. The tranche is the smallest of: X's excess, Y's deficit, a share of the index's value, a share of the thinner registry pool's 2 % depth, and the index's daily and per-plan limits. Each token also has a daily auction limit shared by every index that holds it. A pair that missed recently auctions at half size or less.
  • Price. The curve compares two references, fixed when the tranche starts: X's and Y's USD prices. It starts a premium P above that ratio. The premium decays to the reference ratio over the first 15 % of the duration. The price then falls to a knee K below the reference by 75 %, falls to the floor F by 90 %, and stays flat at F until the end.
TierWhenDurationPremium PBase floor F0Knee allowance A
LIVEa live feed, or an in-session co-signed price (crypto: any hour)30 min1.00 %0.75 % (1.25 % for signed crypto)0.25 %
EXTENDEDpre-market, after hours and overnight, on co-signed 24/5 quotes45 min2.00 %2.00 %0.40 %
POOL_TWAPweekends, holidays, or no fresh 24/5 quote: a co-signed pool TWAP60 min3.00 %2.00 %0.60 %

A tranche takes the worse tier of its two tokens. With feePart the listed fees of the two tokens' primary venues (plus the stable / WETH bridge when their quotes differ):

F = min(6.00 %, F0 + feePart)
K = min(F, feePart + A)

So near the knee the vault pays about the route cost plus A. Below the knee, only a market that moved against the vault leaves room for a profit.

Amounts. For a fill of sellAmount of X at time t, the vault asks for:

buyAmount = ceil( sellAmount × refX × 10^decY × m(t) / (refY × 10^decX × 1e18) )

m(t) is the curve multiplier, scaled by 1e18. Every rounding step favours the vault: the premium rounds up and the discounts round down. Never compute the amount yourself for a live fill. Read it from quoteFill in the same block, or let maxBuyAmount bound it.

Weekend tranches. On the POOL_TWAP tier the sell token is never valued below 97 % of its last two-key price and the buy token never above 103 %. A weekend tranche therefore fills only while the pools agree with that price within 3 %, or have moved in the index's favour.

Finding auctions​

Live feed. GET https://api.bankr.bot/indexes/auctions/live?chain=base (or robinhood) is public, open to any origin and cached for 5 seconds; after that a copy up to 60 seconds old may answer while it refreshes, so check asOf. It reads at most 100 indexes per chain, live tranches first. It returns:

FieldWhat it is
tranches[]every live tranche: index, its rebalancer, and the tranche. That carries the tokens with decimals, tier, startAt / endAt, premiumBps, kneeBps, floorBps, feePartBps, the start references refSellUsd / refBuyUsd, initialSellRaw, soldRaw, and live.remainingSellRaw
quotequoteFill now, as a third-party filler, for sellAmountRaw: the whole remainder less 5 bps of headroom (see Size), raised to minSellRaw when that is under it (the whole rest once the remainder is under the 1 % minimum). It carries buyAmountRaw, minSellRaw, maxSellRaw, priceE18, and refusal (the error a fill would revert with now, or null)
previews[]previewStart for every vault with an active plan: the pair startTranche would pick now, its USD size and tier, or the refusal that stops it

An index's history, with every fill, is at GET https://api.bankr.bot/indexes/<index>/auctions?chain=base. It pages newest first through nextCursor.

On chain. The same facts come from the Rebalancer:

  • Views: trancheOf(vault), quoteFill(vault, sellAmount, filler), previewStart(vault), planOf(vault), pairStateOf(vault, X, Y), assetDayOf(asset) and cooldownOf(vault, token). They never revert; quoteFill and previewStart return a refusal selector instead.
  • Events: TrancheStarted, TrancheFilled, TrancheClosed, StartSkipped (a candidate pair a start passed over, with the reason) and EarlyFill.

Filling​

function fill(address vault, uint64 trancheId, uint256 sellAmount, uint256 maxBuyAmount, address recipient,
bytes calldata data) external returns (uint256 buyAmount, bool frozen);
  1. The Rebalancer computes buyAmount from the curve and checks it is at most maxBuyAmount.
  2. The vault sends sellAmount of X to recipient. That can be any address except zero and the vault.
  3. The vault calls indexAuctionCall on msg.sender, the caller of fill.
  4. When the callback returns, the vault's balance of Y must have grown by at least buyAmount (PaymentShort otherwise). Anything you pay above buyAmount stays with the index.
interface IIndexAuctionCallee {
/// Called by the vault after `sellAmount` of `sellToken` reached the fill's recipient. Before returning, the callee
/// must have transferred >= `buyAmount` of `buyToken` to msg.sender (the vault).
function indexAuctionCall(address rebalancer, uint64 trancheId, address sellToken, uint256 sellAmount,
address buyToken, uint256 buyAmount, bytes calldata data) external;
}

Inside the callback you may swap anywhere, unlock the Uniswap v4 PoolManager and use flash loans. You may not touch the vault, which is locked (its buys, sells, redeems, claims and auctionFill revert Locked), or the Rebalancer (nonReentrant). Your callee should accept the call only from a vault of Bankr's index factory, during a fill it started itself. Otherwise anyone can call it with made-up arguments.

Without a contract. IndexAuctionRouter.fillFromWallet(vault, trancheId, sellAmount, maxBuyAmount, recipient) fills from a wallet. It pulls at most maxBuyAmount of Y from you through an ERC-20 allowance to the router, and sends X to recipient, which must not be the router itself (InvalidRecipient). Never name the router as the recipient of your own fill either: X sent there is not yours any more, and the router can only sweep it to Bankr's treasury (rescue).

Size. The remainder is stored per index share and scales with the index's circulating supply at the moment of the fill: buys of the index grow it, and sells and redeems shrink it. A fill must be at least 1 % of the tranche, or the whole rest. Circulating supply includes the holder-fee shares still being released to holders, and that release can only lower the remainder, block by block. A transaction lands a block or more after you read it, so a fill sized to exactly the maxSell you read can revert FillSize when it lands. fillFromWallet and Bankr's backstop (BackstopFiller.bid) read quoteFill inside the transaction and clamp the size to the maxSell they get there, scaling your maxBuyAmount (or the backstop's minDelivered) to the clamped size at your own rate, rounded up. So the amount they sell can be less than the sellAmount you asked for. They return only what the index received (buyAmount, or the backstop's delivered), not the amount sold: read that from the TrancheFilled event or from your balance change. A direct fill caller should clamp the same way in its own transaction, or leave headroom: sell maxSell × (1 − δ), where δ is a few basis points plus the fall you measure over the last few blocks for as long as your transaction takes to land, as the feed's quote does.

Timing. No fill lands in the second its tranche starts (TooEarly), and none after endAt (TrancheOver). Bankr's backstop may fill at any point, like any filler (since 2026-10-08). It fills at the curve price, never below the floor, and keeps at most bankrFeeBps of the ask (0.20 %, capped at 0.50 % in its code) out of its own route's surplus, so a fill of yours beats it whenever your route returns more than the ask plus that fee.

Checks a fill must pass​

Every check refuses; none changes a price.

ErrorMeaning
WrongTranche, TooEarly, TrancheOvernot the vault's live tranche, its first second, or after its end
FillSize(minSell, maxSell)sellAmount is outside the fillable range at the current supply
BuyAboveMax(buyAmount, maxBuyAmount)the curve asks more than your limit
BelowLiveFloor(buyAmount, floor)at the live references (co-signed prices at most 30 s old, a TWAP at most 180 s), the curve price is below the floor F. A reference moved against the vault since the start
BelowPoolFloor(buyAmount, floor)at the current mid price of X's and Y's primary pools, the curve price is below the floor F. A pool moved against the vault
NoReference(asset)no fresh two-key price for X or Y right now; it usually clears within seconds
LossBudgetthis fill would take the plan past its loss budget
ComponentInShortfall, PaymentShort, InvalidRecipientY is in shortfall, your callback paid less than buyAmount, or recipient is zero or the vault
AuctionsArePausedthe guardian paused auctions; live tranches run out

A fill can also freeze the plan instead of filling. That happens when X or Y was paused by its issuer, changed its multiplier or registry entry, or when Bankr's signing key rotated. fill then returns frozen = true without moving tokens.

Start checks. startTranche passes over a pair (StartSkipped) when its references fail one of three guards:

  • a spike (more than 15 % against any reference sample of the last 90 minutes, 30 % for signed crypto, with at least one sample 30 to 90 minutes old);
  • an unconfirmed drift (more than 10 % since the plan started, unless the pools moved alike);
  • a market check against every registry pool's spot and 30-minute average.

Repeated skips only delay a rebalance; nothing moves.

Early fills​

A fill in the first 7.5 % of the duration pays most of the premium. Once such fills add up to 10 % of a tranche, the market has valued the pair above the reference by about the premium:

  • the tranche counts as early, and X (as seller) and Y (as buyer) rest for an hour, so the next auctions of them start from fresher prices;
  • a token's third early tranche in a plan rests it for 24 hours.

Early fills are always welcome. They pay the index a premium; the rest only waits for prices to catch up.

Notes​

  • Bankr wallets. A fill from a Bankr wallet that receives a tokenized stock counts as buying that stock. It follows the same region rules as a stock buy.
  • Reference filler. Bankr publishes a reference callee, DexFiller, and an open-source reference bot together with the auction launch. DexFiller routes X → quote token → Y through the index's own registry pools; extend it, or route anywhere you like.
  • Minting is never priced by an auction. Buys, sells and redeems of an index stay pro rata to its basket, so no auction fill changes what a holder's share is worth beyond the trade itself.