Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
053c095
feat(skills): add unwindleg-yield-rotator-sUSDh-USDCx-sBTC structural…
TheBigMacBTC May 11, 2026
9cd2806
chore(unwindleg): blank author + author-agent (placeholders for now)
TheBigMacBTC May 11, 2026
31d85f4
feat(unwindleg): full 5-leg unwind implementation across the Hermetic…
TheBigMacBTC May 11, 2026
3019978
fix(unwindleg): waitForTxConfirmation between inline legs to avoid no…
TheBigMacBTC May 11, 2026
da1e3b4
fix(unwindleg): asset-name static map + unstake-broadcast race + resi…
TheBigMacBTC May 11, 2026
585c1c4
chore(unwindleg): set metadata.author + author-agent to clear CI
TheBigMacBTC May 11, 2026
6da6a3b
fix(unwindleg): HARD BLOCKER inlineUnstake arg-count + proper tuple d…
TheBigMacBTC May 11, 2026
2cd0130
refactor(unwindleg): scope to 4 legs (unwind = remove leverage); drop…
TheBigMacBTC May 12, 2026
da88d91
Revert "refactor(unwindleg): scope to 4 legs (unwind = remove leverag…
TheBigMacBTC May 12, 2026
fdda79f
refactor(unwindleg): de-bundle leg-3 swap + pre-leg-5 readonly into d…
TheBigMacBTC May 12, 2026
7411eff
docs(unwindleg): refresh AGENT.md — drop primitive references, docume…
TheBigMacBTC May 12, 2026
c9eec5a
docs(unwindleg): clarify Hermetica cooldown clock semantic at consume…
TheBigMacBTC May 12, 2026
43560a4
feat(unwindleg): enforce --mempool-depth-limit before each inline bro…
TheBigMacBTC May 12, 2026
18add8a
fix(unwindleg): address Diego gap analysis Blockers 1 + 2 on PR #605
TheBigMacBTC May 16, 2026
6eabcf9
fix(unwindleg): leg-5 ft-trait vault routing + Allow-mode PCs on legs…
TheBigMacBTC May 23, 2026
0c6ac02
fix(unwindleg): leg-5 redeem amount must equal recorded collateral (n…
TheBigMacBTC May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: unwindleg-hermeticaunstake-zestrepay-yield-rotator-sUSDh-USDCx-sBTC-agent
skill: unwindleg-hermeticaunstake-zestrepay-yield-rotator-sUSDh-USDCx-sBTC
description: "Coordinates a 5-leg unwind of the wind skill's (#604) position. Phase 1: unstake sUSDh on Hermetica (irreversible, starts 7d cooldown). Phase 2 (after cooldown): silo-withdraw -> swap USDh -> USDCx on Bitflow -> repay Zest USDCx debt -> withdraw sBTC collateral. Never broadcasts wind operations."
---

# Agent briefing — unwindleg

## Your job

Take a wallet from {sUSDh held, USDCx debt on Zest, sBTC collateral on Zest} back to {sBTC held, debt zero}. Five legs across a 7-day cooldown.

## Pre-flight (always run before any write)

1. `doctor --wallet <addr>` — must return `status: success`. Confirms the Bitflow swap primitive is installed, Hermetica state is readable, and Bitflow's USDh + borrow + collateral tokens resolve.
2. Read the wallet's sUSDh balance from the doctor output. Confirm `--susdh-amount-base` ≤ balance.
3. If a checkpoint already exists for the wallet (doctor surfaces it), do NOT call `run`. Either call `resume` (if the operator wants to continue the existing unwind) or `cancel` (if abandoning it).

## Run phase 1 (broadcast unstake)

```
run --wallet <addr> --susdh-amount-base <base> --min-sbtc-withdraw-sats <sats> --confirm=UNWIND
```

Expected outcomes:

- **`status: success` with `data.checkpoint.step = "unstake_confirmed"`** — unstake broadcast, silo claim recorded, cooldown is running. The response includes `cooldownExpiresAt` ISO timestamp. Schedule phase 2 (`resume`) for after that timestamp + the operator's grace margin.
- **`status: error` with `error.code = "CLAIM_ID_INDETERMINATE"`** — unstake broadcast but the silo claim-id counter didn't advance within `--wait-seconds`. The unstake tx is on chain; the controller just couldn't snapshot the claim-id atomically. Recovery: read `staking-silo-v1-1.get-current-claim-id` after the unstake tx confirms, then hand-edit the checkpoint to record the claim-id, then run `resume`. The error data includes `unstakeTxid` and `preClaimId` for forensics.
- **`status: error` with `error.code = "INSUFFICIENT_SUSDH_BALANCE"`** — operator requested more sUSDh than the wallet holds. Reduce `--susdh-amount-base`.
- **`status: error` with `error.code = "SIGNER_UNAVAILABLE"`** — none of `AIBTC_SESSION_FILE` / `STACKS_PRIVATE_KEY` / `CLIENT_MNEMONIC` resolved to a key matching `--wallet`. Surface the per-path attempt list to the operator and stop.

## Wait the cooldown

7 days by default. Read `data.checkpoint.cooldownExpiresAt` and the operator's `--cooldown-grace-seconds` (default 300). Do not call `resume` before `cooldownExpiresAt + grace`. The skill will reject early `resume` with `COOLDOWN_NOT_EXPIRED` including `secondsRemaining`.

## Run phase 2 (broadcast legs 2-5)

```
resume --wallet <addr> --confirm=UNWIND
```

This broadcasts in order: silo-withdraw → swap → repay → withdraw. If any leg fails, the checkpoint records the partial state and the next `resume` picks up from there. Idempotent across retries.

Expected outcomes:

- **`status: success` with `data.checkpoint.step = "complete"`** — all 5 legs broadcast. Wallet now holds sBTC (no sUSDh, no Zest debt). The response includes all 5 txids: `unstakeTxid`, `claimTxid`, `swapTxid`, `repayTxid`, `withdrawTxid`.
- **`status: blocked` with `error.code = "COOLDOWN_NOT_EXPIRED"`** — too early. Wait `error.data.secondsRemaining` seconds and retry.
- **`status: error` with `error.code = "SWAP_OUTPUT_UNKNOWN"`** — the Bitflow swap primitive didn't expose observed-out balances. Inspect the primitive's response; this usually means the primitive's API changed shape. Repair before retrying.
- **`status: error` with `error.code = "BROADCAST_FAILED"`** — one of the inline broadcasts (silo-withdraw / repay / collateral-remove-redeem) was rejected by the node. The error includes the reason; do not retry until the underlying cause is resolved.

## Recovery shortcuts

- **Silo claim exists on chain but checkpoint is wrong:** the silo claim is the source of truth. Call `staking-silo-v1-1.withdraw(claim-id)` directly via any Stacks tool to recover the USDh. Then complete legs 3-5 manually or fix the checkpoint and `resume`.
- **Operator wants partial unwind:** this skill is full-unwind for a given `--susdh-amount-base`. For partial collateral withdraw at the end, the operator should call Zest's `collateral-remove-redeem` directly with a specific `amount` instead of letting this skill use `max-uint128`.

## What you must NEVER do

- Broadcast any wind operation. The wind skill at https://github.com/BitflowFinance/bff-skills/pull/604 owns that path.
- Bypass the cooldown by re-broadcasting an unstake that already has a pending silo claim. The silo claim from the first unstake still exists; just wait for it.
- Hand-edit the checkpoint file unless explicitly recovering from `CLAIM_ID_INDETERMINATE`. The checkpoint is the controller's truth; arbitrary edits create inconsistent state.
- Set `--cooldown-grace-seconds 0`. Miner-time skew means the on-chain `unlock-ts` check can fire microseconds after the wall-clock matches. The default 300s margin is cheap insurance.

## Output contract

Every subcommand prints exactly one JSON object: `{ status, action, data, error }`. The agent should treat `status: "success"` as advance, `status: "blocked"` as wait-and-retry per `error.next`, and `status: "error"` as halt-and-surface-to-operator.

## Companion

Wind path: https://github.com/BitflowFinance/bff-skills/pull/604
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
name: unwindleg-hermeticaunstake-zestrepay-yield-rotator-sUSDh-USDCx-sBTC
description: "Unwind-only companion to the wind-leg yield rotator: unstakes sUSDh on Hermetica (creates a 7-day silo claim), waits the cooldown, withdraws USDh from the silo, swaps USDh->USDCx via Bitflow, repays the USDCx debt on Zest, and withdraws the sBTC collateral. Closes the loop opened by https://github.com/BitflowFinance/bff-skills/pull/604."
metadata:
author: "Terese678"
author-agent: "Merged Vale"
user-invocable: "false"
arguments: "doctor | status | plan | run | resume | cancel"
entry: "unwindleg-hermeticaunstake-zestrepay-yield-rotator-sUSDh-USDCx-sBTC/unwindleg-hermeticaunstake-zestrepay-yield-rotator-sUSDh-USDCx-sBTC.ts"
requires: "wallet, signing, settings, bitflow-swap-aggregator"
tags: "defi, write, mainnet-only, requires-funds, l2"
---

# Unwind-Leg: HermeticaUnstake → ZestRepay Yield Unwinder

## Scope

Unwind-only. Five legs in order, with a built-in 7-day pause between legs 1 and 2:

1. **Unstake sUSDh on Hermetica** — inline `staking-v1-1.unstake(amount, none)`. Burns sUSDh from the wallet, creates a claim in `staking-silo-v1-1` with a 7-day cooldown.
2. **Wait cooldown** — the silo's `unlock-ts` for the new claim. Default 7 days (`staking-state-v1.get-cooldown-window` = `u604800`). `resume` blocks with `COOLDOWN_NOT_EXPIRED` until elapsed.
3. **Silo withdraw** — inline `staking-silo-v1-1.withdraw(claim-id)`. Releases USDh from the silo to the wallet.
4. **Swap USDh → USDCx** — shells out to `bitflow-swap-aggregator`. The Bitflow aggregator selects the venue (typically `BITFLOW_STABLE_XY_4` stableswap at small sizes, `dlmm_8` DLMM at sizes that move the stableswap pool).
5. **Repay USDCx debt on Zest** — inline `v0-4-market.repay(ft, amount, (some wallet))`. Clears the borrow position opened by the wind skill.
6. **Withdraw sBTC collateral** — inline `v0-4-market.collateral-remove-redeem(ft, max-uint128, min-sbtc-sats, (some wallet), none)`. Releases the sBTC collateral back to the wallet. The redeem-amount sentinel `max-uint128` instructs the contract to redeem all available collateral; `min-sbtc-sats` enforces a slippage floor.

The forward path (wind) is the companion `windleg` skill's job at https://github.com/BitflowFinance/bff-skills/pull/604. This skill never broadcasts wind operations.

## Asset journey

| Step | Wallet receives | Wallet sends | On-chain effect |
|---|---|---|---|
| Unstake | (silo claim) | sUSDh | sUSDh burned via `staking-v1-1.unstake`; silo creates a claim record with `amount-usdh = amount * ratio / usdh-base` and `unlock-ts = now + cooldown-window`. |
| Cooldown | — | — | 7-day wait (default `staking-state-v1.cooldown-window`). |
| Silo withdraw | USDh | (silo claim) | `staking-silo-v1-1.withdraw(claim-id)` transfers the claim's USDh amount from the silo's USDh reserve to the wallet. |
| Swap | USDCx (or borrow asset) | USDh | Bitflow aggregator routes through the best USDh venue at quote time. |
| Repay | (Zest debt cleared) | USDCx | `v0-4-market.repay(usdcx-token, amount, (some wallet))` reduces the wallet's debt position. |
| Withdraw | sBTC | (Zest collateral) | `v0-4-market.collateral-remove-redeem(sbtc-token, max-uint128, min-sats, (some wallet), none)` releases sBTC from the market vault to the wallet. |

The skill name spells the unwind direction: **sUSDh** (in) → **USDCx** (debt cleared) → **sBTC** (out).

## What it does

Drives the five-leg unwind under operator control. The skill is intentionally simpler than the wind skill — there is no strategy-score gate, because the user is exiting a position, not entering one. The cooldown is the only structural pause; otherwise the legs broadcast in sequence.

State machine:

```
idle
└─run→ unstake_confirmed
└─resume after cooldown→ claim_confirmed
└─→ swap_confirmed
└─→ repay_confirmed
└─→ complete (= sBTC in wallet, debt zero)
```

Every state transition is persisted to a checkpoint at `~/.aibtc/state/unwindleg-.../`. Resume picks up from whichever step the prior run left off at.

## Why agents need it

The wind skill creates a position that **cannot be unwound on short notice** because of the Hermetica 7-day silo cooldown. Without this companion skill, every wind broadcast strands the operator manually managing the silo claim, swap, repay, and withdraw legs across multiple sessions. With it, the operator runs `run` once to start the unwind, waits the cooldown (the skill blocks `resume` until `unlock-ts + --cooldown-grace-seconds`), and runs `resume` to broadcast the remaining four legs as a single atomic-from-the-operator's-POV sequence.

## Verified contracts (from canonical sources, not peer skills)

| Identifier | Source of verification |
|---|---|
| `SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG.staking-v1-1.unstake(uint)` | Hiro `/v2/contracts/source` — `(define-public (unstake (amount uint)))`. Single arg (the sister function `stake` takes `(amount, affiliate)`; unstake does not). Calls `staking-silo-v1-1.create-claim` internally and burns sUSDh. |
| `SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG.staking-silo-v1-1.withdraw(uint)` | Hiro `/v2/contracts/source` — `(define-public (withdraw (claim-id uint)))`. Transfers the claim's USDh from silo reserve to the recipient after the claim's `ts` value is reached. |
| `SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG.staking-silo-v1-1.get-current-claim-id` (read-only) | Used to snapshot the silo's claim counter before/after `unstake` to deterministically identify the new claim id without parsing contract events. |
| `SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG.staking-silo-v1-1.get-claim(uint)` (read-only) | Returns the claim record `{ recipient: principal, amount: uint, ts: uint }`. The `ts` field is the cooldown expiry (Stacks block time, via `get-stacks-block-info? time`) and drives the `COOLDOWN_NOT_EXPIRED` gate in `resume`. |
| `SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-4-market.repay(<ft-trait>, uint, optional principal)` | Hiro `/v2/contracts/source` — verified the function exists and matches the wind skill's borrow leg endpoint. |
| `SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-4-market.collateral-remove-redeem(<ft-trait>, uint, uint, optional principal, optional (list 3 (buff 8192)))` | Hiro `/v2/contracts/source` — the user-facing collateral withdraw path; the 4th arg is the receiver, the 5th is optional Pyth price feeds. |
| `SPN5AK…HSG.usdh-token-v1` (decimals: 8, asset name: `usdh`) | Hiro Clarity source + Bitflow `/v1/tokens` registry. |
| `SPN5AK…HSG.susdh-token-v1` (decimals: 8, asset name: `susdh`) | Hiro Clarity source. Burned by `unstake`. |
| Borrow + collateral asset contracts | Resolved at runtime from the Bitflow `/api/quotes/v1/tokens` registry against `--borrow-asset` and `--collateral-asset`. Defaults: `USDCx` and `sBTC`. |

## Inline broadcasts

This skill broadcasts four of its five legs inline (only the swap leg uses an existing primitive). The inline broadcasts adapt to both `@stacks/transactions` v6 and v7 at runtime — they pick `Pc.principal(...).willSendEq(...).ft(...)` when the v7 `Pc` builder is exported, fall back to `makeStandardFungiblePostCondition` + `FungibleConditionCode` + `createAssetInfo` for v6. `AnchorMode.Any` is passed only when the enum is exported (v7 omits it). `STACKS_MAINNET` constant is preferred over `new StacksMainnet()`. `broadcastTransaction` tries the v7+ object-arg shape first, falls back positional.

Post-conditions are deny-by-default + explicit allowances per leg:

| Leg | Post-condition |
|---|---|
| Unstake | wallet sends exactly `--susdh-amount-base` sUSDh. |
| Silo withdraw | silo contract sends exactly `claim.amount` USDh to the wallet. |
| Repay | wallet sends at most `observedUsdcxBase` of the borrow asset (Zest may take less if debt is smaller). |
| Withdraw | market vault sends at least `--min-sbtc-withdraw-sats` of the collateral asset to the wallet. |

## Signer (matches bff-skills primitives)

Same resolver chain used by `bitflow-swap-aggregator` and the wind skill — a wallet that signs the wind legs signs the unwind legs with no extra configuration. Each path is verified against `--wallet` and rejected on mismatch:

1. **`AIBTC_SESSION_FILE`** — encrypted session at `~/.aibtc/sessions/<wallet-id>.json` (written by `bun run wallet/wallet.ts unlock`). Decrypted with AES-256-GCM via the 32-byte key at `~/.aibtc/sessions/.session-key`. Active wallet id read from `AIBTC_WALLET_ID` env or `~/.aibtc/config.json#activeWalletId`.
2. **`STACKS_PRIVATE_KEY`** — raw hex key in env. Derivation uses `@stacks/transactions.getAddressFromPrivateKey(key, "mainnet")`.
3. **`CLIENT_MNEMONIC`** — 12/24-word mnemonic in env. Derivation uses `@stacks/wallet-sdk.generateWallet({ secretKey, password: "" })`.

Returns `SIGNER_UNAVAILABLE` with the per-path attempt list if no path matches `--wallet`.

## Safety notes

- **Write skill. Burns sUSDh, repays Zest debt, and withdraws collateral.**
- **Cooldown is non-negotiable.** The on-chain `staking-silo-v1-1.withdraw` reverts if `unlock-ts` hasn't been reached. The skill enforces this off-chain too via `COOLDOWN_NOT_EXPIRED` so the operator doesn't burn fees on a doomed broadcast. Use `--cooldown-grace-seconds` (default 300) to keep a small margin past `unlock-ts` for miner-time skew.
- Explicit `--confirm=UNWIND` required for both `run` and `resume`. No defaults.
- Cancel does NOT cancel the silo claim. If the operator runs `cancel` after `unstake_confirmed`, the on-chain claim still exists and can be withdrawn via `staking-silo-v1-1.withdraw(claim-id)` directly — the local checkpoint is cleared, the chain state isn't.
- `--min-sbtc-withdraw-sats` (default 1) is the post-condition floor on the collateral withdraw. Operators with a known expected payout should set this to ~95% of expected to catch unexpected slippage in the market accounting.
- The swap leg goes through `bitflow-swap-aggregator`'s slippage gate (`--slippage-bps`, default 150). USDh-stable routes are tight; 150 bps is a safe default.
- No HODLMM LP-destination integration. The swap leg routes through whichever Bitflow USDh venue the aggregator selects.

## Commands

### doctor
```bash
bun run skills/unwindleg-hermeticaunstake-zestrepay-yield-rotator-sUSDh-USDCx-sBTC/unwindleg-hermeticaunstake-zestrepay-yield-rotator-sUSDh-USDCx-sBTC.ts doctor --wallet <addr>
```

### status
```bash
bun run .../unwindleg-...ts status --wallet <addr>
```
Returns the current checkpoint plus, if `claimId` is recorded, the silo claim's `unlock-ts` + seconds remaining until cooldown elapses.

### plan
```bash
bun run .../unwindleg-...ts plan --wallet <addr> --susdh-amount-base <base>
```

### run
```bash
bun run .../unwindleg-...ts run --wallet <addr> --susdh-amount-base <base> --min-sbtc-withdraw-sats <sats> --confirm=UNWIND
```

`run` broadcasts the unstake leg, then attempts to advance through legs 2-5. If the cooldown hasn't elapsed (almost always the case on a freshly-broadcast unstake), it returns with the checkpoint at `unstake_confirmed` and the cooldown's expiry timestamp. The operator schedules `resume` for after the cooldown.

### resume
```bash
bun run .../unwindleg-...ts resume --wallet <addr> --confirm=UNWIND
```

Picks up wherever the prior run halted. Blocks with `COOLDOWN_NOT_EXPIRED` (including `secondsRemaining` in the error data) if the silo cooldown isn't over. Once past the cooldown, broadcasts silo-withdraw → swap → repay → withdraw in sequence; if any leg fails, the checkpoint records the partial state and the next `resume` picks up from there.

### cancel
```bash
bun run .../unwindleg-...ts cancel --wallet <addr>
```

## Output contract

All commands print exactly one JSON object to stdout:

```json
{ "status": "success | blocked | error", "action": "...", "data": {}, "error": null }
```

`error.message` is reachable as the registry minimum `{ "error": "<message>" }` shape when unwrapped one level.

## Known constraints

- Mainnet only.
- sUSDh → USDCx → sBTC path only.
- Borrow asset defaults to `USDCx`; collateral asset defaults to `sBTC`. Both can be overridden via `--borrow-asset` and `--collateral-asset` for symmetry with the wind skill.
- The Hermetica cooldown is configurable per-principal by Hermetica admins (`staking-state-v1.set-custom-cooldown`). The skill reads the actual `unlock-ts` from the silo claim, so any per-principal override is honored automatically.
- `cancel` does not affect the on-chain silo claim. To recover sUSDh from a claim that has been cancelled at the controller level, the operator calls `staking-silo-v1-1.withdraw(claim-id)` directly via any Stacks tool after the cooldown.
- The collateral-withdraw leg uses `max-uint128` as the redeem-amount sentinel. If the operator wants partial collateral withdraw, they should call the Zest market directly with a specific `amount` value instead of using this skill's `resume` for the final leg.

## Companion skill

Wind path (`sBTC → supply Zest → borrow USDCx → swap to USDh → stake Hermetica`): https://github.com/BitflowFinance/bff-skills/pull/604

## HODLMM integration declaration

**No.** The swap leg uses the Bitflow aggregator, which selects between `BITFLOW_STABLE_XY_4` (stableswap) and `dlmm_8` (HODLMM DLMM) based on quote. Even when it routes through `dlmm_8`, the skill does not LP into HODLMM as a destination — it consumes the venue as a router. Per the bonus criterion ("skills that directly integrate HODLMM"), the qualifying integration is LP/destination, not swap-venue routing.
Loading
Loading