Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dca/AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,5 @@ User: "DCA 100 STX into sBTC, 10 orders, daily"
- Confirm the on-chain result (tx hash)
- Update plan state file with execution log entry
- Report completion with summary: order number, amount swapped, avg entry price, remaining orders

- `DCA_FEE_USTX` (env, optional): swap fee in micro-STX. Validated whole number, > 0, <= 1000000. Default 50000.
4 changes: 4 additions & 0 deletions dca/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,7 @@ Three wallet sources (checked in order):
Winner of AIBTC x Bitflow Skills Pay the Bills competition.
Original author: @k9dreamermacmini-coder
Competition PR: https://github.com/BitflowFinance/bff-skills/pull/31

## Fee configuration (2026-07-15 field audit F-14)

The swap fee is `DCA_FEE_USTX` (env), validated (`^\d+$`, must be > 0, capped at 1,000,000 µSTX = 1 STX), default **50000 µSTX** — previously a hardcoded 5000 µSTX, which is 10–50× below peer skills and an underpriced-fee stuck-nonce risk. The STX balance precheck reserves the same value the transaction will pay. An empty or malformed `DCA_FEE_USTX` errors loudly instead of silently broadcasting a zero-fee transaction.
33 changes: 31 additions & 2 deletions dca/dca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,31 @@ const DCA_DIR = path.join(os.homedir(), ".aibtc", "dca");
const WALLETS_FILE = path.join(os.homedir(), ".aibtc", "wallets.json");
const WALLETS_DIR = path.join(os.homedir(), ".aibtc", "wallets");
const STACKS_API = "https://api.hiro.so";

// F-14: default swap fee in micro-STX. 5000 was 10–50x below every peer skill
// (deposit/withdraw 50k, zest/swap-aggregator 70k, limit-order 100k,
// move-liquidity 250k); 50000 is the modal value.
const DEFAULT_DCA_FEE_USTX = 50_000n;
// Sanity ceiling: 1 STX. A fat-fingered DCA_FEE_USTX should error, not pay it.
const MAX_DCA_FEE_USTX = 1_000_000n;

function resolveDcaFeeUstx(): bigint {
const raw = process.env.DCA_FEE_USTX;
if (raw === undefined || raw === "") return DEFAULT_DCA_FEE_USTX;
// Strict integer parse: BigInt("") is 0n (a silent ZERO-FEE mainnet tx —
// the exact stuck-nonce failure this fee exists to prevent) and
// BigInt("0.05") throws an unlabeled SyntaxError. Validate like the
// repo's parseNonNegativeBigInt convention instead.
if (!/^\d+$/.test(raw)) {
throw new Error(`DCA_FEE_USTX must be a whole number of micro-STX, got "${raw}"`);
}
const fee = BigInt(raw);
if (fee === 0n) throw new Error("DCA_FEE_USTX must be > 0 (a zero-fee tx strands the head nonce)");
if (fee > MAX_DCA_FEE_USTX) {
throw new Error(`DCA_FEE_USTX ${raw} exceeds the ${MAX_DCA_FEE_USTX} uSTX (1 STX) sanity cap`);
}
return fee;
}
const EXPLORER_BASE = "https://explorer.hiro.so/txid";

const FREQUENCIES: Record<string, number> = {
Expand Down Expand Up @@ -373,7 +398,11 @@ async function executeDirectSwap(opts: {
network,
senderKey: opts.stxPrivateKey,
anchorMode: AnchorMode.Any,
fee: 5000n,
// 2026-07-15 field audit F-14: an underpriced hardcoded fee is a
// stuck-head-nonce seed (one stuck tx stalls every later tx from the
// signer). Configurable via DCA_FEE_USTX (validated); default matches
// the repo's modal contract-call fee.
fee: resolveDcaFeeUstx(),
});

const broadcastRes = await broadcastTransaction({ transaction: tx, network });
Expand Down Expand Up @@ -860,7 +889,7 @@ async function cmdRun(
if (plan.tokenInSymbol.toUpperCase() === "STX") {
try {
const balAtomic = await getStxBalance(walletKeys.stxAddress);
const neededAtomic = Number(humanToAtomic(plan.orderSizeHuman, plan.tokenInDecimals)) + 5000; // +fee
const neededAtomic = Number(humanToAtomic(plan.orderSizeHuman, plan.tokenInDecimals)) + Number(resolveDcaFeeUstx()); // + the same fee the tx will pay
if (balAtomic < neededAtomic) {
const balHuman = atomicToHuman(balAtomic, 6);
fail(
Expand Down
15 changes: 13 additions & 2 deletions defi-portfolio-scanner/defi-portfolio-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ const SKILL_NAME = "defi-portfolio-scanner";
const REQUEST_TIMEOUT = 10_000; // 10 seconds per protocol
const HIRO_TIMEOUT = 15_000;

// Zest LTV liquidation-risk thresholds, as FRACTIONS in [0, 1] (display
// multiplies by 100). NOTE (2026-07-15 field audit F-13): no scanner code path
// currently populates ZestPosition.ltv with a number — both construction sites
// set null — so this risk block is DORMANT until the Zest reserve-data parser
// computes a real fractional LTV. The threshold scale below is corrected now so
// the flags work the moment ltv is populated; population is tracked separately
// (requires verifying the on-chain get-user-reserve-data value scale against a
// live position).
const ZEST_LTV_CRITICAL = 0.85;
const ZEST_LTV_WARNING = 0.70;

const ENDPOINTS = {
bitflowPools: "https://bff.bitflowapis.finance/api/app/v1/pools",
zestContract: {
Expand Down Expand Up @@ -731,14 +742,14 @@ function computeRiskScore(scanData: ScanData): {
// 3. Zest LTV risk
for (const pos of protocols.zest.positions) {
if (pos.ltv !== null) {
if (pos.ltv > 85) {
if (pos.ltv > ZEST_LTV_CRITICAL) {
score += 30;
factors.push({
factor: "zest-ltv-critical",
severity: "critical",
detail: `Zest position ${pos.asset} has LTV ${(pos.ltv * 100).toFixed(1)}% — liquidation risk imminent.`,
});
} else if (pos.ltv > 70) {
} else if (pos.ltv > ZEST_LTV_WARNING) {
score += 15;
factors.push({
factor: "zest-ltv-warning",
Expand Down
Loading