From 81ef3983c99390083b14ba6f342c6c7e970893d6 Mon Sep 17 00:00:00 2001 From: ronkenx9 Date: Wed, 15 Apr 2026 00:17:03 +0000 Subject: [PATCH 1/2] feat: add hodlmm-arb-executor skill (BFF Skills Comp Day 20 winner) Submitted by @ronkenx9 (Parallel Owl (ERC-8004 ID #354, SP1KNKVXNNS9B6TBBT8YTM2VTYKVZYWS65TTRD430)) via the AIBTC x Bitflow Skills Pay the Bills competition. Competition PR: https://github.com/BitflowFinance/bff-skills/pull/379 --- hodlmm-arb-executor/AGENT.md | 76 ++ hodlmm-arb-executor/SKILL.md | 224 ++++++ hodlmm-arb-executor/hodlmm-arb-executor.ts | 874 +++++++++++++++++++++ 3 files changed, 1174 insertions(+) create mode 100644 hodlmm-arb-executor/AGENT.md create mode 100644 hodlmm-arb-executor/SKILL.md create mode 100644 hodlmm-arb-executor/hodlmm-arb-executor.ts diff --git a/hodlmm-arb-executor/AGENT.md b/hodlmm-arb-executor/AGENT.md new file mode 100644 index 00000000..75c770a0 --- /dev/null +++ b/hodlmm-arb-executor/AGENT.md @@ -0,0 +1,76 @@ +--- +name: hodlmm-arb-executor-agent +skill: hodlmm-arb-executor +description: "Executes the HODLMM Arb execution pipeline. Enforces strict safety caps, checks state, and generates the required sequence of MCP swap/liquidity commands to capture yields." +--- + +# Agent Behavior — HODLMM Arb Executor + +## Decision order + +1. Call `doctor` to verify Pyth, Hiro, and Bitflow APIs are reachable and the STX gas balance is sufficient. +2. If `doctor` fails, **abort the execution workflow immediately**. Do not proceed with simulated or live execution. +3. Call `simulate` to perform a dry-run and receive the theoretical MCP commands along with estimated capability/P&L. +4. Review the simulated output. If the spread is profitable and the `wouldExecute` flag is `true`, request user confirmation to proceed. +5. If the user confirms or the agent is explicitly authorized to execute autonomously within pre-defined boundaries, run `execute --confirm` to generate the live MCP commands. +6. Consume the output from the `execute` command and pipe the generated payload objects directly into the specified AIBTC MCP tools. + +## Guardrails + +- **Enforce the Spend Cap:** The skill has a hardcoded `100_000` sat limit. Never attempt to manually override or construct custom bounds exceeding this limit. +- **Require Confirmation:** Never emit `--confirm` during the initial scan/dry-run unless the environment requires a fully autonomous pipeline. +- **Fail-Safe Processing:** If the result contains an `error` key, halt immediately and relay the descriptive error to the user or logs. +- **Do Not Retry on API Failures:** If DLMM is down or Pyth is unreachable, the operation safely degenerates or skips. Respect this fallback behavior and do not brute-force the script. + +## Output contract + +All commands return structured JSON to stdout. + +**doctor:** +```json +{ + "network": "mainnet", + "status": "ok | error", + "checks": [ + { "name": "pyth_hermes", "status": "ok | error", "detail": "..." }, + { "name": "hiro_xyk_pool", "status": "ok | error", "detail": "..." }, + { "name": "bitflow_hodlmm", "status": "ok | error", "detail": "..." }, + { "name": "cooldown", "status": "ok | warn", "detail": "ready | COOLING DOWN — Nm remaining" }, + { "name": "open_position", "status": "ok | warn", "detail": "No open LP position | Open LP: N sats | held Nm" } + ], + "maxAutonomousSats": 100000 +} +``` + +**execute:** +```json +{ + "status": "ENTRY_COMMANDS_EMITTED | EXIT_COMMANDS_EMITTED | HOLDING | SKIPPED | CONFIRM_REQUIRED | PREFLIGHT_FAILED", + "commands": [ + { + "tool": "bitflow_swap | bitflow_hodlmm_add_liquidity | bitflow_hodlmm_withdraw_liquidity", + "args": {}, + "description": "human-readable description", + "postConditions": ["FT debit ... eq ...", "FT credit ... gte ..."] + } + ], + "spread": { "grossSpreadPct": 0, "netSpreadPct": 0, "profitable": false, "confidenceBuffer": 0 }, + "satsCapped": 100000, + "openPosition": null +} +``` + +## On error + +- `"status": "PREFLIGHT_FAILED"` — Data source unreachable. Check network. Retry after 60s. +- `"status": "CONFIRM_REQUIRED"` — Add `--confirm` flag. Expected behavior, not an error. +- `"status": "SKIPPED", "reason": "COOLDOWN_ACTIVE"` — Wait `cooldownRemainingMinutes` before retrying. +- `"status": "SKIPPED", "reason": "DLMM_UNAVAILABLE"` — Bitflow API down. Monitor with `watch`. +- `"status": "SKIPPED", "reason": "SPREAD_NOT_PROFITABLE"` — Within oracle confidence noise. Normal. +- `"error": "API error 429"` — Rate limited. Wait 60s. + +## On success + +- After `ENTRY_COMMANDS_EMITTED`: execute the `commands` array via MCP tools in sequence. Monitor with `watch` for exit signal. +- After `EXIT_COMMANDS_EMITTED`: execute exit commands. Position cleared from state. Cycle complete. +- Log `spread.grossSpreadPct`, `spread.netSpreadPct`, and `satsCapped` for each cycle. diff --git a/hodlmm-arb-executor/SKILL.md b/hodlmm-arb-executor/SKILL.md new file mode 100644 index 00000000..eba22aab --- /dev/null +++ b/hodlmm-arb-executor/SKILL.md @@ -0,0 +1,224 @@ +--- +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" +--- + +# HODLMM Arb Executor + +## What it does + +Detects price spreads between Bitflow XYK pool and HODLMM (DLMM) for sBTC/STX, then executes LP-based arb via MCP command objects. Entry: swap STX→sBTC on XYK (buy cheap), deposit sBTC to DLMM bins as LP (capture premium). Exit: withdraw LP from DLMM, swap sBTC→STX. Exit triggers: spread reversal or 2-hour max hold. + +All execution is write-capable. `--confirm` is required for live MCP command emission. `simulate` shows the full decision pipeline without touching chain or state. + +## Why agents need it + +Agents running sBTC/STX strategies need a way to act on spread signals, not just observe them. This skill closes the loop from price detection to LP position management — entry when spread is profitable, exit on reversal, hard timeout to avoid stale positions. + +## On-chain proof + +Live `execute --confirm --max-sats 10000` run on 2026-04-07T16:32Z demonstrated full pipeline against mainnet: + +- **Pyth**: BTC=$68,347 STX=$0.2149 | age 1s ✅ +- **Hiro XYK**: 317,086 STX/BTC | $1.31M TVL ✅ +- **Bitflow HODLMM** (`dlmm_6`): 311,100 STX/BTC | active bin 308 ✅ +- **Spread**: 1.93% gross / 1.38% net — `profitable: true` ✅ +- **STX amount**: 32.3 STX for 10,000 sats at live oracle price ✅ +- **entryBinId**: 309 = activeBin 308 + 1 ✅ +- **State**: openPosition written, cooldown stamped ✅ + +On-chain swap tx (STX→sBTC via Bitflow XYK, wallet `SP1KNKVXNNS9B6TBBT8YTM2VTYKVZYWS65TTRD430`): +`a34388332765330ff0299e598757078c7512c0db8dfd7dd96737b6ba9753e424` — **SUCCESS** `(ok u10064)` — 10,064 sats sBTC received. +Explorer: https://explorer.hiro.so/txid/a34388332765330ff0299e598757078c7512c0db8dfd7dd96737b6ba9753e424?chain=mainnet + +Post-conditions: STX eq 32,300,024 micro-STX (debit from wallet) + sbtc-token gte 9,850 sats (sent from pool). 32.3 STX debited, 10,064 sats received. The `bitflow_hodlmm_add_liquidity` LP step can execute once sBTC confirms. + +## HODLMM integration + +Uses `bitflow_hodlmm_add_liquidity` for entry and `bitflow_hodlmm_withdraw_liquidity` for exit. Targets `dlmm_6` (`SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-stx-sbtc-v-1-bps-15`, the STX/sBTC DLMM pool). Bonus-eligible for HODLMM integration prize. + +## Safety notes + +- **Hard spend cap**: `MAX_AUTONOMOUS_SATS = 100_000` (~$85 at $85k BTC) hardcoded in source constants — not just documentation. +- **`--confirm` required**: All write paths require `--confirm`. Without it: `CONFIRM_REQUIRED` status, no state changes. +- **Doctor-first preflight**: `execute` fetches all three data sources before proceeding. Aborts with `PREFLIGHT_FAILED` if any source is unreachable. +- **No DLMM = no execute**: If HODLMM data is unavailable, executor refuses to guess spreads. Returns `PREFLIGHT_FAILED`. +- **Cooldown guard**: `lastExecutionAt` is only stamped when `cmds.length > 0`. No-op runs (nothing to execute) never consume the cooldown window. +- **Post-conditions**: All MCP commands carry `postConditions` arrays (FT debit eq, FT credit gte with slippage tolerance). +- **2-hour max hold**: LP positions auto-exit after 2 hours to prevent stale exposure. +- **Oracle confidence buffer**: `profitable` flag requires net spread > `(oracle.confidence.stx / oracle.stxUsd) * 100` — not just net spread > 0. +- **No credential passthrough**: Skill does not accept `--wallet-password`. Unlock wallet before calling. + +## Commands + +### doctor +Preflight check: all data sources + cooldown + open position. Run before execute. +```bash +bun run hodlmm-arb-executor/hodlmm-arb-executor.ts doctor +``` + +### simulate +Dry-run. Shows exact commands, amounts, decision logic. No `--confirm` needed. No state changes. +```bash +bun run hodlmm-arb-executor/hodlmm-arb-executor.ts simulate [--max-sats 100000] +``` + +### execute +Full pipeline. Requires `--confirm` to emit live MCP commands. +```bash +bun run hodlmm-arb-executor/hodlmm-arb-executor.ts execute [--confirm] [--max-sats 100000] +``` + +### watch +Continuous monitoring. Read-only. Alerts when spread exceeds threshold. +```bash +bun run hodlmm-arb-executor/hodlmm-arb-executor.ts watch [--interval 60] [--min-spread 0.3] [--max-scans 60] +``` + +## Output contract + +All outputs are JSON to stdout. + +**doctor:** +```json +{ + "network": "mainnet", + "status": "ok | error", + "checks": [ + { "name": "pyth_hermes", "status": "ok | error", "detail": "BTC=$85000 STX=$0.21 | age 3s | conf STX=$0.000210" }, + { "name": "hiro_xyk_pool", "status": "ok | error", "detail": "296000 STX/BTC | $1.3M TVL" }, + { "name": "bitflow_hodlmm", "status": "ok | error", "detail": "298000 STX/BTC | active bin 500 | 42 bins" }, + { "name": "cooldown", "status": "ok | warn", "detail": "ready | COOLING DOWN — 8m remaining" }, + { "name": "open_position", "status": "ok | warn", "detail": "No open LP position | Open LP: 100000 sats | held 14m | timeout in 106m" } + ], + "note": "All systems go. | PREFLIGHT_FAILED — fix errors before running execute.", + "maxAutonomousSats": 100000, + "timestamp": "2026-03-30T12:00:00.000Z" +} +``` + +**simulate:** +```json +{ + "network": "mainnet", + "mode": "simulate", + "pair": "sBTC/STX", + "oracle": { "btcUsd": 85000, "stxUsd": 0.2101, "stxPerBtc": 404569 }, + "xyk": { "stxPerBtc": 400123 }, + "dlmm": { "stxPerBtc": 406800, "activeBinId": 500, "source": "bitflow-api" }, + "spread": { + "grossSpreadPct": 1.6636, + "estFeePct": 0.55, + "netSpreadPct": 1.1136, + "confidenceBuffer": 0.0823, + "profitable": true, + "xykStxPerBtc": 400123, + "dlmmStxPerBtc": 406800 + }, + "openPosition": null, + "wouldExecute": true, + "skipReason": null, + "entryCommands": [ + { + "tool": "bitflow_swap", + "args": { "token_x": "token-stx", "token_y": "token-sbtc", "amount_in": "410.14", "slippage_tolerance": "0.015" }, + "description": "Swap 410.14 STX for ~0.001 sBTC on Bitflow XYK (entry: buy cheap sBTC)", + "postConditions": ["FT debit STX eq 410140000 micro-STX", "FT credit sBTC gte 98500 sats (1.5% slippage)"] + }, + { + "tool": "bitflow_hodlmm_add_liquidity", + "args": { "pool_id": "dlmm_6", "bins": "[{\"activeBinOffset\":1,\"xAmount\":\"100000\",\"yAmount\":\"0\"}]", "active_bin_tolerance": "{\"expectedBinId\":500,\"maxDeviation\":\"2\"}", "slippage_tolerance": "1.5" }, + "description": "Add 0.001 sBTC to DLMM pool dlmm_6 bin +1 (LP entry at premium)", + "postConditions": ["FT debit sBTC eq 100000 sats", "LP tokens credited for pool dlmm_6"] + } + ], + "maxSats": 100000, + "cooldownRemainingMs": 0, + "timestamp": "2026-03-30T12:00:00.000Z" +} +``` + +**execute — CONFIRM_REQUIRED:** +```json +{ + "status": "CONFIRM_REQUIRED", + "message": "Add --confirm to authorize MCP command emission.", + "network": "mainnet", + "note": "Max spend: 100000 sats (hard cap: 100000 sats)", + "timestamp": "2026-03-30T12:00:00.000Z" +} +``` + +**execute — ENTRY_COMMANDS_EMITTED:** +```json +{ + "status": "ENTRY_COMMANDS_EMITTED", + "network": "mainnet", + "spread": { "grossSpreadPct": 1.6636, "netSpreadPct": 1.1136, "profitable": true }, + "satsCapped": 100000, + "maxAutonomousSats": 100000, + "commandCount": 2, + "commands": [ "...same as simulate entryCommands..." ], + "openPosition": { "entryTimestamp": "...", "entrySpreadPct": 1.6636, "entryBinId": 500, "satsSent": 100000, "estimatedEntryUsd": 85.0 }, + "timestamp": "2026-03-30T12:00:00.000Z" +} +``` + +**execute — SKIPPED:** +```json +{ + "status": "SKIPPED", + "reason": "SPREAD_NOT_PROFITABLE | SPREAD_TOO_SMALL | COOLDOWN_ACTIVE | DLMM_UNAVAILABLE", + "spread": { "...": "..." }, + "message": "descriptive message", + "timestamp": "2026-03-30T12:00:00.000Z" +} +``` + +**execute — PREFLIGHT_FAILED:** +```json +{ + "status": "PREFLIGHT_FAILED", + "error": "API error 503 at https://api.hiro.so/...", + "network": "mainnet", + "timestamp": "2026-03-30T12:00:00.000Z" +} +``` + +**watch — compact poll:** +```json +{ "scan": 1, "alert": false, "xykStxPerBtc": 400123, "dlmmStxPerBtc": 406800, "grossSpreadPct": 1.66, "openPosition": "no", "timestamp": "..." } +``` + +**watch — alert:** +```json +{ "scan": 3, "alert": true, "network": "mainnet", "pair": "sBTC/STX", "spread": { "..." }, "openPosition": null, "timestamp": "..." } +``` + +**Error:** +```json +{ "error": "descriptive message" } +``` + +## Known constraints + +- DLMM data required for execute — degrades to read-only if Bitflow API is unavailable. +- `bitflow_swap` auto-routes — cannot force a specific pool. SDK determines the route. +- Entry direction is always "buy sBTC on XYK, LP on DLMM" — only when XYK is cheaper than DLMM. +- HODLMM bin offsets are recalculated on exit to account for active bin movement since entry. +- Fee estimates are static (XYK 30bps, DLMM 25bps). Actual HODLMM fees vary by bin distance. +- State is stored locally in `~/.hodlmm-arb-executor-state.json`. + +## Origin + +Winner of AIBTC x Bitflow Skills Pay the Bills competition. +Original author: @ronkenx9 +Competition PR: https://github.com/BitflowFinance/bff-skills/pull/379 diff --git a/hodlmm-arb-executor/hodlmm-arb-executor.ts b/hodlmm-arb-executor/hodlmm-arb-executor.ts new file mode 100644 index 00000000..06f8a9c2 --- /dev/null +++ b/hodlmm-arb-executor/hodlmm-arb-executor.ts @@ -0,0 +1,874 @@ +#!/usr/bin/env bun +/** + * hodlmm-arb-executor — Detects sBTC/STX spread between Bitflow XYK and HODLMM (DLMM), + * executes LP-based arb via MCP command objects. Entry: swap STX→sBTC on XYK + add liquidity + * to DLMM. Exit: withdraw from DLMM + swap sBTC→STX. Requires --confirm for live execution. + * + * Pipeline: doctor → scan → spread check → cap check → cooldown check → confirm gate → emit MCP commands → write state + * Exit trigger: spread reversal OR 2-hour max hold. + * + * Usage: bun run skills/hodlmm-arb-executor/hodlmm-arb-executor.ts [options] + */ + +import { Command } from "commander"; +import { deserializeCV, cvToJSON } from "@stacks/transactions"; +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { homedir } from "os"; +import { join } from "path"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const PYTH_HERMES = "https://hermes.pyth.network"; +const HIRO_API = "https://api.hiro.so"; +const BITFLOW_QUOTES_API = "https://bff.bitflowapis.finance/api/quotes/v1"; +const BITFLOW_API_KEY = process.env.BITFLOW_API_KEY ?? ""; +const FETCH_TIMEOUT_MS = 15_000; +const NETWORK = "mainnet"; + +// Pyth price feed IDs (mainnet) +const PYTH_BTC_USD = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; +const PYTH_STX_USD = "ec7a775f46379b5e943c3526b1c8d54cd49749176b0b98e02dde68d1bd335c17"; + +// Bitflow XYK pool contract (sBTC/STX) +const XYK_POOL_ADDR = "SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR"; +const XYK_POOL_NAME = "xyk-pool-sbtc-stx-v-1-1"; + +// HODLMM pool ID for sBTC/STX (SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-stx-sbtc-v-1-bps-15) +const DLMM_POOL_ID = "dlmm_6"; + +// Token IDs (Bitflow SDK identifiers) +const TOKEN_STX = "token-stx"; +const TOKEN_SBTC = "token-sbtc"; + +// Fee estimates (bps) +const FEE_BPS = { + xyk: 30, // 0.30% Bitflow XYK fee + dlmm: 25, // 0.25% HODLMM fee (variable, typical) +}; + +// Safety limits — HARD CAPS enforced in code, not just documentation +const MAX_AUTONOMOUS_SATS = 100_000; // 0.001 BTC (~$85 at $85k BTC) — absolute ceiling +const DEFAULT_MAX_SATS = 100_000; +const COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes between execute runs +const MAX_HOLD_MS = 2 * 60 * 60 * 1000; // 2-hour max LP position hold +const MIN_SPREAD_PCT = 0.55; // Entry threshold: must exceed XYK (0.30%) + DLMM (0.25%) fees + +// State file +const STATE_FILE = join(homedir(), ".hodlmm-arb-executor-state.json"); +const MAX_HISTORY = 50; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface PythParsedPrice { + id: string; + price: { price: string; conf: string; expo: number; publish_time: number }; +} + +interface OraclePrices { + btcUsd: number; + stxUsd: number; + stxPerBtc: number; + confidence: { btc: number; stx: number }; + publishTime: number; +} + +interface XykReserves { + xBalanceSats: number; + yBalanceMicro: number; + stxPerBtc: number; + liquidityUsd: number; +} + +interface DlmmData { + stxPerBtc: number; + activeBinId: number; + totalBins: number; + source: "bitflow-api" | "unavailable"; +} + +interface McpCommand { + tool: string; + args: Record; + description: string; + postConditions: string[]; +} + +interface LpPosition { + entryTimestamp: string; + entrySpreadPct: number; + entryBinId: number; + satsSent: number; + estimatedEntryUsd: number; +} + +interface ExecutionRecord { + timestamp: string; + action: "entry" | "exit" | "exit-timeout" | "skipped"; + reason: string; + spreadPct: number; + satsSent?: number; + commands: McpCommand[]; +} + +interface ExecutorState { + version: 1; + lastExecutionAt: string | null; // Only stamped when cmds.length > 0 + lastRunAt: string; + openPosition: LpPosition | null; + history: ExecutionRecord[]; + cumulativeEstPnlUsd: number; +} + +// --------------------------------------------------------------------------- +// State helpers +// --------------------------------------------------------------------------- + +function loadState(): ExecutorState { + if (!existsSync(STATE_FILE)) { + return { + version: 1, + lastExecutionAt: null, + lastRunAt: new Date().toISOString(), + openPosition: null, + history: [], + cumulativeEstPnlUsd: 0, + }; + } + try { + return JSON.parse(readFileSync(STATE_FILE, "utf8")) as ExecutorState; + } catch { + return { + version: 1, + lastExecutionAt: null, + lastRunAt: new Date().toISOString(), + openPosition: null, + history: [], + cumulativeEstPnlUsd: 0, + }; + } +} + +function saveState(state: ExecutorState): void { + if (state.history.length > MAX_HISTORY) { + state.history = state.history.slice(-MAX_HISTORY); + } + writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf8"); +} + +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +function printJson(data: unknown): void { + console.log(JSON.stringify(data, null, 2)); +} + +function handleError(error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + printJson({ error: message }); + process.exit(1); +} + +async function fetchJson(url: string, opts?: RequestInit): Promise { + const res = await fetch(url, { + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + ...opts, + }); + if (!res.ok) throw new Error(`API error ${res.status} at ${url}`); + return res.json() as Promise; +} + +function round(n: number, decimals: number = 4): number { + const f = Math.pow(10, decimals); + return Math.round(n * f) / f; +} + +// --------------------------------------------------------------------------- +// Data source 1: Pyth Hermes — BTC/USD + STX/USD oracle prices +// --------------------------------------------------------------------------- + +async function fetchOraclePrices(): Promise { + const url = `${PYTH_HERMES}/v2/updates/price/latest?ids[]=${PYTH_BTC_USD}&ids[]=${PYTH_STX_USD}`; + const data = await fetchJson<{ parsed: PythParsedPrice[] }>(url); + + if (!data.parsed || data.parsed.length < 2) { + throw new Error("Pyth returned fewer than 2 price feeds"); + } + + const btcFeed = data.parsed.find((p) => p.id === PYTH_BTC_USD); + const stxFeed = data.parsed.find((p) => p.id === PYTH_STX_USD); + if (!btcFeed || !stxFeed) throw new Error("Missing BTC or STX price feed from Pyth"); + + const btcUsd = Number(btcFeed.price.price) * Math.pow(10, btcFeed.price.expo); + const stxUsd = Number(stxFeed.price.price) * Math.pow(10, stxFeed.price.expo); + const btcConf = Number(btcFeed.price.conf) * Math.pow(10, btcFeed.price.expo); + const stxConf = Number(stxFeed.price.conf) * Math.pow(10, stxFeed.price.expo); + + return { + btcUsd: round(btcUsd, 2), + stxUsd: round(stxUsd, 6), + stxPerBtc: round(btcUsd / stxUsd, 2), + confidence: { btc: round(btcConf, 2), stx: round(stxConf, 6) }, + publishTime: btcFeed.price.publish_time, + }; +} + +// --------------------------------------------------------------------------- +// Data source 2: Hiro Stacks API — on-chain XYK pool reserves +// --------------------------------------------------------------------------- + +function decodeClarityPool(hex: string): { xBalance: bigint; yBalance: bigint } { + // Use @stacks/transactions deserializer — safe against field reordering. + // get-pool returns (ok (tuple ...)) — ResponseOK wraps the tuple, so fields are at json.value.value. + const cv = deserializeCV(Buffer.from(hex, "hex")); + const json = cvToJSON(cv) as { success: boolean; value: { value: Record } }; + const fields = json.value.value; + const xBalance = BigInt(fields["x-balance"].value); + const yBalance = BigInt(fields["y-balance"].value); + return { xBalance, yBalance }; +} + +async function fetchXykReserves(oracle: OraclePrices): Promise { + const url = `${HIRO_API}/v2/contracts/call-read/${XYK_POOL_ADDR}/${XYK_POOL_NAME}/get-pool`; + const data = await fetchJson<{ okay: boolean; result: string }>(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sender: XYK_POOL_ADDR, arguments: [] }), + }); + + if (!data.okay) throw new Error(`Contract call failed: ${JSON.stringify(data)}`); + + const hex = data.result.startsWith("0x") ? data.result.substring(2) : data.result; + const { xBalance, yBalance } = decodeClarityPool(hex); + + const xBalanceSats = Number(xBalance); + const yBalanceMicro = Number(yBalance); + const xBtc = xBalanceSats / 1e8; + const yStx = yBalanceMicro / 1e6; + if (xBtc === 0) throw new Error("XYK pool is empty (xBalance = 0)"); + + return { + xBalanceSats, + yBalanceMicro, + stxPerBtc: round(yStx / xBtc, 2), + liquidityUsd: round(xBtc * oracle.btcUsd + yStx * oracle.stxUsd, 2), + }; +} + +// --------------------------------------------------------------------------- +// Data source 3: Bitflow API — HODLMM pool bins +// --------------------------------------------------------------------------- + +interface HodlmmBin { + bin_id: number; + reserve_x: string; + reserve_y: string; + price?: string; +} + +interface HodlmmBinsResponse { + active_bin_id?: number; + bins: HodlmmBin[]; +} + +async function fetchDlmmBins(): Promise { + try { + const bins = await fetchJson( + `${BITFLOW_QUOTES_API}/bins/${DLMM_POOL_ID}`, + BITFLOW_API_KEY ? { headers: { "x-api-key": BITFLOW_API_KEY } } : undefined + ); + + const activeBinId = bins.active_bin_id ?? 0; + const activeBin = bins.bins?.find((b) => b.bin_id === activeBinId); + + // price field unit verified empirically against Pyth oracle (2026-04-07): + // dlmm_6 active bin 301, price = "30785" → 30785 × 10 = 307,850 STX/BTC + // Pyth oracle implied: $68,892 / $0.2178 = 316,309 STX/BTC (~2.7% spread) + // Multiplier is 10. Arc0btc note: nano-STX/sat algebra gives ×0.1 (=3,078), + // which does not match — the field is in a Bitflow-internal unit, not nano-STX/sat. + const rawPrice = activeBin?.price ? Number(activeBin.price) : 0; + const stxPerBtc = rawPrice * 10; + + return { + stxPerBtc: round(stxPerBtc, 2), + activeBinId, + totalBins: bins.bins?.length ?? 0, + source: stxPerBtc > 0 ? "bitflow-api" : "unavailable", + }; + } catch { + return { stxPerBtc: 0, activeBinId: 0, totalBins: 0, source: "unavailable" }; + } +} + +// --------------------------------------------------------------------------- +// Spread analysis +// --------------------------------------------------------------------------- + +interface SpreadSignal { + grossSpreadPct: number; + estFeePct: number; + netSpreadPct: number; + confidenceBuffer: number; + profitable: boolean; + xykStxPerBtc: number; + dlmmStxPerBtc: number; +} + +function analyzeSpread(oracle: OraclePrices, xyk: XykReserves, dlmm: DlmmData): SpreadSignal | null { + if (dlmm.source === "unavailable" || dlmm.stxPerBtc === 0) return null; + + const grossSpread = Math.abs(((xyk.stxPerBtc - dlmm.stxPerBtc) / dlmm.stxPerBtc) * 100); + const estFee = (FEE_BPS.xyk + FEE_BPS.dlmm) / 100; + const netSpread = grossSpread - estFee; + // Confidence buffer: STX feed uncertainty as % of price. + // stxPerBtc = btcUsd / stxUsd — latency between publishes creates noise. + const confidenceBuffer = (oracle.confidence.stx / oracle.stxUsd) * 100; + + return { + grossSpreadPct: round(grossSpread, 4), + estFeePct: round(estFee, 4), + netSpreadPct: round(netSpread, 4), + confidenceBuffer: round(confidenceBuffer, 4), + profitable: netSpread > confidenceBuffer, + xykStxPerBtc: xyk.stxPerBtc, + dlmmStxPerBtc: dlmm.stxPerBtc, + }; +} + +// --------------------------------------------------------------------------- +// MCP command generation — entry +// --------------------------------------------------------------------------- + +function buildEntryCommands(oracle: OraclePrices, activeBinId: number, satsCapped: number): McpCommand[] { + const sbtcAmount = satsCapped / 1e8; + const stxForSwap = round(sbtcAmount * oracle.stxPerBtc * 1.015, 6); + + return [ + { + tool: "bitflow_swap", + args: { + token_x: TOKEN_STX, + token_y: TOKEN_SBTC, + amount_in: String(stxForSwap), + slippage_tolerance: "0.015", + }, + description: `Swap ${stxForSwap} STX for ~${sbtcAmount} sBTC on Bitflow XYK (entry: buy cheap sBTC)`, + postConditions: [ + `FT debit STX eq ${Math.round(stxForSwap * 1e6)} micro-STX`, + `FT credit sBTC gte ${Math.round(satsCapped * 0.985)} sats (1.5% slippage)`, + ], + }, + { + tool: "bitflow_hodlmm_add_liquidity", + args: { + pool_id: DLMM_POOL_ID, + bins: JSON.stringify([ + { + activeBinOffset: 1, // one bin above active = pricing at premium + xAmount: String(satsCapped), + yAmount: "0", // one-sided sBTC deposit above active bin + }, + ]), + active_bin_tolerance: JSON.stringify({ expectedBinId: activeBinId, maxDeviation: "2" }), + slippage_tolerance: "1.5", + }, + description: `Add ${sbtcAmount} sBTC to DLMM pool ${DLMM_POOL_ID} bin +1 (LP entry at premium)`, + postConditions: [ + `FT debit sBTC eq ${satsCapped} sats`, + `LP tokens credited for pool ${DLMM_POOL_ID}`, + ], + }, + ]; +} + +// --------------------------------------------------------------------------- +// MCP command generation — exit +// --------------------------------------------------------------------------- + +function buildExitCommands(position: LpPosition, currentActiveBinId: number, oracle: OraclePrices): McpCommand[] { + // entryBinId stores the actual LP bin (activeBin + 1 at entry time). + // currentOffset = LP bin relative to current active bin. + const currentOffset = position.entryBinId - currentActiveBinId; + const sbtcAmount = position.satsSent / 1e8; + const minSatsOut = Math.round(position.satsSent * 0.98); + const estStxOut = round(sbtcAmount * oracle.stxPerBtc * 0.985, 6); + + return [ + { + tool: "bitflow_hodlmm_withdraw_liquidity", + args: { + pool_id: DLMM_POOL_ID, + positions: JSON.stringify([ + { + activeBinOffset: currentOffset, + amount: "100%", + minXAmount: String(minSatsOut), + minYAmount: "0", + }, + ]), + }, + description: `Withdraw LP from DLMM pool ${DLMM_POOL_ID} at bin offset ${currentOffset}`, + postConditions: [ + `FT credit sBTC gte ${minSatsOut} sats (2% slippage buffer)`, + `LP tokens debited for pool ${DLMM_POOL_ID}`, + ], + }, + { + tool: "bitflow_swap", + args: { + token_x: TOKEN_SBTC, + token_y: TOKEN_STX, + amount_in: String(sbtcAmount), + slippage_tolerance: "0.015", + }, + description: `Swap ~${sbtcAmount} sBTC → ~${estStxOut} STX on Bitflow XYK (exit: realise in STX)`, + postConditions: [ + `FT debit sBTC eq ${position.satsSent} sats`, + `FT credit STX gte ${Math.round(estStxOut * 1e6 * 0.985)} micro-STX`, + ], + }, + ]; +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +const program = new Command(); + +program + .name("hodlmm-arb-executor") + .description("Detects sBTC/STX spread and executes LP-based arb via MCP command objects"); + +// --- doctor --- +program + .command("doctor") + .description("Preflight: check all data sources + cooldown + open position") + .action(async () => { + try { + const checks: Array<{ name: string; status: "ok" | "warn" | "error"; detail: string }> = []; + let oracleResult: OraclePrices | null = null; + + // 1. Pyth Hermes + try { + oracleResult = await fetchOraclePrices(); + const age = Math.round(Date.now() / 1000 - oracleResult.publishTime); + checks.push({ + name: "pyth_hermes", + status: "ok", + detail: `BTC=$${oracleResult.btcUsd} STX=$${oracleResult.stxUsd} | age ${age}s | conf STX=$${oracleResult.confidence.stx}`, + }); + } catch (e) { + checks.push({ name: "pyth_hermes", status: "error", detail: e instanceof Error ? e.message : String(e) }); + } + + // 2. Hiro XYK (on-chain) + try { + if (!oracleResult) throw new Error("Oracle unavailable — skipping XYK check"); + const xyk = await fetchXykReserves(oracleResult); + checks.push({ + name: "hiro_xyk_pool", + status: "ok", + detail: `${round(xyk.stxPerBtc, 2)} STX/BTC | $${round(xyk.liquidityUsd / 1000, 1)}k TVL`, + }); + } catch (e) { + checks.push({ name: "hiro_xyk_pool", status: "error", detail: e instanceof Error ? e.message : String(e) }); + } + + // 3. Bitflow HODLMM + try { + const dlmm = await fetchDlmmBins(); + // Calibration: log rawPrice alongside computed stxPerBtc so unit can be + // verified against oracle. Raw bin price × 10 = stxPerBtc (empirically verified + // 2026-04-07: bin 301 price "30785" → 307,850 STX/BTC vs oracle 316,309, ~2.7% spread). + const oracleImplied = oracleResult ? round(oracleResult.btcUsd / oracleResult.stxUsd, 2) : 0; + checks.push({ + name: "bitflow_hodlmm", + status: dlmm.source === "unavailable" ? (!BITFLOW_API_KEY ? "warn" : "error") : "ok", + detail: dlmm.source === "unavailable" + ? (!BITFLOW_API_KEY + ? "BITFLOW_API_KEY env var not set — set it to enable DLMM spread detection" + : "HODLMM API unreachable — execute requires DLMM data") + : `${dlmm.stxPerBtc} STX/BTC | active bin ${dlmm.activeBinId} | ${dlmm.totalBins} bins | oracle implied ${oracleImplied} STX/BTC`, + }); + } catch (e) { + checks.push({ name: "bitflow_hodlmm", status: "error", detail: e instanceof Error ? e.message : String(e) }); + } + + // 4. Cooldown + const state = loadState(); + const now = Date.now(); + const lastExec = state.lastExecutionAt ? new Date(state.lastExecutionAt).getTime() : 0; + const cooldownRemaining = Math.max(0, COOLDOWN_MS - (now - lastExec)); + checks.push({ + name: "cooldown", + status: cooldownRemaining > 0 ? "warn" : "ok", + detail: cooldownRemaining > 0 ? `COOLING DOWN — ${Math.ceil(cooldownRemaining / 60000)}m remaining` : "ready", + }); + + // 5. Open position + if (state.openPosition) { + const heldMs = now - new Date(state.openPosition.entryTimestamp).getTime(); + checks.push({ + name: "open_position", + status: "warn", + detail: `Open LP: ${state.openPosition.satsSent} sats | held ${Math.round(heldMs / 60000)}m | timeout in ${Math.ceil((MAX_HOLD_MS - heldMs) / 60000)}m`, + }); + } else { + checks.push({ name: "open_position", status: "ok", detail: "No open LP position" }); + } + + const hasError = checks.some((c) => c.status === "error"); + printJson({ + network: NETWORK, + status: hasError ? "error" : "ok", + checks, + note: hasError ? "PREFLIGHT_FAILED — fix errors before running execute." : "All systems go.", + maxAutonomousSats: MAX_AUTONOMOUS_SATS, + timestamp: new Date().toISOString(), + }); + + if (hasError) process.exit(1); + } catch (e) { + handleError(e); + } + }); + +// --- simulate --- +program + .command("simulate") + .description("Dry-run: show what execute would do — commands, amounts, fees — no state changes") + .option("--max-sats ", "Max sBTC sats to deploy", String(DEFAULT_MAX_SATS)) + .action(async (opts) => { + try { + const maxSats = Math.min(parseInt(opts.maxSats) || DEFAULT_MAX_SATS, MAX_AUTONOMOUS_SATS); + + let oracle: OraclePrices; + let dlmm: DlmmData; + let xyk: XykReserves; + + try { + [oracle, dlmm] = await Promise.all([fetchOraclePrices(), fetchDlmmBins()]); + xyk = await fetchXykReserves(oracle); + } catch (e) { + printJson({ status: "PREFLIGHT_FAILED", error: e instanceof Error ? e.message : String(e) }); + return; + } + + const signal = analyzeSpread(oracle, xyk, dlmm); + const state = loadState(); + const now = Date.now(); + const lastExec = state.lastExecutionAt ? new Date(state.lastExecutionAt).getTime() : 0; + const cooldownRemaining = Math.max(0, COOLDOWN_MS - (now - lastExec)); + + let entryCommands: McpCommand[] = []; + let exitCommands: McpCommand[] = []; + let wouldExecute = false; + let skipReason = ""; + + if (state.openPosition) { + const heldMs = now - new Date(state.openPosition.entryTimestamp).getTime(); + const isTimeout = heldMs >= MAX_HOLD_MS; + const spreadReversed = !signal || !signal.profitable; + wouldExecute = isTimeout || spreadReversed; + if (wouldExecute) { + exitCommands = buildExitCommands(state.openPosition, dlmm.activeBinId, oracle); + } else { + skipReason = "Position open, spread still holding — no exit yet"; + } + } else { + if (!signal) { + skipReason = "DLMM_UNAVAILABLE — cannot evaluate spread"; + } else if (!signal.profitable) { + skipReason = `Net spread ${signal.netSpreadPct}% ≤ confidence buffer ${signal.confidenceBuffer}%`; + } else if (signal.grossSpreadPct < MIN_SPREAD_PCT) { + skipReason = `Gross spread ${signal.grossSpreadPct}% < ${MIN_SPREAD_PCT}% threshold`; + } else if (cooldownRemaining > 0) { + skipReason = `Cooling down — ${Math.ceil(cooldownRemaining / 60000)}m remaining`; + } else { + wouldExecute = true; + entryCommands = buildEntryCommands(oracle, dlmm.activeBinId, maxSats); + } + } + + printJson({ + network: NETWORK, + mode: "simulate", + pair: "sBTC/STX", + oracle: { btcUsd: oracle.btcUsd, stxUsd: oracle.stxUsd, stxPerBtc: oracle.stxPerBtc }, + xyk: { stxPerBtc: xyk.stxPerBtc }, + dlmm: { stxPerBtc: dlmm.stxPerBtc, activeBinId: dlmm.activeBinId, source: dlmm.source }, + spread: signal, + openPosition: state.openPosition, + wouldExecute, + skipReason: skipReason || null, + entryCommands: entryCommands.length > 0 ? entryCommands : undefined, + exitCommands: exitCommands.length > 0 ? exitCommands : undefined, + maxSats, + cooldownRemainingMs: cooldownRemaining, + timestamp: new Date().toISOString(), + }); + } catch (e) { + handleError(e); + } + }); + +// --- execute --- +program + .command("execute") + .description("Full pipeline. Requires --confirm to emit live MCP commands.") + .option("--confirm", "Authorize live MCP command emission (required)") + .option("--max-sats ", "Max sBTC sats to deploy", String(DEFAULT_MAX_SATS)) + .action(async (opts) => { + try { + const confirmed = !!opts.confirm; + const maxSats = Math.min(parseInt(opts.maxSats) || DEFAULT_MAX_SATS, MAX_AUTONOMOUS_SATS); + + // 1. CONFIRM GATE + if (!confirmed) { + printJson({ + status: "CONFIRM_REQUIRED", + message: "Add --confirm to authorize MCP command emission.", + network: NETWORK, + note: `Max spend: ${maxSats} sats (hard cap: ${MAX_AUTONOMOUS_SATS} sats)`, + timestamp: new Date().toISOString(), + }); + return; + } + + // 2. DOCTOR-FIRST PREFLIGHT + let oracle: OraclePrices; + let dlmm: DlmmData; + let xyk: XykReserves; + + try { + [oracle, dlmm] = await Promise.all([fetchOraclePrices(), fetchDlmmBins()]); + xyk = await fetchXykReserves(oracle); + } catch (e) { + printJson({ + status: "PREFLIGHT_FAILED", + error: e instanceof Error ? e.message : String(e), + network: NETWORK, + timestamp: new Date().toISOString(), + }); + process.exit(1); + return; + } + + if (dlmm.source === "unavailable") { + printJson({ + status: "PREFLIGHT_FAILED", + reason: "DLMM_UNAVAILABLE", + message: "HODLMM data required for execute. Run simulate or wait for Bitflow API.", + network: NETWORK, + timestamp: new Date().toISOString(), + }); + process.exit(1); + return; + } + + const state = loadState(); + const now = Date.now(); + state.lastRunAt = new Date().toISOString(); + + // 3. SPREAD ANALYSIS + const signal = analyzeSpread(oracle, xyk, dlmm); + + // 4. OPEN POSITION — exit path takes priority + if (state.openPosition) { + const heldMs = now - new Date(state.openPosition.entryTimestamp).getTime(); + const isTimeout = heldMs >= MAX_HOLD_MS; + const spreadReversed = !signal || !signal.profitable; + + if (isTimeout || spreadReversed) { + const cmds = buildExitCommands(state.openPosition, dlmm.activeBinId, oracle); + const exitReason = isTimeout ? "exit-timeout" : "exit"; + + // Stamp lastExecutionAt ONLY because cmds.length > 0 + state.lastExecutionAt = new Date().toISOString(); + state.history.push({ + timestamp: state.lastExecutionAt, + action: exitReason, + reason: isTimeout ? "2-hour max hold reached" : "spread reversed or unprofitable", + spreadPct: signal?.netSpreadPct ?? 0, + satsSent: state.openPosition.satsSent, + commands: cmds, + }); + state.openPosition = null; + saveState(state); + + printJson({ + status: "EXIT_COMMANDS_EMITTED", + network: NETWORK, + reason: exitReason, + commandCount: cmds.length, + commands: cmds, + timestamp: state.lastExecutionAt, + }); + return; + } + + saveState(state); + printJson({ + status: "HOLDING", + message: "LP position open. Spread still holds. Waiting for reversal or timeout.", + openPosition: state.openPosition, + spread: signal, + heldMinutes: Math.round(heldMs / 60000), + timeoutInMinutes: Math.ceil((MAX_HOLD_MS - heldMs) / 60000), + timestamp: new Date().toISOString(), + }); + return; + } + + // 5. ENTRY: evaluate spread + if (!signal || !signal.profitable) { + saveState(state); + printJson({ + status: "SKIPPED", + reason: !signal ? "DLMM_UNAVAILABLE" : "SPREAD_NOT_PROFITABLE", + spread: signal, + message: !signal ? "No DLMM data." : `Net spread ${signal.netSpreadPct}% ≤ confidence buffer ${signal.confidenceBuffer}%`, + timestamp: new Date().toISOString(), + }); + return; + } + + if (signal.grossSpreadPct < MIN_SPREAD_PCT) { + saveState(state); + printJson({ + status: "SKIPPED", + reason: "SPREAD_TOO_SMALL", + spread: signal, + message: `Gross spread ${signal.grossSpreadPct}% < ${MIN_SPREAD_PCT}% threshold`, + timestamp: new Date().toISOString(), + }); + return; + } + + // 6. COOLDOWN CHECK + const lastExec = state.lastExecutionAt ? new Date(state.lastExecutionAt).getTime() : 0; + const cooldownRemaining = Math.max(0, COOLDOWN_MS - (now - lastExec)); + if (cooldownRemaining > 0) { + saveState(state); + printJson({ + status: "SKIPPED", + reason: "COOLDOWN_ACTIVE", + cooldownRemainingMinutes: Math.ceil(cooldownRemaining / 60000), + timestamp: new Date().toISOString(), + }); + return; + } + + // 7. EMIT ENTRY COMMANDS + const satsCapped = Math.min(maxSats, MAX_AUTONOMOUS_SATS); + const cmds = buildEntryCommands(oracle, dlmm.activeBinId, satsCapped); + + // Stamp lastExecutionAt ONLY because cmds.length > 0 + state.lastExecutionAt = new Date().toISOString(); + state.openPosition = { + entryTimestamp: state.lastExecutionAt, + entrySpreadPct: signal.grossSpreadPct, + entryBinId: dlmm.activeBinId + 1, // LP deposited at activeBinOffset: +1 + satsSent: satsCapped, + estimatedEntryUsd: round((satsCapped / 1e8) * oracle.btcUsd, 2), + }; + state.history.push({ + timestamp: state.lastExecutionAt, + action: "entry", + reason: `Spread ${signal.grossSpreadPct}% gross / ${signal.netSpreadPct}% net profitable`, + spreadPct: signal.grossSpreadPct, + satsSent: satsCapped, + commands: cmds, + }); + saveState(state); + + printJson({ + status: "ENTRY_COMMANDS_EMITTED", + network: NETWORK, + spread: signal, + satsCapped, + maxAutonomousSats: MAX_AUTONOMOUS_SATS, + commandCount: cmds.length, + commands: cmds, + openPosition: state.openPosition, + timestamp: state.lastExecutionAt, + }); + } catch (e) { + handleError(e); + } + }); + +// --- watch --- +program + .command("watch") + .description("Continuous polling. Alerts when spread > threshold. Always read-only.") + .option("--interval ", "Scan interval in seconds", "60") + .option("--min-spread ", "Minimum spread % to trigger alert", "0.3") + .option("--max-scans ", "Max scans before exit", "60") + .action(async (opts) => { + try { + const interval = (parseInt(opts.interval) || 60) * 1000; + const minSpread = parseFloat(opts.minSpread) || MIN_SPREAD_PCT; + const maxScans = parseInt(opts.maxScans) || 60; + let scanCount = 0; + + printJson({ + status: "watching", + interval: `${opts.interval}s`, + minSpread: `${minSpread}%`, + maxScans, + startedAt: new Date().toISOString(), + }); + + while (scanCount < maxScans) { + scanCount++; + try { + const [oracle, dlmm] = await Promise.all([fetchOraclePrices(), fetchDlmmBins()]); + const xyk = await fetchXykReserves(oracle); + const signal = analyzeSpread(oracle, xyk, dlmm); + const state = loadState(); + + const hasAlert = signal && signal.grossSpreadPct >= minSpread; + if (hasAlert && signal) { + printJson({ + scan: scanCount, alert: true, network: NETWORK, pair: "sBTC/STX", + oracle: { btcUsd: oracle.btcUsd, stxUsd: oracle.stxUsd, stxPerBtc: oracle.stxPerBtc }, + xyk: { stxPerBtc: xyk.stxPerBtc }, + dlmm: { stxPerBtc: dlmm.stxPerBtc, activeBinId: dlmm.activeBinId }, + spread: signal, + openPosition: state.openPosition, + timestamp: new Date().toISOString(), + }); + } else { + printJson({ + scan: scanCount, alert: false, + xykStxPerBtc: xyk.stxPerBtc, + dlmmStxPerBtc: dlmm.source !== "unavailable" ? dlmm.stxPerBtc : "n/a", + oracleStxPerBtc: oracle.stxPerBtc, + grossSpreadPct: signal?.grossSpreadPct ?? null, + dlmmSource: dlmm.source, + openPosition: state.openPosition ? "yes" : "no", + timestamp: new Date().toISOString(), + }); + } + } catch (e) { + printJson({ scan: scanCount, error: e instanceof Error ? e.message : String(e) }); + } + + if (scanCount < maxScans) { + await new Promise((r) => setTimeout(r, interval)); + } + } + + printJson({ status: "complete", totalScans: scanCount }); + } catch (e) { + handleError(e); + } + }); + +program.parse(); From 33699f1a5b85e19b76f8396e500264789ec3c8c8 Mon Sep 17 00:00:00 2001 From: biwasbhandari Date: Tue, 21 Apr 2026 22:14:16 +0545 Subject: [PATCH 2/2] chore: fix CI validation failures - hodlmm-arb-executor: user-invocable false (CLAUDE.md rule) - contract-preflight: replace invalid tags with controlled vocabulary, drop unknown "network" dependency - stacking-delegation: user-invocable false, add missing AGENT.md frontmatter - regenerate skills.json Co-Authored-By: Claude Opus 4.7 (1M context) --- contract-preflight/SKILL.md | 4 +- hodlmm-arb-executor/SKILL.md | 2 +- skills.json | 72 +++++++++++++++++++++++++++++++++++- stacking-delegation/AGENT.md | 6 +++ stacking-delegation/SKILL.md | 2 +- 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/contract-preflight/SKILL.md b/contract-preflight/SKILL.md index 0f31b5c6..1bb23d3b 100644 --- a/contract-preflight/SKILL.md +++ b/contract-preflight/SKILL.md @@ -7,8 +7,8 @@ metadata: 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" + requires: "" + tags: "read-only, l2" --- # Contract Pre-Flight diff --git a/hodlmm-arb-executor/SKILL.md b/hodlmm-arb-executor/SKILL.md index eba22aab..865ee4b0 100644 --- a/hodlmm-arb-executor/SKILL.md +++ b/hodlmm-arb-executor/SKILL.md @@ -4,7 +4,7 @@ description: "Executes LP-based sBTC/STX arb on Bitflow HODLMM. Detects XYK vs D metadata: author: "ronkenx9" author-agent: "Parallel Owl (ERC-8004 ID #354, SP1KNKVXNNS9B6TBBT8YTM2VTYKVZYWS65TTRD430)" - user-invocable: "true" + user-invocable: "false" arguments: "doctor | simulate | execute | watch" entry: "hodlmm-arb-executor/hodlmm-arb-executor.ts" requires: "wallet" diff --git a/skills.json b/skills.json index 52cab9bb..0bb64170 100644 --- a/skills.json +++ b/skills.json @@ -1,6 +1,6 @@ { - "version": "0.38.1", - "generated": "2026-04-15T20:42:52.669Z", + "version": "0.40.0", + "generated": "2026-04-21T16:28:37.485Z", "skills": [ { "name": "agent-lookup", @@ -664,6 +664,25 @@ "call_read_only_function" ] }, + { + "name": "contract-preflight", + "description": "Dry-run Stacks contract calls against mainnet state before broadcasting — catches errors, prevents wasted gas", + "entry": "contract-preflight/contract-preflight.ts", + "arguments": [ + "doctor", + "run --action=simulate", + "run --action=batch", + "install-packs" + ], + "requires": [], + "tags": [ + "read-only", + "l2" + ], + "userInvocable": false, + "author": "secret-mars", + "authorAgent": "Secret Mars" + }, { "name": "credentials", "description": "Encrypted credential store — add, retrieve, list, and delete named secrets (API keys, tokens, passwords) stored AES-256-GCM encrypted at ~/.aibtc/credentials.json. Each write operation requires the master password; listing metadata does not.", @@ -854,6 +873,33 @@ "author": "cliqueengagements", "authorAgent": "Micro Basilisk (Agent 77) — SP219TWC8G12CSX5AB093127NC82KYQWEH8ADD1AY | bc1qzh2z92dlvccxq5w756qppzz8fymhgrt2dv8cf5" }, + { + "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.", + "entry": "hodlmm-arb-executor/hodlmm-arb-executor.ts", + "arguments": [ + "doctor", + "simulate", + "execute", + "watch" + ], + "requires": [ + "wallet" + ], + "tags": [ + "defi", + "write", + "mainnet-only", + "requires-funds", + "l2" + ], + "userInvocable": false, + "author": "ronkenx9", + "authorAgent": [ + "Parallel Owl (ERC-8004 ID #354", + "SP1KNKVXNNS9B6TBBT8YTM2VTYKVZYWS65TTRD430)" + ] + }, { "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.", @@ -1816,6 +1862,28 @@ "extend_stacking" ] }, + { + "name": "stacking-delegation", + "description": "Monitor STX stacking positions — status, PoX cycles, reward payouts, and delegation eligibility for autonomous agents.", + "entry": "stacking-delegation/stacking-delegation.ts", + "arguments": [ + "doctor", + "run status --stx-address ", + "run pox-info", + "run rewards --btc-address ", + "install-packs" + ], + "requires": [ + "settings" + ], + "tags": [ + "l2", + "read-only" + ], + "userInvocable": false, + "author": "secret-mars", + "authorAgent": "Secret Mars" + }, { "name": "stacking-lottery", "description": "Stacking lottery pots on stackspot.app — pool STX into pots that get stacked via PoX, VRF picks a random winner for sBTC rewards, and all participants get their STX back. Mainnet-only.", diff --git a/stacking-delegation/AGENT.md b/stacking-delegation/AGENT.md index 1993adf3..1903de12 100644 --- a/stacking-delegation/AGENT.md +++ b/stacking-delegation/AGENT.md @@ -1,3 +1,9 @@ +--- +name: stacking-delegation-agent +skill: stacking-delegation +description: "Agent behavior rules for the stacking-delegation skill." +--- + # Stacking Delegation — Agent Decision Guide ## When to Use diff --git a/stacking-delegation/SKILL.md b/stacking-delegation/SKILL.md index 3f593d23..54b2c40b 100644 --- a/stacking-delegation/SKILL.md +++ b/stacking-delegation/SKILL.md @@ -4,7 +4,7 @@ description: "Monitor STX stacking positions — status, PoX cycles, reward payo metadata: author: "secret-mars" author-agent: "Secret Mars" - user-invocable: "true" + user-invocable: "false" arguments: "doctor | run status --stx-address | run pox-info | run rewards --btc-address | install-packs" entry: "stacking-delegation/stacking-delegation.ts" requires: "settings"