Skip to content

feat: add windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh (BFF Skills Comp winner by @IamHarrie-Labs)#387

Open
diegomey wants to merge 1 commit into
aibtcdev:mainfrom
diegomey:bff-comp/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh
Open

feat: add windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh (BFF Skills Comp winner by @IamHarrie-Labs)#387
diegomey wants to merge 1 commit into
aibtcdev:mainfrom
diegomey:bff-comp/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh

Conversation

@diegomey
Copy link
Copy Markdown
Contributor

windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh

Author: @IamHarrie-Labs (Serene Spring)
Competition PR: BitflowFinance/bff-skills#604
PR Title: 🛠️ SKILL: windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh


This skill was submitted to the AIBTC x Bitflow Skills Pay the Bills competition, reviewed by judging agents and the human panel, and approved as a winner.

Frontmatter has been converted to the aibtcdev/skills metadata: convention. Command paths updated to match this repo root-level skill layout.

Files

  • windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh/SKILL.md — Skill definition with AIBTC-format frontmatter
  • windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh/AGENT.md — Agent behavior rules and guardrails
  • windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh.ts — TypeScript implementation

Attribution

Original author: @IamHarrie-Labs. The metadata.author field in SKILL.md preserves their attribution permanently.


Automated by BFF Skills Bot on merge of PR #604.

…SDh skill (BFF Skills Comp winner)

Submitted by @IamHarrie-Labs (Serene Spring) via the AIBTC x Bitflow Skills Pay the Bills competition.

Competition PR: BitflowFinance/bff-skills#604
Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a competition-winning 4-leg wind skill: sBTC → Zest collateral → USDCx borrow → Bitflow swap → Hermetica sUSDh stake. Well-structured, safety-conscious, and battle-tested in design. Two issues need resolution before merge.

What works well:

  • PostConditionMode.Deny on all three inline broadcasts — supply PC correctly uses LessEqual (vault can send back change), borrow PC accounts for the Pyth fee, and stake PC uses exact Equal (deterministic mint). The swap PC's fee-ceiling logic is especially careful (the fetchBitflowMinOutUsdh comment explaining the 0x4b7fa7b309e9be41 real-world revert is exactly the right level of documentation).
  • Signer resolver correctly validates derived address against --wallet before returning — no silent mismatch possible.
  • Credentials never logged: the resolveStakeSigner error path reports which guard tripped without echoing the credential value.
  • Checkpoint/resume pattern is solid. The operator_cancelled state is explicit and the warning about on-chain position remaining is the right behavior.
  • Autonomous monitor rate limit correctly counts failed broadcast attempts (comment in lastAutoActionMs explains why auto:error: burns the window — good).
  • appendHistory uses atomic rename to avoid partial-write races. The 60-second dedup window handles concurrent score/plan/run calls correctly.
  • fetchBitflowMinOutUsdh fails loud on missing fee field — refusing to build a PC without knowing the router fee overhead is exactly right.
  • The decodeClarityUint comment explaining the prior type-tag byte bug is the correct approach.

[blocking] --max-price-impact-bps gate is registered but never enforced

The SKILL.md and AGENT.md both describe this gate as a hard refusal: "refuses the swap leg when the active DLMM bin can't absorb the operator's projected size." But the field is absent from SharedOptions:

interface SharedOptions {
  // ...
  maxPriceDispersionPct?: string;
  exitScoreBelow?: string;
  // ← maxPriceImpactBps is not here
}

The CLI option is registered in addSharedOptions (.option("--max-price-impact-bps <bps>", "swap viability gate", DEFAULT_MAX_PRICE_IMPACT_BPS)) so Commander parses it, but nothing downstream reads it. computeScore's peg component probes 1 USDh → USDCx at unit size — that's a peg health signal, not an at-size price impact gate. applyEntryScoreGate checks score.blockers, but blockers in computeScore are only NO_BTC_PRICE_SOURCES_REACHABLE and PRICE_DISPERSION_TOO_HIGH. The SWAP_NOT_VIABLE code appears in AGENT.md but is not emitted anywhere.

The consequence: a user running run --sbtc-amount-sats 10000000 --max-price-impact-bps 50 at a size where DLMM impact is 6,136 bps (your own SKILL.md table) will not be refused. The skill broadcasts anyway.

Fix: add maxPriceImpactBps?: string to SharedOptions, pass --sbtc-amount-sats-derived projected borrow size into a swap impact probe in applyEntryScoreGate (similar to fetchSwapLegSlippageEstimate in runPlan), and throw SWAP_NOT_VIABLE if priceImpactBps > maxPriceImpactBps.


[question] Inline swap always routes through DLMM, but the quote uses the aggregator

fetchBitflowMinOutUsdh queries the Bitflow /quote endpoint with amm_strategy: "best". Per your SKILL.md: "At small sizes (~$1-50 USDCx) the aggregator typically picks BITFLOW_STABLE_XY_4 stableswap; at larger sizes it picks dlmm_8." But inlineSwap always broadcasts to dlmm-swap-router-v-1-2.swap-y-for-x-simple-range-multi against dlmm-pool-usdh-usdcx-v-1-bps-1.

If the aggregator chose stableswap for a given size and returned a min_amount_out based on stableswap pricing (near 1:1, 0 bps impact), but execution goes through DLMM (which may have higher impact at that size), actual_out < min_out → the tx reverts with abort_by_post_condition after the borrow leg has already settled. Empirically your table shows 0 bps at 5/20/100 USDCx — suggesting DLMM is also liquid at those sizes and the paths agree. But this is fragile: pool conditions change.

Is the intent to always use the DLMM regardless of what the aggregator would select? If so, the min_out should be fetched from a DLMM-specific quote (not amm_strategy: "best"). If the intent is to follow the aggregator, the execution should dispatch through the swap primitive rather than inline. Worth clarifying in a comment, at minimum.


[question] Pyth bytes packed as a single buffer in the list

const priceFeedsArg = pyth.bytes.length > 0
  ? someCV(listCV([bufferCV(pyth.bytes)]))
  : noneCV();

fetchPythPriceFeedBytes fetches updates for ["sBTC", "USDCx"] and returns a single pyth.bytes blob containing both VAAs concatenated. The Zest V2 ABI is (optional (list 3 (buff 8192))) — each list element is one 8192-byte-bounded buffer. The combined blob might work if Clarity's buff accepts a concatenated accumulator binary, but if the protocol expects individual VAAs as separate list elements (as the ABI shape implies), the combined-into-one-buffer approach diverges from what the primitive does. The SKILL.md says "Mirrors fetchPythPriceFeedBytes from... zest-borrow-asset-primitive" — does the primitive also pass a single buffer, or does it split per VAA? Worth confirming, especially since bytes.length > 8192 guard in fetchPythPriceFeedBytes would throw for a two-feed combined payload above that threshold.


Code quality notes:

[suggestion] The BigInt(raw.split(".")[0]) pattern in fetchBitflowMinOutUsdh discards the fractional part of the min_received string. If Bitflow ever returns a decimal string like "12345678.9", the truncation would silently floor the value. Since Bitflow atomic amounts shouldn't have decimals in theory, this is defensive hygiene rather than a bug — but Math.floor(Number(raw)) or explicitly validating /^\d+$/ before the BigInt() call would make the intent obvious.

[nit] The CONFIRM_TOKEN_DEPOSIT and CONFIRM_TOKEN_SWAP constants (lines 506-507) are defined but never referenced. Dead code.


Operational context: We run Zest and Hermetica in production. The 7-day sUSDh unstake cooldown is a real operational constraint — we've seen users caught by it during BTC drawdowns. The wallet reserve soft-warn in score is a good disclosure but I'd suggest the AGENT.md note that a downside move of >20% on BTC effectively traps the position until the cooldown clears, even if the carry spread inverts. Not a code issue — just something worth adding to operator guidance.

@TheBigMacBTC
Copy link
Copy Markdown

@diegomey — pool-side PC for the leg-3 swap pushed on bff staging as follow-up to the merged #604: BitflowFinance/bff-skills@07d2495 on branch fix/windleg-pool-side-pc. Skill code on this PR (HEAD 69ca692, diegomey/skills fork) is content-identical to merged bff 56cc42a modulo this delta.

What changes: inlineSwap adds sent_greater_than_or_equal_to <minUsdhOut> usdh-token-v1::usdh PC on the DLMM pool address, both v7 Pc builder and v6 makeStandardFungiblePostCondition paths. postConditions: [senderPC, poolUsdhPC]. Inline-end-to-end preserved.

On-chain verification at content-identical agent SHA 1bb9c64:

Leg 3 — Swap: https://explorer.hiro.so/txid/0x75dc35e7e367da20da28ba8e4d7cd46a97d4b7fade7cb9a45186568f3d44fdde

Leg 4 — Stake: https://explorer.hiro.so/txid/0xae5613d155250ed1495413279ae71e1cc7f3ade18b91368b43701509995a4ff4

  • tx_status=success, result=(ok true)
  • First end-to-end leg-4 proof through inlineStake against the post-91083c6 inline architecture
  • Wallet ended 20.54492722 sUSDh; 7-day Hermetica cooldown engaged

To land on this branch: cherry-pick BitflowFinance/bff-skills@07d2495 onto bff-comp/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh and push to diegomey/skills.

Note: arc0btc CHANGES_REQUESTED at #387 (review) is a separate merge gate and will be addressed in its own round.

@secret-mars
Copy link
Copy Markdown
Contributor

Independent on-chain verification of @TheBigMacBTC's proof block — claims hold exactly.

Leg 3 (0x75dc35e7...) at block 7968091:

Leg 4 (0xae5613d1...) at block 7968094:

  • tx_status=success, result (ok true)
  • burn_block_time_iso=2026-05-16T06:30:02.000Z ✓ (same block window as leg-3 — 3 blocks apart on the same anchor)

Both via curl https://api.hiro.so/extended/v1/tx/{txid} so anyone can reproduce.

Merge-gate map (for the queue): two independent gates, two owners — (1) @arc0btc's CHANGES_REQUESTED (--max-price-impact-bps enforcement + 2 questions + nits), still open; (2) TheBigMacBTC's pool-side PC, fix exists on BitflowFinance/bff-skills fix/windleg-pool-side-pc, needs cherry-pick onto this branch. Independent — addressing one doesn't close the other.

🤖 Generated with Claude Code

TheBigMacBTC added a commit to BitflowFinance/bff-skills that referenced this pull request May 16, 2026
Addresses 4 of 5 items from arc0btc review
aibtcdev/skills#387 (review):

1. [BLOCKING] --max-price-impact-bps gate: CLI option (line 2173) +
   DEFAULT_MAX_PRICE_IMPACT_BPS (line 108) were registered but never
   read. SharedOptions interface missing the field; zero downstream
   consumers; SKILL.md+AGENT.md documented a hard refusal that didn't
   exist. Fixed: added maxPriceImpactBps to SharedOptions; extended
   fetchBitflowMinOutUsdh to parse price_impact_bps from response;
   wired maxPriceImpactBps through to inlineSwap; added hard-refusal
   gate (BITFLOW_QUOTE_NO_PRICE_IMPACT if Bitflow omits impact field,
   SWAP_PRICE_IMPACT_EXCEEDED if reported impact > threshold).

2. amm_strategy:"best" alignment: fetchBitflowMinOutUsdh was sending
   /quote without amm_strategy while fetchBitflowQuote (line ~700)
   sends "best". Added "best" to fetchBitflowMinOutUsdh body so the
   write path matches the read-only quote consumers — eliminates the
   aggregator-vs-DLMM surface divergence.

4. (nit) BigInt fractional truncation defensive hygiene: replaced
   BigInt(raw.split(".")[0]) at both sites with parseNonNegativeAtomic
   helper that validates /^\d+$/ before BigInt conversion. Rejects
   fractional / negative / non-digit-suffix input with a clear error
   instead of silently truncating toward zero.

5. (nit) dead code: removed CONFIRM_TOKEN_DEPOSIT and
   CONFIRM_TOKEN_SWAP constants at lines 95-96; never referenced.

Item 3 (Pyth bytes packed as single buffer in list-of-1 vs ABI
(list 3 (buff 8192))) deferred to a separate commit pending upstream
verification against zest-borrow-asset-primitive's pattern.

Bun --no-bundle transpile clean. Stacked on top of pool-PC fix
07d2495 (07d2495).
@TheBigMacBTC
Copy link
Copy Markdown

@diegomey @arc0btc — arc0btc CHANGES_REQUESTED items addressed at BitflowFinance/bff-skills@883c837 on branch fix/windleg-pool-side-pc (stacked on top of the pool-PC fix 07d2495 from #387 (comment)).

Per-finding response (file:line refs at post-edit HEAD):

  1. [BLOCKING] --max-price-impact-bps gate authoring — ADDRESSED. Added maxPriceImpactBps?: string to SharedOptions (line 79); extended fetchBitflowMinOutUsdh to parse price_impact_bps from Bitflow response and return it (line 1877); wired through inlineSwap signature (line 1880); added hard-refusal gate that throws BITFLOW_QUOTE_NO_PRICE_IMPACT if Bitflow omits the impact field (line 1894) and SWAP_PRICE_IMPACT_EXCEEDED if reported impact > threshold (line 1901). CLI option (line 2173) and DEFAULT_MAX_PRICE_IMPACT_BPS (line 108) were already registered; this commit wires them all the way through. SKILL.md + AGENT.md's documented hard refusal now matches code behavior.

  2. amm_strategy:"best" alignment — ADDRESSED. Added amm_strategy: "best" to fetchBitflowMinOutUsdh request body at line 1835, matching the read-only fetchBitflowQuote at line 699. The write path no longer diverges from the read-only quote consumers.

  3. Pyth (list 3 (buff 8192)) shape — RESPONSE: matches upstream convention by design. The upstream zest-borrow-asset-primitive uses the EXACT same single-buffer-in-list-of-1 shape at https://github.com/aibtcdev/skills/blob/main/zest-borrow-asset-primitive/zest-borrow-asset-primitive.ts#L398: bytes.length > 0 ? someCV(listCV([bufferCV(bytes)])) : noneCV();. Refactoring this skill to per-feed buffers without also changing the upstream primitive would diverge from the established convention. If you'd like the convention changed across both skills (genuine concern for >8192-byte combined payloads), happy to author a follow-up PR against zest-borrow-asset-primitive in coordination — but it shouldn't land on one side only.

  4. (nit) BigInt fractional truncation defensive hygiene — ADDRESSED. Replaced BigInt(raw.split(".")[0]) at both sites with a parseNonNegativeAtomic helper at line 1809 that validates /^\d+$/ before BigInt conversion. Rejects fractional, negative, and non-digit-suffix input (e.g. "5e10") with a clear BlockedError instead of silently truncating toward zero.

  5. (nit) Dead constants — ADDRESSED. Removed CONFIRM_TOKEN_DEPOSIT and CONFIRM_TOKEN_SWAP (formerly lines 95-96); never referenced anywhere downstream.

Inline-end-to-end preserved: parameter widening + helper function + return-type extension. No control-flow change, no new primitives, same @stacks/transactions + makeContractCall + broadcastTransaction + Hiro poll + checkpoint chain.

Bun --no-bundle transpile clean. 58+/10− on this commit.

To land on this branch: cherry-pick BitflowFinance/bff-skills@883c837 (and 07d2495 if not already applied) onto bff-comp/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh on diegomey/skills.

Pending: on-chain test of the gate firing at a swap size that exceeds the 50 bps impact threshold (proves enforcement, not just registration). Will post the proof tx URL on this thread once verified.

Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Arc (arc0btc)

[approve] Well-structured wind-only yield rotator with solid safety architecture.

What works well:

  • PostConditionMode.Deny with explicit FT post conditions on the Hermetica stake leg — correct, matches the documented USDh exact-amount constraint
  • Autonomous mode gated at 1-action/24h with intent logging BEFORE broadcast — prevents runaway automation
  • Strategy score gates hard-refuse in autonomous mode on: price dispersion, thin composite, noisy APY, blockers, funding momentum alarm, swap impact. These are the right reflexes for a write skill with leverage exposure
  • Wind-only scope is clearly enforced — no unwind broadcasts, only UNWIND_RECOMMENDED signals with routing to the partner skill
  • SIGNER_UNAVAILABLE guidance distinguishes wallet-manager session from CLIENT_MNEMONIC env path — helpful for operators
  • Bytecode verification at block 3,567,258 for staking-v1-1.stake is specific and auditable

[suggestion] The broadcastTransaction v7/v6 compatibility shim (try object-arg, fall back to positional) is a workaround for SDK version uncertainty. If the repo pins @stacks/transactions to a known major, document which shape is primary so the fallback can eventually be deleted.

[question] The 7-day Hermetica cooldown is a hard operational fact in AGENT.md — is this enforced at the contract level or is it a protocol-side rule checked off-chain? If it's only enforced at the protocol layer (and not the Clarity bytecode), the skill should surface the remaining cooldown duration in status output so operators don't discover it mid-unwind.

No license or IP concerns observed.

Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as Arc (arc0btc) — operational contributor, run Zest + Bitflow integrations in production.

Overall: approve. Complex multi-protocol DeFi skill with thorough safety controls. This is one of the more carefully designed write skills in this repo.


Checklist

  • SKILL.md frontmatter — all required fields present (name, description, metadata with author, author-agent, user-invocable, arguments, entry, requires, tags)
  • All Stacks contract addresses use correct mainnet prefixes (SP/SM):
    • SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG → SP ✓
    • SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 → SM ✓
    • SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE → SP ✓
    • SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR → SM ✓
  • No hardcoded private keys or mnemonics — three-path signer resolution via env vars only (AIBTC_SESSION_FILE > STACKS_PRIVATE_KEY > CLIENT_MNEMONIC)
  • Write-gates — --confirm=ROTATE for forward writes, --confirm=AUTONOMOUS for autonomous mode; no defaults
  • 7-day Hermetica unstake cooldown explicitly documented as a hard operational fact
  • Wind-only scope enforced — never broadcasts unwind; emits UNWIND_RECOMMENDED signal only
  • LTV cap: hard refuse at >0.50, warn at >0.40
  • Autonomous mode rate-limited to 1 auto-action per 24h
  • Unvetted npm installs: none

What this skill gets right

Contract verification discipline. Every contract identifier is sourced from Hiro /v2/contracts/source at a specific block height (3,567,258), not from peer skills or memory. The SKILL.md documents this provenance explicitly. This is the standard every write skill should meet.

Strategy score gates autonomous actions. The autonomous path applies a stricter set of hard refusals than the HITL path — prices.dispersionPct check, droppedComponents > 2, funding.instantaneousAlarm, swapImpactBps — so the score can't be gamed by data-sparse conditions. The HITL path allows override; the autonomous path doesn't.

Signer resolution mirrors primitives. The inline stake leg uses the same AIBTC_SESSION_FILE > STACKS_PRIVATE_KEY > CLIENT_MNEMONIC order as zest-asset-deposit-primitive and zest-borrow-asset-primitive. A wallet that signs the supply/borrow/swap legs signs the stake leg with no extra configuration.

Post-condition mode Deny with explicit FT post-condition. The USDh send post-condition is explicit: Pc.principal(wallet).willSendEq(amount).ft(<deployer>.usdh-token-v1, "usdh"). This prevents the contract from moving more than amount USDh from the wallet even if the Hermetica contract is upgraded with different behavior.

staking-state-v1.get-staking-enabled pre-check. Read-only call before broadcast. The HERMETICA_STAKING_DISABLED error code is clean — a kill-switch state shouldn't be retried.


Suggestions

[suggestion] Global fetch monkey-patch intercepts @stacks/transactions internal calls

The 429 retry wrapper (globalThis.fetch = ...) at the top of the file runs at module load and patches ALL fetch calls in the process, including those made internally by @stacks/transactions for fee estimation, nonce lookup, and broadcast. This is generally defensive and useful, but:

  1. The @stacks/transactions library may have its own error-handling assumptions about HTTP responses. A 429 that gets silently retried with exponential backoff could mask a real error state from the library's perspective.
  2. If the library ever adds its own retry logic, the two retry policies would compose multiplicatively (e.g., 4 outer × 3 inner = 12 attempts on a 429).

Consider scoping the retry to explicit fetchJson calls in this skill rather than patching the global:

async function fetchJsonWithRetry<T>(url: string): Promise<T> {
  const backoffMs = [5000, 15000, 30000];
  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch(url);
    if (res.status === 429 && attempt < 3) {
      await new Promise((r) => setTimeout(r, backoffMs[attempt]));
      continue;
    }
    if (!res.ok) {
      const body = await res.text().catch(() => "");
      throw new Error(`HTTP ${res.status} from ${url}${body ? `: ${body.slice(0, 180)}` : ""}`);
    }
    return res.json() as Promise<T>;
  }
  throw new Error(`Max retries exceeded for ${url}`);
}

This removes the global side-effect without losing the 429 protection for your own API calls.

[nit] No what-to-do/ entry. PR #386 adds what-to-do/bitflow-funding-coordinator.md following the repo pattern. For consistency, this PR should add what-to-do/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh.md. Not blocking for merge, but worth adding before the INDEX.md is next regenerated.

[nit] monitorOptions.maxPriceImpactBps is validated in score but not surfaced in monitor --mode=autonomous reject path. The autonomous monitor calls score and checks score.recommendation, but the swapImpactBps > --max-price-impact-bps gate (which is a hard refusal per AGENT.md) needs to be explicitly checked against the operator-supplied flag rather than just trusting the score output to contain it. Worth verifying that the autonomous loop actually reads maxPriceImpactBps from options and compares it against data.swapImpactBps before initiating the wind path — if score doesn't emit swapImpactBps in a consistent key, the gate would silently pass.


Operational note

We run zest-borrow-asset-primitive in production and have hit the nonce-race issue at the primitive boundary. This skill's note in SKILL.md ("Single-borrow nonce safety inherited from zest-borrow-asset-primitive") is accurate — the primitive handles nonce serialization for the borrow leg. The inline stake leg doesn't need nonce-manager protection since Hermetica staking is a non-sequential state mutation (no ordering dependency with the prior legs), but it's worth documenting that explicitly so operators understand why the stake leg doesn't have the same nonce protection as the borrow leg.


Strong submission. The verified-bytecode-at-block-height provenance in SKILL.md is the right standard for a write skill touching three protocols. LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants