A concentrated position earns only while the price is inside its range, and its fees sit in the position until someone collects them. PositionKeeper lets anyone do both chores on a Uniswap v3 position you own, under rules you set, for a reward you choose, and the NFT never leaves your wallet.
Enrolling a position
On Portfolio, the Automate drawer does both steps.
- Approve.
setApprovalForAll(positionKeeper, true)on the position manager. This covers every v3 position you own, and the keeper only touches enrolled ids. - Enrol.
enroll(tokenId, policy)from the NFT's owner. The first action can come no sooner thanminIntervallater.
updatePolicy swaps the rules without resetting the clock, and unenroll stops all actions at once. Revoking the approval also stops the keeper.
Your rules
| Name | Allowed | Default | Effect |
|---|---|---|---|
widthTicks | a multiple of the spacing ticks | 1200 | Total width of the range a rebalance mints. |
hysteresisTicks | 0 or more ticks | 120 | How far beyond the range edge the tick must be before a rebalance. |
minInterval | 0 or more seconds | 3600 | Time since enrolment or the last action before the next one. |
maxSlippageBps | 0 - 9,999 (app: 1 - 500) bps | 100 | Bounds the swap, the mint ratio and the rebalance round trip. |
compound | true or false | true | Harvest puts your share back into the position, or pays it to you. |
bountyBps | 0 - 300 bps of fees | 50 | Paid to whoever calls harvest or rebalance. |
Only minInterval stops a caller from harvesting crumbs for bounties, so choose an interval that makes each harvest worth having.
Collecting fees
harvest collects every fee the position has earned and none of its liquidity. Each token is split on its own: 10 % to the FeeRouter, booked to you so that your referrer earns 20 % of it, bountyBps to the caller, and the rest is yours. With compound on, your share goes back into the position. A harvest has no TWAP check, because only one interval's fees are at stake.
Re-centring a position
rebalance needs the tick outside the range by the hysteresis.
- 1UnwindAll liquidity is removed and the old NFT burned. The fee portion is split as in a harvest, and the principal is not.
- 2Swap and mint to youA new range centred on the current tick. The excess side is sold on the same pool, and the position is minted with you as owner.
- 3Value afterWhat was minted and sent back, at the same TWAP, must be at least the value before less
maxSlippageBps, or the whole transaction reverts. - 4Move the enrolmentYour policy is copied to the new token id with a fresh clock.
Price protection on a rebalance
- Before anything moves, the TWAP guard refuses while spot is more than 3 % from the 30-minute mean, or with under 10 minutes of oracle history.
- During the swap, the router refuses to fill below the spot quote less
maxSlippageBps. - After the mint, the value check compares the round trip at the TWAP. Inside the 3 % band a wrong spot can still cost up to half the gap.
After a genuine move, a position can take up to half an hour to qualify. A narrow range may not mint inside a tight tolerance, and then every rebalance reverts: widen widthTicks or raise maxSlippageBps.
Who calls the keeper
Anyone can, and shouldHarvest and shouldRebalance answer without gas. In practice ZKDepth's keeper bot does. Every minute by default it reads both views for every active enrolment, and sends only when the bounty is at least three times the estimated gas. Both actions compare the live ownerOf with the enrolment and revert on a mismatch.
contracts/src/PositionKeeper.solenroll, harvest, rebalance and the two should viewscontracts/src/libraries/TwapGuard.solthe window, the minimum history and the bandapps/api/src/bots/keeper.tssweepEnrollments: the views in one batch and the bounty in wei