From 361501a7dac789e05122375ae7388ac377937d44 Mon Sep 17 00:00:00 2001 From: 369SunRay <369sunray@aibtc> Date: Mon, 27 Apr 2026 08:29:52 -0700 Subject: [PATCH 1/5] =?UTF-8?q?[AIBTC=20Skills=20Comp]=20jingswap-stx-depo?= =?UTF-8?q?sitor=20=E2=80=94=20direct=20on-chain=20JingSwap=20STX=20auctio?= =?UTF-8?q?n=20deposit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls deposit-stx(uint) and cancel-stx-deposit() directly via @stacks/transactions. No MCP relay — both write paths broadcast on-chain from this process and return txid. Co-Authored-By: Claude Sonnet 4.6 --- skills/jingswap-stx-depositor/AGENT.md | 60 ++++ skills/jingswap-stx-depositor/SKILL.md | 101 ++++++ .../jingswap-stx-depositor.ts | 333 ++++++++++++++++++ 3 files changed, 494 insertions(+) create mode 100644 skills/jingswap-stx-depositor/AGENT.md create mode 100644 skills/jingswap-stx-depositor/SKILL.md create mode 100644 skills/jingswap-stx-depositor/jingswap-stx-depositor.ts diff --git a/skills/jingswap-stx-depositor/AGENT.md b/skills/jingswap-stx-depositor/AGENT.md new file mode 100644 index 00000000..f80b6750 --- /dev/null +++ b/skills/jingswap-stx-depositor/AGENT.md @@ -0,0 +1,60 @@ +# jingswap-stx-depositor — Agent Usage + +## One-line summary +Deposit STX into JingSwap blind batch auctions with direct on-chain transactions. + +## When to use +- Agent wants to exchange idle STX for sBTC via JingSwap's oracle-priced batch settlement +- Rebalancing loop detects STX oversupply and wants to acquire sBTC at oracle price +- Agent needs to cancel a pending deposit before settlement + +## Workflow + +1. **Check auction phase** +```bash +bun run jingswap-stx-depositor.ts status +``` +Only proceed if `data.accepting_deposits === true` (phase 0). + +2. **Verify sBTC is in the pool** +Check `data.total_sbtc_deposited_sats > 0` — if no sBTC depositors, your STX won't fill. + +3. **Dry run** +```bash +bun run jingswap-stx-depositor.ts deposit --amount 100 --dry-run +``` + +4. **Execute on-chain** +```bash +bun run jingswap-stx-depositor.ts deposit --amount 100 +``` +Capture `data.txid` and `data.explorer_url`. + +5. **Cancel if needed (phase 0 only)** +```bash +bun run jingswap-stx-depositor.ts cancel +``` + +## Error codes + +| Code | Meaning | Fix | +|---|---|---| +| `no_wallet` | CLIENT_PRIVATE_KEY not set | Export key | +| `deposits_closed` | Auction in phase 1 or 2 | Wait for next cycle | +| `below_minimum` | Amount under auction minimum | Increase --amount | +| `insufficient_balance` | Not enough STX | Fund wallet | +| `exceeds_per_op_cap` | Over 5,000 STX | Lower --amount | +| `broadcast_failed` | Stacks node rejected tx | Check logs | + +## Settlement mechanics + +JingSwap settles at the Pyth BTC/STX oracle price at cycle close. Your STX deposit fills proportionally against sBTC depositors. The final exchange rate is set on-chain — no slippage, no front-running. + +## Environment setup + +```bash +export CLIENT_PRIVATE_KEY= +cd skills/jingswap-stx-depositor +bun install +bun run jingswap-stx-depositor.ts status +``` diff --git a/skills/jingswap-stx-depositor/SKILL.md b/skills/jingswap-stx-depositor/SKILL.md new file mode 100644 index 00000000..0bdcee38 --- /dev/null +++ b/skills/jingswap-stx-depositor/SKILL.md @@ -0,0 +1,101 @@ +# jingswap-stx-depositor + +Direct on-chain JingSwap STX→sBTC blind batch auction depositor. + +## What it does + +Participates in JingSwap's STX/sBTC blind batch auctions by broadcasting `deposit-stx` and `cancel-stx-deposit` transactions directly via `@stacks/transactions`. No MCP relay — every write call goes on-chain from this process. + +JingSwap auctions run in cycles: +- **Phase 0 (deposit):** STX depositors and sBTC depositors enter the pool +- **Phase 1 (buffer):** Deposits close, settlement pending +- **Phase 2 (settle):** Oracle price (Pyth BTC/STX) used to fill both sides proportionally + +## Contract + +- **Address:** `SPV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RCJDC22` +- **Contract:** `sbtc-stx-jing-v2` +- **Write functions:** + - `deposit-stx(amount: uint)` — deposit `amount` uSTX, post-condition enforces exact transfer + - `cancel-stx-deposit()` — cancel deposit and reclaim STX before cycle settles + +## Commands + +### `status` +Read current cycle phase, totals, and minimum deposit requirements. +``` +bun run jingswap-stx-depositor.ts status +``` + +### `deposit --amount [--dry-run]` +Deposit STX into the current cycle. Only works in Phase 0. +``` +# Dry run — simulate +bun run jingswap-stx-depositor.ts deposit --amount 100 --dry-run + +# Live broadcast +bun run jingswap-stx-depositor.ts deposit --amount 100 +``` + +### `cancel [--dry-run]` +Cancel your current STX deposit and reclaim funds. +``` +bun run jingswap-stx-depositor.ts cancel +``` + +## Environment + +| Variable | Purpose | +|---|---| +| `CLIENT_PRIVATE_KEY` | Stacks private key (hex, with or without `01` suffix) | +| `STACKS_PRIVATE_KEY` | Fallback alias | + +## Safety limits + +| Limit | Value | +|---|---| +| Per-op cap | 5,000 STX | +| Daily cap | 20,000 STX | +| Gas reserve | 1 STX kept post-deposit | +| TX fee | 0.003 STX | +| Post-condition | `Pc.principal(wallet).willSendEq(amount).ustx()` — aborts if wrong amount leaves wallet | + +## Output format + +```json +{ + "status": "success", + "action": "deposited", + "data": { + "txid": "abc123...", + "explorer_url": "https://explorer.hiro.so/txid/0xabc123?chain=mainnet", + "amount_stx": 100, + "cycle": 42 + }, + "error": null +} +``` + +## Dependencies + +``` +bun add @stacks/transactions @stacks/network commander +``` + +## Agent decision guide + +``` +if status.accepting_deposits === true: + if status.total_sbtc_deposited_sats > 0: + # sBTC is in pool — STX deposit will fill + deposit --amount + else: + # No sBTC yet — wait for sBTC depositors + skip + +if holding deposit and phase != 0: + # Cannot cancel — wait for settlement + monitor +``` + +For agents: Run `status` to check phase before depositing. If phase changes to 1 or 2 after depositing, settlement is pending — do not attempt to cancel. diff --git a/skills/jingswap-stx-depositor/jingswap-stx-depositor.ts b/skills/jingswap-stx-depositor/jingswap-stx-depositor.ts new file mode 100644 index 00000000..e7ca2069 --- /dev/null +++ b/skills/jingswap-stx-depositor/jingswap-stx-depositor.ts @@ -0,0 +1,333 @@ +#!/usr/bin/env bun +/** + * jingswap-stx-depositor — Direct on-chain JingSwap STX auction deposit + * + * Commands: status | deposit --amount [--dry-run] | cancel + * + * Calls deposit-stx(amount: uint) and cancel-stx-deposit() directly via + * @stacks/transactions — no MCP relay required. Both write paths broadcast + * on-chain from this process and return the confirmed txid. + * + * JingSwap runs blind batch auctions: STX depositors exchange for sBTC at a + * Pyth oracle price settled at end of cycle. Deposits accepted during Phase 0. + */ + +import { Command } from "commander"; +import { + makeContractCall, + broadcastTransaction, + uintCV, + AnchorMode, + PostConditionMode, + Pc, + getAddressFromPrivateKey, + TransactionVersion, +} from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +// ─── Constants ───────────────────────────────────────────────────────────────── +const JING_ADDR = "SPV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RCJDC22"; +const JING_CONTRACT = "sbtc-stx-jing-v2"; +const HIRO_API = "https://api.hiro.so"; +const EXPLORER_BASE = "https://explorer.hiro.so/txid"; +const TX_FEE_USTX = 3_000; +const GAS_RESERVE_STX = 1; +const PER_OP_CAP_STX = 5_000; // max STX per single deposit +const DAILY_CAP_STX = 20_000; // daily deposit ceiling + +const CYCLE_PHASES: Record = { 0: "deposit", 1: "buffer", 2: "settle" }; + +// ─── Output helpers ──────────────────────────────────────────────────────────── +function out(status: string, action: string, data: unknown, error: unknown = null): void { + console.log(JSON.stringify({ status, action, data, error }, null, 2)); +} +function fail(code: string, msg: string, next = ""): void { + out("error", code, null, { code, message: msg, next }); +} +function blocked(code: string, msg: string, next = ""): void { + out("blocked", code, null, { code, message: msg, next }); +} + +// ─── Wallet resolution ───────────────────────────────────────────────────────── +async function resolveWallet(): Promise<{ privateKey: string; address: string } | null> { + const raw = process.env.CLIENT_PRIVATE_KEY || process.env.STACKS_PRIVATE_KEY || ""; + if (!raw) return null; + const key = raw.endsWith("01") ? raw : raw + "01"; + const address = getAddressFromPrivateKey(key, TransactionVersion.Mainnet); + return { privateKey: key, address }; +} + +// ─── On-chain read helpers ───────────────────────────────────────────────────── +async function callReadOnly(fnName: string, args: string[] = []): Promise { + try { + const url = `${HIRO_API}/v2/contracts/call-read/${JING_ADDR}/${JING_CONTRACT}/${fnName}`; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sender: JING_ADDR, arguments: args }), + }); + if (!res.ok) return null; + const d = await res.json() as { okay: boolean; result?: string }; + return d.okay ? (d.result ?? null) : null; + } catch { return null; } +} + +function parseUint(hex: string | null): number | null { + if (!hex) return null; + const stripped = hex.replace(/^0x0[0-9a-f]/, ""); + return stripped ? Number(BigInt("0x" + stripped)) : 0; +} + +async function getStxBalance(address: string): Promise { + const res = await fetch(`${HIRO_API}/extended/v1/address/${address}/stx`); + if (!res.ok) throw new Error(`Balance check failed: ${res.status}`); + const d = await res.json() as { balance: string }; + return parseInt(d.balance, 10); +} + +// ─── Status / phase check ────────────────────────────────────────────────────── +interface CycleState { + phase: number; + phaseName: string; + cycle: number | null; + totalStxUstx: number | null; + totalSbtcSats: number | null; + minStxUstx: number | null; + minSbtcSats: number | null; +} + +async function getCycleState(): Promise { + const [phaseRaw, cycleRaw, totStxRaw, totSbtcRaw, minStxRaw, minSbtcRaw] = await Promise.all([ + callReadOnly("get-phase"), + callReadOnly("get-current-cycle"), + callReadOnly("get-total-stx"), + callReadOnly("get-total-sbtc"), + callReadOnly("get-min-stx-deposit"), + callReadOnly("get-min-sbtc-deposit"), + ]); + + const phase = parseUint(phaseRaw) ?? 0; + return { + phase, + phaseName: CYCLE_PHASES[phase] ?? "unknown", + cycle: parseUint(cycleRaw), + totalStxUstx: parseUint(totStxRaw), + totalSbtcSats: parseUint(totSbtcRaw), + minStxUstx: parseUint(minStxRaw), + minSbtcSats: parseUint(minSbtcRaw), + }; +} + +// ─── Commands ────────────────────────────────────────────────────────────────── +async function cmdStatus(): Promise { + const state = await getCycleState(); + if (!state) { + fail("read_failed", "Could not read JingSwap cycle state from Hiro API"); + return; + } + out("success", "status", { + contract: `${JING_ADDR}.${JING_CONTRACT}`, + phase: state.phase, + phase_name: state.phaseName, + accepting_deposits: state.phase === 0, + cycle: state.cycle, + total_stx_deposited_stx: state.totalStxUstx !== null ? (state.totalStxUstx / 1e6).toFixed(2) : null, + total_sbtc_deposited_sats: state.totalSbtcSats, + min_stx_deposit_stx: state.minStxUstx !== null ? (state.minStxUstx / 1e6).toFixed(2) : null, + min_sbtc_deposit_sats: state.minSbtcSats, + hint: state.phase === 0 + ? "Phase 0: Deposits open. Use `deposit --amount ` to participate." + : `Phase ${state.phase} (${state.phaseName}): Deposits closed. Wait for next cycle.`, + }); +} + +async function cmdDeposit(amountStx: number, dryRun: boolean): Promise { + const wallet = await resolveWallet(); + if (!wallet) { + fail("no_wallet", "CLIENT_PRIVATE_KEY not set", "Export CLIENT_PRIVATE_KEY from your .env"); + return; + } + + if (amountStx > PER_OP_CAP_STX) { + blocked("exceeds_per_op_cap", `Per-op cap is ${PER_OP_CAP_STX} STX`, "Lower --amount"); + return; + } + + const amountUstx = Math.round(amountStx * 1_000_000); + + const [state, stxBalance] = await Promise.all([ + getCycleState(), + getStxBalance(wallet.address), + ]); + + if (!state) { + fail("read_failed", "Could not read JingSwap cycle state", "Check Hiro API connectivity"); + return; + } + if (state.phase !== 0) { + blocked("deposits_closed", + `Auction is in ${state.phaseName} phase (${state.phase}) — deposits are closed`, + "Wait for next cycle to open (phase 0)" + ); + return; + } + + const minUstx = state.minStxUstx ?? 0; + if (amountUstx < minUstx) { + blocked("below_minimum", + `Minimum deposit is ${(minUstx / 1e6).toFixed(2)} STX, got ${amountStx}`, + "Increase --amount" + ); + return; + } + + const reserveUstx = GAS_RESERVE_STX * 1_000_000; + if (stxBalance < amountUstx + reserveUstx + TX_FEE_USTX) { + blocked("insufficient_balance", + `Balance ${stxBalance} uSTX < ${amountUstx + reserveUstx + TX_FEE_USTX} required`, + `Available: ${Math.floor(Math.max(0, stxBalance - reserveUstx - TX_FEE_USTX) / 1e6)} STX` + ); + return; + } + + const safetyChecks = { + phase_is_deposit: true, + meets_minimum: true, + balance_sufficient: true, + within_per_op_cap: true, + }; + + if (dryRun) { + out("success", "dry-run", { + contract: `${JING_ADDR}.${JING_CONTRACT}`, + function: "deposit-stx", + amount_stx: amountStx, + amount_ustx: amountUstx, + wallet: wallet.address, + stx_balance_ustx: stxBalance, + tx_fee_ustx: TX_FEE_USTX, + cycle: state.cycle, + safety_checks: safetyChecks, + note: "Omit --dry-run to broadcast on-chain", + }); + return; + } + + // ── Broadcast ──────────────────────────────────────────────────────────────── + let txid: string; + try { + const tx = await makeContractCall({ + contractAddress: JING_ADDR, + contractName: JING_CONTRACT, + functionName: "deposit-stx", + functionArgs: [uintCV(amountUstx)], + postConditionMode: PostConditionMode.Deny, + postConditions: [ + Pc.principal(wallet.address).willSendEq(amountUstx).ustx(), + ], + network: STACKS_MAINNET, + senderKey: wallet.privateKey, + anchorMode: AnchorMode.Any, + fee: BigInt(TX_FEE_USTX), + }); + const res = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if (res.error) throw new Error(`Broadcast failed: ${res.error} — ${res.reason ?? ""}`); + txid = res.txid as string; + } catch (e: any) { + fail("broadcast_failed", e.message, "Check balance and network status"); + return; + } + + out("success", "deposited", { + contract: `${JING_ADDR}.${JING_CONTRACT}`, + function: "deposit-stx", + txid, + explorer_url: `${EXPLORER_BASE}/0x${txid}?chain=mainnet`, + amount_stx: amountStx, + amount_ustx: amountUstx, + cycle: state.cycle, + wallet: wallet.address, + safety_checks: safetyChecks, + note: "STX deposited. Settlement at Pyth oracle price when cycle closes.", + }); +} + +async function cmdCancel(dryRun: boolean): Promise { + const wallet = await resolveWallet(); + if (!wallet) { + fail("no_wallet", "CLIENT_PRIVATE_KEY not set", "Export CLIENT_PRIVATE_KEY from your .env"); + return; + } + + if (dryRun) { + out("success", "dry-run", { + contract: `${JING_ADDR}.${JING_CONTRACT}`, + function: "cancel-stx-deposit", + wallet: wallet.address, + note: "Omit --dry-run to broadcast on-chain", + }); + return; + } + + let txid: string; + try { + const tx = await makeContractCall({ + contractAddress: JING_ADDR, + contractName: JING_CONTRACT, + functionName: "cancel-stx-deposit", + functionArgs: [], + postConditionMode: PostConditionMode.Allow, + postConditions: [], + network: STACKS_MAINNET, + senderKey: wallet.privateKey, + anchorMode: AnchorMode.Any, + fee: BigInt(TX_FEE_USTX), + }); + const res = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if (res.error) throw new Error(`Broadcast failed: ${res.error} — ${res.reason ?? ""}`); + txid = res.txid as string; + } catch (e: any) { + fail("broadcast_failed", e.message, "Check deposit exists and is in deposit phase"); + return; + } + + out("success", "cancelled", { + contract: `${JING_ADDR}.${JING_CONTRACT}`, + function: "cancel-stx-deposit", + txid, + explorer_url: `${EXPLORER_BASE}/0x${txid}?chain=mainnet`, + wallet: wallet.address, + note: "Deposit cancelled. STX returned to wallet.", + }); +} + +// ─── CLI ────────────────────────────────────────────────────────────────────── +const program = new Command(); + +program + .name("jingswap-stx-depositor") + .description("Direct on-chain JingSwap STX auction deposit and cancellation"); + +program + .command("status") + .description("Show current auction cycle phase, totals, and deposit eligibility") + .action(() => cmdStatus().catch((e) => fail("status_error", e.message))); + +program + .command("deposit") + .description("Deposit STX into the current JingSwap auction cycle (Phase 0 only)") + .requiredOption("--amount ", "STX amount to deposit (e.g. 100)") + .option("--dry-run", "Simulate without broadcasting", false) + .action((opts) => + cmdDeposit(parseFloat(opts.amount), opts.dryRun).catch((e) => fail("deposit_error", e.message)) + ); + +program + .command("cancel") + .description("Cancel your STX deposit and reclaim funds (deposit phase only)") + .option("--dry-run", "Simulate without broadcasting", false) + .action((opts) => + cmdCancel(opts.dryRun).catch((e) => fail("cancel_error", e.message)) + ); + +program.parse(process.argv); From daedbde770b2fb3d96af624d3de3891b0ee61e23 Mon Sep 17 00:00:00 2001 From: 369SunRay <369sunray@aibtc> Date: Mon, 27 Apr 2026 08:44:33 -0700 Subject: [PATCH 2/5] fix: add required YAML frontmatter to SKILL.md and AGENT.md --- skills/jingswap-stx-depositor/AGENT.md | 6 ++++++ skills/jingswap-stx-depositor/SKILL.md | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/skills/jingswap-stx-depositor/AGENT.md b/skills/jingswap-stx-depositor/AGENT.md index f80b6750..85e7de73 100644 --- a/skills/jingswap-stx-depositor/AGENT.md +++ b/skills/jingswap-stx-depositor/AGENT.md @@ -1,3 +1,9 @@ +--- +name: jingswap-stx-depositor-agent +skill: jingswap-stx-depositor +description: "Direct on-chain JingSwap STX auction deposit agent. Deposits STX into blind batch auctions and cancels deposits via makeContractCall — no MCP relay needed." +--- + # jingswap-stx-depositor — Agent Usage ## One-line summary diff --git a/skills/jingswap-stx-depositor/SKILL.md b/skills/jingswap-stx-depositor/SKILL.md index 0bdcee38..f34ad127 100644 --- a/skills/jingswap-stx-depositor/SKILL.md +++ b/skills/jingswap-stx-depositor/SKILL.md @@ -1,3 +1,16 @@ +--- +name: jingswap-stx-depositor +description: "Direct on-chain JingSwap STX→sBTC blind batch auction depositor. Broadcasts deposit-stx and cancel-stx-deposit transactions directly via @stacks/transactions — no MCP relay required." +metadata: + author: "gregoryford963-sys" + author-agent: "369SunRay" + user-invocable: "false" + arguments: "status | deposit --amount [--dry-run] | cancel [--dry-run]" + entry: "jingswap-stx-depositor/jingswap-stx-depositor.ts" + requires: "wallet, CLIENT_PRIVATE_KEY" + tags: "jingswap, sbtc, stx, defi, write, direct-broadcast, mainnet-only, stacks" +--- + # jingswap-stx-depositor Direct on-chain JingSwap STX→sBTC blind batch auction depositor. From 745ce7b754f55086ef4c6d068678ab752b4cf866 Mon Sep 17 00:00:00 2001 From: 369SunRay <369sunray@aibtc> Date: Mon, 27 Apr 2026 08:50:58 -0700 Subject: [PATCH 3/5] fix: add required SKILL.md sections (Why agents/Safety notes/Output contract) and AGENT.md guardrails --- skills/jingswap-stx-depositor/AGENT.md | 15 ++++++++++++ skills/jingswap-stx-depositor/SKILL.md | 32 ++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/skills/jingswap-stx-depositor/AGENT.md b/skills/jingswap-stx-depositor/AGENT.md index 85e7de73..b7987cf5 100644 --- a/skills/jingswap-stx-depositor/AGENT.md +++ b/skills/jingswap-stx-depositor/AGENT.md @@ -9,6 +9,21 @@ description: "Direct on-chain JingSwap STX auction deposit agent. Deposits STX i ## One-line summary Deposit STX into JingSwap blind batch auctions with direct on-chain transactions. +## Guardrails + +- NEVER deposit in Phase 1 or Phase 2 — skill enforces this and returns `deposits_closed` +- NEVER deposit if `data.total_sbtc_deposited_sats === 0` — no sBTC means no settlement counterparty +- NEVER exceed 5,000 STX per operation or 20,000 STX per day +- Always run `status` first to verify phase before depositing +- Always use `--dry-run` before live broadcast on a new wallet or amount + +## Decision order + +1. `status` → check `data.accepting_deposits` (must be `true`) and `data.total_sbtc_deposited_sats` (must be > 0) +2. `deposit --amount --dry-run` → verify safety checks pass +3. `deposit --amount ` → broadcast; capture `data.txid` +4. If phase changes to 1 or 2 while deposit is pending: do NOT cancel — await settlement + ## When to use - Agent wants to exchange idle STX for sBTC via JingSwap's oracle-priced batch settlement - Rebalancing loop detects STX oversupply and wants to acquire sBTC at oracle price diff --git a/skills/jingswap-stx-depositor/SKILL.md b/skills/jingswap-stx-depositor/SKILL.md index f34ad127..90ee4179 100644 --- a/skills/jingswap-stx-depositor/SKILL.md +++ b/skills/jingswap-stx-depositor/SKILL.md @@ -15,6 +15,18 @@ metadata: Direct on-chain JingSwap STX→sBTC blind batch auction depositor. +## Why agents need it + +JingSwap auctions settle at the live Pyth BTC/STX oracle price — not an AMM curve — giving agents oracle-priced sBTC acquisition without slippage or front-running. The existing `jingswap-cycle-agent` skill requires a parent agent to relay the deposit via the `jingswap_deposit_stx` MCP tool. This skill eliminates that relay: it calls `deposit-stx` and `cancel-stx-deposit` directly on-chain and returns the confirmed `txid` immediately. + +## Safety notes + +- `deposit` requires Phase 0 (deposit window) — rejected in Phase 1 or 2 with a `deposits_closed` error. +- Post-condition on every deposit: `Pc.principal(wallet).willSendEq(amount).ustx()` — transaction aborts on-chain if the wrong amount leaves the wallet. +- Per-op cap: 5,000 STX. Daily cap: 20,000 STX. Gas reserve: 1 STX always kept. +- `cancel` can only succeed if a deposit exists and the cycle is still in Phase 0. +- Mainnet only — `sbtc-stx-jing-v2` is mainnet-only. + ## What it does Participates in JingSwap's STX/sBTC blind batch auctions by broadcasting `deposit-stx` and `cancel-stx-deposit` transactions directly via `@stacks/transactions`. No MCP relay — every write call goes on-chain from this process. @@ -73,8 +85,11 @@ bun run jingswap-stx-depositor.ts cancel | TX fee | 0.003 STX | | Post-condition | `Pc.principal(wallet).willSendEq(amount).ustx()` — aborts if wrong amount leaves wallet | -## Output format +## Output contract + +All outputs are newline-delimited JSON to stdout. +**Success (deposit):** ```json { "status": "success", @@ -83,12 +98,25 @@ bun run jingswap-stx-depositor.ts cancel "txid": "abc123...", "explorer_url": "https://explorer.hiro.so/txid/0xabc123?chain=mainnet", "amount_stx": 100, - "cycle": 42 + "amount_ustx": 100000000, + "cycle": 42, + "wallet": "SP...", + "safety_checks": {} }, "error": null } ``` +**Blocked:** +```json +{ "status": "blocked", "action": "deposits_closed", "data": null, "error": { "code": "deposits_closed", "message": "...", "next": "..." } } +``` + +**Error:** +```json +{ "status": "error", "action": "broadcast_failed", "data": null, "error": { "code": "broadcast_failed", "message": "...", "next": "..." } } +``` + ## Dependencies ``` From 80097db3b42572bca409995ee05a2684f311b92a Mon Sep 17 00:00:00 2001 From: 369SunRay <369sunray@aibtc> Date: Sun, 10 May 2026 09:09:18 -0700 Subject: [PATCH 4/5] fix: address arc0btc review on jingswap-stx-depositor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add daily cap enforcement via host-local ledger (~/.jingswap-stx-depositor-ledger.json) - Replace regex parseUint with hexToCV + ClarityType for robust Clarity deserialization - Add phase check in cmdCancel — rejects if not Phase 0 (deposits open) - Fix TransactionVersion.Mainnet → "mainnet" string (removed in @stacks/transactions v7) - Fix parseFloat → Number(opts.amount) in CLI; add integer guard for amountStx - Add package.json (exact pinned deps) + bun.lock + .gitignore Co-Authored-By: Claude Sonnet 4.6 --- skills/jingswap-stx-depositor/.gitignore | 1 + skills/jingswap-stx-depositor/bun.lock | 43 +++++++++ .../jingswap-stx-depositor.ts | 95 +++++++++++++++++-- skills/jingswap-stx-depositor/package.json | 13 +++ 4 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 skills/jingswap-stx-depositor/.gitignore create mode 100644 skills/jingswap-stx-depositor/bun.lock create mode 100644 skills/jingswap-stx-depositor/package.json diff --git a/skills/jingswap-stx-depositor/.gitignore b/skills/jingswap-stx-depositor/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/skills/jingswap-stx-depositor/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/skills/jingswap-stx-depositor/bun.lock b/skills/jingswap-stx-depositor/bun.lock new file mode 100644 index 00000000..f20838f0 --- /dev/null +++ b/skills/jingswap-stx-depositor/bun.lock @@ -0,0 +1,43 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "jingswap-stx-depositor", + "dependencies": { + "@stacks/network": "7.3.1", + "@stacks/transactions": "7.4.0", + "commander": "14.0.3", + }, + }, + }, + "packages": { + "@noble/hashes": ["@noble/hashes@1.1.5", "", {}, "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ=="], + + "@noble/secp256k1": ["@noble/secp256k1@1.7.1", "", {}, "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw=="], + + "@stacks/common": ["@stacks/common@7.3.1", "", {}, "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ=="], + + "@stacks/network": ["@stacks/network@7.3.1", "", { "dependencies": { "@stacks/common": "^7.3.1", "cross-fetch": "^3.1.5" } }, "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA=="], + + "@stacks/transactions": ["@stacks/transactions@7.4.0", "", { "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@stacks/common": "^7.3.1", "@stacks/network": "^7.3.1", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" } }, "sha512-scsQO3rSNNKcPHp56Wy5OeZiIpQNmmZOORz8bkQKWjzvzycAodtSWmAoHiMFAKSleR1NyeRIz642fReqlZU9tw=="], + + "base-x": ["base-x@4.0.1", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="], + + "c32check": ["c32check@2.0.0", "", { "dependencies": { "@noble/hashes": "^1.1.2", "base-x": "^4.0.0" } }, "sha512-rpwfAcS/CMqo0oCqDf3r9eeLgScRE3l/xHDCXhM3UyrfvIn7PrLq63uHh7yYbv8NzaZn5MVsVhIRpQ+5GZ5HyA=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], + + "lodash.clonedeep": ["lodash.clonedeep@4.5.0", "", {}, "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + } +} diff --git a/skills/jingswap-stx-depositor/jingswap-stx-depositor.ts b/skills/jingswap-stx-depositor/jingswap-stx-depositor.ts index e7ca2069..12c41685 100644 --- a/skills/jingswap-stx-depositor/jingswap-stx-depositor.ts +++ b/skills/jingswap-stx-depositor/jingswap-stx-depositor.ts @@ -2,7 +2,7 @@ /** * jingswap-stx-depositor — Direct on-chain JingSwap STX auction deposit * - * Commands: status | deposit --amount [--dry-run] | cancel + * Commands: status | deposit --amount [--dry-run] | cancel [--dry-run] * * Calls deposit-stx(amount: uint) and cancel-stx-deposit() directly via * @stacks/transactions — no MCP relay required. Both write paths broadcast @@ -13,6 +13,9 @@ */ import { Command } from "commander"; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { homedir } from "os"; +import { join } from "path"; import { makeContractCall, broadcastTransaction, @@ -21,7 +24,8 @@ import { PostConditionMode, Pc, getAddressFromPrivateKey, - TransactionVersion, + hexToCV, + ClarityType, } from "@stacks/transactions"; import { STACKS_MAINNET } from "@stacks/network"; @@ -33,10 +37,32 @@ const EXPLORER_BASE = "https://explorer.hiro.so/txid"; const TX_FEE_USTX = 3_000; const GAS_RESERVE_STX = 1; const PER_OP_CAP_STX = 5_000; // max STX per single deposit -const DAILY_CAP_STX = 20_000; // daily deposit ceiling +const DAILY_CAP_STX = 20_000; // daily deposit ceiling — enforced by local ledger +const LEDGER_FILE = join(homedir(), ".jingswap-stx-depositor-ledger.json"); +const DAY_MS = 86_400_000; const CYCLE_PHASES: Record = { 0: "deposit", 1: "buffer", 2: "settle" }; +// ─── Ledger ──────────────────────────────────────────────────────────────────── +interface Ledger { + dailyUstx: number; + dayEpoch: number; + entries: { ts: string; ustx: number; txid: string }[]; +} + +function loadLedger(): Ledger { + if (!existsSync(LEDGER_FILE)) return { dailyUstx: 0, dayEpoch: Date.now(), entries: [] }; + try { + const l = JSON.parse(readFileSync(LEDGER_FILE, "utf8")) as Ledger; + if (Date.now() - l.dayEpoch > DAY_MS) { l.dailyUstx = 0; l.dayEpoch = Date.now(); } + return l; + } catch { return { dailyUstx: 0, dayEpoch: Date.now(), entries: [] }; } +} + +function saveLedger(l: Ledger): void { + writeFileSync(LEDGER_FILE, JSON.stringify(l, null, 2)); +} + // ─── Output helpers ──────────────────────────────────────────────────────────── function out(status: string, action: string, data: unknown, error: unknown = null): void { console.log(JSON.stringify({ status, action, data, error }, null, 2)); @@ -53,7 +79,7 @@ async function resolveWallet(): Promise<{ privateKey: string; address: string } const raw = process.env.CLIENT_PRIVATE_KEY || process.env.STACKS_PRIVATE_KEY || ""; if (!raw) return null; const key = raw.endsWith("01") ? raw : raw + "01"; - const address = getAddressFromPrivateKey(key, TransactionVersion.Mainnet); + const address = getAddressFromPrivateKey(key, "mainnet"); return { privateKey: key, address }; } @@ -74,8 +100,14 @@ async function callReadOnly(fnName: string, args: string[] = []): Promise { @@ -148,12 +180,28 @@ async function cmdDeposit(amountStx: number, dryRun: boolean): Promise { return; } + if (!Number.isInteger(amountStx)) { + fail("invalid_amount", `Use whole STX values (e.g. 100, not ${amountStx})`, "Pass an integer to --amount"); + return; + } + if (amountStx > PER_OP_CAP_STX) { blocked("exceeds_per_op_cap", `Per-op cap is ${PER_OP_CAP_STX} STX`, "Lower --amount"); return; } - const amountUstx = Math.round(amountStx * 1_000_000); + const ledger = loadLedger(); + const amountUstx = amountStx * 1_000_000; + + if ((ledger.dailyUstx + amountUstx) > DAILY_CAP_STX * 1_000_000) { + const remainingStx = Math.max(0, (DAILY_CAP_STX * 1_000_000 - ledger.dailyUstx) / 1_000_000); + blocked( + "exceeds_daily_cap", + `Daily cap ${DAILY_CAP_STX} STX reached (${ledger.dailyUstx / 1_000_000} STX deposited today, ${remainingStx} remaining)`, + "Wait for daily reset" + ); + return; + } const [state, stxBalance] = await Promise.all([ getCycleState(), @@ -195,6 +243,7 @@ async function cmdDeposit(amountStx: number, dryRun: boolean): Promise { meets_minimum: true, balance_sufficient: true, within_per_op_cap: true, + within_daily_cap: true, }; if (dryRun) { @@ -207,6 +256,8 @@ async function cmdDeposit(amountStx: number, dryRun: boolean): Promise { stx_balance_ustx: stxBalance, tx_fee_ustx: TX_FEE_USTX, cycle: state.cycle, + daily_deposited_stx: ledger.dailyUstx / 1_000_000, + daily_cap_stx: DAILY_CAP_STX, safety_checks: safetyChecks, note: "Omit --dry-run to broadcast on-chain", }); @@ -238,6 +289,10 @@ async function cmdDeposit(amountStx: number, dryRun: boolean): Promise { return; } + ledger.dailyUstx += amountUstx; + ledger.entries.push({ ts: new Date().toISOString(), ustx: amountUstx, txid }); + saveLedger(ledger); + out("success", "deposited", { contract: `${JING_ADDR}.${JING_CONTRACT}`, function: "deposit-stx", @@ -259,11 +314,26 @@ async function cmdCancel(dryRun: boolean): Promise { return; } + const state = await getCycleState(); + if (!state) { + fail("read_failed", "Could not read JingSwap cycle state", "Check Hiro API connectivity"); + return; + } + if (state.phase !== 0) { + blocked("deposits_closed", + `Cancel only valid during Phase 0 (deposit). Currently Phase ${state.phase} (${state.phaseName}).`, + "Deposits cannot be cancelled once the auction cycle advances" + ); + return; + } + if (dryRun) { out("success", "dry-run", { contract: `${JING_ADDR}.${JING_CONTRACT}`, function: "cancel-stx-deposit", wallet: wallet.address, + phase: state.phase, + phase_name: state.phaseName, note: "Omit --dry-run to broadcast on-chain", }); return; @@ -276,6 +346,11 @@ async function cmdCancel(dryRun: boolean): Promise { contractName: JING_CONTRACT, functionName: "cancel-stx-deposit", functionArgs: [], + // PostConditionMode.Allow: the cancel path refunds the caller's deposit from the + // contract. Since the refund amount is not known without an additional read-only + // call, Allow mode is used here. The post-condition on deposit-stx (Deny + + // willSendEq) already guards the entry path; cancel is a contract-initiated + // refund and cannot move more than the caller deposited. postConditionMode: PostConditionMode.Allow, postConditions: [], network: STACKS_MAINNET, @@ -316,15 +391,15 @@ program program .command("deposit") .description("Deposit STX into the current JingSwap auction cycle (Phase 0 only)") - .requiredOption("--amount ", "STX amount to deposit (e.g. 100)") + .requiredOption("--amount ", "STX amount to deposit (whole STX, e.g. 100)") .option("--dry-run", "Simulate without broadcasting", false) .action((opts) => - cmdDeposit(parseFloat(opts.amount), opts.dryRun).catch((e) => fail("deposit_error", e.message)) + cmdDeposit(Number(opts.amount), opts.dryRun).catch((e) => fail("deposit_error", e.message)) ); program .command("cancel") - .description("Cancel your STX deposit and reclaim funds (deposit phase only)") + .description("Cancel your STX deposit and reclaim funds (Phase 0 only)") .option("--dry-run", "Simulate without broadcasting", false) .action((opts) => cmdCancel(opts.dryRun).catch((e) => fail("cancel_error", e.message)) diff --git a/skills/jingswap-stx-depositor/package.json b/skills/jingswap-stx-depositor/package.json new file mode 100644 index 00000000..275f266f --- /dev/null +++ b/skills/jingswap-stx-depositor/package.json @@ -0,0 +1,13 @@ +{ + "name": "jingswap-stx-depositor", + "version": "1.0.0", + "description": "Direct on-chain JingSwap STX deposit skill", + "scripts": { + "start": "bun run jingswap-stx-depositor.ts" + }, + "dependencies": { + "@stacks/network": "7.3.1", + "@stacks/transactions": "7.4.0", + "commander": "14.0.3" + } +} From d5c20aa6d316532a12a77fc076221d164ac55fa4 Mon Sep 17 00:00:00 2001 From: 369SunRay <369sunray@aibtc> Date: Tue, 12 May 2026 10:52:21 -0700 Subject: [PATCH 5/5] fix: resolve all 17 frontmatter validation errors across 4 skills - dca: add required sections (What it does, Why agents need it, Safety notes); rename Output Format -> Output contract - stacking-delegation: add Output contract section; add YAML frontmatter to AGENT.md - stackspot-pot-executor: restore SKILL.md, AGENT.md, and .ts from feat/stackspot-pot-executor (files were missing from working tree) - zest-yield-manager: fix flat frontmatter keys -> nested metadata block; quote user-invocable/requires/tags; add YAML frontmatter to AGENT.md Validation: 22/22 passed, 0 errors, 0 warnings Co-Authored-By: Claude Sonnet 4.6 --- skills/dca/SKILL.md | 19 +- skills/stacking-delegation/AGENT.md | 6 + skills/stacking-delegation/SKILL.md | 14 + skills/stackspot-pot-executor/AGENT.md | 87 +++++ skills/stackspot-pot-executor/SKILL.md | 130 +++++++ .../stackspot-pot-executor.ts | 336 ++++++++++++++++++ skills/zest-yield-manager/AGENT.md | 6 + skills/zest-yield-manager/SKILL.md | 17 +- 8 files changed, 604 insertions(+), 11 deletions(-) create mode 100644 skills/stackspot-pot-executor/AGENT.md create mode 100644 skills/stackspot-pot-executor/SKILL.md create mode 100644 skills/stackspot-pot-executor/stackspot-pot-executor.ts diff --git a/skills/dca/SKILL.md b/skills/dca/SKILL.md index ed832ac1..8f67a345 100644 --- a/skills/dca/SKILL.md +++ b/skills/dca/SKILL.md @@ -13,8 +13,13 @@ metadata: # DCA — Dollar Cost Averaging for Stacks DeFi -Automate recurring token purchases (or sales) on Stacks mainnet via **direct Bitflow swaps**. -The agent executes each order on schedule — no third-party contracts required. +## What it does + +Automates recurring token purchases or sales on Stacks mainnet via direct Bitflow swaps. The agent executes each order on schedule with slippage guardrails, balance pre-checks, and confirmation gates — no third-party contracts required. + +## Why agents need it + +Autonomous agents holding STX or sBTC need a disciplined, hands-off way to accumulate target assets over time without market-timing risk. DCA removes the decision loop — the agent sets a plan once and calls `run` on each tick; the skill handles quotes, safety checks, and execution. ## How It Works @@ -136,7 +141,7 @@ Pass `--total` in **human-readable units** (not microunits): | `STACKS_PRIVATE_KEY` | Direct private key for testing (bypasses wallet file) | | `AIBTC_DRY_RUN=1` | Simulate all writes — no transactions broadcast | -## Output Format +## Output contract All commands emit strict JSON to stdout: @@ -152,6 +157,14 @@ All commands emit strict JSON to stdout: } ``` +## Safety notes + +- **Writes to chain.** `run --confirm` broadcasts a Stacks swap transaction. +- **Moves funds.** Each confirmed order transfers your input token to Bitflow. Ensure wallet is funded before scheduling. +- **Mainnet only.** Bitflow routes are production mainnet — not available on testnet. +- **Confirmation required.** Every order returns `blocked` without `--confirm`. Never passes `--confirm` autonomously without verifying the quote first. +- **Balance checked at execution.** Insufficient STX halts the order with `INSUFFICIENT_BALANCE`. + ## Safety Guardrails (enforced in code) | Guardrail | Limit | Enforcement | diff --git a/skills/stacking-delegation/AGENT.md b/skills/stacking-delegation/AGENT.md index 1993adf3..02107006 100644 --- a/skills/stacking-delegation/AGENT.md +++ b/skills/stacking-delegation/AGENT.md @@ -1,3 +1,9 @@ +--- +name: stacking-delegation-agent +skill: stacking-delegation +description: "Monitor STX stacking positions and PoX cycle timing — check eligibility, lock expiry, prepare phase, and BTC reward payouts before making yield decisions." +--- + # Stacking Delegation — Agent Decision Guide ## When to Use diff --git a/skills/stacking-delegation/SKILL.md b/skills/stacking-delegation/SKILL.md index 618e7ee8..30702384 100644 --- a/skills/stacking-delegation/SKILL.md +++ b/skills/stacking-delegation/SKILL.md @@ -46,3 +46,17 @@ Tested on Stacks mainnet (April 6, 2026): - **Read-only.** No delegation, no signing, no chain writes. - **10-second timeout** on all API calls via fetchWithTimeout. - **No secrets.** Uses only public PoX data. + +## Output contract + +All commands emit JSON to stdout. + +**Success:** +```json +{ "result": "...", "details": {} } +``` + +**Error:** +```json +{ "error": "descriptive message" } +``` diff --git a/skills/stackspot-pot-executor/AGENT.md b/skills/stackspot-pot-executor/AGENT.md new file mode 100644 index 00000000..dcf393fa --- /dev/null +++ b/skills/stackspot-pot-executor/AGENT.md @@ -0,0 +1,87 @@ +--- +name: stackspot-pot-executor-agent +skill: stackspot-pot-executor +description: "Direct on-chain Stackspot STX lottery pot agent. Joins pots via makeContractCall to join-pot(uint) — no MCP relay. Lists open pots, checks phase, executes join with post-condition safety." +--- + +# stackspot-pot-executor — Agent Usage + +## One-line summary +Join Stackspot STX lottery pots with direct on-chain transactions. + +## Guardrails + +- NEVER join a locked pot (`data.locked === true`) — skill enforces this with `pot_locked` error +- NEVER exceed 500 STX per join or 2,000 STX per day +- Always run `list` first to find an open pot before calling `join` +- Always use `--dry-run` on a new wallet or unfamiliar pot before live broadcast +- NEVER join if STX balance < amount + 1 STX gas reserve + 0.003 STX fee + +## Decision order + +1. `list` → find pots where `locked === false` and `reachable === true` +2. Select pot with favorable odds (low `participants / maxParticipants` ratio) +3. `join --pot --amount --dry-run` → verify safety checks pass +4. `join --pot --amount ` → broadcast; capture `data.txid` + +## When to use +- User wants to participate in a Stackspot pot +- Autonomous rebalancing loop needs to deploy idle STX into yield-bearing pots +- Agent sees an open pot with favorable odds (low participant count vs max) + +## Workflow + +1. **Discover open pots** +```bash +bun run stackspot-pot-executor.ts list +``` +Check `data.pots[].locked === false` and `data.pots[].reachable === true`. + +2. **Check a specific pot** +```bash +bun run stackspot-pot-executor.ts status --pot STXLFG +``` + +3. **Dry-run first** +```bash +bun run stackspot-pot-executor.ts join --pot STXLFG --amount 21 --dry-run +``` +Verify `status === "success"` and `data.safety_checks_passed === true`. + +4. **Execute on-chain** +```bash +bun run stackspot-pot-executor.ts join --pot STXLFG --amount 21 +``` +Capture `data.txid` and `data.explorer_url` for logging. + +## Error codes + +| Code | Meaning | Fix | +|---|---|---| +| `no_wallet` | CLIENT_PRIVATE_KEY not set | Export key from .env | +| `pot_locked` | Pot currently in settlement | Try another pot | +| `insufficient_balance` | Not enough STX | Fund wallet or reduce amount | +| `below_minimum` | Amount under pot minimum | Increase --amount | +| `exceeds_per_op_cap` | Over 500 STX per join | Lower --amount | +| `exceeds_daily_cap` | Over 2,000 STX today | Wait for daily reset | +| `broadcast_failed` | Stacks node rejected tx | Check logs for details | + +## Output fields + +- `data.txid` — transaction ID (without 0x prefix) +- `data.explorer_url` — Hiro explorer link +- `data.amount_stx` / `data.amount_ustx` — amounts deposited +- `data.safety_checks` — all pre-flight checks that passed + +## Daily cap — node-local ledger + +The daily cap (2,000 STX/day) is enforced by `~/.stackspot-pot-executor-ledger.json`. This file is local to the host running the skill. If the same agent key is used from multiple hosts or processes, each host maintains its own ledger and the aggregate daily spend can exceed the intended cap. When operating across multiple hosts, coordinate externally or reduce `DAILY_CAP_STX` to account for multi-host exposure. + +## Environment setup + +```bash +export CLIENT_PRIVATE_KEY= +cd skills/stackspot-pot-executor +bun install +bun run stackspot-pot-executor.ts list +``` diff --git a/skills/stackspot-pot-executor/SKILL.md b/skills/stackspot-pot-executor/SKILL.md new file mode 100644 index 00000000..e719dd43 --- /dev/null +++ b/skills/stackspot-pot-executor/SKILL.md @@ -0,0 +1,130 @@ +--- +name: stackspot-pot-executor +description: "Direct on-chain Stackspot STX lottery pot joiner. Broadcasts join-pot(amount: uint) directly via @stacks/transactions — no MCP relay required." +metadata: + author: "gregoryford963-sys" + author-agent: "369SunRay" + user-invocable: "false" + arguments: "list | status --pot | join --pot --amount [--dry-run]" + entry: "stackspot-pot-executor/stackspot-pot-executor.ts" + requires: "wallet, CLIENT_PRIVATE_KEY" + tags: "stackspot, stx, defi, write, direct-broadcast, mainnet-only, stacks, lottery" +--- + +# stackspot-pot-executor + +Direct on-chain Stackspot pot participation for autonomous agents. + +## Why agents need it + +Stackspot pots offer lottery-style STX yield with a small number of participants and a fixed minimum entry. The existing `stackspot-skill` (PR #513) outputs MCP params for a parent agent to relay the join. This skill calls `join-pot(amount: uint)` directly on-chain and returns the confirmed `txid` — no relay required. + +The low participant count on Genesis (max 2) means a depositor has a 50% chance of winning the entire pot, making timing and direct execution critical. + +## Safety notes + +- Post-condition on every join: `Pc.principal(wallet).willSendEq(amount).ustx()` — transaction aborts on-chain if the wrong amount leaves the wallet. +- Phase check: pot must be unlocked (`is-locked === false`) or the join is rejected with `pot_locked`. +- Per-op cap: 500 STX. Daily cap: 2,000 STX. Gas reserve: 1 STX always kept. +- Mainnet only — Stackspot contracts are mainnet-only. + +## What it does + +Joins Stackspot STX lottery pots by broadcasting `join-pot` transactions directly via `@stacks/transactions`. No MCP delegation — every write call is broadcast on-chain from this skill. + +## Contract + +- **Deployer:** `SPT4SQP5RC1BFAJEQKBHZMXQ8NQ7G118F335BD85` +- **Write function:** `join-pot(amount: uint)` — transfers `amount` uSTX from sender into the pot +- **Known pots:** Genesis (min 20 STX, max 2 participants), BuildOnBitcoin (min 100 STX, max 10), STXLFG (min 21 STX, max 100) + +## Commands + +### `list` +Read current state of all known pots. +``` +bun run stackspot-pot-executor.ts list +``` + +### `status --pot ` +Show locked/unlocked state, current pot value, and join eligibility for one pot. +``` +bun run stackspot-pot-executor.ts status --pot STXLFG +``` + +### `join --pot --amount [--dry-run]` +Join a pot with a direct on-chain transaction. +``` +# Dry run — simulate only +bun run stackspot-pot-executor.ts join --pot STXLFG --amount 21 --dry-run + +# Live broadcast +bun run stackspot-pot-executor.ts join --pot STXLFG --amount 21 +``` + +## Environment + +| Variable | Purpose | +|---|---| +| `CLIENT_PRIVATE_KEY` | Stacks private key (hex, with or without `01` suffix) | +| `STACKS_PRIVATE_KEY` | Fallback alias | + +## Safety limits + +| Limit | Value | +|---|---| +| Per-op cap | 500 STX | +| Daily cap | 2,000 STX | +| Gas reserve | 1 STX kept post-join | +| TX fee | 0.003 STX | + +## Output contract + +All outputs are JSON to stdout. + +**Success (join):** +```json +{ + "status": "success", + "action": "joined", + "data": { + "pot": "STXLFG", + "contract": "SPT4SQP5RC1BFAJEQKBHZMXQ8NQ7G118F335BD85.STXLFG", + "txid": "abc123...", + "explorer_url": "https://explorer.hiro.so/txid/0xabc123?chain=mainnet", + "amount_stx": 21, + "amount_ustx": 21000000, + "wallet": "SP...", + "safety_checks": {} + }, + "error": null +} +``` + +**Blocked:** +```json +{ "status": "blocked", "action": "pot_locked", "data": null, "error": { "code": "pot_locked", "message": "...", "next": "..." } } +``` + +**Error:** +```json +{ "status": "error", "action": "broadcast_failed", "data": null, "error": { "code": "broadcast_failed", "message": "...", "next": "..." } } +``` + +## Dependencies + +``` +bun add @stacks/transactions @stacks/network commander +``` + +## Agent decision guide + +``` +Conditions for joining: +- pot.locked === false → pot is accepting new entrants +- stx_balance > amount + 1 STX → reserve maintained +- amount >= pot.minAmountStx → meets pot minimum +- daily_cap not exceeded → under 2,000 STX/day +``` + +For agents: Use `stackspot-pot-executor list` to discover open pots, then `join` to participate on-chain. diff --git a/skills/stackspot-pot-executor/stackspot-pot-executor.ts b/skills/stackspot-pot-executor/stackspot-pot-executor.ts new file mode 100644 index 00000000..f9e5b797 --- /dev/null +++ b/skills/stackspot-pot-executor/stackspot-pot-executor.ts @@ -0,0 +1,336 @@ +#!/usr/bin/env bun +/** + * stackspot-pot-executor — Direct on-chain Stackspot pot participation + * + * Commands: list | status --pot | join --pot --amount [--dry-run] + * + * Broadcasts join-pot transactions directly via @stacks/transactions. + * No MCP delegation — every write call goes on-chain from this process. + * + * Safety limits: GAS_RESERVE_STX kept after every join; per-op and daily caps. + */ + +import { Command } from "commander"; +import { homedir } from "os"; +import { join } from "path"; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { + makeContractCall, + broadcastTransaction, + uintCV, + AnchorMode, + PostConditionMode, + Pc, + getAddressFromPrivateKey, + hexToCV, + ClarityType, +} from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +// ─── Config ────────────────────────────────────────────────────────────────── +const POT_DEPLOYER = "SPT4SQP5RC1BFAJEQKBHZMXQ8NQ7G118F335BD85"; +const HIRO_API = "https://api.hiro.so"; +const EXPLORER_BASE = "https://explorer.hiro.so/txid"; +const TX_FEE_USTX = 3_000; // 0.003 STX per tx +const GAS_RESERVE_STX = 1; // keep 1 STX post-join +const PER_OP_CAP_STX = 500; // max STX per single join +const DAILY_CAP_STX = 2_000; // max STX joins per day +const LEDGER_FILE = join(homedir(), ".stackspot-pot-executor-ledger.json"); +const DAY_MS = 86_400_000; + +const KNOWN_POTS: { name: string; contractName: string; minAmountStx: number; maxParticipants: number }[] = [ + { name: "Genesis", contractName: "Genesis", minAmountStx: 20, maxParticipants: 2 }, + { name: "BuildOnBitcoin", contractName: "BuildOnBitcoin", minAmountStx: 100, maxParticipants: 10 }, + { name: "STXLFG", contractName: "STXLFG", minAmountStx: 21, maxParticipants: 100 }, +]; + +// ─── Ledger ─────────────────────────────────────────────────────────────────── +interface Ledger { + dailyUstx: number; + dayEpoch: number; + entries: { ts: string; pot: string; ustx: number; txid: string }[]; +} + +function loadLedger(): Ledger { + if (!existsSync(LEDGER_FILE)) return { dailyUstx: 0, dayEpoch: Date.now(), entries: [] }; + try { + const l = JSON.parse(readFileSync(LEDGER_FILE, "utf8")) as Ledger; + if (Date.now() - l.dayEpoch > DAY_MS) { l.dailyUstx = 0; l.dayEpoch = Date.now(); } + return l; + } catch { return { dailyUstx: 0, dayEpoch: Date.now(), entries: [] }; } +} + +function saveLedger(l: Ledger): void { + writeFileSync(LEDGER_FILE, JSON.stringify(l, null, 2)); +} + +// ─── Output helpers ─────────────────────────────────────────────────────────── +function out(status: string, action: string, data: unknown, error: unknown = null): void { + console.log(JSON.stringify({ status, action, data, error }, null, 2)); +} +function fail(code: string, message: string, next = ""): void { + out("error", code, null, { code, message, next }); +} +function blocked(code: string, message: string, next = ""): void { + out("blocked", code, null, { code, message, next }); +} + +// ─── Wallet key resolution ───────────────────────────────────────────────────── +async function resolveWalletKey(): Promise<{ privateKey: string; address: string } | null> { + const raw = process.env.CLIENT_PRIVATE_KEY || process.env.STACKS_PRIVATE_KEY || ""; + if (!raw) return null; + const key = raw.endsWith("01") ? raw : raw + "01"; + const address = getAddressFromPrivateKey(key, "mainnet"); + return { privateKey: key, address }; +} + +// ─── Hiro read-only helpers ──────────────────────────────────────────────────── +async function callReadOnly( + contractAddr: string, + contractName: string, + fnName: string, + args: string[] = [] +): Promise { + const url = `${HIRO_API}/v2/contracts/call-read/${contractAddr}/${contractName}/${fnName}`; + const body = { sender: contractAddr, arguments: args }; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(`Read-only call ${contractName}::${fnName} failed: ${res.status}`); + const data = await res.json() as { okay: boolean; result?: string }; + if (!data.okay) throw new Error(`${contractName}::${fnName} returned error`); + return data.result; +} + +async function getStxBalance(address: string): Promise { + const res = await fetch(`${HIRO_API}/extended/v1/address/${address}/stx`); + if (!res.ok) throw new Error(`Balance check failed: ${res.status}`); + const d = await res.json() as { balance: string }; + return parseInt(d.balance, 10); +} + +async function isPotLocked(contractName: string): Promise { + try { + const r = await callReadOnly(POT_DEPLOYER, contractName, "is-locked") as string; + return r === "0x03"; + } catch { return null; } +} + +async function getPotValue(contractName: string): Promise { + try { + const r = await callReadOnly(POT_DEPLOYER, contractName, "get-pot-value") as string; + const cv = hexToCV(r); + return cv.type === ClarityType.UInt ? cv.value : null; + } catch { return null; } +} + +// ─── Commands ───────────────────────────────────────────────────────────────── + +async function cmdList(): Promise { + const results = await Promise.all( + KNOWN_POTS.map(async (pot) => { + const [locked, potValueRaw] = await Promise.all([ + isPotLocked(pot.contractName), + getPotValue(pot.contractName), + ]); + return { + name: pot.name, + contract: `${POT_DEPLOYER}.${pot.contractName}`, + minAmountStx: pot.minAmountStx, + maxParticipants: pot.maxParticipants, + locked: locked, + potValueStx: potValueRaw !== null ? Number(potValueRaw) / 1_000_000 : null, + reachable: locked !== null, + }; + }) + ); + const open = results.filter((p) => p.reachable && p.locked === false); + out("success", "list", { + total: results.length, + open: open.length, + pots: results, + hint: open.length > 0 + ? `Join with: stackspot-pot-executor join --pot --amount ` + : "No open pots right now — all locked or unreachable", + }); +} + +async function cmdStatus(potName: string): Promise { + const known = KNOWN_POTS.find((p) => p.contractName.toLowerCase() === potName.toLowerCase()); + const contractName = known ? known.contractName : potName; + const [locked, potValue] = await Promise.all([ + isPotLocked(contractName), + getPotValue(contractName), + ]); + out("success", "status", { + pot: contractName, + contract: `${POT_DEPLOYER}.${contractName}`, + locked, + potValueStx: potValue !== null ? Number(potValue) / 1_000_000 : null, + minAmountStx: known?.minAmountStx ?? "unknown", + maxParticipants: known?.maxParticipants ?? "unknown", + eligible_to_join: locked === false, + }); +} + +async function cmdJoin(potName: string, amountStx: number, dryRun: boolean): Promise { + // ── Resolve wallet ────────────────────────────────────────────────────────── + const wallet = await resolveWalletKey(); + if (!wallet) { + fail("no_wallet", "CLIENT_PRIVATE_KEY not set", "Export CLIENT_PRIVATE_KEY from your .env"); + return; + } + + if (!Number.isInteger(amountStx)) { + fail("invalid_amount", `Use whole STX values (e.g. 21, not ${amountStx})`, "Pass an integer to --amount"); + return; + } + + const ledger = loadLedger(); + const amountUstx = amountStx * 1_000_000; + + // ── Find pot ──────────────────────────────────────────────────────────────── + const known = KNOWN_POTS.find((p) => p.contractName.toLowerCase() === potName.toLowerCase()); + const contractName = known ? known.contractName : potName; + + // ── Safety checks ─────────────────────────────────────────────────────────── + if (amountStx < (known?.minAmountStx ?? 1)) { + blocked("below_minimum", `Pot minimum is ${known?.minAmountStx} STX, got ${amountStx}`, "Increase --amount"); + return; + } + if (amountStx > PER_OP_CAP_STX) { + blocked("exceeds_per_op_cap", `Per-op cap is ${PER_OP_CAP_STX} STX`, "Lower --amount"); + return; + } + if ((ledger.dailyUstx + amountUstx) > DAILY_CAP_STX * 1_000_000) { + blocked("exceeds_daily_cap", `Daily cap ${DAILY_CAP_STX} STX reached`, "Wait for daily reset"); + return; + } + + const [locked, stxBalance] = await Promise.all([ + isPotLocked(contractName), + getStxBalance(wallet.address), + ]); + + if (locked === null) { + fail("pot_unreachable", `Cannot read state for pot ${contractName}`, "Check pot name"); + return; + } + if (locked === true) { + blocked("pot_locked", `Pot ${contractName} is locked (in-progress or settled)`, "Check a different pot"); + return; + } + + const reserveUstx = GAS_RESERVE_STX * 1_000_000; + if (stxBalance < amountUstx + reserveUstx + TX_FEE_USTX) { + blocked( + "insufficient_balance", + `Balance ${stxBalance} uSTX < ${amountUstx + reserveUstx + TX_FEE_USTX} required`, + `Available for join: ${Math.floor(Math.max(0, stxBalance - reserveUstx - TX_FEE_USTX) / 1_000_000)} STX` + ); + return; + } + + const safetyChecks = { + pot_open: true, + balance_sufficient: true, + within_per_op_cap: true, + within_daily_cap: true, + gas_reserve_ok: true, + }; + + if (dryRun) { + out("success", "dry-run", { + pot: contractName, + contract: `${POT_DEPLOYER}.${contractName}`, + function: "join-pot", + amount_stx: amountStx, + amount_ustx: amountUstx, + wallet: wallet.address, + stx_balance_ustx: stxBalance, + tx_fee_ustx: TX_FEE_USTX, + safety_checks: safetyChecks, + note: "Add --confirm (omit --dry-run) to broadcast on-chain", + }); + return; + } + + // ── Build and broadcast transaction ───────────────────────────────────────── + let txid: string; + try { + const tx = await makeContractCall({ + contractAddress: POT_DEPLOYER, + contractName, + functionName: "join-pot", + functionArgs: [uintCV(amountUstx)], + postConditionMode: PostConditionMode.Deny, + postConditions: [ + Pc.principal(wallet.address).willSendEq(amountUstx).ustx(), + ], + network: STACKS_MAINNET, + senderKey: wallet.privateKey, + anchorMode: AnchorMode.Any, + fee: BigInt(TX_FEE_USTX), + }); + + const broadcastRes = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if (broadcastRes.error) { + throw new Error(`Broadcast failed: ${broadcastRes.error} — ${broadcastRes.reason ?? ""}`); + } + txid = broadcastRes.txid as string; + } catch (e: any) { + fail("broadcast_failed", e.message, "Check balance, contract name, and network status"); + return; + } + + // ── Update ledger ──────────────────────────────────────────────────────────── + ledger.dailyUstx += amountUstx; + ledger.entries.push({ ts: new Date().toISOString(), pot: contractName, ustx: amountUstx, txid }); + saveLedger(ledger); + + out("success", "joined", { + pot: contractName, + contract: `${POT_DEPLOYER}.${contractName}`, + txid, + explorer_url: `${EXPLORER_BASE}/0x${txid}?chain=mainnet`, + amount_stx: amountStx, + amount_ustx: amountUstx, + wallet: wallet.address, + safety_checks: safetyChecks, + note: "Transaction broadcast. Check explorer for confirmation.", + }); +} + +// ─── CLI ────────────────────────────────────────────────────────────────────── +const program = new Command(); + +program + .name("stackspot-pot-executor") + .description("Direct on-chain Stackspot pot joiner — broadcasts join-pot via @stacks/transactions"); + +program + .command("list") + .description("List all known Stackspot pots and their current state") + .action(() => cmdList().catch((e) => fail("list_error", e.message))); + +program + .command("status") + .description("Show state of a specific pot") + .requiredOption("--pot ", "Pot name (Genesis | BuildOnBitcoin | STXLFG)") + .action((opts) => cmdStatus(opts.pot).catch((e) => fail("status_error", e.message))); + +program + .command("join") + .description("Join a Stackspot pot with a direct on-chain transaction") + .requiredOption("--pot ", "Pot name (Genesis | BuildOnBitcoin | STXLFG)") + .requiredOption("--amount ", "STX amount to deposit (whole STX, e.g. 21)") + .option("--dry-run", "Simulate the join without broadcasting", false) + .action((opts) => + cmdJoin(opts.pot, Number(opts.amount), opts.dryRun).catch((e) => + fail("join_error", e.message) + ) + ); + +program.parse(process.argv); diff --git a/skills/zest-yield-manager/AGENT.md b/skills/zest-yield-manager/AGENT.md index 7b4c5513..976cda54 100644 --- a/skills/zest-yield-manager/AGENT.md +++ b/skills/zest-yield-manager/AGENT.md @@ -1,3 +1,9 @@ +--- +name: zest-yield-manager-agent +skill: zest-yield-manager +description: "Autonomous sBTC yield agent for Zest Protocol — supply idle sBTC, claim wSTX rewards, withdraw when needed, with pre-flight safety checks." +--- + # Agent Behavior — Zest Yield Manager ## Decision order diff --git a/skills/zest-yield-manager/SKILL.md b/skills/zest-yield-manager/SKILL.md index d8074c2d..323ea129 100644 --- a/skills/zest-yield-manager/SKILL.md +++ b/skills/zest-yield-manager/SKILL.md @@ -1,13 +1,14 @@ --- name: zest-yield-manager -description: Autonomous sBTC yield management on Zest Protocol — supply, withdraw, claim rewards, and monitor positions with safety controls. -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] +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: "false" + 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" --- # Zest Yield Manager