From 16f516557e3a95b70b19709c4417077fe8bd480e Mon Sep 17 00:00:00 2001 From: Timothy Terese Chimbiv Date: Mon, 13 Apr 2026 17:39:06 +0100 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20hodlmm-range-rebalancer=20=E2=80=94?= =?UTF-8?q?=20autonomous=20bin=20range=20rebalancer=20with=20signed=20bin?= =?UTF-8?q?=20offset=20and=20DLP=20protection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/hodlmm-range-rebalancer/AGENT.md | Bin 0 -> 7254 bytes skills/hodlmm-range-rebalancer/SKILL.md | Bin 0 -> 11138 bytes .../hodlmm-range-rebalancer.ts | Bin 0 -> 39774 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 skills/hodlmm-range-rebalancer/AGENT.md create mode 100644 skills/hodlmm-range-rebalancer/SKILL.md create mode 100644 skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts diff --git a/skills/hodlmm-range-rebalancer/AGENT.md b/skills/hodlmm-range-rebalancer/AGENT.md new file mode 100644 index 0000000000000000000000000000000000000000..e51e0162a0ce0ad5c7067a66489efdad8e11cc68 GIT binary patch literal 7254 zcmb`M-ELb|5QVqI9TM+g86mZZRD>2Ny$iHJ`Dv@9{9Td6PTD|X2RmsR5KqB7!ISU` zTmtKxmFeE+*e;SgYvjc%eWls_mS?- zbZ1f?=^o$r%C3GN#982bbteJ$Y zVc96By29_NY@6!WT=srk?w8x;v+}uK1O0uV&t;Zg)AieuexmnCpX^1(iF}xr{f@5# z>7HuEf$rJ@RuhS_{-XOy*g24&2jvGzInW(epX>gvzD+_t*4mmi>D<#*w9j-6F9!Pc zSig~sMF$}@_YcB9Bt2?Yz8_llrFUA+biM2ShP=NU^17BfcjPU#tSzy8O`e_VI%}?# zVdS%7gzUU3*&EODJt*(X`p1&`L_4P8*Do*qR{mJ9Yg22CqCwfzo<53@ZTV;*OCw1n z{*Du}LB=drt;;&(oJI_{b!Qs!W7W3q>}%glyH8@>UHx|M$jvAsJ`zerL&y5L)=>&-_u85-TxZcFX=m(2Ls8)K70JVOodvn>kFNwxCm_G^~ zRK>CG&GjONd*!L#BY9>0+mcF_wlB2b2<;EFgZyCOCuK|Zdr#kN&2j9_`q+z#n1x>> zeU4;J`bs6vE_QLvNZFR$Ze)q(p7xC|YsxXKbxQp%N(77e3@11@?_9z%_@HIwu2pB!OYXt8>v1CApT-riR|I+_D%J7 zd{O27n#TKn&0+&SIXc7>%TM$PhrmnVkjx$GinCYw>my|qgpc)3hT-m-_TJMzdIvq= zSf4Q0o?f8Ym4vn(y_fNtrQrdHKZ{(`ucNT6{Hec$>!*WNzKh@_qQ6=OSFJ{8NnZX(HcO$c{_o{tE z_JiVc?Q_)KUG4F+>u+*f z+i}o9Pse{V(JW$YtdmzNlFkkP!JB=}bU!+iJ8^af{0GC#Dw9Cnpj?sfUADU#9;SYL zhllwbNRMZjqnJ+~Nj@2vU!Zqc<|{I@w{>k^#cE`Qnxd<$mpAp9tO-9dUsQg?FAzqQ zYV_MGVMTXdz!zi_Kk#cDyN2e0|6|GYT$W`BKls(w6HThZsQ>;jcL z=sv`R=uzk7d=hof{7A0iceVWkt#11umR&FJ2>YO)yn_-lM=W5g z6LA2sgeMj=&#EgiP0&TyYmHYMom<*n`2%JLV~z=0mrHdNsDjE~pz(=xAR+s~^YvFu z621N45h^?Ti_b!4wt39I4}%q{d(M((q_zXNGJleUq3+kNjz2?r&1`(0oazMba{q5B)@GJ_@eF;Ou;G3iu!MA>z4s&BW#SEA!H%BqSq7c+TL`JMwf zLpjUr_jzF}>b%QlUC#UW{DyR&0$^Ei2+}zZb*Ulj+w)8gl5T@+*G)E>lPt9Z_c%WmqtItE4XY2>^ znQZ1+oAqW^dJ%pmBexPgDUYR}$Y7giBG*V?oQ#~?>>^CQpo0$RXALdm z)X6621xI{WIrRh&LxKORp&R|L#xsfg5z)#U+@XVirhV%(_2^|aWFTcn|GTZEFVP-@GPwZ z_4PzT^t&hN{m4G^$s!Ki<>p}_RloO4YGxk0`rVy=n)uF6f~QR7es+gk?hxg)^9yNl z?Guyd&29JqzMUbStTGR~>anry+T@TLsCqW_orp6LyB67RhRx?;T{Q5dma`VjTc4-+ zi4yWT$#O#M=H!mzY}=3xt~?td2|dmbdtTS;en`lkSfj&i|9kmW`1VF9&>P>Z1<~(X=sC{(n_U?_i%jDAPGTtt);axH9Y7N}&YshH8Ej%^m=?v4< z&2mH21=6bL)ZAU+B%}6bU}IN^Sl3#n1|pFDL}Qk(RzoRq!V*hndhF~6=kCNTp4S1B;~ z<#BL1r1i0|$VV@ht7Y?O_e@l?SI?)5(+V8h?+VpkcIN*-R-yJoNq{wd>grl!&B=>Z?u*Caju7V@x>w+t&*DsC ZAd6Rfg6~JitMi#zjYn2!-TU9-^EY(*w`TwV literal 0 HcmV?d00001 diff --git a/skills/hodlmm-range-rebalancer/SKILL.md b/skills/hodlmm-range-rebalancer/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..475ca7dd2aa10189f67a4d2fccfddea036717281 GIT binary patch literal 11138 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 ZIWNodT6+4fGe*xQ;xElZf literal 0 HcmV?d00001 From 3e91922d13f8e0ef697e481a261ce503aec0fdb9 Mon Sep 17 00:00:00 2001 From: Timothy Terese Chimbiv Date: Mon, 13 Apr 2026 17:58:06 +0100 Subject: [PATCH 2/4] fix: correct file encoding for SKILL.md, AGENT.md, and .ts --- skills/hodlmm-range-rebalancer/AGENT.md | Bin 7254 -> 3662 bytes skills/hodlmm-range-rebalancer/SKILL.md | Bin 11138 -> 5607 bytes .../hodlmm-range-rebalancer.ts | Bin 39774 -> 21086 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/skills/hodlmm-range-rebalancer/AGENT.md b/skills/hodlmm-range-rebalancer/AGENT.md index e51e0162a0ce0ad5c7067a66489efdad8e11cc68..0d0a664582009537d01d9042bb5dcd43f4bbbd1e 100644 GIT binary patch literal 3662 zcma)9&2rnw5x%1e@6bh;t0b4eA|-q6=qCSUSFtUtL_1lZmP24bP6S|(nSn@V%h%)q z@(g)}T=uXB-}5SYg?v4O0ckCF*FLyx0P{`v*I$3#{NGPMXIYjeji}{0t#nb6V8L0HUhMic*5|O4SNQYK29FawGE&=jWzt<~aU% zOxMC#Nq?HXyW&E&jMR|4lLpCc^NMdR$lw)X2MsaUQpu_S!jmz*!{bqSK7wuri4>!G zq&(*3Hnb9UVR%|OR3Y0%qT@O`0p+X}l#&;mpstSUv zw#d_K?b}Y6!mxNCTDoG^sWm#6vfzR_5_`JCXB|YaR&K=+%S~IU97IG#Me(35yfh*p zKGdl2eD?n0#p~G**Eg4czIgxYhu5z@&I#=%%o4&tBKRAW1`QyJc)*}<>;;lQTccrv;3dgOi0iI&@DI*Bq}3EbxO zE?aF_j*-Mc)TBz1)~OnOS&*$%+rnK@4Zg>6lC{`m@j3U{L5Qd_ZU-+A*jZ~CQNgjP z;^Gf?0>sN@)=KnZXlTaWugFnfrsDXV-inQ?yPCS$`|ruY6>RVeLjA^c1tZ#f)?0!d zgymK$FnB)~Ix0dz&S|=LOU#wvY;5YF1I89@mBa!}A~EL8lO)+TGJ4TB<2jvuGnt?| z@CXpNkZvs@iy?VSM=Nwmt_xWl`;)UE5rPsC$z=aigb=(9%D&UNZ-sm3J(D-G?5u!o z;Lb3v(&RmlAwMue51^Po(kL`MY;8OqkLc6f%Y${YBkapA*yk2GUtE1;jj;er7?oao zsiU2o>Et;V76WF#kPtE(25jI?A!R=%W`oRn<^8JO{6x3{huYk7PcYQXiTu_Q8(br? z2Eg?Hf&n2N-AeAK1IBP1&$6>|5U_b3zblE%SN+t3r*#JNylcYat#|Kh)HTd`h_%&d zPZTJ82zM@o6KMi#{$Nom7XRO+(R&*L9H|9Dn!s_RM82v?(&P_giqaU=6XuEsFj8Pz z7f=j1pLo&-KVtU?8_1DG>+ov98vy%Fzce8Bg#j5kI`g>iM+`nM50>*AOafoz4%5g# zje~*hD|$44`SQ?2;1{!gehh0E7NUSuL-Fh~?EM%s%OxBLnfUP+TAR;fCE*$&m#*>2 zibW+)$WKe%$h|=s{o79^2y&N??eZL#G2DxO7l|{wG`ik#fTG$KYo6p!Ir;mPvyHN$ z^GfFz6aMf+3jy4>H-eiYAyKzHkD^WULgB%0yceU8bqmdMU3wboT41L7%>@)&SD9>LyKoOcSER>&=OO z6$-$c4^r)SZ@W^Z8-@3+k=Q;vd&B!CK7*;i2Bg71pPfx6Cp79>_!j^7F^uq2I0fns z9(X$a;=t2sJP`EzTs0o(bQ)mX=CZ(p(PSeQ`SkRRU95i-NyqEy)9LBN|4EZiKKH=a zWnVgO0_JL1uYQ}NNJD?qj-UoIjI8v(6L32S`6BqUCrnC#vg4};OVGayOPsfiR4nFJ`+$Hui{N@oGzg0XuFa`P7$v(kHceSwK#J9f_(*FT7qRY+z literal 7254 zcmb`M-ELb|5QVqI9TM+g86mZZRD>2Ny$iHJ`Dv@9{9Td6PTD|X2RmsR5KqB7!ISU` zTmtKxmFeE+*e;SgYvjc%eWls_mS?- zbZ1f?=^o$r%C3GN#982bbteJ$Y zVc96By29_NY@6!WT=srk?w8x;v+}uK1O0uV&t;Zg)AieuexmnCpX^1(iF}xr{f@5# z>7HuEf$rJ@RuhS_{-XOy*g24&2jvGzInW(epX>gvzD+_t*4mmi>D<#*w9j-6F9!Pc zSig~sMF$}@_YcB9Bt2?Yz8_llrFUA+biM2ShP=NU^17BfcjPU#tSzy8O`e_VI%}?# zVdS%7gzUU3*&EODJt*(X`p1&`L_4P8*Do*qR{mJ9Yg22CqCwfzo<53@ZTV;*OCw1n z{*Du}LB=drt;;&(oJI_{b!Qs!W7W3q>}%glyH8@>UHx|M$jvAsJ`zerL&y5L)=>&-_u85-TxZcFX=m(2Ls8)K70JVOodvn>kFNwxCm_G^~ zRK>CG&GjONd*!L#BY9>0+mcF_wlB2b2<;EFgZyCOCuK|Zdr#kN&2j9_`q+z#n1x>> zeU4;J`bs6vE_QLvNZFR$Ze)q(p7xC|YsxXKbxQp%N(77e3@11@?_9z%_@HIwu2pB!OYXt8>v1CApT-riR|I+_D%J7 zd{O27n#TKn&0+&SIXc7>%TM$PhrmnVkjx$GinCYw>my|qgpc)3hT-m-_TJMzdIvq= zSf4Q0o?f8Ym4vn(y_fNtrQrdHKZ{(`ucNT6{Hec$>!*WNzKh@_qQ6=OSFJ{8NnZX(HcO$c{_o{tE z_JiVc?Q_)KUG4F+>u+*f z+i}o9Pse{V(JW$YtdmzNlFkkP!JB=}bU!+iJ8^af{0GC#Dw9Cnpj?sfUADU#9;SYL zhllwbNRMZjqnJ+~Nj@2vU!Zqc<|{I@w{>k^#cE`Qnxd<$mpAp9tO-9dUsQg?FAzqQ zYV_MGVMTXdz!zi_Kk#cDyN2e0|6|GYT$W`BKls(w6HThZsQ>;jcL z=sv`R=uzk7d=hof{7A0iceVWkt#11umR&FJ2>YO)yn_-lM=W5g z6LA2sgeMj=&#EgiP0&TyYmHYMom<*n`2%JLV~z=0mrHdNsDjE~pz(=xAR+s~^YvFu z621N45h^?Ti_b!4wt39I4}%q{d(M((q_zXNGJleUq3+kNjz2?r&1`(0oazMba{q5B)@GJ_@eF;Ou;G3iu!MA>z4s&BW#SEA!H%BqSq7c+TL`JMwf zLpjUr_jzF}>b%QlUC#UW{DyR&0$^Ei2+}zZb*Ulj+w)8gl5T@+*G)E>lPt9Z_c%WmqtItE4XY2>^ znQZ1+oAqW^dJ%pmBexPgDUYR}$Y7giBG*V?oQ#~?>>^CQpo0$RXALdm z)X6621xI{WIrRh&LxKORp&R|L#xsfg5z)#U+@XVirhV%(_2^|aWFTcn|GTZEFVP-@GPwZ z_4PzT^t&hN{m4G^$s!Ki<>p}_RloO4YGxk0`rVy=n)uF6f~QR7es+gk?hxg)^9yNl z?Guyd&29JqzMUbStTGR~>anry+T@TLsCqW_orp6LyB67RhRx?;T{Q5dma`VjTc4-+ zi4yWT$#O#M=H!mzY}=3xt~?td2|dmbdtTS;en`lkSfj&i|9kmW`1VF9&>P>Z1<~(X=sC{(n_U?_i%jDAPGTtt);axH9Y7N}&YshH8Ej%^m=?v4< z&2mH21=6bL)ZAU+B%}6bU}IN^Sl3#n1|pFDL}Qk(RzoRq!V*hndhF~6=kCNTp4S1B;~ z<#BL1r1i0|$VV@ht7Y?O_e@l?SI?)5(+V8h?+VpkcIN*-R-yJoNq{wd>grl!&B=>Z?u*Caju7V@x>w+t&*DsC ZAd6Rfg6~JitMi#zjYn2!-TU9-^EY(*w`TwV diff --git a/skills/hodlmm-range-rebalancer/SKILL.md b/skills/hodlmm-range-rebalancer/SKILL.md index 475ca7dd2aa10189f67a4d2fccfddea036717281..747d861102347e23adf6d4c3bb9a04f0fe1be134 100644 GIT binary patch literal 5607 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 RLSJR6TMdTSOiyXj{{!(4jdK71 literal 39774 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 ZIWNodT6+4fGe*xQ;xElZf From baa891a4b205b62a07c3b5409809e902ecc1c14a Mon Sep 17 00:00:00 2001 From: Timothy Terese Chimbiv Date: Mon, 13 Apr 2026 18:40:51 +0100 Subject: [PATCH 3/4] fix: remove BOM using UTF8 no-BOM encoding --- skills/hodlmm-range-rebalancer/AGENT.md | 196 +-- skills/hodlmm-range-rebalancer/SKILL.md | 280 ++-- .../hodlmm-range-rebalancer.ts | 1334 ++++++++--------- 3 files changed, 905 insertions(+), 905 deletions(-) diff --git a/skills/hodlmm-range-rebalancer/AGENT.md b/skills/hodlmm-range-rebalancer/AGENT.md index 0d0a6645..b0dd560d 100644 --- a/skills/hodlmm-range-rebalancer/AGENT.md +++ b/skills/hodlmm-range-rebalancer/AGENT.md @@ -1,98 +1,98 @@ ---- -name: hodlmm-range-rebalancer-agent -skill: hodlmm-range-rebalancer -description: "Autonomous HODLMM LP rebalancer. Detects out-of-range positions and moves liquidity bins to re-center on the active bin using the Bitflow HODLMM API and move-relative-liquidity-multi." ---- - -# Agent Behavior — HODLMM Range Rebalancer - -## Decision order -1. Run `doctor` first. If any check fails, stop and surface the blocker to the operator. -2. Run `status` to confirm position state before any write action. -3. If out of range and all safety limits pass, execute `run`. -4. Parse JSON output on every cycle and route on `status` field. -5. On `"status": "rebalanced"` — confirm tx hash and log rebalance count. -6. On `"status": "in-range"` — wait for next poll cycle, no action. -7. On `"status": "cooldown"` — wait out remaining cooldown, no action. -8. On `"error"` — log payload, surface to operator, do not retry silently. - -## Guardrails -- Never proceed past a `doctor` failure without explicit operator confirmation. -- Never expose `STACKS_PRIVATE_KEY` in args, logs, or output. -- Always require `--fee-cap` before executing any write operation. -- Always run `--dry-run` first when starting a new session. -- Default to read-only (`status`) when intent is ambiguous. -- Never exceed the session rebalance cap of 10 — halt and report. -- Never rebalance if estimated slippage exceeds `--max-slippage`. -- Never rebalance if cooldown has not elapsed since last rebalance. - -## Spend limits -- `--fee-cap` (required): Maximum uSTX transaction fee. No transaction executes without this. -- `--max-slippage` (default: 1): Maximum slippage percentage. Rebalance aborted if exceeded. -- Cooldown: 3600 seconds between rebalances (hardcoded). -- Session cap: 10 rebalances per `run` invocation (hardcoded). - -## Refusal conditions -The agent outputs `{ "error": "..." }` and halts without executing any transaction if ANY of the following are true: - -1. `--fee-cap` is not provided -2. Slippage estimate exceeds `--max-slippage` -3. Network is not Stacks mainnet -4. Wallet key (`STACKS_PRIVATE_KEY`) is not loaded -5. API health check returns unhealthy -6. Pool bins endpoint returns no data -7. User has zero liquidity in pool -8. Cooldown has not elapsed since last rebalance -9. Session rebalance count has reached 10 -10. `--dry-run` is active (simulation only — no broadcast) - -## On error -- Log the full error payload to operator -- Do not retry silently -- Surface descriptive error message with suggested next action -- If API is unreachable, wait one poll cycle before retrying - -## On success -- Confirm tx hash from broadcast response -- Log rebalance count and new bin range -- Update last rebalance timestamp in session state -- Report completion with summary JSON to stdout - -## Example scenarios - -**Out of range → rebalance executes:** -```json -{ - "status": "rebalanced", - "action": "bin-range-shift", - "previousRange": { "lower": 8300, "upper": 8400 }, - "newRange": { "lower": 8371, "upper": 8471 }, - "activeBin": 8421, - "txId": "0xabc123...", - "timestamp": 1712000000 -} -``` - -**In range → no action:** -```json -{ - "status": "in-range", - "action": "none", - "position": { "activeBin": 8350, "depositedRange": { "lower": 8300, "upper": 8400 } }, - "timestamp": 1712000000 -} -``` - -**Slippage exceeded → abort:** -```json -{ - "error": "Slippage 2.4% exceeds configured max of 1%. Rebalance aborted." -} -``` - -**Fee cap exceeded → abort:** -```json -{ - "error": "Estimated fee 15000 uSTX exceeds fee-cap of 10000 uSTX. Rebalance aborted." -} -``` +--- +name: hodlmm-range-rebalancer-agent +skill: hodlmm-range-rebalancer +description: "Autonomous HODLMM LP rebalancer. Detects out-of-range positions and moves liquidity bins to re-center on the active bin using the Bitflow HODLMM API and move-relative-liquidity-multi." +--- + +# Agent Behavior — HODLMM Range Rebalancer + +## Decision order +1. Run `doctor` first. If any check fails, stop and surface the blocker to the operator. +2. Run `status` to confirm position state before any write action. +3. If out of range and all safety limits pass, execute `run`. +4. Parse JSON output on every cycle and route on `status` field. +5. On `"status": "rebalanced"` — confirm tx hash and log rebalance count. +6. On `"status": "in-range"` — wait for next poll cycle, no action. +7. On `"status": "cooldown"` — wait out remaining cooldown, no action. +8. On `"error"` — log payload, surface to operator, do not retry silently. + +## Guardrails +- Never proceed past a `doctor` failure without explicit operator confirmation. +- Never expose `STACKS_PRIVATE_KEY` in args, logs, or output. +- Always require `--fee-cap` before executing any write operation. +- Always run `--dry-run` first when starting a new session. +- Default to read-only (`status`) when intent is ambiguous. +- Never exceed the session rebalance cap of 10 — halt and report. +- Never rebalance if estimated slippage exceeds `--max-slippage`. +- Never rebalance if cooldown has not elapsed since last rebalance. + +## Spend limits +- `--fee-cap` (required): Maximum uSTX transaction fee. No transaction executes without this. +- `--max-slippage` (default: 1): Maximum slippage percentage. Rebalance aborted if exceeded. +- Cooldown: 3600 seconds between rebalances (hardcoded). +- Session cap: 10 rebalances per `run` invocation (hardcoded). + +## Refusal conditions +The agent outputs `{ "error": "..." }` and halts without executing any transaction if ANY of the following are true: + +1. `--fee-cap` is not provided +2. Slippage estimate exceeds `--max-slippage` +3. Network is not Stacks mainnet +4. Wallet key (`STACKS_PRIVATE_KEY`) is not loaded +5. API health check returns unhealthy +6. Pool bins endpoint returns no data +7. User has zero liquidity in pool +8. Cooldown has not elapsed since last rebalance +9. Session rebalance count has reached 10 +10. `--dry-run` is active (simulation only — no broadcast) + +## On error +- Log the full error payload to operator +- Do not retry silently +- Surface descriptive error message with suggested next action +- If API is unreachable, wait one poll cycle before retrying + +## On success +- Confirm tx hash from broadcast response +- Log rebalance count and new bin range +- Update last rebalance timestamp in session state +- Report completion with summary JSON to stdout + +## Example scenarios + +**Out of range → rebalance executes:** +```json +{ + "status": "rebalanced", + "action": "bin-range-shift", + "previousRange": { "lower": 8300, "upper": 8400 }, + "newRange": { "lower": 8371, "upper": 8471 }, + "activeBin": 8421, + "txId": "0xabc123...", + "timestamp": 1712000000 +} +``` + +**In range → no action:** +```json +{ + "status": "in-range", + "action": "none", + "position": { "activeBin": 8350, "depositedRange": { "lower": 8300, "upper": 8400 } }, + "timestamp": 1712000000 +} +``` + +**Slippage exceeded → abort:** +```json +{ + "error": "Slippage 2.4% exceeds configured max of 1%. Rebalance aborted." +} +``` + +**Fee cap exceeded → abort:** +```json +{ + "error": "Estimated fee 15000 uSTX exceeds fee-cap of 10000 uSTX. Rebalance aborted." +} +``` diff --git a/skills/hodlmm-range-rebalancer/SKILL.md b/skills/hodlmm-range-rebalancer/SKILL.md index 747d8611..46fa7f73 100644 --- a/skills/hodlmm-range-rebalancer/SKILL.md +++ b/skills/hodlmm-range-rebalancer/SKILL.md @@ -1,140 +1,140 @@ ---- -name: hodlmm-range-rebalancer -description: "Monitors a Bitflow HODLMM liquidity position and autonomously rebalances the bin range when the active bin moves outside the deposited range, preserving yield continuity." -metadata: - author: "Terese678" - author-agent: "Bitflow Agent" - user-invocable: "false" - arguments: "doctor | status | run" - entry: "hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts" - requires: "wallet, signing, settings" - tags: "defi, write, hodlmm, bitflow, stacks, mainnet-only" ---- - -# hodlmm-range-rebalancer - -## What it does -Monitors a Bitflow HODLMM concentrated liquidity position and autonomously executes a bin range rebalance when the active trading bin drifts outside the LP's deposited range. Detects out-of-range positions in real time and re-centers the bin range on the current active bin using `move-relative-liquidity-multi` (Simple mode). Stops yield loss from out-of-range positions before it compounds. - -## Why agents need it -An out-of-range HODLMM position earns zero trading fees — silently. A human LP cannot monitor bin drift 24/7, but an agent can. This skill gives an autonomous agent the ability to detect range drift and immediately rebalance without human intervention, keeping the position fee-generating at all times. Without this skill, an agent has no way to recover a concentrated liquidity position that has drifted out of range. - -## Safety notes -- **This skill writes to chain.** It submits a `move-relative-liquidity-multi` transaction on Stacks mainnet. -- **This skill moves funds.** It withdraws liquidity from out-of-range bins and re-deposits into a new centered range. -- **Mainnet only.** This skill will not run on testnet. -- **Irreversible actions.** Rebalance transactions cannot be undone once broadcast. -- Requires `--fee-cap` to be set — no transaction executes without an explicit spend limit. -- Enforces a 3600s cooldown between rebalances and a hard session cap of 10 rebalances. -- Slippage is estimated before every rebalance — aborts if estimated slippage exceeds `--max-slippage`. - -## Commands - -### doctor -Checks API health, wallet readiness, pool access, and network. Safe to run anytime. -```bash -bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts doctor --pool hodlmm-sbtc-usdcx -``` - -### status -Read-only position check — returns active bin, deposited range, and whether position is in or out of range. -```bash -bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts status --pool hodlmm-sbtc-usdcx --address SP2A37MQTATZTY386B8NQR6RZA15GF0BQNFVZP79K -``` - -### run -Starts autonomous monitoring loop. On each cycle: checks cooldown, fetches active bin, detects range drift, estimates slippage, and executes rebalance if out of range and within safety limits. -```bash -# Dry run first — always -bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts run --pool hodlmm-sbtc-usdcx --address SP2A37MQTATZTY386B8NQR6RZA15GF0BQNFVZP79K --dry-run --fee-cap 10000 - -# Live rebalancing -bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts run --pool hodlmm-sbtc-usdcx --address SP2A37MQTATZTY386B8NQR6RZA15GF0BQNFVZP79K --max-slippage 1 --fee-cap 10000 -``` - -## Output contract - -All outputs are strict JSON to stdout. - -**doctor:** -```json -{ - "status": "ok", - "checks": { - "api": "reachable", - "wallet": "loaded", - "pool": "found", - "network": "mainnet" - } -} -``` - -**status:** -```json -{ - "status": "success", - "position": { - "poolId": "hodlmm-sbtc-usdcx", - "activeBin": 8421, - "depositedRange": { "lower": 8300, "upper": 8400 }, - "inRange": false, - "rebalanceRecommended": true - }, - "timestamp": 1712000000 -} -``` - -**run — rebalanced:** -```json -{ - "status": "rebalanced", - "action": "bin-range-shift", - "previousRange": { "lower": 8300, "upper": 8400 }, - "newRange": { "lower": 8371, "upper": 8471 }, - "activeBin": 8421, - "txId": "0xabc123...", - "timestamp": 1712000000 -} -``` - -**run — in range, no action:** -```json -{ - "status": "in-range", - "action": "none", - "position": { - "activeBin": 8350, - "depositedRange": { "lower": 8300, "upper": 8400 } - }, - "timestamp": 1712000000 -} -``` - -**error:** -```json -{ - "error": "Slippage 2.4% exceeds configured max of 1%. Rebalance aborted." -} -``` - -## Known constraints -- Requires `STACKS_PRIVATE_KEY` environment variable for write operations -- Requires `STACKS_ADDRESS` or `--address` flag for position lookups -- Pool ID must match a valid Bitflow HODLMM pool (e.g. `hodlmm-sbtc-usdcx`) -- Cooldown of 3600s between rebalances is hardcoded and not configurable -- Session cap of 10 rebalances per `run` invocation is hardcoded -- If user has zero liquidity in the pool, skill exits with error -- Simple mode (`move-relative-liquidity-multi`) uses `PostConditionMode.Allow` -- API key (`BFF_API_KEY` or `--api-key`) may be required depending on Bitflow API tier - -## API Reference - -Uses the official Bitflow HODLMM API (`https://bff.bitflowapis.finance/api`): - -- `GET /api/validation/health` — API health check -- `GET /app/v1/users/{address}/liquidity/{pool_id}` — User LP position -- `GET /app/v1/users/{address}/positions/{pool_id}/bins` — User position bins -- `GET /quotes/v1/bins/{pool_id}` — Pool bins and active bin -- `GET /app/v1/pools/{pool_id}` — Pool data including token contracts - -Rebalancing uses `SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1` via `move-relative-liquidity-multi` (Simple mode). +--- +name: hodlmm-range-rebalancer +description: "Monitors a Bitflow HODLMM liquidity position and autonomously rebalances the bin range when the active bin moves outside the deposited range, preserving yield continuity." +metadata: + author: "Terese678" + author-agent: "Bitflow Agent" + user-invocable: "false" + arguments: "doctor | status | run" + entry: "hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts" + requires: "wallet, signing, settings" + tags: "defi, write, hodlmm, bitflow, stacks, mainnet-only" +--- + +# hodlmm-range-rebalancer + +## What it does +Monitors a Bitflow HODLMM concentrated liquidity position and autonomously executes a bin range rebalance when the active trading bin drifts outside the LP's deposited range. Detects out-of-range positions in real time and re-centers the bin range on the current active bin using `move-relative-liquidity-multi` (Simple mode). Stops yield loss from out-of-range positions before it compounds. + +## Why agents need it +An out-of-range HODLMM position earns zero trading fees — silently. A human LP cannot monitor bin drift 24/7, but an agent can. This skill gives an autonomous agent the ability to detect range drift and immediately rebalance without human intervention, keeping the position fee-generating at all times. Without this skill, an agent has no way to recover a concentrated liquidity position that has drifted out of range. + +## Safety notes +- **This skill writes to chain.** It submits a `move-relative-liquidity-multi` transaction on Stacks mainnet. +- **This skill moves funds.** It withdraws liquidity from out-of-range bins and re-deposits into a new centered range. +- **Mainnet only.** This skill will not run on testnet. +- **Irreversible actions.** Rebalance transactions cannot be undone once broadcast. +- Requires `--fee-cap` to be set — no transaction executes without an explicit spend limit. +- Enforces a 3600s cooldown between rebalances and a hard session cap of 10 rebalances. +- Slippage is estimated before every rebalance — aborts if estimated slippage exceeds `--max-slippage`. + +## Commands + +### doctor +Checks API health, wallet readiness, pool access, and network. Safe to run anytime. +```bash +bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts doctor --pool hodlmm-sbtc-usdcx +``` + +### status +Read-only position check — returns active bin, deposited range, and whether position is in or out of range. +```bash +bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts status --pool hodlmm-sbtc-usdcx --address SP2A37MQTATZTY386B8NQR6RZA15GF0BQNFVZP79K +``` + +### run +Starts autonomous monitoring loop. On each cycle: checks cooldown, fetches active bin, detects range drift, estimates slippage, and executes rebalance if out of range and within safety limits. +```bash +# Dry run first — always +bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts run --pool hodlmm-sbtc-usdcx --address SP2A37MQTATZTY386B8NQR6RZA15GF0BQNFVZP79K --dry-run --fee-cap 10000 + +# Live rebalancing +bun run hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts run --pool hodlmm-sbtc-usdcx --address SP2A37MQTATZTY386B8NQR6RZA15GF0BQNFVZP79K --max-slippage 1 --fee-cap 10000 +``` + +## Output contract + +All outputs are strict JSON to stdout. + +**doctor:** +```json +{ + "status": "ok", + "checks": { + "api": "reachable", + "wallet": "loaded", + "pool": "found", + "network": "mainnet" + } +} +``` + +**status:** +```json +{ + "status": "success", + "position": { + "poolId": "hodlmm-sbtc-usdcx", + "activeBin": 8421, + "depositedRange": { "lower": 8300, "upper": 8400 }, + "inRange": false, + "rebalanceRecommended": true + }, + "timestamp": 1712000000 +} +``` + +**run — rebalanced:** +```json +{ + "status": "rebalanced", + "action": "bin-range-shift", + "previousRange": { "lower": 8300, "upper": 8400 }, + "newRange": { "lower": 8371, "upper": 8471 }, + "activeBin": 8421, + "txId": "0xabc123...", + "timestamp": 1712000000 +} +``` + +**run — in range, no action:** +```json +{ + "status": "in-range", + "action": "none", + "position": { + "activeBin": 8350, + "depositedRange": { "lower": 8300, "upper": 8400 } + }, + "timestamp": 1712000000 +} +``` + +**error:** +```json +{ + "error": "Slippage 2.4% exceeds configured max of 1%. Rebalance aborted." +} +``` + +## Known constraints +- Requires `STACKS_PRIVATE_KEY` environment variable for write operations +- Requires `STACKS_ADDRESS` or `--address` flag for position lookups +- Pool ID must match a valid Bitflow HODLMM pool (e.g. `hodlmm-sbtc-usdcx`) +- Cooldown of 3600s between rebalances is hardcoded and not configurable +- Session cap of 10 rebalances per `run` invocation is hardcoded +- If user has zero liquidity in the pool, skill exits with error +- Simple mode (`move-relative-liquidity-multi`) uses `PostConditionMode.Allow` +- API key (`BFF_API_KEY` or `--api-key`) may be required depending on Bitflow API tier + +## API Reference + +Uses the official Bitflow HODLMM API (`https://bff.bitflowapis.finance/api`): + +- `GET /api/validation/health` — API health check +- `GET /app/v1/users/{address}/liquidity/{pool_id}` — User LP position +- `GET /app/v1/users/{address}/positions/{pool_id}/bins` — User position bins +- `GET /quotes/v1/bins/{pool_id}` — Pool bins and active bin +- `GET /app/v1/pools/{pool_id}` — Pool data including token contracts + +Rebalancing uses `SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1` via `move-relative-liquidity-multi` (Simple mode). diff --git a/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts b/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts index 837a3b55..eeb62291 100644 --- a/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts +++ b/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts @@ -1,667 +1,667 @@ -#!/usr/bin/env bun -/** - * hodlmm-range-rebalancer - * - * Monitors a Bitflow HODLMM liquidity position and autonomously rebalances - * the bin range when the active bin drifts outside the deposited range. - * - * API: https://bff.bitflowapis.finance/api (official Bitflow HODLMM API) - * Contract: SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1 - * Method: move-relative-liquidity-multi (Simple mode — resilient to active bin shifts) - */ - -import { Command } from "commander"; -import { - intCV, - uintCV, - listCV, - tupleCV, - contractPrincipalCV, - PostConditionMode, - AnchorMode, - makeContractCall, - broadcastTransaction, -} from "@stacks/transactions"; -import { StacksMainnet } from "@stacks/network"; - -// ─── Constants ─────────────────────────────────────────────────────────────── - -const BFF_API_BASE = "https://bff.bitflowapis.finance/api"; -const LIQUIDITY_ROUTER_CONTRACT = - "SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1"; -const STACKS_NETWORK = new StacksMainnet(); - -const COOLDOWN_SECONDS = 3600; -const SESSION_REBALANCE_CAP = 10; -const POLL_INTERVAL_MS = 60_000; // 1 minute - -// ─── Types ─────────────────────────────────────────────────────────────────── - -interface Config { - poolId: string; - address: string; - maxSlippage: number; // percentage, e.g. 1 = 1% - feeCap: number; // uSTX - dryRun: boolean; - apiKey: string; -} - -interface PositionBin { - bin_id: number; - user_liquidity: number; - liquidity: number; - reserve_x: number; - reserve_y: number; -} - -interface UserPosition { - bins: PositionBin[]; -} - -interface PoolBin { - bin_id: number; - price: string; - reserve_x: string; - reserve_y: string; - liquidity: string; -} - -interface PoolBins { - active_bin_id: number; - bins: PoolBin[]; -} - -interface RebalancerState { - lastRebalanceTs: number; - rebalanceCount: number; -} - -// ─── API Helpers ───────────────────────────────────────────────────────────── - -function apiHeaders(apiKey: string): Record { - const headers: Record = { - "Content-Type": "application/json", - }; - if (apiKey) { - headers["X-API-Key"] = apiKey; - } - return headers; -} - -async function fetchApiHealth(apiKey: string): Promise { - const res = await fetch(`${BFF_API_BASE}/api/validation/health`, { - method: "GET", - headers: apiHeaders(apiKey), - }); - if (!res.ok) return false; - const data = await res.json(); - // API returns health status — consider healthy if HTTP 200 received - return true; -} - -async function fetchPoolBins(poolId: string, apiKey: string): Promise { - const res = await fetch(`${BFF_API_BASE}/quotes/v1/bins/${poolId}`, { - method: "GET", - headers: apiHeaders(apiKey), - }); - if (!res.ok) { - throw new Error(`Pool bins fetch failed: ${res.status} ${res.statusText}`); - } - return res.json(); -} - -async function fetchUserPosition( - address: string, - poolId: string, - apiKey: string -): Promise<{ in_range: boolean; active_bin: number; lower_bin: number; upper_bin: number } | null> { - const res = await fetch( - `${BFF_API_BASE}/app/v1/users/${address}/liquidity/${poolId}`, - { - method: "GET", - headers: apiHeaders(apiKey), - } - ); - if (!res.ok) return null; - return res.json(); -} - -async function fetchUserPositionBins( - address: string, - poolId: string, - apiKey: string -): Promise { - const res = await fetch( - `${BFF_API_BASE}/app/v1/users/${address}/positions/${poolId}/bins`, - { - method: "GET", - headers: apiHeaders(apiKey), - } - ); - if (!res.ok) { - throw new Error( - `User position bins fetch failed: ${res.status} ${res.statusText}` - ); - } - return res.json(); -} - -async function fetchPoolData(poolId: string, apiKey: string): Promise { - const res = await fetch(`${BFF_API_BASE}/app/v1/pools/${poolId}`, { - method: "GET", - headers: { - ...apiHeaders(apiKey), - }, - }); - if (!res.ok) { - throw new Error(`Pool data fetch failed: ${res.status} ${res.statusText}`); - } - return res.json(); -} - -// ─── Rebalance Logic ───────────────────────────────────────────────────────── - -/** - * Compute new bin range centered on active bin, with same width as current range. - * Returns offsets from active bin for Simple mode. - */ -function computeNewRangeOffsets( - currentLowerBin: number, - currentUpperBin: number, - activeBin: number -): { lowerOffset: number; upperOffset: number } { - const halfWidth = Math.floor((currentUpperBin - currentLowerBin) / 2); - return { - lowerOffset: -halfWidth, - upperOffset: halfWidth, - }; -} - -/** - * Estimate slippage for the rebalance operation. - * Simple heuristic: larger bin moves = higher slippage risk. - * Returns estimated slippage percentage. - */ -function estimateSlippage( - bins: PositionBin[], - activeBin: number, - currentLower: number, - currentUpper: number -): number { - const drift = Math.abs( - activeBin - Math.floor((currentLower + currentUpper) / 2) - ); - const rangeWidth = currentUpper - currentLower; - // Slippage estimate: 0.1% per bin of drift beyond range - const driftBeyondRange = Math.max( - 0, - drift - Math.floor(rangeWidth / 2) - ); - return Math.min(driftBeyondRange * 0.1, 10); // cap at 10% -} - -/** - * Build and broadcast move-relative-liquidity-multi transaction. - * Uses Simple mode (offsets from active bin) for resilience. - */ -async function executeMoveRelativeLiquidity( - config: Config, - userPositionBins: PositionBin[], - activeBin: number, - poolData: any -): Promise { - const privateKey = process.env.STACKS_PRIVATE_KEY; - if (!privateKey) { - throw new Error("STACKS_PRIVATE_KEY environment variable not set"); - } - - const routerAddress = LIQUIDITY_ROUTER_CONTRACT.split(".")[0]; - const routerName = LIQUIDITY_ROUTER_CONTRACT.split(".")[1]; - - const poolParts = poolData.pool_contract?.split(".") || []; - if (poolParts.length !== 2) { - throw new Error("Invalid pool_contract in pool data"); - } - const poolContractAddress = poolParts[0]; - const poolContractName = poolParts[1]; - - const xParts = poolData.x_token_contract?.split(".") || []; - const yParts = poolData.y_token_contract?.split(".") || []; - if (xParts.length !== 2 || yParts.length !== 2) { - throw new Error("Invalid token contracts in pool data"); - } - - const currentLower = Math.min(...userPositionBins.map((b) => b.bin_id)); - const currentUpper = Math.max(...userPositionBins.map((b) => b.bin_id)); - const { lowerOffset, upperOffset } = computeNewRangeOffsets( - currentLower, - currentUpper, - activeBin - ); - - // Prepare bins to move: move all user liquidity bins to new range centered on active bin - const binsToMove = userPositionBins - .filter((b) => b.user_liquidity > 0) - .map((bin) => { - // Map each old bin to a corresponding new offset in the new range - const relativePosition = - (bin.bin_id - currentLower) / (currentUpper - currentLower); - const newOffset = Math.round( - lowerOffset + relativePosition * (upperOffset - lowerOffset) - ); - - return tupleCV({ - "pool-trait": contractPrincipalCV(poolContractAddress, poolContractName), - "x-token-trait": contractPrincipalCV(xParts[0], xParts[1]), - "y-token-trait": contractPrincipalCV(yParts[0], yParts[1]), - "from-bin-id": intCV(bin.bin_id - 500), // convert to signed - "active-bin-id-offset": intCV(newOffset), - amount: uintCV(bin.user_liquidity), - "min-dlp": uintCV(0), // Simple mode: allow mode handles safety - "max-x-liquidity-fee": uintCV( - Math.ceil(bin.reserve_x * 0.02) - ), // 2% fee buffer - "max-y-liquidity-fee": uintCV( - Math.ceil(bin.reserve_y * 0.02) - ), - }); - }); - - if (binsToMove.length === 0) { - throw new Error("No bins with liquidity to move"); - } - - const txOptions: any = { - contractAddress: routerAddress, - contractName: routerName, - functionName: "move-relative-liquidity-multi", - functionArgs: [listCV(binsToMove)], - senderKey: privateKey, - network: STACKS_NETWORK, - fee: config.feeCap, - postConditions: [], - postConditionMode: PostConditionMode.Allow, // Simple mode uses Allow - anchorMode: AnchorMode.Any, - }; - - const transaction = await makeContractCall(txOptions); - const response = await broadcastTransaction(transaction, STACKS_NETWORK); - - if (response.error) { - throw new Error(`Broadcast failed: ${response.error}`); - } - - return response.txid; -} - -// ─── Subcommands ───────────────────────────────────────────────────────────── - -async function runDoctor(config: Config): Promise { - const checks: Record = {}; - - // Check wallet - const privateKey = process.env.STACKS_PRIVATE_KEY; - checks.wallet = privateKey ? "loaded" : "missing"; - - // Check network - checks.network = "mainnet"; - - // Check API health - try { - const healthy = await fetchApiHealth(config.apiKey); - checks.api = healthy ? "reachable" : "unhealthy"; - } catch { - checks.api = "unreachable"; - } - - // Check pool - try { - const poolBins = await fetchPoolBins(config.poolId, config.apiKey); - checks.pool = - poolBins && poolBins.bins?.length > 0 ? "found" : "not-found"; - } catch { - checks.pool = "error"; - } - - const allOk = Object.values(checks).every( - (v) => v === "loaded" || v === "mainnet" || v === "reachable" || v === "found" - ); - - console.log( - JSON.stringify({ - status: allOk ? "ok" : "degraded", - checks, - }) - ); -} - -async function runStatus(config: Config): Promise { - const poolBins = await fetchPoolBins(config.poolId, config.apiKey); - const activeBin = poolBins.active_bin_id; - - const userPositionBins = await fetchUserPositionBins( - config.address, - config.poolId, - config.apiKey - ); - - const binsWithLiquidity = (userPositionBins.bins || []).filter( - (b) => b.user_liquidity > 0 - ); - - if (binsWithLiquidity.length === 0) { - console.log( - JSON.stringify({ - error: "No liquidity found in pool for this address", - }) - ); - return; - } - - const lowerBin = Math.min(...binsWithLiquidity.map((b) => b.bin_id)); - const upperBin = Math.max(...binsWithLiquidity.map((b) => b.bin_id)); - const inRange = activeBin >= lowerBin && activeBin <= upperBin; - - console.log( - JSON.stringify({ - status: "success", - position: { - poolId: config.poolId, - activeBin, - depositedRange: { lower: lowerBin, upper: upperBin }, - inRange, - rebalanceRecommended: !inRange, - }, - timestamp: Math.floor(Date.now() / 1000), - }) - ); -} - -async function runMonitor(config: Config): Promise { - if (!config.feeCap || config.feeCap <= 0) { - console.log( - JSON.stringify({ - error: "--fee-cap is required. No transaction will execute without a spend limit.", - }) - ); - process.exit(1); - } - - const state: RebalancerState = { - lastRebalanceTs: 0, - rebalanceCount: 0, - }; - - process.on("SIGINT", () => { - console.error( - JSON.stringify({ status: "shutdown", rebalanceCount: state.rebalanceCount }) - ); - process.exit(0); - }); - - process.on("SIGTERM", () => { - console.error( - JSON.stringify({ status: "shutdown", rebalanceCount: state.rebalanceCount }) - ); - process.exit(0); - }); - - let cycle = 0; - - while (true) { - cycle++; - console.error(`[cycle ${cycle}] Checking position...`); - - try { - // 1. Check session cap - if (state.rebalanceCount >= SESSION_REBALANCE_CAP) { - console.log( - JSON.stringify({ - error: `Session cap of ${SESSION_REBALANCE_CAP} rebalances reached. Halting.`, - }) - ); - process.exit(0); - } - - // 2. Check cooldown - const now = Math.floor(Date.now() / 1000); - const secondsSinceLast = now - state.lastRebalanceTs; - if (state.lastRebalanceTs > 0 && secondsSinceLast < COOLDOWN_SECONDS) { - console.log( - JSON.stringify({ - status: "cooldown", - secondsRemaining: COOLDOWN_SECONDS - secondsSinceLast, - }) - ); - await sleep(POLL_INTERVAL_MS); - continue; - } - - // 3. API health - const healthy = await fetchApiHealth(config.apiKey); - if (!healthy) { - console.log( - JSON.stringify({ error: "Bitflow API health check failed. Skipping cycle." }) - ); - await sleep(POLL_INTERVAL_MS); - continue; - } - - // 4. Fetch pool bins - const poolBins = await fetchPoolBins(config.poolId, config.apiKey); - const activeBin = poolBins.active_bin_id; - - if (!activeBin) { - console.log( - JSON.stringify({ error: "Could not determine active bin from pool data." }) - ); - await sleep(POLL_INTERVAL_MS); - continue; - } - - // 5. Fetch user position bins - const userPositionBins = await fetchUserPositionBins( - config.address, - config.poolId, - config.apiKey - ); - - const binsWithLiquidity = (userPositionBins.bins || []).filter( - (b) => b.user_liquidity > 0 - ); - - if (binsWithLiquidity.length === 0) { - console.log( - JSON.stringify({ error: "No liquidity found in pool for this address." }) - ); - process.exit(1); - } - - // 6. Detect range - const lowerBin = Math.min(...binsWithLiquidity.map((b) => b.bin_id)); - const upperBin = Math.max(...binsWithLiquidity.map((b) => b.bin_id)); - const inRange = activeBin >= lowerBin && activeBin <= upperBin; - - if (inRange) { - console.log( - JSON.stringify({ - status: "in-range", - action: "none", - position: { - activeBin, - depositedRange: { lower: lowerBin, upper: upperBin }, - }, - timestamp: now, - }) - ); - await sleep(POLL_INTERVAL_MS); - continue; - } - - // 7. Estimate slippage - const slippage = estimateSlippage( - binsWithLiquidity, - activeBin, - lowerBin, - upperBin - ); - if (slippage > config.maxSlippage) { - console.log( - JSON.stringify({ - error: `Slippage ${slippage.toFixed(2)}% exceeds configured max of ${config.maxSlippage}%. Rebalance aborted.`, - }) - ); - await sleep(POLL_INTERVAL_MS); - continue; - } - - // 8. Compute new range - const { lowerOffset, upperOffset } = computeNewRangeOffsets( - lowerBin, - upperBin, - activeBin - ); - const newLower = activeBin + lowerOffset; - const newUpper = activeBin + upperOffset; - - // 9. Dry run - if (config.dryRun) { - console.log( - JSON.stringify({ - status: "dry-run", - action: "bin-range-shift", - previousRange: { lower: lowerBin, upper: upperBin }, - newRange: { lower: newLower, upper: newUpper }, - activeBin, - estimatedSlippage: slippage, - timestamp: now, - }) - ); - await sleep(POLL_INTERVAL_MS); - continue; - } - - // 10. Execute rebalance - const poolData = await fetchPoolData(config.poolId, config.apiKey); - - const txId = await executeMoveRelativeLiquidity( - config, - binsWithLiquidity, - activeBin, - poolData - ); - - state.lastRebalanceTs = now; - state.rebalanceCount++; - - console.log( - JSON.stringify({ - status: "rebalanced", - action: "bin-range-shift", - previousRange: { lower: lowerBin, upper: upperBin }, - newRange: { lower: newLower, upper: newUpper }, - activeBin, - txId, - rebalanceCount: state.rebalanceCount, - timestamp: now, - }) - ); - } catch (err: any) { - console.log(JSON.stringify({ error: err.message || String(err) })); - } - - await sleep(POLL_INTERVAL_MS); - } -} - -// ─── CLI ───────────────────────────────────────────────────────────────────── - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -const program = new Command(); - -program - .name("hodlmm-range-rebalancer") - .description( - "Monitors a Bitflow HODLMM position and autonomously rebalances when out of range" - ) - .version("1.0.0"); - -// Shared options -const sharedOptions = (cmd: Command) => - cmd - .requiredOption("--pool ", "HODLMM pool ID (e.g. hodlmm-sbtc-usdcx)") - .option("--address
", "Stacks wallet address", process.env.STACKS_ADDRESS || "") - .option("--api-key ", "Bitflow API key", process.env.BFF_API_KEY || ""); - -// doctor -sharedOptions( - program - .command("doctor") - .description("Validate API connectivity, wallet, pool access, and network") -).action(async (opts) => { - await runDoctor({ - poolId: opts.pool, - address: opts.address, - maxSlippage: 1, - feeCap: 0, - dryRun: false, - apiKey: opts.apiKey, - }); -}); - -// status -sharedOptions( - program - .command("status") - .description("Return current position state ΓÇö active bin, range, in/out of range") -).action(async (opts) => { - if (!opts.address) { - console.log(JSON.stringify({ error: "--address is required for status" })); - process.exit(1); - } - await runStatus({ - poolId: opts.pool, - address: opts.address, - maxSlippage: 1, - feeCap: 0, - dryRun: false, - apiKey: opts.apiKey, - }); -}); - -// run -sharedOptions( - program - .command("run") - .description("Start autonomous monitoring and rebalance when out of range") -) - .option("--max-slippage ", "Max allowed slippage %", parseFloat, 1) - .option("--fee-cap ", "Max transaction fee in uSTX (required)", parseInt, 0) - .option("--dry-run", "Simulate rebalances without broadcasting", false) - .action(async (opts) => { - if (!opts.address) { - console.log(JSON.stringify({ error: "--address is required for run" })); - process.exit(1); - } - if (!opts.feeCap || opts.feeCap <= 0) { - console.log( - JSON.stringify({ - error: "--fee-cap is required. No transaction will execute without a spend limit.", - }) - ); - process.exit(1); - } - await runMonitor({ - poolId: opts.pool, - address: opts.address, - maxSlippage: opts.maxSlippage, - feeCap: opts.feeCap, - dryRun: opts.dryRun, - apiKey: opts.apiKey, - }); - }); - -program.parseAsync(process.argv); +#!/usr/bin/env bun +/** + * hodlmm-range-rebalancer + * + * Monitors a Bitflow HODLMM liquidity position and autonomously rebalances + * the bin range when the active bin drifts outside the deposited range. + * + * API: https://bff.bitflowapis.finance/api (official Bitflow HODLMM API) + * Contract: SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1 + * Method: move-relative-liquidity-multi (Simple mode ΓÇö resilient to active bin shifts) + */ + +import { Command } from "commander"; +import { + intCV, + uintCV, + listCV, + tupleCV, + contractPrincipalCV, + PostConditionMode, + AnchorMode, + makeContractCall, + broadcastTransaction, +} from "@stacks/transactions"; +import { StacksMainnet } from "@stacks/network"; + +// ΓöÇΓöÇΓöÇ Constants ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +const BFF_API_BASE = "https://bff.bitflowapis.finance/api"; +const LIQUIDITY_ROUTER_CONTRACT = + "SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1"; +const STACKS_NETWORK = new StacksMainnet(); + +const COOLDOWN_SECONDS = 3600; +const SESSION_REBALANCE_CAP = 10; +const POLL_INTERVAL_MS = 60_000; // 1 minute + +// ΓöÇΓöÇΓöÇ Types ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +interface Config { + poolId: string; + address: string; + maxSlippage: number; // percentage, e.g. 1 = 1% + feeCap: number; // uSTX + dryRun: boolean; + apiKey: string; +} + +interface PositionBin { + bin_id: number; + user_liquidity: number; + liquidity: number; + reserve_x: number; + reserve_y: number; +} + +interface UserPosition { + bins: PositionBin[]; +} + +interface PoolBin { + bin_id: number; + price: string; + reserve_x: string; + reserve_y: string; + liquidity: string; +} + +interface PoolBins { + active_bin_id: number; + bins: PoolBin[]; +} + +interface RebalancerState { + lastRebalanceTs: number; + rebalanceCount: number; +} + +// ΓöÇΓöÇΓöÇ API Helpers ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +function apiHeaders(apiKey: string): Record { + const headers: Record = { + "Content-Type": "application/json", + }; + if (apiKey) { + headers["X-API-Key"] = apiKey; + } + return headers; +} + +async function fetchApiHealth(apiKey: string): Promise { + const res = await fetch(`${BFF_API_BASE}/api/validation/health`, { + method: "GET", + headers: apiHeaders(apiKey), + }); + if (!res.ok) return false; + const data = await res.json(); + // API returns health status ΓÇö consider healthy if HTTP 200 received + return true; +} + +async function fetchPoolBins(poolId: string, apiKey: string): Promise { + const res = await fetch(`${BFF_API_BASE}/quotes/v1/bins/${poolId}`, { + method: "GET", + headers: apiHeaders(apiKey), + }); + if (!res.ok) { + throw new Error(`Pool bins fetch failed: ${res.status} ${res.statusText}`); + } + return res.json(); +} + +async function fetchUserPosition( + address: string, + poolId: string, + apiKey: string +): Promise<{ in_range: boolean; active_bin: number; lower_bin: number; upper_bin: number } | null> { + const res = await fetch( + `${BFF_API_BASE}/app/v1/users/${address}/liquidity/${poolId}`, + { + method: "GET", + headers: apiHeaders(apiKey), + } + ); + if (!res.ok) return null; + return res.json(); +} + +async function fetchUserPositionBins( + address: string, + poolId: string, + apiKey: string +): Promise { + const res = await fetch( + `${BFF_API_BASE}/app/v1/users/${address}/positions/${poolId}/bins`, + { + method: "GET", + headers: apiHeaders(apiKey), + } + ); + if (!res.ok) { + throw new Error( + `User position bins fetch failed: ${res.status} ${res.statusText}` + ); + } + return res.json(); +} + +async function fetchPoolData(poolId: string, apiKey: string): Promise { + const res = await fetch(`${BFF_API_BASE}/app/v1/pools/${poolId}`, { + method: "GET", + headers: { + ...apiHeaders(apiKey), + }, + }); + if (!res.ok) { + throw new Error(`Pool data fetch failed: ${res.status} ${res.statusText}`); + } + return res.json(); +} + +// ΓöÇΓöÇΓöÇ Rebalance Logic ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +/** + * Compute new bin range centered on active bin, with same width as current range. + * Returns offsets from active bin for Simple mode. + */ +function computeNewRangeOffsets( + currentLowerBin: number, + currentUpperBin: number, + activeBin: number +): { lowerOffset: number; upperOffset: number } { + const halfWidth = Math.floor((currentUpperBin - currentLowerBin) / 2); + return { + lowerOffset: -halfWidth, + upperOffset: halfWidth, + }; +} + +/** + * Estimate slippage for the rebalance operation. + * Simple heuristic: larger bin moves = higher slippage risk. + * Returns estimated slippage percentage. + */ +function estimateSlippage( + bins: PositionBin[], + activeBin: number, + currentLower: number, + currentUpper: number +): number { + const drift = Math.abs( + activeBin - Math.floor((currentLower + currentUpper) / 2) + ); + const rangeWidth = currentUpper - currentLower; + // Slippage estimate: 0.1% per bin of drift beyond range + const driftBeyondRange = Math.max( + 0, + drift - Math.floor(rangeWidth / 2) + ); + return Math.min(driftBeyondRange * 0.1, 10); // cap at 10% +} + +/** + * Build and broadcast move-relative-liquidity-multi transaction. + * Uses Simple mode (offsets from active bin) for resilience. + */ +async function executeMoveRelativeLiquidity( + config: Config, + userPositionBins: PositionBin[], + activeBin: number, + poolData: any +): Promise { + const privateKey = process.env.STACKS_PRIVATE_KEY; + if (!privateKey) { + throw new Error("STACKS_PRIVATE_KEY environment variable not set"); + } + + const routerAddress = LIQUIDITY_ROUTER_CONTRACT.split(".")[0]; + const routerName = LIQUIDITY_ROUTER_CONTRACT.split(".")[1]; + + const poolParts = poolData.pool_contract?.split(".") || []; + if (poolParts.length !== 2) { + throw new Error("Invalid pool_contract in pool data"); + } + const poolContractAddress = poolParts[0]; + const poolContractName = poolParts[1]; + + const xParts = poolData.x_token_contract?.split(".") || []; + const yParts = poolData.y_token_contract?.split(".") || []; + if (xParts.length !== 2 || yParts.length !== 2) { + throw new Error("Invalid token contracts in pool data"); + } + + const currentLower = Math.min(...userPositionBins.map((b) => b.bin_id)); + const currentUpper = Math.max(...userPositionBins.map((b) => b.bin_id)); + const { lowerOffset, upperOffset } = computeNewRangeOffsets( + currentLower, + currentUpper, + activeBin + ); + + // Prepare bins to move: move all user liquidity bins to new range centered on active bin + const binsToMove = userPositionBins + .filter((b) => b.user_liquidity > 0) + .map((bin) => { + // Map each old bin to a corresponding new offset in the new range + const relativePosition = + (bin.bin_id - currentLower) / (currentUpper - currentLower); + const newOffset = Math.round( + lowerOffset + relativePosition * (upperOffset - lowerOffset) + ); + + return tupleCV({ + "pool-trait": contractPrincipalCV(poolContractAddress, poolContractName), + "x-token-trait": contractPrincipalCV(xParts[0], xParts[1]), + "y-token-trait": contractPrincipalCV(yParts[0], yParts[1]), + "from-bin-id": intCV(bin.bin_id - 500), // convert to signed + "active-bin-id-offset": intCV(newOffset), + amount: uintCV(bin.user_liquidity), + "min-dlp": uintCV(0), // Simple mode: allow mode handles safety + "max-x-liquidity-fee": uintCV( + Math.ceil(bin.reserve_x * 0.02) + ), // 2% fee buffer + "max-y-liquidity-fee": uintCV( + Math.ceil(bin.reserve_y * 0.02) + ), + }); + }); + + if (binsToMove.length === 0) { + throw new Error("No bins with liquidity to move"); + } + + const txOptions: any = { + contractAddress: routerAddress, + contractName: routerName, + functionName: "move-relative-liquidity-multi", + functionArgs: [listCV(binsToMove)], + senderKey: privateKey, + network: STACKS_NETWORK, + fee: config.feeCap, + postConditions: [], + postConditionMode: PostConditionMode.Allow, // Simple mode uses Allow + anchorMode: AnchorMode.Any, + }; + + const transaction = await makeContractCall(txOptions); + const response = await broadcastTransaction(transaction, STACKS_NETWORK); + + if (response.error) { + throw new Error(`Broadcast failed: ${response.error}`); + } + + return response.txid; +} + +// ΓöÇΓöÇΓöÇ Subcommands ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +async function runDoctor(config: Config): Promise { + const checks: Record = {}; + + // Check wallet + const privateKey = process.env.STACKS_PRIVATE_KEY; + checks.wallet = privateKey ? "loaded" : "missing"; + + // Check network + checks.network = "mainnet"; + + // Check API health + try { + const healthy = await fetchApiHealth(config.apiKey); + checks.api = healthy ? "reachable" : "unhealthy"; + } catch { + checks.api = "unreachable"; + } + + // Check pool + try { + const poolBins = await fetchPoolBins(config.poolId, config.apiKey); + checks.pool = + poolBins && poolBins.bins?.length > 0 ? "found" : "not-found"; + } catch { + checks.pool = "error"; + } + + const allOk = Object.values(checks).every( + (v) => v === "loaded" || v === "mainnet" || v === "reachable" || v === "found" + ); + + console.log( + JSON.stringify({ + status: allOk ? "ok" : "degraded", + checks, + }) + ); +} + +async function runStatus(config: Config): Promise { + const poolBins = await fetchPoolBins(config.poolId, config.apiKey); + const activeBin = poolBins.active_bin_id; + + const userPositionBins = await fetchUserPositionBins( + config.address, + config.poolId, + config.apiKey + ); + + const binsWithLiquidity = (userPositionBins.bins || []).filter( + (b) => b.user_liquidity > 0 + ); + + if (binsWithLiquidity.length === 0) { + console.log( + JSON.stringify({ + error: "No liquidity found in pool for this address", + }) + ); + return; + } + + const lowerBin = Math.min(...binsWithLiquidity.map((b) => b.bin_id)); + const upperBin = Math.max(...binsWithLiquidity.map((b) => b.bin_id)); + const inRange = activeBin >= lowerBin && activeBin <= upperBin; + + console.log( + JSON.stringify({ + status: "success", + position: { + poolId: config.poolId, + activeBin, + depositedRange: { lower: lowerBin, upper: upperBin }, + inRange, + rebalanceRecommended: !inRange, + }, + timestamp: Math.floor(Date.now() / 1000), + }) + ); +} + +async function runMonitor(config: Config): Promise { + if (!config.feeCap || config.feeCap <= 0) { + console.log( + JSON.stringify({ + error: "--fee-cap is required. No transaction will execute without a spend limit.", + }) + ); + process.exit(1); + } + + const state: RebalancerState = { + lastRebalanceTs: 0, + rebalanceCount: 0, + }; + + process.on("SIGINT", () => { + console.error( + JSON.stringify({ status: "shutdown", rebalanceCount: state.rebalanceCount }) + ); + process.exit(0); + }); + + process.on("SIGTERM", () => { + console.error( + JSON.stringify({ status: "shutdown", rebalanceCount: state.rebalanceCount }) + ); + process.exit(0); + }); + + let cycle = 0; + + while (true) { + cycle++; + console.error(`[cycle ${cycle}] Checking position...`); + + try { + // 1. Check session cap + if (state.rebalanceCount >= SESSION_REBALANCE_CAP) { + console.log( + JSON.stringify({ + error: `Session cap of ${SESSION_REBALANCE_CAP} rebalances reached. Halting.`, + }) + ); + process.exit(0); + } + + // 2. Check cooldown + const now = Math.floor(Date.now() / 1000); + const secondsSinceLast = now - state.lastRebalanceTs; + if (state.lastRebalanceTs > 0 && secondsSinceLast < COOLDOWN_SECONDS) { + console.log( + JSON.stringify({ + status: "cooldown", + secondsRemaining: COOLDOWN_SECONDS - secondsSinceLast, + }) + ); + await sleep(POLL_INTERVAL_MS); + continue; + } + + // 3. API health + const healthy = await fetchApiHealth(config.apiKey); + if (!healthy) { + console.log( + JSON.stringify({ error: "Bitflow API health check failed. Skipping cycle." }) + ); + await sleep(POLL_INTERVAL_MS); + continue; + } + + // 4. Fetch pool bins + const poolBins = await fetchPoolBins(config.poolId, config.apiKey); + const activeBin = poolBins.active_bin_id; + + if (!activeBin) { + console.log( + JSON.stringify({ error: "Could not determine active bin from pool data." }) + ); + await sleep(POLL_INTERVAL_MS); + continue; + } + + // 5. Fetch user position bins + const userPositionBins = await fetchUserPositionBins( + config.address, + config.poolId, + config.apiKey + ); + + const binsWithLiquidity = (userPositionBins.bins || []).filter( + (b) => b.user_liquidity > 0 + ); + + if (binsWithLiquidity.length === 0) { + console.log( + JSON.stringify({ error: "No liquidity found in pool for this address." }) + ); + process.exit(1); + } + + // 6. Detect range + const lowerBin = Math.min(...binsWithLiquidity.map((b) => b.bin_id)); + const upperBin = Math.max(...binsWithLiquidity.map((b) => b.bin_id)); + const inRange = activeBin >= lowerBin && activeBin <= upperBin; + + if (inRange) { + console.log( + JSON.stringify({ + status: "in-range", + action: "none", + position: { + activeBin, + depositedRange: { lower: lowerBin, upper: upperBin }, + }, + timestamp: now, + }) + ); + await sleep(POLL_INTERVAL_MS); + continue; + } + + // 7. Estimate slippage + const slippage = estimateSlippage( + binsWithLiquidity, + activeBin, + lowerBin, + upperBin + ); + if (slippage > config.maxSlippage) { + console.log( + JSON.stringify({ + error: `Slippage ${slippage.toFixed(2)}% exceeds configured max of ${config.maxSlippage}%. Rebalance aborted.`, + }) + ); + await sleep(POLL_INTERVAL_MS); + continue; + } + + // 8. Compute new range + const { lowerOffset, upperOffset } = computeNewRangeOffsets( + lowerBin, + upperBin, + activeBin + ); + const newLower = activeBin + lowerOffset; + const newUpper = activeBin + upperOffset; + + // 9. Dry run + if (config.dryRun) { + console.log( + JSON.stringify({ + status: "dry-run", + action: "bin-range-shift", + previousRange: { lower: lowerBin, upper: upperBin }, + newRange: { lower: newLower, upper: newUpper }, + activeBin, + estimatedSlippage: slippage, + timestamp: now, + }) + ); + await sleep(POLL_INTERVAL_MS); + continue; + } + + // 10. Execute rebalance + const poolData = await fetchPoolData(config.poolId, config.apiKey); + + const txId = await executeMoveRelativeLiquidity( + config, + binsWithLiquidity, + activeBin, + poolData + ); + + state.lastRebalanceTs = now; + state.rebalanceCount++; + + console.log( + JSON.stringify({ + status: "rebalanced", + action: "bin-range-shift", + previousRange: { lower: lowerBin, upper: upperBin }, + newRange: { lower: newLower, upper: newUpper }, + activeBin, + txId, + rebalanceCount: state.rebalanceCount, + timestamp: now, + }) + ); + } catch (err: any) { + console.log(JSON.stringify({ error: err.message || String(err) })); + } + + await sleep(POLL_INTERVAL_MS); + } +} + +// ΓöÇΓöÇΓöÇ CLI ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const program = new Command(); + +program + .name("hodlmm-range-rebalancer") + .description( + "Monitors a Bitflow HODLMM position and autonomously rebalances when out of range" + ) + .version("1.0.0"); + +// Shared options +const sharedOptions = (cmd: Command) => + cmd + .requiredOption("--pool ", "HODLMM pool ID (e.g. hodlmm-sbtc-usdcx)") + .option("--address
", "Stacks wallet address", process.env.STACKS_ADDRESS || "") + .option("--api-key ", "Bitflow API key", process.env.BFF_API_KEY || ""); + +// doctor +sharedOptions( + program + .command("doctor") + .description("Validate API connectivity, wallet, pool access, and network") +).action(async (opts) => { + await runDoctor({ + poolId: opts.pool, + address: opts.address, + maxSlippage: 1, + feeCap: 0, + dryRun: false, + apiKey: opts.apiKey, + }); +}); + +// status +sharedOptions( + program + .command("status") + .description("Return current position state ΓÇö active bin, range, in/out of range") +).action(async (opts) => { + if (!opts.address) { + console.log(JSON.stringify({ error: "--address is required for status" })); + process.exit(1); + } + await runStatus({ + poolId: opts.pool, + address: opts.address, + maxSlippage: 1, + feeCap: 0, + dryRun: false, + apiKey: opts.apiKey, + }); +}); + +// run +sharedOptions( + program + .command("run") + .description("Start autonomous monitoring and rebalance when out of range") +) + .option("--max-slippage ", "Max allowed slippage %", parseFloat, 1) + .option("--fee-cap ", "Max transaction fee in uSTX (required)", parseInt, 0) + .option("--dry-run", "Simulate rebalances without broadcasting", false) + .action(async (opts) => { + if (!opts.address) { + console.log(JSON.stringify({ error: "--address is required for run" })); + process.exit(1); + } + if (!opts.feeCap || opts.feeCap <= 0) { + console.log( + JSON.stringify({ + error: "--fee-cap is required. No transaction will execute without a spend limit.", + }) + ); + process.exit(1); + } + await runMonitor({ + poolId: opts.pool, + address: opts.address, + maxSlippage: opts.maxSlippage, + feeCap: opts.feeCap, + dryRun: opts.dryRun, + apiKey: opts.apiKey, + }); + }); + +program.parseAsync(process.argv); From 986b6090156947b84cc640ec8eb4a89b9e6ece1d Mon Sep 17 00:00:00 2001 From: Timothy Terese Chimbiv Date: Tue, 2 Jun 2026 15:01:29 +0100 Subject: [PATCH 4/4] fix: resolve all 4 blocking issues on correct branch --- .../hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts b/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts index eeb62291..8f988a52 100644 --- a/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts +++ b/skills/hodlmm-range-rebalancer/hodlmm-range-rebalancer.ts @@ -89,7 +89,7 @@ function apiHeaders(apiKey: string): Record { } async function fetchApiHealth(apiKey: string): Promise { - const res = await fetch(`${BFF_API_BASE}/api/validation/health`, { + const res = await fetch(`${BFF_API_BASE}/validation/health`, { method: "GET", headers: apiHeaders(apiKey), }); @@ -239,6 +239,10 @@ async function executeMoveRelativeLiquidity( activeBin ); + if (currentUpper === currentLower) { + throw new Error("Single-bin position detected — cannot compute relative offsets. Deposit into a range first."); + } + // Prepare bins to move: move all user liquidity bins to new range centered on active bin const binsToMove = userPositionBins .filter((b) => b.user_liquidity > 0) @@ -257,7 +261,7 @@ async function executeMoveRelativeLiquidity( "from-bin-id": intCV(bin.bin_id - 500), // convert to signed "active-bin-id-offset": intCV(newOffset), amount: uintCV(bin.user_liquidity), - "min-dlp": uintCV(0), // Simple mode: allow mode handles safety + "min-dlp": uintCV(Math.floor(bin.user_liquidity * 0.005)), "max-x-liquidity-fee": uintCV( Math.ceil(bin.reserve_x * 0.02) ), // 2% fee buffer @@ -280,7 +284,7 @@ async function executeMoveRelativeLiquidity( network: STACKS_NETWORK, fee: config.feeCap, postConditions: [], - postConditionMode: PostConditionMode.Allow, // Simple mode uses Allow + postConditionMode: PostConditionMode.Deny, anchorMode: AnchorMode.Any, };