diff --git a/skills.json b/skills.json new file mode 100644 index 00000000..62f4667d --- /dev/null +++ b/skills.json @@ -0,0 +1,611 @@ +{ + "generated": "2026-05-23T00:19:29.165Z", + "source": "BitflowFinance/bff-skills", + "count": 32, + "skills": [ + { + "name": "bitflow-funding-coordinator", + "description": "Coordinates Bitflow funding swaps into route-ready target tokens for downstream strategy skills.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | status | plan | run | resume | cancel", + "entry": "bitflow-funding-coordinator/bitflow-funding-coordinator.ts", + "requires": "wallet, signing, settings, bitflow-swap-aggregator, nonce-manager", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "bitflow-funding-coordinator", + "files": [ + "AGENT.md", + "what-to-do.md", + "bitflow-funding-coordinator.ts", + "SKILL.md" + ] + }, + { + "name": "bitflow-hodlmm-deposit", + "description": "HODLMM-Primitive. Deposits selected assets into Bitflow HODLMM bins with proof-ready guardrails.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | status | run", + "entry": "bitflow-hodlmm-deposit/bitflow-hodlmm-deposit.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, infrastructure, l2" + }, + "directory": "bitflow-hodlmm-deposit", + "files": [ + "AGENT.md", + "SKILL.md", + "bitflow-hodlmm-deposit.ts" + ] + }, + { + "name": "bitflow-hodlmm-withdraw", + "description": "Withdraws Bitflow HODLMM liquidity across one or more bins with proof-ready guardrails.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | status | run", + "entry": "bitflow-hodlmm-withdraw/bitflow-hodlmm-withdraw.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, infrastructure, l2" + }, + "directory": "bitflow-hodlmm-withdraw", + "files": [ + "AGENT.md", + "bitflow-hodlmm-withdraw.ts", + "SKILL.md" + ] + }, + { + "name": "bitflow-hodlmm-zest-yield-loop", + "description": "Composes accepted HODLMM primitives with Zest position reads into a checkpointed HODLMM-Zest yield router.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | status | plan | run | resume | cancel", + "entry": "bitflow-hodlmm-zest-yield-loop/bitflow-hodlmm-zest-yield-loop.ts", + "requires": "wallet, signing, settings, bitflow-hodlmm-withdraw, bitflow-hodlmm-deposit, hodlmm-move-liquidity, zest-yield-manager", + "tags": "defi, write, mainnet-only, requires-funds, infrastructure, l2" + }, + "directory": "bitflow-hodlmm-zest-yield-loop", + "files": [ + "AGENT.md", + "what-to-do.md", + "bitflow-hodlmm-zest-yield-loop.ts", + "SKILL.md" + ] + }, + { + "name": "bitflow-limit-order", + "description": "Agent-powered limit orders on Bitflow — set price targets, auto-execute swaps when conditions are met.", + "metadata": { + "author": "ClankOS", + "author-agent": "Grim Seraph", + "user-invocable": "false", + "arguments": "doctor | set --pair

--side --price --amount [--slippage ] [--expires ] | list [--status ] [--events] [--order-id ] | cancel | run [--confirm] [--watch ] [--confirm-ticks ] [--wallet-password ] | install-packs", + "entry": "bitflow-limit-order/bitflow-limit-order.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "bitflow-limit-order", + "files": [ + "AGENT.md", + "bitflow-limit-order.ts", + "SKILL.md" + ] + }, + { + "name": "bitflow-swap-aggregator", + "description": "Executes Bitflow aggregator swaps with route quotes, explicit confirmation, and proof-ready transaction output.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | tokens | quote | plan | run", + "entry": "bitflow-swap-aggregator/bitflow-swap-aggregator.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "bitflow-swap-aggregator", + "files": [ + "AGENT.md", + "bitflow-swap-aggregator.ts", + "SKILL.md" + ] + }, + { + "name": "bitflow-user-earnings-primitive", + "description": "Read-only Bitflow user-earnings reporter. Wraps the 5 BFF App API /earnings/* endpoints with default-and-drill-down CLI, pass-through-no-derivation, decimal-string monetary normalization, pool-type-aware APR/APY semantics (verbatim from Bitflow frontend tooltips), and a _dataQuality envelope flagging 9 known upstream issues. Zero credentials, zero wallet state, zero broadcast.", + "metadata": { + "author": "TheBigMacBTC", + "author-agent": "MacBotMini", + "user-invocable": "true", + "arguments": "doctor | run --address [--period 1d|7d|30d|life] [--pool ] [--history] [--events] [--rollups] [--amm dlmm|stableswapv2|xyk] [--start-date ] [--end-date ] [--days ] [--limit ] [--page ] [--format json|text|table]", + "entry": "bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts", + "tags": "defi, read-only, l2, infrastructure" + }, + "directory": "bitflow-user-earnings-primitive", + "files": [ + "AGENT.md", + "bitflow-user-earnings-primitive.ts", + "SKILL.md" + ] + }, + { + "name": "bitflow-zest-sbtc-leverage-cycle", + "description": "Composes Zest borrow, Bitflow swap, and Zest deposit primitives into one sBTC leverage cycle.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | status | plan | run | resume | cancel", + "entry": "bitflow-zest-sbtc-leverage-cycle/bitflow-zest-sbtc-leverage-cycle.ts", + "requires": "wallet, signing, settings, zest-borrow-asset-primitive, bitflow-swap-aggregator, zest-asset-deposit-primitive", + "tags": "defi, write, mainnet-only, requires-funds, infrastructure, l2" + }, + "directory": "bitflow-zest-sbtc-leverage-cycle", + "files": [ + "AGENT.md", + "bitflow-zest-sbtc-leverage-cycle.ts", + "SKILL.md" + ] + }, + { + "name": "contract-preflight", + "description": "Dry-run Stacks contract calls against mainnet state before broadcasting — catches errors, prevents wasted gas", + "metadata": { + "author": "secret-mars", + "author-agent": "Secret Mars", + "user-invocable": "false", + "arguments": "doctor | run --action=simulate | run --action=batch | install-packs", + "entry": "contract-preflight/contract-preflight.ts", + "requires": "network", + "tags": "safety, simulation, stacks, clarity, defi" + }, + "directory": "contract-preflight", + "files": [ + "AGENT.md", + "SKILL.md", + "contract-preflight.ts" + ] + }, + { + "name": "dca", + "description": "Dollar Cost Averaging (DCA) for Stacks DeFi — automate recurring buys or sells of any Bitflow token pair via direct swaps. The agent executes each order on schedule with mandatory confirmation, slippage guardrails, balance checks, full tx logging, and Telegram-friendly status summaries. HODLMM pairs supported automatically via SDK route resolver with optional explicit HODLMM-only mode.", + "metadata": { + "author": "k9dreamermacmini-coder", + "author-agent": "Graphite Elan", + "user-invocable": "false", + "arguments": "doctor | install-packs | setup | plan | run | status | cancel | list", + "entry": "dca/dca.ts", + "requires": "wallet", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "dca", + "files": [ + "AGENT.md", + "dca.ts", + "SKILL.md" + ] + }, + { + "name": "defi-portfolio-scanner", + "description": "Cross-protocol DeFi position aggregator for Stacks wallets — 5 parallel scanners covering Bitflow HODLMM LP bins, Zest lending/borrowing (V2 pool-borrow-v2-3), ALEX pool shares, Styx bridge deposits, and Hiro wallet balances. Produces a unified portfolio view with USD estimation (CoinGecko) and risk scoring.", + "metadata": { + "author": "azagh72-creator", + "author-agent": "Flying Whale", + "user-invocable": "false", + "arguments": "doctor | scan --address | summary --address ", + "entry": "defi-portfolio-scanner/defi-portfolio-scanner.ts", + "tags": "defi, read-only, mainnet-only, l2" + }, + "directory": "defi-portfolio-scanner", + "files": [ + "AGENT.md", + ".gitignore", + "package.json", + "defi-portfolio-scanner.ts", + "SKILL.md" + ] + }, + { + "name": "dog-intelligence", + "description": "On-chain intelligence for DOG•GO•TO•THE•MOON rune — forensic analysis, LTH vs STH metrics, multi-chain whale tracking, multi-exchange markets, cross-chain data, and airdrop analytics powered by DOG DATA's Bitcoin full node.", + "metadata": { + "author": "LimaDevBTC", + "author-agent": "Xored Pike", + "user-invocable": "false", + "arguments": "doctor | run --action pulse | run --action whales | run --action diamond | run --action airdrop | run --action lth-sth | run --action markets | run --action multichain | run --action bitcoin | install-packs", + "entry": "dog-intelligence/dog-intelligence.ts", + "tags": "read-only, infrastructure, defi, l1" + }, + "directory": "dog-intelligence", + "files": [ + "AGENT.md", + "package.json", + "SKILL.md", + "dog-intelligence.ts" + ] + }, + { + "name": "hermetica-yield-rotator", + "description": "Cross-protocol yield rotator for Stacks mainnet. Monitors Hermetica USDh staking APY vs Bitflow HODLMM dlmm_1 APR from live on-chain data, assesses wallet position, and executes yield rotation between protocols when the differential exceeds a configurable threshold. Write-capable: outputs MCP commands for stake, initiate-unstake, complete-unstake, and cross-protocol rotate actions.", + "metadata": { + "author": "cliqueengagements", + "author-agent": "Micro Basilisk (Agent 77) — SP219TWC8G12CSX5AB093127NC82KYQWEH8ADD1AY | bc1qzh2z92dlvccxq5w756qppzz8fymhgrt2dv8cf5", + "user-invocable": "true", + "arguments": "doctor | install-packs | run [--wallet ] [--action ] [--amount ] [--confirm]", + "entry": "hermetica-yield-rotator/hermetica-yield-rotator.ts", + "tags": "defi, hermetica, usdh, staking, bitflow, yield, rotation, actions, mainnet-only, l2" + }, + "directory": "hermetica-yield-rotator", + "files": [ + "AGENT.md", + "SKILL.md", + "hermetica-yield-rotator.ts" + ] + }, + { + "name": "hodlmm-arb-executor", + "description": "Executes LP-based sBTC/STX arb on Bitflow HODLMM. Detects XYK vs DLMM price spread, enters via swap + add-liquidity-simple, exits on spread reversal or 2h timeout. Write-capable; requires --confirm. Emits MCP command objects.", + "metadata": { + "author": "ronkenx9", + "author-agent": "Parallel Owl (ERC-8004 ID #354, SP1KNKVXNNS9B6TBBT8YTM2VTYKVZYWS65TTRD430)", + "user-invocable": "true", + "arguments": "doctor | simulate | execute | watch", + "entry": "hodlmm-arb-executor/hodlmm-arb-executor.ts", + "requires": "wallet", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "hodlmm-arb-executor", + "files": [ + "AGENT.md", + "hodlmm-arb-executor.ts", + "SKILL.md" + ] + }, + { + "name": "hodlmm-bin-guardian", + "description": "Monitors Bitflow HODLMM bins to keep LP positions in the active earning range. Fetches live pool state via Bitflow's HODLMM app API, checks if a wallet's position is in-range, computes slippage from Bitflow-native price data, and outputs a JSON recommendation. Read-only — rebalance actions require explicit human approval.", + "metadata": { + "author": "cliqueengagements", + "author-agent": "Micro Basilisk (Agent 77) — SP219TWC8G12CSX5AB093127NC82KYQWEH8ADD1AY | bc1qzh2z92dlvccxq5w756qppzz8fymhgrt2dv8cf5", + "user-invocable": "true", + "arguments": "doctor | install-packs | run [--wallet ] [--pool-id ]", + "entry": "hodlmm-bin-guardian/hodlmm-bin-guardian.ts", + "tags": "defi, read-only, mainnet-only, l2" + }, + "directory": "hodlmm-bin-guardian", + "files": [ + "AGENT.md", + "hodlmm-bin-guardian.ts", + "SKILL.md" + ] + }, + { + "name": "hodlmm-flow", + "description": "Swap flow intelligence for Bitflow HODLMM — analyzes on-chain swap transactions to compute direction bias, flow toxicity, bin velocity, whale concentration, and bot/organic classification for LP decision-making.", + "metadata": { + "author": "ClankOS", + "author-agent": "Grim Seraph", + "user-invocable": "false", + "arguments": "doctor | install-packs | flow --pool-id | flow --all", + "entry": "hodlmm-flow/hodlmm-flow.ts", + "tags": "l2, defi, read-only, mainnet-only" + }, + "directory": "hodlmm-flow", + "files": [ + "AGENT.md", + "SKILL.md", + "hodlmm-flow.ts" + ] + }, + { + "name": "hodlmm-inventory-balancer", + "description": "Detects HODLMM LP inventory drift (token-ratio imbalance from one-sided swap flow) and restores the target ratio via a corrective Bitflow swap plus a hodlmm-move-liquidity redeploy, gated by the 4h per-pool cooldown.", + "metadata": { + "author": "cliqueengagements", + "author-agent": "Micro Basilisk — Agent #77", + "user-invocable": "false", + "arguments": "install-packs | doctor | status | recommend | run", + "entry": "hodlmm-inventory-balancer/hodlmm-inventory-balancer.ts", + "requires": "wallet, signing, settings, bitflow, hodlmm-bin-guardian, hodlmm-move-liquidity", + "tags": "defi, write, mainnet-only, requires-funds" + }, + "directory": "hodlmm-inventory-balancer", + "files": [ + "AGENT.md", + "hodlmm-inventory-balancer.ts", + "SKILL.md" + ] + }, + { + "name": "hodlmm-move-liquidity", + "description": "HODLMM Move-Liquidity & Auto-Rebalancer — withdraw from drifted bins, re-deposit around the current active bin. Includes autonomous monitoring loop.", + "metadata": { + "author": "cliqueengagements", + "author-agent": "Micro Basilisk (Agent 77) — SP219TWC8G12CSX5AB093127NC82KYQWEH8ADD1AY | bc1qzh2z92dlvccxq5w756qppzz8fymhgrt2dv8cf5", + "user-invocable": "false", + "arguments": "doctor | scan | run | auto | install-packs", + "entry": "hodlmm-move-liquidity/hodlmm-move-liquidity.ts", + "requires": "wallet, signing", + "tags": "defi, write, mainnet-only, requires-funds" + }, + "directory": "hodlmm-move-liquidity", + "files": [ + "AGENT.md", + "hodlmm-move-liquidity.ts", + "SKILL.md" + ] + }, + { + "name": "hodlmm-pulse", + "description": "Fee velocity and volume momentum tracker for Bitflow HODLMM pools — detects entry windows by comparing today's fee capture against the 7-day baseline, building a local time-series to surface trend direction (accelerating, stable, cooling).", + "metadata": { + "author": "ghislo749", + "author-agent": "Grim Seraph", + "user-invocable": "false", + "arguments": "doctor | scan | track | report", + "entry": "hodlmm-pulse/hodlmm-pulse.ts", + "tags": "defi, read-only, mainnet-only, l2, infrastructure" + }, + "directory": "hodlmm-pulse", + "files": [ + "AGENT.md", + "hodlmm-pulse.ts", + "SKILL.md" + ] + }, + { + "name": "hodlmm-range-keeper", + "description": "Active HODLMM position manager that monitors bin drift, estimates accrued fees, and re-centers liquidity around the active bin when profitable.", + "metadata": { + "author": "tearful-saw", + "author-agent": "Elegant Orb", + "user-invocable": "false", + "arguments": "doctor | status | plan | recenter | run | history | install-packs", + "entry": "hodlmm-range-keeper/hodlmm-range-keeper.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "hodlmm-range-keeper", + "files": [ + "AGENT.md", + "hodlmm-range-keeper.ts", + "SKILL.md" + ] + }, + { + "name": "hodlmm-risk", + "description": "HODLMM volatility risk monitor — reads Bitflow HODLMM pool state, computes current-state volatility proxy from bin distribution, scores regime (calm/elevated/crisis), and emits position-sizing or liquidity-pull signals for LP agents. Read-only; no wallet required.", + "metadata": { + "author": "locallaunchsc-cloud", + "author-agent": "Risk Sentinel", + "user-invocable": "false", + "arguments": "assess-pool | assess-position | regime-snapshot", + "entry": "hodlmm-risk/hodlmm-risk.ts", + "tags": "l2, defi, read-only, mainnet-only" + }, + "directory": "hodlmm-risk", + "files": [ + "AGENT.md", + "hodlmm-risk.ts", + "SKILL.md" + ] + }, + { + "name": "hodlmm-signal-allocator", + "description": "Signal-gated HODLMM yield allocator. Reads aibtc.news signals and Quantum Readiness Index alongside live HODLMM APR to compute a risk-adjusted yield score, then executes a Bitflow swap to prepare wallet for HODLMM deposit when conditions align.", + "metadata": { + "author": "IamHarrie-Labs", + "author-agent": "Serene Spring", + "user-invocable": "false", + "arguments": "doctor | scan --pool --wallet | run --pool --wallet --amount-stx [--confirm] [--dry-run]", + "entry": "hodlmm-signal-allocator/hodlmm-signal-allocator.ts", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "hodlmm-signal-allocator", + "files": [ + "AGENT.md", + "hodlmm-signal-allocator.ts", + "SKILL.md" + ] + }, + { + "name": "jingswap-cycle-agent", + "description": "JingSwap STX↔sBTC cycle monitor and participation agent. Reads live cycle state and prices directly from the Stacks contract via Hiro API and Pyth oracle — no API key required. Outputs PARTICIPATE / MONITOR / WAIT_FOR_DEPOSIT_PHASE / NO_SBTC_AVAILABLE with oracle-vs-DEX discount analysis.", + "metadata": { + "author": "teflonmusk", + "author-agent": "Dual Cougar", + "user-invocable": "false", + "arguments": "doctor | status | analyze | participate", + "entry": "jingswap-cycle-agent/jingswap-cycle-agent.ts", + "requires": "jingswap_deposit_stx (aibtc MCP) for live execution", + "tags": "jingswap, sbtc, stx, defi, execution, mainnet-only, stacks, bitflow" + }, + "directory": "jingswap-cycle-agent", + "files": [ + "AGENT.md", + "SKILL.md", + "jingswap-cycle-agent.ts" + ] + }, + { + "name": "sbtc-auto-funnel", + "description": "Monitor sBTC balance and auto-route excess above a reserve threshold to Zest yield", + "metadata": { + "author": "secret-mars", + "author-agent": "Secret Mars", + "user-invocable": "false", + "arguments": "doctor | run --action=check | run --action=funnel | install-packs", + "entry": "sbtc-auto-funnel/sbtc-auto-funnel.ts", + "requires": "wallet, signing", + "tags": "defi, yield, sbtc, zest, automation" + }, + "directory": "sbtc-auto-funnel", + "files": [ + "AGENT.md", + "SKILL.md", + "sbtc-auto-funnel.ts" + ] + }, + { + "name": "sbtc-yield-maximizer", + "description": "Routes idle sBTC to the highest safe live yield path and executes capped Zest supply when Zest is the best current route.", + "metadata": { + "author": "Ololadestephen", + "author-agent": "Wide Eden", + "user-invocable": "false", + "arguments": "doctor | install-packs | status | run", + "entry": "sbtc-yield-maximizer/sbtc-yield-maximizer.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "sbtc-yield-maximizer", + "files": [ + "AGENT.md", + "sbtc-yield-maximizer.ts", + "SKILL.md" + ] + }, + { + "name": "stacking-delegation", + "description": "Monitor STX stacking positions — status, PoX cycles, reward payouts, and delegation eligibility for autonomous agents.", + "metadata": { + "author": "secret-mars", + "author-agent": "Secret Mars", + "user-invocable": "true", + "arguments": "doctor | run status --stx-address | run pox-info | run rewards --btc-address | install-packs", + "entry": "stacking-delegation/stacking-delegation.ts", + "requires": "settings", + "tags": "l2, read-only" + }, + "directory": "stacking-delegation", + "files": [ + "AGENT.md", + "stacking-delegation.ts", + "SKILL.md" + ] + }, + { + "name": "stacks-alpha-engine", + "description": "Cross-protocol yield executor for Zest, Hermetica, Granite, and HODLMM with 3-tier yield mapping, sBTC Proof-of-Reserve verification, and multi-gate safety pipeline", + "metadata": { + "author": "cliqueengagements", + "author-agent": "Micro Basilisk (Agent 77) — SP219TWC8G12CSX5AB093127NC82KYQWEH8ADD1AY | bc1qzh2z92dlvccxq5w756qppzz8fymhgrt2dv8cf5", + "user-invocable": "false", + "arguments": "doctor | scan | deploy | withdraw | rebalance | migrate | emergency | install-packs", + "entry": "stacks-alpha-engine/stacks-alpha-engine.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "stacks-alpha-engine", + "files": [ + "AGENT.md", + "SKILL.md", + "stacks-alpha-engine.ts" + ] + }, + { + "name": "windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh", + "description": "Wind-only yield rotator: supply sBTC on Zest, borrow USDCx on Zest, swap USDCx->USDh on Bitflow Quote Engine (viability-gated), stake USDh inline on Hermetica staking-v1-1 (returns sUSDh). Score gates entry; monitor (HITL or autonomous, 1-action/24h cap) detects when conditions become viable; outputs UNWIND signal for the partner unwinder skill but never broadcasts unwind itself.", + "metadata": { + "author": "IamHarrie-Labs", + "author-agent": "Serene Spring", + "user-invocable": "false", + "arguments": "doctor | status | score | plan | run | resume | monitor | cancel", + "entry": "windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh/windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh.ts", + "requires": "wallet, signing, settings, zest-asset-deposit-primitive, zest-borrow-asset-primitive, bitflow-swap-aggregator", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh", + "files": [ + "AGENT.md", + "windleg-zestlend-hermeticastake-yield-rotator-sBTC-USDCx-sUSDh.ts", + "SKILL.md" + ] + }, + { + "name": "zest-asset-deposit-primitive", + "description": "Deposits a selected asset into Zest V2 collateral with explicit confirmation and proof-ready checks.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | status | plan | run", + "entry": "zest-asset-deposit-primitive/zest-asset-deposit-primitive.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, infrastructure, l2" + }, + "directory": "zest-asset-deposit-primitive", + "files": [ + "AGENT.md", + "SKILL.md", + "zest-asset-deposit-primitive.ts" + ] + }, + { + "name": "zest-auto-repay", + "description": "Autonomous Zest Protocol LTV guardian — monitors borrowing positions, detects liquidation risk, and executes safe repayments with enforced spend limits to protect collateral on Stacks mainnet.", + "metadata": { + "author": "azagh72-creator", + "author-agent": "Flying Whale", + "user-invocable": "false", + "arguments": "doctor | run --action=status | run --action=monitor | run --action=repay | run --action=emergency-repay", + "entry": "zest-auto-repay/zest-auto-repay.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "zest-auto-repay", + "files": [ + "AGENT.md", + "zest-auto-repay.ts", + "SKILL.md" + ] + }, + { + "name": "zest-borrow-asset-primitive", + "description": "Borrows a selected asset from Zest against existing collateral with explicit confirmation and proof-ready safety checks.", + "metadata": { + "author": "macbotmini-eng", + "author-agent": "Hex Stallion", + "user-invocable": "false", + "arguments": "doctor | status | plan | run", + "entry": "zest-borrow-asset-primitive/zest-borrow-asset-primitive.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, infrastructure, l2" + }, + "directory": "zest-borrow-asset-primitive", + "files": [ + "AGENT.md", + "SKILL.md", + "zest-borrow-asset-primitive.ts" + ] + }, + { + "name": "zest-yield-manager", + "description": "Autonomous sBTC yield management on Zest Protocol — supply, withdraw, claim rewards, and monitor positions with safety controls.", + "metadata": { + "author": "secret-mars", + "author-agent": "Secret Mars", + "user-invocable": "true", + "arguments": "doctor | run | install-packs", + "entry": "zest-yield-manager/zest-yield-manager.ts", + "requires": "wallet, signing, settings", + "tags": "defi, write, mainnet-only, requires-funds, l2" + }, + "directory": "zest-yield-manager", + "files": [ + "AGENT.md", + "zest-yield-manager.ts", + "SKILL.md" + ] + } + ] +} diff --git a/skills/bitflow-user-earnings-primitive/AGENT.md b/skills/bitflow-user-earnings-primitive/AGENT.md new file mode 100644 index 00000000..21f1520a --- /dev/null +++ b/skills/bitflow-user-earnings-primitive/AGENT.md @@ -0,0 +1,83 @@ +--- +name: bitflow-user-earnings-primitive-agent +skill: bitflow-user-earnings-primitive +description: "Read-only agent persona for querying Bitflow user-earnings data with pool-type-aware APR/APY semantics, decimal-string monetary normalization, and explicit upstream data-quality disclosure. Zero broadcast, zero credentials, zero wallet state. Caller provides --address always." +--- + +# bitflow-user-earnings-primitive-agent + +Autonomous agent persona for operating the `bitflow-user-earnings-primitive` skill. The agent answers operator questions about Bitflow earnings, drilling into evidence layers when asked, and faithfully surfacing known upstream data-quality issues. + +PRD: https://github.com/BitflowFinance/bff-skills/issues/609 + +## Decision order + +1. **Health first** — Run `doctor` before any earnings query. If BFF API is unreachable or returns `down` status, emit an error signal and halt with a clear blocker description. +2. **Validate input** — Confirm the target address is a valid Stacks principal (starts with `SP`, 41 chars, Crockford-base32 checksum-valid). Reject malformed addresses before making any HTTP call. The BFF server does NOT validate addresses — it returns 200 with empty payload for malformed input, which would silently mislead the operator. +3. **Default query** — For the operator question *"what are my earnings?"*, run `run --address SP...` with NO drill-down flags. The default returns the official displayed P&L cards at `period_type=life`, matching what the Bitflow frontend's "My Earnings" view shows. +4. **Drill-down on demand** — When the operator asks for specifics ("which pool drove that?", "show me the swaps", "what's my 30-day APR?"), use the appropriate drill-down flag (`--pool`, `--events`, `--period 30d`, etc.). Never default to drill-down without intent — the headline is the canonical answer. +5. **Honor `_dataQuality` flags** — Every response carries a `_dataQuality` field. The agent MUST surface relevant flags to the operator when they affect interpretation. Examples: + - If `rollups_empty_upstream` is set on a `--rollups` response, tell the operator the endpoint is broken upstream and recommend `--history` for time-series. + - If `events_null_timestamps` is set on a `--events` response, tell the operator events are ordered by `eventId` not timestamp. + - If `no_position_in_pool` is set, the user simply has no position in that pool — surface as zero earnings, not as an error. +6. **Honor pool-type APR/APY semantics** — When reporting APR/APY values to the operator, ALWAYS clarify which formula applies based on the pool's `amm_type`. For `dlmm` pools, use the active-bin 24h-annualized definition. For `xyk` / `stableswapv2` pools, use the historic-average organic-APY definition. Both definitions are quoted verbatim from the Bitflow frontend tooltips in SKILL.md. The agent must NEVER conflate them. + +## Guardrails + +- **Read-only enforcement** — This agent MUST NOT call any skill or tool that creates, signs, or broadcasts transactions. It is strictly an observer. +- **No private key access** — Never request, accept, log, or persist private keys, seed phrases, or wallet passwords. The skill has zero credential surface — keep it that way. +- **No derivation** — The agent does not compute APR, APY, IL, P&L, or any other metric. Every number reported comes from a BFF API response field as-shipped. If the operator asks for a derived metric (e.g., "what's my net P&L after IL?"), the agent must say "this skill does not compute that; you'd need a separate analytical layer." +- **Caller-provided address** — The agent must require `--address` as an explicit argument from the operator. Do NOT pull addresses from credentials stores, environment variables, or wallet state. The skill is address-stateless. +- **Surface upstream warts honestly** — The 9-item `_dataQuality` envelope is not optional. When upstream data has known issues, the agent must explain them, not hide them. +- **No financial advice** — Earnings figures are observations, not recommendations. Output must never include language like "you should claim now" or "we recommend rebalancing." +- **Single address per invocation** — The agent queries exactly one address per run. Batch operations must be orchestrated by the calling layer, not handled internally. +- **Rate-limit compliance** — BFF API enforces 500 req/min/IP shared. The agent should batch related drill-downs (e.g., default + `--pool` + `--events` in sequence) and avoid rapid re-scans of the same address within the 120s cache TTL window. + +## On error + +| Error code | Agent response | +|---|---| +| `INVALID_ADDRESS_FORMAT` | Surface to operator with the rule violation (length, character set, checksum). Do not retry. | +| `INVALID_PERIOD_TYPE` | Surface the accepted set from the `hint` field. Do not retry without operator input. | +| `UPSTREAM_RATE_LIMITED` | Wait for the rate-limit window to clear (default backoff 60s). Surface to operator if persistent. | +| `UPSTREAM_TIMEOUT` | Retry once with longer timeout (15s → 30s). If still timeout, surface as upstream unavailability. | +| `UPSTREAM_5XX` | Surface to operator. BFF is having server-side issues; not a skill bug. Do not retry silently. | +| `UPSTREAM_UNREACHABLE` | Verify network connectivity. Surface as infrastructure issue. | +| `UNEXPECTED_RESPONSE_SHAPE` | Surface as schema-drift signal — Bitflow may have shipped a breaking change. Notify operator that skill update may be needed. | + +## On success + +- Parse the normalized envelope (`status: "success"`, `data: {...}`, `error: null`). +- Identify which endpoint was hit via `data.endpoint` (one of: `pnl`, `pnl_pool`, `history`, `events`, `rollups`). +- Surface the headline numbers (USD totals, per-pool cards) before drilling. +- Always quote the `_source.upstream_url` and `_source.fetched_at` when the operator asks for provenance. +- Check `_dataQuality` for any active flags; surface relevant ones to the operator. +- If APR/APY values are present, clarify the pool-type-specific definition (HODLMM vs Classic). + +## Polling cadence + +| Context | Interval | Rationale | +|---|---|---| +| Routine earnings check | On operator demand | Earnings change slowly; no need for active polling | +| Pre-claim check (hypothetical claim-fees integration) | At claim-decision time | Verify accrued amount exceeds gas threshold | +| Dashboard refresh | Every 2 minutes | Within 120s server cache window — efficient | +| Post-swap audit | Within 5 minutes of swap | Verify the swap registered in `/earnings/events` | +| Backfill review | Once daily | `/earnings` history is daily-aggregated; pull once per day max | + +## Pool-type APR/APY clarification protocol + +When the operator asks an APR/APY question, the agent's response template: + +> **For HODLMM pools (`dlmm_*`):** "Your `[pool]` shows an APR of `[value]%`. Bitflow calculates this from the pool's fees earned in the last 24 hours divided by current TVL, annualized over 365 days — active-bin only, no compounding." +> +> **For Classic/Legacy pools (`xyk` / `stableswapv2`):** "Your `[pool]` shows an APY of `[value]%`. Bitflow calculates this as a historic average — total trading fees collected into the pool divided by TVL, averaged across daily 24-hour snapshots. Some Classic pools also include farming rewards on top, which are NOT in this number." + +Never report APR/APY without the pool-type-specific clarification. + +## Composition notes + +This skill is a leaf in the dependency tree. It does not call other skills. Future skills that may consume this primitive via `metadata.requires`: + +- A hypothetical `bitflow-claim-fees-primitive` (write-leg) — would call this skill with `--pool ` to decide whether accrued fees exceed gas threshold before broadcasting a claim. +- A future `bitflow-positions-primitive` (sibling) — would expose the adjacent `/users/{addr}/positions/*` surface (impermanent loss, bins, lots, history); orthogonal but cross-referenceable with this skill. +- An operator-facing dashboard — would consume the same JSON the agent reads, populating an earnings card view. diff --git a/skills/bitflow-user-earnings-primitive/SKILL.md b/skills/bitflow-user-earnings-primitive/SKILL.md new file mode 100644 index 00000000..92e16570 --- /dev/null +++ b/skills/bitflow-user-earnings-primitive/SKILL.md @@ -0,0 +1,170 @@ +--- +name: bitflow-user-earnings-primitive +description: "Read-only Bitflow user-earnings reporter. Wraps the 5 BFF App API /earnings/* endpoints with default-and-drill-down CLI, pass-through-no-derivation, decimal-string monetary normalization, pool-type-aware APR/APY semantics (verbatim from Bitflow frontend tooltips), and a _dataQuality envelope flagging 9 known upstream issues. Zero credentials, zero wallet state, zero broadcast." +metadata: + author: "TheBigMacBTC" + author-agent: "MacBotMini" + user-invocable: "true" + arguments: "doctor | run --address [--period 1d|7d|30d|life] [--pool ] [--history] [--events] [--rollups] [--amm dlmm|stableswapv2|xyk] [--start-date ] [--end-date ] [--days ] [--limit ] [--page ] [--format json|text|table]" + entry: "bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts" + requires: "" + tags: "defi, read-only, l2, infrastructure" +--- + +# bitflow-user-earnings-primitive + +Read-only Bitflow user-earnings reporter for AI agents operating on the Stacks blockchain. Answers the question *"how much has user X earned on Bitflow, and how do I prove it?"* by wrapping the 5 BFF App API endpoints under `/api/app/v1/users/{user_address}/earnings/*` behind a single CLI with a sensible default (the official displayed P&L cards) and drill-down flags for the underlying layers (history, per-swap events, single-pool detail, time-series rollups). + +PRD reference: https://github.com/BitflowFinance/bff-skills/issues/609 + +## What it does + +The skill is a **courier with annotations**: it passes through Bitflow's pre-computed earnings values unchanged, normalizes minor schema inconsistencies (USD typed as `number` in one endpoint vs decimal-`string` in others), validates Stacks address format client-side, and adds a `_dataQuality` envelope flagging 9 known upstream issues so the calling agent can reason about what it's reading. + +The skill performs **no computation, derivation, or synthesis** on Bitflow's data. Every monetary value, APR, percentage, range bound, or fee figure surfaced comes from a BFF API response field as-shipped. Bitflow's backend computes; the skill couriers and annotates. + +| Endpoint | Surfaced via | What it returns | +|---|---|---| +| `GET /api/app/v1/users/{addr}/earnings/pnl` | `run` (default) | Headline P&L cards across all user's pools — the "official displayed P&L" matching the Bitflow frontend's "My Earnings" view | +| `GET .../earnings/pnl/{pool_id}` | `--pool ` | Single-pool drill-down card; 404 mapped to `{cards: []}` with `_dataQuality.no_position_in_pool` | +| `GET .../earnings` | `--history` | Daily-aggregated per-bin earnings history (up to 365 days) | +| `GET .../earnings/events` | `--events` | Per-swap event-level audit trail (paginated, max 100/page) | +| `GET .../earnings/rollups` | `--rollups` | Pre-aggregated time-series rollups (currently broken upstream — `_dataQuality.rollups_empty_upstream` warning) | + +## Why agents need it + +Existing Bitflow coverage on https://github.com/aibtcdev/skills is exclusively write-side. **No upstream skill exposes user-earnings reads.** An agent asked *"how much have I earned on Bitflow?"* today has no canonical skill to call — it must improvise direct HTTP, fall back to a write-skill's incidental balance check, or admit ignorance. This primitive fills that gap. + +The need is also a documentation gap: Bitflow's developer docs at https://docs.bitflow.finance cover the SDK (swaps) and the `/ticker` public API but do not document the user-earnings surface in prose. The OpenAPI spec at https://bff.bitflowapis.finance/api/app/openapi.json is the only contract Bitflow ships, and its 200 responses are inline-typed without `$ref` synthesis. A primitive that wraps + documents this surface gives downstream agent developers the prose layer Bitflow hasn't written. + +## Safety notes + +- **Read-only.** Makes zero on-chain transactions. Every call is an HTTPS GET against the public BFF App API. +- **No private keys.** Never requests, accepts, or stores private keys, seed phrases, or wallet passwords. +- **No wallet mutation.** Earnings data is observed, never modified. +- **No credentials.** Skill does NOT read from any credentials store. Caller provides `--address ` always. +- **Mainnet only.** Targets the live Bitflow BFF API; no testnet support. +- **Rate-limit aware.** BFF API enforces 500 req/min/IP shared (no API key). Skill does not implement client-side rate limiting; caller is responsible for budget. Upgrade path: email `help@bitflow.finance`. + +## Pool-type APR/APY semantics (verbatim from Bitflow frontend) + +The `apr` / `apy` / `feeTvl.apy` fields returned by the BFF App earnings endpoints carry **pool-type-dependent semantics** — the same JSON key has two definitions depending on AMM type. The skill never computes these values; Bitflow's backend does. But agents calling this skill must know which formula applies to which pool: + +**HODLMM pools** (`amm_type: "dlmm"`, pool IDs like `dlmm_N`): + +> *"APR is calculated from the pool's fees earned in the last 24 hours divided by its current TVL, then annualized over 365 days. It shows your potential returns from fees in the active bin without compounding taken into account."* — Bitflow frontend, HODLMM pool tooltip (verbatim) + +**Classic / Legacy pools** (`amm_type: "xyk"` or `"stableswapv2"`): + +> *"The Organic Pool APY shown is the historic average. Organic APY = total trading fees collected into the pool / TVL. Each day, a 24 hour organic APY is calculated, then included in the historic average. Some pools offer additional farming rewards on top for pooling/staking."* — Bitflow frontend, Classic pool tooltip (verbatim) + +**All 9 live pools on `/api/app/v1/pools` are currently `dlmm_*` as of 2026-05-22.** The Classic/Legacy semantics are forward-looking — currently no live legacy pool exposes earnings through these endpoints. + +## Commands + +### `doctor` + +Checks BFF API health and prints OpenAPI version. Safe to run anytime. Does not touch any user address. + +```bash +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts doctor +``` + +### `run` + +Core read. Default returns the canonical earnings answer: all the user's active-pool earnings cards at `period_type=life` (same data the Bitflow frontend's "My Earnings" view renders). Drill-down flags expose the underlying layers. + +```bash +# Default — headline P&L cards for all pools, period=life +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts run --address SP1BXRXA0Z67MB6G31FP1R52ZX5GQTZ5008KZG77A + +# Single-pool drill-down +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts run --address SP... --pool dlmm_1 + +# Daily-aggregated history +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts run --address SP... --history --days 30 + +# Per-swap event audit trail +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts run --address SP... --events --limit 50 + +# Time-series rollups (currently broken upstream, warning flag set) +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts run --address SP... --rollups --period daily + +# AMM filter +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts run --address SP... --amm dlmm + +# Period override on default +bun run bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts run --address SP... --period 30d +``` + +## Output contract + +All outputs are JSON to stdout. + +**Success envelope:** +```json +{ + "status": "success", + "action": "earnings.pnl", + "data": { + "address": "SP...", + "endpoint": "pnl", + "period": "life", + "cards": [ /* normalized card objects */ ], + "pool_tokens": { /* echo of upstream poolTokens map */ }, + "_dataQuality": { /* flags for any of the 9 known issues that apply */ }, + "_source": { + "upstream_url": "https://bff.bitflowapis.finance/api/app/v1/users/SP.../earnings/pnl?period_type=life", + "fetched_at": "2026-05-22T22:00:00Z", + "upstream_cache_age_hint": "may be ≤120s old" + } + }, + "error": null +} +``` + +**Error envelope:** +```json +{ + "status": "error", + "action": "earnings.pnl", + "data": null, + "error": { + "error_code": "INVALID_ADDRESS_FORMAT | INVALID_PERIOD_TYPE | PNL_CARD_NOT_FOUND | UPSTREAM_RATE_LIMITED | UPSTREAM_TIMEOUT | UPSTREAM_5XX | UPSTREAM_UNREACHABLE | UNEXPECTED_RESPONSE_SHAPE", + "message": "human-readable single sentence", + "hint": "remediation guidance", + "upstream_status": 400, + "upstream_body": { /* preserved upstream error envelope */ }, + "retryable": false + } +} +``` + +**Special case:** when `/pnl/{pool_id}` returns 404 `PNL_CARD_NOT_FOUND`, the skill maps to `{status: "success", data: {cards: [], _dataQuality: {no_position_in_pool: ...}}}` — NOT an error. The truthful answer to "what's the user's earnings in pool X?" when they have no position is "zero", which is a successful zero-data result. + +## Data-quality envelope (9 known upstream issues) + +Every successful response carries a `_dataQuality` field, populated when relevant: + +| # | Flag | Trigger condition | +|---|---|---| +| 1 | `rollups_empty_upstream` | `--rollups` returned `[]` for an address that has earnings history (`/earnings`) — endpoint currently broken | +| 2 | `events_null_timestamps` | Any event in the response has `timestamp: null` (currently 100/100 events upstream) | +| 3 | `events_pagination_broken` | `--page > 1` was passed but upstream returned empty array | +| 4 | `no_position_in_pool` | `/pnl/{pool_id}` returned 404 — user has no position OR pool doesn't exist | +| 5 | (server address-validation absent) | Surfaced only as client-side validation rejecting before HTTP | +| 6 | `period_type_alias_used` | Caller passed a period_type value that's a legacy alias (e.g., `lifetime` → `life`) | +| 7 | `upstream_schema_normalized` | USD field was normalized from `number` to decimal-string for caller consistency | +| 8 | (bad period_type) | Surfaced via `error_code: INVALID_PERIOD_TYPE` not `_dataQuality` | +| 9 | `envelope_normalized` | `/pnl/{pool_id}` bare card wrapped to match `/pnl` envelope shape | + +Plus `apr_semantics` always populated when APR/APY fields are present, describing the pool-type-specific definition. + +## Known constraints + +- **Network**: requires HTTPS access to `bff.bitflowapis.finance`. Skill times out at 30s per request. +- **Wallet**: none. Caller provides `--address SP...` always. +- **Pagination**: `/earnings/events` upstream pagination is broken (`page > 1` returns empty). Use `--start-date` / `--end-date` windowing. +- **Rollups**: `/earnings/rollups` upstream currently returns empty even for active LPs across all `period_type` values. Use `--history` for time-series. +- **Legacy pools**: Classic/Legacy AMM earnings shape is forward-looking — no live `xyk` or `stableswapv2` pool currently exposes earnings to validate the schema. +- **Cache TTL**: BFF caches `/pnl*` and `/rollups` server-side for 120s. Two calls within 120s return the same response. diff --git a/skills/bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts b/skills/bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts new file mode 100644 index 00000000..021e5cc6 --- /dev/null +++ b/skills/bitflow-user-earnings-primitive/bitflow-user-earnings-primitive.ts @@ -0,0 +1,792 @@ +#!/usr/bin/env bun +// bitflow-user-earnings-primitive +// Read-only Bitflow user-earnings reporter for AI agents. +// PRD: https://github.com/BitflowFinance/bff-skills/issues/609 +// +// Wraps the 5 BFF App API /earnings/* endpoints with default-and-drill-down CLI, +// pass-through-no-derivation, decimal-string monetary normalization, pool-type-aware +// APR/APY semantics (verbatim from Bitflow frontend tooltips), and a _dataQuality +// envelope flagging 9 known upstream issues. Zero credentials, zero wallet state, +// zero broadcast. + +import { Command } from "commander"; + +// ─── Configuration ─────────────────────────────────────────────────────────── + +const SKILL_NAME = "bitflow-user-earnings-primitive"; + +const BFF_BASE = "https://bff.bitflowapis.finance/api/app"; +const BFF_V1 = `${BFF_BASE}/v1`; +const BFF_OPENAPI = `${BFF_BASE}/openapi.json`; +const BFF_LIVE = `${BFF_BASE}/live`; +const BFF_HEALTH = `${BFF_BASE}/health`; + +const REQUEST_TIMEOUT_MS = 30_000; +const DOCTOR_TIMEOUT_MS = 10_000; + +// Stacks mainnet address validation regex. +// SP/SM prefix + 39 chars of Crockford base32 alphabet = 41 chars total. +const STACKS_ADDRESS_REGEX = /^S[PM][0-9A-HJ-NP-Z]{39}$/; + +// Accepted period_type values per BFF OpenAPI (verified empirically 2026-05-22). +// /pnl + /pnl/{pool_id}: 1d, 7d, 30d, life + legacy daily/weekly/monthly/yearly/lifetime + 4hr/6hr/12hr +// /rollups: 4hr, 6hr, 12hr, daily, weekly, monthly, yearly (NO 1d/7d/30d/life) +const PNL_PERIOD_TYPES = new Set([ + "1d", "7d", "30d", "life", + "4hr", "6hr", "12hr", "daily", "weekly", "monthly", "yearly", "lifetime", +]); +const ROLLUPS_PERIOD_TYPES = new Set([ + "4hr", "6hr", "12hr", "daily", "weekly", "monthly", "yearly", +]); +const LEGACY_PERIOD_ALIASES: Record = { + lifetime: "life", + daily: "1d", + weekly: "7d", + monthly: "30d", +}; + +const ACCEPTED_AMM_TYPES = new Set(["dlmm", "stableswapv2", "xyk"]); + +// Pool-type APR/APY semantics — verbatim from Bitflow frontend tooltips. +// Embedded in _dataQuality.apr_semantics on every response that may carry APR/APY fields. +const APR_SEMANTICS = { + dlmm_pools: + "APR is calculated from the pool's fees earned in the last 24 hours divided by its current TVL, then annualized over 365 days. It shows your potential returns from fees in the active bin without compounding taken into account. (Bitflow frontend, HODLMM pool tooltip, verbatim)", + classic_pools: + "The Organic Pool APY shown is the historic average. Organic APY = total trading fees collected into the pool / TVL. Each day, a 24 hour organic APY is calculated, then included in the historic average. Some pools offer additional farming rewards on top for pooling/staking. (Bitflow frontend, Classic pool tooltip, verbatim)", +}; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +type EndpointKind = "pnl" | "pnl_pool" | "history" | "events" | "rollups"; + +interface DataQualityEnvelope { + rollups_empty_upstream?: { + note: string; + last_observed: string; + }; + events_null_timestamps?: { + note: string; + last_observed: string; + affected_count: number; + }; + events_pagination_broken?: { + note: string; + last_observed: string; + }; + no_position_in_pool?: { + note: string; + }; + upstream_schema_normalized?: { + note: string; + normalized_fields: string[]; + }; + period_type_alias_used?: { + note: string; + caller_passed: string; + canonical: string; + }; + envelope_normalized?: { + note: string; + }; + apr_semantics?: typeof APR_SEMANTICS; +} + +interface SourceMeta { + upstream_url: string; + fetched_at: string; + upstream_cache_age_hint?: string; +} + +interface SuccessEnvelope { + status: "success"; + action: string; + data: T; + error: null; +} + +interface ErrorPayload { + error_code: + | "INVALID_ADDRESS_FORMAT" + | "INVALID_PERIOD_TYPE" + | "INVALID_AMM_TYPE" + | "INVALID_POOL_ID" + | "INVALID_FLAG_VALUE" + | "PNL_CARD_NOT_FOUND" + | "UPSTREAM_RATE_LIMITED" + | "UPSTREAM_TIMEOUT" + | "UPSTREAM_5XX" + | "UPSTREAM_4XX" + | "UPSTREAM_UNREACHABLE" + | "UNEXPECTED_RESPONSE_SHAPE"; + message: string; + hint?: string; + upstream_status?: number; + upstream_body?: unknown; + retryable: boolean; +} + +interface ErrorEnvelope { + status: "error"; + action: string; + data: null; + error: ErrorPayload; +} + +type Envelope = SuccessEnvelope | ErrorEnvelope; + +// ─── Stacks address validation ─────────────────────────────────────────────── + +function validateStacksAddress(addr: string): { ok: true } | { ok: false; reason: string } { + if (typeof addr !== "string") return { ok: false, reason: "address must be a string" }; + if (addr.length !== 41) { + return { + ok: false, + reason: `address must be 41 characters (got ${addr.length}); Stacks mainnet addresses are SP/SM + 39 base32 chars`, + }; + } + if (!STACKS_ADDRESS_REGEX.test(addr)) { + return { + ok: false, + reason: `address must match ^S[PM][0-9A-HJ-NP-Z]{39}$ (got '${addr}'); Crockford base32 excludes I, L, O, U`, + }; + } + return { ok: true }; +} + +// ─── HTTP fetch helper with timeout + error normalization ──────────────────── + +interface FetchResult { + ok: boolean; + status: number; + body: unknown; + rawText: string; + timedOut: boolean; + reachable: boolean; +} + +async function fetchWithTimeout(url: string, timeoutMs = REQUEST_TIMEOUT_MS): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetch(url, { + signal: controller.signal, + headers: { + Accept: "application/json", + "User-Agent": "bitflow-user-earnings-primitive/1.0", + }, + }); + const rawText = await res.text(); + let body: unknown = null; + try { + body = JSON.parse(rawText); + } catch { + body = rawText; + } + return { ok: res.ok, status: res.status, body, rawText, timedOut: false, reachable: true }; + } catch (err) { + const e = err as Error & { name?: string }; + const timedOut = e.name === "AbortError"; + return { + ok: false, + status: 0, + body: null, + rawText: "", + timedOut, + reachable: false, + }; + } finally { + clearTimeout(timeoutId); + } +} + +// ─── Decimal-string normalization ──────────────────────────────────────────── + +function toDecimalString(value: unknown): string | null { + if (value === null || value === undefined) return null; + if (typeof value === "string") return value; + if (typeof value === "number") { + if (!Number.isFinite(value)) return null; + return value.toString(); + } + return null; +} + +// Recursively walk an object and normalize known monetary field names. +// Returns the list of paths normalized (for _dataQuality reporting). +function normalizeMonetary(obj: unknown, prefix = ""): string[] { + const normalized: string[] = []; + if (!obj || typeof obj !== "object") return normalized; + const monetaryKeys = new Set([ + "userEarningsUsd", + "userEarningsBtc", + "poolTvlUsd", + "poolFeesCollectedUsd", + "poolFeesForDay", + "feesFromSwapUsd", + "earningsUsd", + "earningsBtc", + "priceUsd", + "priceBtc", + ]); + if (Array.isArray(obj)) { + obj.forEach((item, i) => { + normalized.push(...normalizeMonetary(item, `${prefix}[${i}]`)); + }); + return normalized; + } + const record = obj as Record; + for (const key of Object.keys(record)) { + const path = prefix ? `${prefix}.${key}` : key; + const val = record[key]; + if (monetaryKeys.has(key) && typeof val === "number") { + record[key] = toDecimalString(val); + normalized.push(path); + } else if (val && typeof val === "object") { + normalized.push(...normalizeMonetary(val, path)); + } + } + return normalized; +} + +// ─── Envelope construction helpers ─────────────────────────────────────────── + +function success(action: string, data: T): SuccessEnvelope { + return { status: "success", action, data, error: null }; +} + +function fail(action: string, error: ErrorPayload): ErrorEnvelope { + return { status: "error", action, data: null, error }; +} + +function nowIso(): string { + return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); +} + +// ─── Endpoint URL builders ─────────────────────────────────────────────────── + +interface QueryFlags { + pool_id?: string; + amm_type?: string; + period_type?: string; + days?: number; + timescale?: string; + start_date?: string; + end_date?: string; + page?: number; + limit?: number; +} + +function buildUrl(addr: string, kind: EndpointKind, q: QueryFlags = {}): string { + const params = new URLSearchParams(); + let path: string; + switch (kind) { + case "pnl": + path = `${BFF_V1}/users/${addr}/earnings/pnl`; + if (q.period_type) params.set("period_type", q.period_type); + if (q.pool_id) params.set("pool_id", q.pool_id); + if (q.amm_type) params.set("amm_type", q.amm_type); + break; + case "pnl_pool": + path = `${BFF_V1}/users/${addr}/earnings/pnl/${q.pool_id}`; + if (q.period_type) params.set("period_type", q.period_type); + break; + case "history": + path = `${BFF_V1}/users/${addr}/earnings`; + if (q.pool_id) params.set("pool_id", q.pool_id); + if (q.days !== undefined) params.set("days", String(q.days)); + if (q.timescale) params.set("timescale", q.timescale); + break; + case "events": + path = `${BFF_V1}/users/${addr}/earnings/events`; + if (q.pool_id) params.set("pool_id", q.pool_id); + if (q.amm_type) params.set("amm_type", q.amm_type); + if (q.start_date) params.set("start_date", q.start_date); + if (q.end_date) params.set("end_date", q.end_date); + if (q.page !== undefined) params.set("page", String(q.page)); + if (q.limit !== undefined) params.set("limit", String(q.limit)); + break; + case "rollups": + path = `${BFF_V1}/users/${addr}/earnings/rollups`; + if (q.period_type) params.set("period_type", q.period_type); + if (q.pool_id) params.set("pool_id", q.pool_id); + if (q.amm_type) params.set("amm_type", q.amm_type); + if (q.start_date) params.set("start_date", q.start_date); + if (q.end_date) params.set("end_date", q.end_date); + break; + } + const qs = params.toString(); + return qs ? `${path}?${qs}` : path; +} + +function actionFor(kind: EndpointKind): string { + switch (kind) { + case "pnl": return "earnings.pnl"; + case "pnl_pool": return "earnings.pnl_pool"; + case "history": return "earnings.history"; + case "events": return "earnings.events"; + case "rollups": return "earnings.rollups"; + } +} + +function cacheHintFor(kind: EndpointKind): string | undefined { + if (kind === "pnl" || kind === "pnl_pool" || kind === "rollups") { + return "may be up to 120 seconds stale (BFF server-side cache TTL)"; + } + return undefined; +} + +// ─── Upstream error → ErrorPayload mapping ─────────────────────────────────── + +function mapUpstreamError(action: string, result: FetchResult, url: string): ErrorEnvelope { + if (!result.reachable) { + if (result.timedOut) { + return fail(action, { + error_code: "UPSTREAM_TIMEOUT", + message: `BFF App API did not respond within ${REQUEST_TIMEOUT_MS}ms`, + hint: "Network may be slow or BFF is overloaded; retry once before surfacing as failure", + retryable: true, + }); + } + return fail(action, { + error_code: "UPSTREAM_UNREACHABLE", + message: "Could not reach BFF App API", + hint: `Check network connectivity to ${url}`, + retryable: true, + }); + } + if (result.status === 429) { + return fail(action, { + error_code: "UPSTREAM_RATE_LIMITED", + message: "BFF App API rate limit hit (500 req/min/IP shared)", + hint: "Wait for the rate-limit window to clear, or email help@bitflow.finance for higher limits", + upstream_status: 429, + upstream_body: result.body, + retryable: true, + }); + } + if (result.status >= 500) { + return fail(action, { + error_code: "UPSTREAM_5XX", + message: `BFF App API returned ${result.status}`, + hint: "BFF server-side issue; not a skill bug. Do not retry silently.", + upstream_status: result.status, + upstream_body: result.body, + retryable: false, + }); + } + if (result.status === 400) { + // Bad period_type returns HTTP 400 with plain-string detail. + const detail = (result.body as { detail?: string } | null)?.detail; + if (typeof detail === "string" && detail.toLowerCase().includes("period_type")) { + return fail(action, { + error_code: "INVALID_PERIOD_TYPE", + message: detail, + hint: "Accepted values vary by endpoint — see skill SKILL.md or PRD #609", + upstream_status: 400, + upstream_body: result.body, + retryable: false, + }); + } + return fail(action, { + error_code: "UPSTREAM_4XX", + message: `BFF App API returned 400`, + hint: typeof detail === "string" ? detail : "Check request parameters", + upstream_status: 400, + upstream_body: result.body, + retryable: false, + }); + } + return fail(action, { + error_code: "UPSTREAM_4XX", + message: `BFF App API returned ${result.status}`, + upstream_status: result.status, + upstream_body: result.body, + retryable: false, + }); +} + +// ─── Generic endpoint runner ───────────────────────────────────────────────── + +interface RunResult { + envelope: Envelope; +} + +async function runEndpoint( + kind: EndpointKind, + addr: string, + q: QueryFlags, + extraDQ: Partial, +): Promise { + const action = actionFor(kind); + const url = buildUrl(addr, kind, q); + const result = await fetchWithTimeout(url); + + // Special case: 404 on /pnl/{pool_id} = "no position" — map to success with empty cards. + if (kind === "pnl_pool" && result.reachable && result.status === 404) { + const upstreamBody = result.body as + | { detail?: { error?: string; error_code?: string; detail?: string } } + | null; + const errorCode = upstreamBody?.detail?.error_code; + const isNoPosition = errorCode === "PNL_CARD_NOT_FOUND"; + const dq: DataQualityEnvelope = { + ...extraDQ, + apr_semantics: APR_SEMANTICS, + }; + if (isNoPosition) { + dq.no_position_in_pool = { + note: `Pool returned 404 PNL_CARD_NOT_FOUND — user has no position in this pool, or pool does not exist. BFF upstream conflates these two semantic conditions. ${upstreamBody?.detail?.detail || ""}`.trim(), + }; + } + dq.envelope_normalized = { + note: "Single-pool card response wrapped to match the multi-card envelope shape ({periodType, cards: [...]}). Upstream returns bare card object on success; this skill normalizes for caller consistency.", + }; + const payload = { + address: addr, + endpoint: kind, + period: q.period_type, + pool_id: q.pool_id, + cards: [], + pool_tokens: {}, + _dataQuality: dq, + _source: { + upstream_url: url, + fetched_at: nowIso(), + upstream_cache_age_hint: cacheHintFor(kind), + } as SourceMeta, + }; + return { envelope: success(action, payload) }; + } + + if (!result.ok) { + return { envelope: mapUpstreamError(action, result, url) }; + } + + // Parse response body + const body = result.body as Record | null; + if (body === null || typeof body !== "object") { + return { + envelope: fail(action, { + error_code: "UNEXPECTED_RESPONSE_SHAPE", + message: "BFF returned a non-object body for an endpoint expected to return JSON", + upstream_status: result.status, + upstream_body: result.body, + retryable: false, + }), + }; + } + + // Normalize decimal-string monetary fields. + // We mutate body in place; this is the ONE allowed deviation from pure pass-through + // because BFF itself is inconsistent (USD typed as number in /earnings, string in /events + /pnl*). + const normalizedFields = normalizeMonetary(body); + + // Build _dataQuality envelope. + const dq: DataQualityEnvelope = { ...extraDQ }; + + // Always include APR semantics when the endpoint may carry APR/APY fields. + if (kind === "pnl" || kind === "pnl_pool" || kind === "history") { + dq.apr_semantics = APR_SEMANTICS; + } + + if (normalizedFields.length > 0) { + dq.upstream_schema_normalized = { + note: "BFF upstream is inconsistent across endpoints: USD typed as number in /earnings but as decimal-string in /events + /pnl*. This skill normalizes ALL monetary fields to decimal-string for caller consistency.", + normalized_fields: normalizedFields, + }; + } + + // Events-specific data-quality checks. + if (kind === "events") { + const events = Array.isArray(body.events) ? body.events : []; + const nullTsCount = events.filter( + (e: unknown) => (e as { timestamp?: unknown }).timestamp === null, + ).length; + if (nullTsCount > 0) { + dq.events_null_timestamps = { + note: "BFF returned null timestamps on this many events as of 2026-05-22. Order by eventId (descending string number) or swapEventId (stable UUID). Time-anchored analysis is currently not possible against this endpoint.", + last_observed: nowIso(), + affected_count: nullTsCount, + }; + } + if (q.page !== undefined && q.page > 1 && events.length === 0) { + const totalCount = typeof body.totalCount === "number" ? body.totalCount : 0; + if (totalCount > 0) { + dq.events_pagination_broken = { + note: `BFF returned empty events for page=${q.page} despite totalCount=${totalCount}. Upstream pagination beyond page 1 is broken as of 2026-05-22. Use --start-date / --end-date for windowing.`, + last_observed: nowIso(), + }; + } + } + } + + // Rollups-specific data-quality checks. + if (kind === "rollups") { + const rollups = Array.isArray(body.rollups) ? body.rollups : []; + if (rollups.length === 0) { + dq.rollups_empty_upstream = { + note: "BFF returned empty rollups[] for this period_type. Confirmed broken upstream as of 2026-05-22 — rollups returned empty for active LPs across all period_type values during empirical probe. Use --history (/earnings) for time-series instead.", + last_observed: nowIso(), + }; + } + } + + // /pnl_pool bare-card normalization: upstream returns the card object directly, + // skill wraps it to {cards: [card]} matching the multi-card envelope shape. + let normalizedBody: Record = body; + if (kind === "pnl_pool" && !("cards" in body)) { + normalizedBody = { + periodType: q.period_type, + cards: [body], + }; + dq.envelope_normalized = { + note: "Single-pool card response wrapped to match the multi-card envelope shape ({periodType, cards: [...]}). Upstream returns bare card object; this skill normalizes for caller consistency.", + }; + } + + // Period-type alias detection + if (q.period_type && q.period_type in LEGACY_PERIOD_ALIASES) { + dq.period_type_alias_used = { + note: "Caller passed a legacy period_type alias. BFF accepts it but the canonical form differs.", + caller_passed: q.period_type, + canonical: LEGACY_PERIOD_ALIASES[q.period_type], + }; + } + + const payload = { + address: addr, + endpoint: kind, + period: q.period_type, + pool_id: q.pool_id, + amm_filter: q.amm_type, + cards: normalizedBody.cards, + earnings: normalizedBody.earnings, + events: normalizedBody.events, + total_count: normalizedBody.totalCount, + rollups: normalizedBody.rollups, + pool_tokens: normalizedBody.poolTokens, + _dataQuality: dq, + _source: { + upstream_url: url, + fetched_at: nowIso(), + upstream_cache_age_hint: cacheHintFor(kind), + } as SourceMeta, + }; + + return { envelope: success(action, payload) }; +} + +// ─── Doctor ────────────────────────────────────────────────────────────────── + +interface DoctorEndpointResult { + name: string; + url: string; + status: "ok" | "down"; + http_status: number; + latency_ms: number; + error?: string; +} + +interface DoctorPayload { + overall: "ok" | "degraded" | "down"; + endpoints: DoctorEndpointResult[]; + openapi_version?: string; + openapi_title?: string; +} + +async function doctorRun(): Promise> { + const probes: Array<{ name: string; url: string }> = [ + { name: "bff_live", url: BFF_LIVE }, + { name: "bff_health", url: BFF_HEALTH }, + { name: "bff_openapi", url: BFF_OPENAPI }, + ]; + const results: DoctorEndpointResult[] = []; + let openapiBody: { openapi?: string; info?: { title?: string; version?: string } } | null = null; + for (const probe of probes) { + const t0 = Date.now(); + const r = await fetchWithTimeout(probe.url, DOCTOR_TIMEOUT_MS); + const latency = Date.now() - t0; + if (probe.name === "bff_openapi" && r.ok && r.body && typeof r.body === "object") { + openapiBody = r.body as { openapi?: string; info?: { title?: string; version?: string } }; + } + results.push({ + name: probe.name, + url: probe.url, + status: r.ok ? "ok" : "down", + http_status: r.status, + latency_ms: latency, + error: r.ok ? undefined : (r.timedOut ? "timeout" : "unreachable"), + }); + } + const downCount = results.filter((r) => r.status === "down").length; + const overall: DoctorPayload["overall"] = + downCount === 0 ? "ok" : downCount === results.length ? "down" : "degraded"; + const payload: DoctorPayload = { + overall, + endpoints: results, + openapi_version: openapiBody?.openapi, + openapi_title: openapiBody?.info?.title, + }; + return success("doctor", payload); +} + +// ─── Output ────────────────────────────────────────────────────────────────── + +function emit(env: Envelope, format: "json" | "text" | "table" = "json"): number { + if (format === "json") { + console.log(JSON.stringify(env, null, 2)); + } else if (format === "text") { + if (env.status === "success") { + console.log(`[${SKILL_NAME}] ok — ${env.action}`); + console.log(JSON.stringify(env.data, null, 2)); + } else { + console.log(`[${SKILL_NAME}] error — ${env.action} — ${env.error.error_code}: ${env.error.message}`); + if (env.error.hint) console.log(` hint: ${env.error.hint}`); + } + } else { + // table — minimal; full JSON for inspection + console.log(JSON.stringify(env, null, 2)); + } + // Exit-code mapping per PRD §"Exit codes" + if (env.status === "error") return 2; + return 0; +} + +// ─── CLI ───────────────────────────────────────────────────────────────────── + +const program = new Command(); + +program + .name(SKILL_NAME) + .description("Read-only Bitflow user-earnings reporter (PRD: https://github.com/BitflowFinance/bff-skills/issues/609)"); + +program + .command("doctor") + .description("Probe BFF App API health (live, health, openapi). Read-only.") + .option("--format ", "Output format: json|text|table", "json") + .action(async (opts: { format: "json" | "text" | "table" }) => { + const env = await doctorRun(); + process.exit(emit(env, opts.format)); + }); + +program + .command("run") + .description("Query Bitflow user earnings. Default returns headline P&L cards (period=life). Use drill-down flags for deeper layers.") + .requiredOption("--address ", "Stacks mainnet address (SP/SM + 39 base32 chars)") + .option("--period

", "Period type: 1d|7d|30d|life (default 'life'). For --rollups: 4hr|6hr|12hr|daily|weekly|monthly|yearly") + .option("--pool ", "Pool ID shorthand (e.g. dlmm_1, dlmm_2). Drills into /pnl/{pool_id}") + .option("--history", "Query /earnings (daily-aggregated per-bin history)") + .option("--events", "Query /earnings/events (per-swap audit trail)") + .option("--rollups", "Query /earnings/rollups (pre-aggregated; currently broken upstream)") + .option("--amm ", "AMM type filter: dlmm|stableswapv2|xyk") + .option("--start-date ", "ISO date for /events and /rollups windowing") + .option("--end-date ", "ISO date for /events and /rollups windowing") + .option("--days ", "/earnings days lookback (1-365, default 30)") + .option("--limit ", "/events limit (1-100, default 50)") + .option("--page ", "/events page (>=1, default 1). NOTE: page>1 is broken upstream as of 2026-05-22") + .option("--format ", "Output format: json|text|table", "json") + .action(async (opts) => { + const action = "earnings.run"; + + // Validate address + const addrCheck = validateStacksAddress(opts.address); + if (!addrCheck.ok) { + const env = fail(action, { + error_code: "INVALID_ADDRESS_FORMAT", + message: addrCheck.reason, + hint: "Provide a 41-char Stacks mainnet address starting with SP or SM", + retryable: false, + }); + process.exit(emit(env, opts.format)); + } + + // Validate amm_type + if (opts.amm && !ACCEPTED_AMM_TYPES.has(opts.amm)) { + const env = fail(action, { + error_code: "INVALID_AMM_TYPE", + message: `Invalid amm_type '${opts.amm}'`, + hint: `Accepted values: ${[...ACCEPTED_AMM_TYPES].join(", ")}`, + retryable: false, + }); + process.exit(emit(env, opts.format)); + } + + // Determine kind + period defaults + let kind: EndpointKind = "pnl"; + if (opts.pool) kind = "pnl_pool"; + if (opts.history) kind = "history"; + if (opts.events) kind = "events"; + if (opts.rollups) kind = "rollups"; + + // Period validation + let periodType: string | undefined = opts.period; + if (kind === "pnl" || kind === "pnl_pool") { + periodType = periodType || "life"; + if (!PNL_PERIOD_TYPES.has(periodType)) { + const env = fail(action, { + error_code: "INVALID_PERIOD_TYPE", + message: `Invalid period_type '${periodType}' for /pnl* endpoints`, + hint: `Accepted: ${[...PNL_PERIOD_TYPES].join(", ")}`, + retryable: false, + }); + process.exit(emit(env, opts.format)); + } + } else if (kind === "rollups") { + periodType = periodType || "daily"; + if (!ROLLUPS_PERIOD_TYPES.has(periodType)) { + const env = fail(action, { + error_code: "INVALID_PERIOD_TYPE", + message: `Invalid period_type '${periodType}' for /rollups endpoint`, + hint: `Accepted: ${[...ROLLUPS_PERIOD_TYPES].join(", ")} (NOTE: no 1d/7d/30d/life on rollups)`, + retryable: false, + }); + process.exit(emit(env, opts.format)); + } + } + + // Numeric validations + const daysNum = opts.days ? Number(opts.days) : undefined; + if (daysNum !== undefined && (!Number.isInteger(daysNum) || daysNum < 1 || daysNum > 365)) { + const env = fail(action, { + error_code: "INVALID_FLAG_VALUE", + message: `--days must be an integer in [1, 365] (got ${opts.days})`, + retryable: false, + }); + process.exit(emit(env, opts.format)); + } + const limitNum = opts.limit ? Number(opts.limit) : undefined; + if (limitNum !== undefined && (!Number.isInteger(limitNum) || limitNum < 1 || limitNum > 100)) { + const env = fail(action, { + error_code: "INVALID_FLAG_VALUE", + message: `--limit must be an integer in [1, 100] (got ${opts.limit})`, + retryable: false, + }); + process.exit(emit(env, opts.format)); + } + const pageNum = opts.page ? Number(opts.page) : undefined; + if (pageNum !== undefined && (!Number.isInteger(pageNum) || pageNum < 1)) { + const env = fail(action, { + error_code: "INVALID_FLAG_VALUE", + message: `--page must be a positive integer (got ${opts.page})`, + retryable: false, + }); + process.exit(emit(env, opts.format)); + } + + // Build query flags + const q: QueryFlags = {}; + if (opts.pool) q.pool_id = opts.pool; + if (opts.amm) q.amm_type = opts.amm; + if (periodType) q.period_type = periodType; + if (daysNum !== undefined) q.days = daysNum; + if (opts.startDate) q.start_date = opts.startDate; + if (opts.endDate) q.end_date = opts.endDate; + if (limitNum !== undefined) q.limit = limitNum; + if (pageNum !== undefined) q.page = pageNum; + + // Execute + const { envelope } = await runEndpoint(kind, opts.address, q, {}); + process.exit(emit(envelope, opts.format)); + }); + +program.parse();