Skip to content

[AIBTC Skills Comp Day 29] hodlmm-dca — Recurring DCA into HODLMM DLMM Pools#12

Closed
sonic-mast wants to merge 5 commits into
mainfrom
skill/hodlmm-dca
Closed

[AIBTC Skills Comp Day 29] hodlmm-dca — Recurring DCA into HODLMM DLMM Pools#12
sonic-mast wants to merge 5 commits into
mainfrom
skill/hodlmm-dca

Conversation

@sonic-mast
Copy link
Copy Markdown
Owner

@sonic-mast sonic-mast commented Apr 24, 2026

Skill Submission

Skill name: hodlmm-dca
Category: Trading / LP
HODLMM integration? Yes — uses Bitflow HODLMM DLMM pool API (active bin, bin step, pool state) and outputs bitflow_hodlmm_add_liquidity MCP command for LP deployment

What it does

Recurring Dollar Cost Averaging directly into Bitflow HODLMM DLMM pools. Traditional DCA buys tokens; hodlmm-dca buys LP positions:

  1. setup — configure pool, STX per run, interval, bin spread
  2. run — check frequency gate; swap fixed STX via BitflowSDK at current active-bin price; output bitflow_hodlmm_add_liquidity command for LP step
  3. status / history — track cumulative positions, next run time
  4. cancel — stop the plan

On-chain proof

Executes swaps via BitflowSDK (@bitflowlabs/core-sdk) using PostConditionMode.Deny. Full execution requires --confirm + wallet unlock.

Why HODLMM-specific DCA beats token DCA

  • Earns LP fees from the first entry — no waiting to accumulate tokens before deploying
  • Active-bin aware: each swap is priced using live HODLMM pool state, not a generic oracle
  • No competing skill combines DCA frequency logic with HODLMM bin-aware LP accumulation

Registry compatibility checklist

  • SKILL.md uses metadata: nested frontmatter
  • AGENT.md starts with YAML frontmatter
  • tags/requires are comma-separated quoted strings
  • user-invocable is "false"
  • entry path is repo-root-relative
  • metadata.author is "sonic-mast"
  • All commands output JSON to stdout
  • --confirm required for all write operations
  • Commander.js for argument parsing

Review outcomes (5 fix rounds)

  • Fixed bin_price formula to DLMM standard: (1 + bin_step/10000)^active_bin
  • Fixed STX direction: resolveSwapTarget() handles both STX-as-tokenX and STX-as-tokenY pools
  • Fixed amount side: buildDepositCmd uses isTargetTokenX flag, not hardcoded amount_y
  • Fixed target decimals: uses swapTarget.targetDecimals for micro-unit conversion
  • Fixed Commander parseInt: explicit (v: string) => parseInt(v, 10) on all options
  • Devin latest round: 0 BUG_ findings
  • GitHub Actions validation: ✅ 0 errors, 0 warnings

Safety model

Limit Value
Max STX per run 500 STX (hardcoded)
Max total per plan 10,000 STX (hardcoded)
Min interval 1 hour (hardcoded)
Max slippage 5% (hardcoded, PostConditionMode.Deny)
Max bin spread ±5 bins (hardcoded)
--confirm gate Required — no silent writes

Built by Sonic Mast@sonic-mast

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the hodlmm-dca skill, which enables automated dollar-cost averaging into Bitflow HODLMM pools. The implementation includes a CLI for managing DCA plans, executing swaps, and generating LP deposit commands. Feedback focuses on correcting the bin_price calculation formula and addressing hardcoded assumptions that STX is always the input and token_y is always the output. These assumptions currently prevent the skill from working correctly with pools where STX is the second token in the pair, leading to incorrect decimal conversions and bin distributions. Additionally, a suggestion was made to use asynchronous imports for the Bitflow SDK to maintain consistency and avoid blocking the event loop.

Comment thread skills/hodlmm-dca/hodlmm-dca.ts Outdated
const binOffset = i - binSpread;
const binId = activeBin + binOffset;
// Token_y-side bins (for acquired token_y from swap)
return `{bin_id: ${binId}, amount_x: 0, amount_y: ${amountPerBin}}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This logic hardcodes the acquired liquidity into amount_y. If the swap resulted in token_x (which occurs if STX is the token_y in the pool pair), the liquidity should be added to amount_x instead. The function should be updated to dynamically assign the amount to the correct token side based on the swap result to ensure the generated MCP command is valid for all pool configurations.

Comment thread skills/hodlmm-dca/hodlmm-dca.ts Outdated
timestamp: new Date().toISOString(),
pool_id: plan.pool_id,
active_bin: pool.active_bin,
bin_price: String(pool.active_bin * pool.bin_step),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The calculation for bin_price as pool.active_bin * pool.bin_step is incorrect for a DLMM pool. The price in a DLMM bin is determined by the formula (1 + bin_step / 10000) ^ bin_id. Multiplying the bin ID by the step does not yield a valid price representation.

Suggested change
bin_price: String(pool.active_bin * pool.bin_step),
bin_price: Math.pow(1 + pool.bin_step / 10000, pool.active_bin).toString(),

Comment thread skills/hodlmm-dca/hodlmm-dca.ts Outdated
bin_price: String(pool.active_bin * pool.bin_step),
stx_amount: plan.stx_per_run,
token_in: "STX",
token_out: pool.token_y_symbol,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The code assumes that the DCA target is always token_y and the input is always STX. If a pool has STX as token_y (e.g., a USDA/STX pool), the skill will attempt to swap STX for STX, which will fail. The logic should dynamically determine which token in the pair is NOT STX and target that for the swap and subsequent LP deployment.

Comment on lines +741 to +743
const amountOutMicro = Math.floor(
swapResult.amountOut * Math.pow(10, pool.token_y_decimals)
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The code uses pool.token_y_decimals to convert the estimated output to micro-units. If the target token is actually token_x, this will use the wrong decimal count, leading to incorrect amounts in the generated MCP command. This should use the decimals of the token identified as the swap target.

Comment thread skills/hodlmm-dca/hodlmm-dca.ts Outdated
slippagePct: number;
dryRun: boolean;
}): Promise<{ txId: string; explorerUrl: string; amountOut: number }> {
const { BitflowSDK } = require("@bitflowlabs/core-sdk");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using require inside an async function is synchronous and blocks the event loop. Since this is a Bun environment and other dependencies are imported dynamically using await import, this should also use await import for consistency and to avoid blocking the execution thread.

Suggested change
const { BitflowSDK } = require("@bitflowlabs/core-sdk");
const { BitflowSDK } = await import("@bitflowlabs/core-sdk");

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread skills/hodlmm-dca/hodlmm-dca.ts
Comment on lines +753 to +758
plan.run_count += 1;
if (!isDryRun) plan.total_deployed += plan.stx_per_run;
plan.next_run_at = new Date(
Date.now() + plan.interval_hours * 3_600_000
).toISOString();
plan.consecutive_failures = 0;
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Dry-run mutates plan state: increments run_count, advances frequency gate, and persists to disk

When run is invoked without --confirm (dry-run mode), lines 809–816 unconditionally increment plan.run_count, advance plan.next_run_at by interval_hours, reset plan.consecutive_failures, save the plan, and append to history. Only total_deployed is guarded by if (!isDryRun) on line 810.

This means a dry-run preview will: (1) close the frequency gate, blocking a subsequent real run --confirm call for the full interval duration; (2) count toward max_runs, potentially marking the plan as completed before any real swap occurs; and (3) pollute history with dry-run entries that affect run_count tracking.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +663 to +664
let stxAddress = process.env.STX_ADDRESS ?? "";
let stxPrivateKey = "";
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Dry-run passes empty senderAddress to BitflowSDK prepareSwap

When --confirm is not provided, stxAddress defaults to process.env.STX_ADDRESS ?? "" at line 663 and stxPrivateKey stays "". Both are passed to executeSwap, which calls sdk.prepareSwap(swapExecutionData, opts.senderAddress, ...) at line 279-283 before the dry-run early return at line 285. If STX_ADDRESS is unset, the SDK receives an empty string as the sender address. Whether this causes a failure depends on the BitflowSDK implementation — it may or may not validate the address during prepareSwap. If it does fail, the error would be caught by the try/catch at line 774, incrementing consecutive_failures and potentially auto-pausing the plan. This is somewhat mitigated by the fact that the broader dry-run state mutation bug (BUG-0002) should be fixed first, but worth keeping in mind.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

brandonjamesmarshall and others added 2 commits April 24, 2026 21:34
…nce API shape, require→import

- bin_price: use DLMM formula (1 + bin_step/10000)^active_bin, not active_bin*bin_step
- resolveSwapTarget(): dynamically determine non-STX token (STX can be token_x or token_y)
- buildDepositCmd: pass isTargetTokenX flag so amount_x/amount_y are set on correct side
- fetchStxBalance: Hiro API returns flat {balance} not nested {stx:{balance}}; add fallback
- executeSwap: require() → await import() to avoid blocking event loop in async context
- fetchPools: switch to quotes API (active_bin field) with KNOWN_SYMBOLS contract lookup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread skills/hodlmm-dca/hodlmm-dca.ts
Comment thread skills/hodlmm-dca/hodlmm-dca.ts Outdated
Comment on lines +241 to +259
function resolveSwapTarget(pool: PoolMeta): {
targetSymbol: string;
targetDecimals: number;
isTargetTokenX: boolean; // true = acquired token goes into amount_x bins
} {
const isStxY = pool.token_y_symbol.toUpperCase() === "STX";
if (isStxY) {
return {
targetSymbol: pool.token_x_symbol,
targetDecimals: pool.token_x_decimals,
isTargetTokenX: true,
};
}
return {
targetSymbol: pool.token_y_symbol,
targetDecimals: pool.token_y_decimals,
isTargetTokenX: false,
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 resolveSwapTarget assumes STX is always one of the pool tokens

The resolveSwapTarget function at line 241 checks if token_y_symbol is "STX" and, if not, assumes STX must be token_x — returning token_y as the swap target. However, if a pool has neither token as STX (e.g., a sBTC/USDCx pool), the function silently returns token_y as the target with isTargetTokenX: false. The swap itself would still go through BitflowSDK (which routes STX → target via intermediate pools), but the LP deposit command would place tokens only on the Y side, which may not match the pool structure. In practice, this is unlikely to cause issues because the DCA skill description says it's for STX-based DCA, and all common HODLMM pools include STX. However, there's no explicit validation at setup time that the chosen pool actually contains STX as one of its tokens.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

const swapTarget = resolveSwapTarget(pool);

// DLMM bin price: (1 + bin_step / 10000) ^ active_bin
const binPrice = Math.pow(1 + pool.bin_step / 10000, pool.active_bin).toFixed(8);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Bin price formula may not match Bitflow HODLMM convention

Line 753 computes binPrice = Math.pow(1 + pool.bin_step / 10000, pool.active_bin). Standard DLMM implementations (e.g. Trader Joe) use a base offset for bin IDs (e.g., binId - 8388608). If Bitflow's HODLMM uses a similar offset convention, this formula would produce wildly incorrect prices for typical bin IDs in the hundreds. The bin_price value is stored in history entries and used in the deposit command context but doesn't gate any safety-critical decision. Since the actual swap uses BitflowSDK's quote engine (not this price), the financial risk is limited to displaying misleading price info in history. Without access to the Bitflow HODLMM specification, I can't confirm whether the formula is correct.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@sonic-mast
Copy link
Copy Markdown
Owner Author

Superseded by new Day 30 submission. Upstream PR BitflowFinance#544 remains open for judging.

@sonic-mast sonic-mast closed this Apr 27, 2026
@sonic-mast
Copy link
Copy Markdown
Owner Author

Superseded by new Day 30 submission. Upstream PR BitflowFinance#544 remains open for judging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants