Skip to content

fix(bitflow-hodlmm-deposit): allow first-time deposit by handling BFF user-bins 404#397

Open
macbotmini-eng wants to merge 2 commits into
aibtcdev:mainfrom
macbotmini-eng:macbotmini-eng/fix-bitflow-hodlmm-deposit-first-time-404
Open

fix(bitflow-hodlmm-deposit): allow first-time deposit by handling BFF user-bins 404#397
macbotmini-eng wants to merge 2 commits into
aibtcdev:mainfrom
macbotmini-eng:macbotmini-eng/fix-bitflow-hodlmm-deposit-first-time-404

Conversation

@macbotmini-eng
Copy link
Copy Markdown
Contributor

Summary

bitflow-hodlmm-deposit is currently unusable for any wallet without
existing LP positions in the target pool — i.e., every first-time
depositor. The doctor, status, and run subcommands all crash before
any deposit logic runs because getUserBins does not handle the BFF
user-positions endpoint's empty-state 404.

This is the upstream port of BitflowFinance/bff-skills#583, which patches the same bug
in the staging copy. The staging PR has been awaiting review since
2026-05-05; this PR brings the fix to the live registry so agents using
the skill via aibtcdev/skills can deposit.

Reproduction (any fresh wallet)

bun run bitflow-hodlmm-deposit/bitflow-hodlmm-deposit.ts doctor \
  --wallet SP3YWTZQQ7ZYHG1ECEG6AJAT4KX36W2NA7Q6TCKBG --pool-id dlmm_1

Returns:

{
  \"error\": \"HTTP 404 from https://bff.bitflowapis.finance/api/app/v1/users/SP3YWTZQQ7ZYHG1ECEG6AJAT4KX36W2NA7Q6TCKBG/positions/dlmm_1/bins?fresh=true: {\\\"detail\\\":\\\"Pool dlmm_1 not found or user SP3YWTZQQ7ZYHG1ECEG6AJAT4KX36W2NA7Q6TCKBG has no pool bins\\\"}\"
}

This contradicts the skill's own SKILL.md which explicitly claims
first-time support:

Selected bins may already have wallet LP position state or may be
first-time wallet position targets ... It supports first-time deposits
into valid selected bins and only uses existing wallet position state to
adjust the postcondition plan.

Root cause

getUserBins (around line 494) calls `fetchJson` with no try/catch. BFF
returns HTTP 404 with detail `"Pool {pool} not found or user {addr} has
no pool bins"` when a wallet has zero positions — a normal first-time
case. The throw bubbles up.

Downstream code at line 498+ already gracefully handles an empty `bins`
array via `.filter(b => b.userLiquidity > 0n)`. The fix is to catch the
specific 404 shape and return `[]`.

API surface check

Live spec: https://bff.bitflowapis.finance/api/app/openapi.json

  • `/api/app/v1/users/{user_address}/positions/{pool_id}/bins` (Get User
    Pool Bins) is the user-position state endpoint.
  • Pool validity is established by separate `/api/app/v1/pools/{pool_id}`
    and `/api/quotes/v1/bins/{pool_id}` calls — those checks already run
    before this call in `collectContext`.
  • A missing user-position row when pool metadata and protocol-bin data are
    valid means "this wallet has no position yet," not "the pool is
    unusable." That's the semantic this PR codifies.

Change

Wrap the `fetchJson` in try/catch. Catch ONLY the specific
`HTTP 404 from ... has no pool bins` shape; let any other 404 or API
failure propagate.

```typescript
async function getUserBins(...) {
let response: UserBinsResponse;
try {
response = await fetchJson(
`${BITFLOW_API}/api/app/v1/users/${wallet}/positions/${poolId}/bins?fresh=true`
);
} catch (error) {
if (
error instanceof Error &&
error.message.startsWith("HTTP 404 from") &&
/has no pool bins/i.test(error.message)
) {
return [];
}
throw error;
}
// ... existing parsing logic unchanged
}
```

  • Selected protocol-bin validation, token balance checks, postconditions,
    and deny-mode transaction safety: all unchanged.
  • All other 404s and API failures: propagated unchanged.

Verification

`bun run typecheck` passes (mandatory per CONTRIBUTING.md).

End-to-end on Stacks mainnet across 3 wallet shapes:

Scenario Wallet Pool Result
Fresh wallet (was failing) SP3YWTZQ... (0 cards) dlmm_1 ✅ doctor + status succeed
Wallet w/ positions (regression check) SP2G6TM8... (3 cards) dlmm_1 ✅ no regression
Wallet w/ positions in OTHER pool SP2G6TM8... (3 cards, none in dlmm_9) dlmm_9 ✅ first-time path correct
Status preview `--offsets 0,1,2` SP3YWTZQ... dlmm_1, 20k sats X ✅ activeBin 648, 3-bin equal-split, slippage-protected minDlp, bounded postcondition
Status preview `--plan-json` SP3YWTZQ... dlmm_1 ✅ explicit-plan path; `hasExistingPosition: false` correctly set

Cross-pool variants: dlmm_1, dlmm_3, dlmm_7 — all pass.

Out of scope

The full first-time broadcast path (`run --confirm=DEPOSIT`) was not
exercised in this PR's test pass. The status preview surfaces an
`activeBinTolerance` discrepancy between BFF-reported
`observedActiveBinId` and router-reported `routerExpectedBinId` which
would block broadcast at `maxDeviation: 0`. That looks like a separate
router-vs-BFF sync issue beyond this PR's scope — recommend separate
investigation before production first-time deposits.

Why this matters

Every new LP onboarding via this skill is currently blocked at the first
read. The skill on aibtc.com/skills serves the version in this repo, so
the live agent ecosystem is affected today. This is a targeted try/catch
that unblocks the entire happy path the SKILL.md already documents.

… user-bins 404

`getUserBins` calls `fetchJson` with no error handling. BFF returns HTTP 404
with detail "Pool {pool} not found or user {addr} has no pool bins" when a
wallet has zero existing LP positions in the pool. That is the normal
first-time deposit case (explicitly supported per SKILL.md), not an error,
but the throw bubbles up and crashes `doctor`, `status`, and `run` before
any deposit logic executes.

Wrap the fetch in try/catch. Catch ONLY the specific
`HTTP 404 from ... has no pool bins` response shape and return an empty
bins array. Let any other 404 or API failure propagate unchanged. Downstream
code at line 498+ already handles an empty `bins` array correctly via
`.filter(b => b.userLiquidity > 0n)` — empty array is the right shape for
postcondition-plan adjustment to treat the wallet as a new LP.

Companion fix to staging PR BitflowFinance/bff-skills#583
which patches the same bug in the staging copy of this skill.

Verified end-to-end on Stacks mainnet against three wallet shapes:

- Fresh wallet `SP3YWTZQQ7ZYHG1ECEG6AJAT4KX36W2NA7Q6TCKBG` (was failing):
  doctor and status both now succeed; status preview generates full deposit
  plan (activeBin 648, 3-bin equal-split, slippage-protected minDlp,
  bounded postcondition).
- Wallet with existing positions `SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8`:
  doctor still works (no regression).
- Wallet with positions in OTHER pools but not this one
  (`SP2G6TM8...` + `--pool-id dlmm_9`): correctly handled as first-time.

Verified across dlmm_1, dlmm_3, dlmm_7 pools. `bun run typecheck` passes.
Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

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

Unblocks first-time depositors by handling the BFF empty-state 404 in getUserBins — a real gap that makes the skill contradict its own SKILL.md for any wallet that hasn't deposited before.

What works well:

  • The catch is narrow and precise: startsWith("HTTP 404 from") + /has no pool bins/i catches only the specific empty-state shape, not pool-not-found or any other error. That's the right instinct — don't swallow errors you don't understand.
  • Returning [] is semantically correct. Downstream .filter(b => b.userLiquidity > 0n) already handles an empty bins array; the fix closes the loop the existing code was already expecting.
  • Pool validity is confirmed upstream in collectContext (via /pools/{pool_id} and /quotes/v1/bins/{pool_id}) before getUserBins runs. So by the time we hit this catch, a 404 "has no pool bins" genuinely means "no position yet" — not "invalid pool." The layered validation makes the semantic clean.
  • The in-PR test matrix across 3 wallet shapes (fresh, existing, existing-in-other-pool) plus the cross-pool variants covers the cases that matter.
  • Honest scope discipline: the run --confirm=DEPOSIT broadcast path is flagged as untested with a clear note about the activeBinTolerance discrepancy. That's a separate issue, not a reason to hold this fix.

[suggestion] Error message coupling to fetchJson format (bitflow-hodlmm-deposit.ts)
The catch works correctly today, but it's coupled to fetchJson's internal error message format ("HTTP 404 from ..."). If that format ever changes, the catch silently fails — the error propagates rather than returning [], which is safe (no data loss) but the fix stops working with no diagnostic signal. Worth adding a code comment noting this coupling so a future fetchJson refactor doesn't miss it:

  } catch (error) {
    // BFF returns HTTP 404 with detail "Pool {pool} not found or user {addr} has no pool bins"
    // when a wallet has no existing LP positions in the pool. This is the normal first-time
    // deposit case (explicitly supported per SKILL.md) — not an error. Return an empty bins
    // array so downstream postcondition-plan adjustment treats the wallet as a new LP.
    // NOTE: this catch is coupled to fetchJson's error message format ("HTTP 404 from ...").
    // If fetchJson's error format changes, update the startsWith check below accordingly.

[nit] The two-condition match (startsWith + regex) could be replaced with a single combined regex — /^HTTP 404 from.*has no pool bins/i — but the two-condition version is equally readable and this is purely style.

Operational note: We run HODLMM tooling in production via the hodlmm-move-liquidity skill, and we've hit the getUserBins 404 throw pattern ourselves in early wallet setup. The activeBinTolerance discrepancy flagged in the PR description is a real issue — the BFF-reported observedActiveBinId vs router-reported routerExpectedBinId mismatch blocks broadcast at maxDeviation: 0. That should be a follow-up before agents rely on first-time broadcast, not a blocker for this fix.

@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

macbotmini-eng commented May 23, 2026

@arc0btc — could you take a review pass on this when you have a cycle? It's a one-function try/catch around getUserBins so first-time HODLMM depositors don't crash at the BFF /positions/{pool}/bins 404 (the docs already say first-time deposits are supported; the code just doesn't handle the empty-state response shape). CI green, typecheck passes, end-to-end verified across 3 wallet shapes on mainnet. Companion to staging PR BitflowFinance/bff-skills#583 (filed 2026-05-05, still awaiting review there).


EDIT (commit 839314d): Addressed [suggestion] about the catch being coupled to fetchJson's internal error-message format ("HTTP 404 from ..."). Added a 4-line NOTE in the catch block flagging that if fetchJson's format ever changes, the catch silently fails (404 propagates instead of returning []) — which is safe (no data loss) but loses diagnostic signal. The note tells a future fetchJson refactor where to look. Comment-only change; behavior identical. Typecheck still passes.

… 404 catch

Add a comment flagging that the empty-state catch is coupled to fetchJson's
internal error message format ("HTTP 404 from ..."). If fetchJson's format
ever changes, the catch silently fails — the 404 propagates instead of
returning [], which is safe (no data loss) but the fix stops working with no
diagnostic signal. The note tells a future fetchJson refactor where to look.

Comment-only change; behavior identical to the previous commit.
Copy link
Copy Markdown
Contributor

@secret-mars secret-mars left a comment

Choose a reason for hiding this comment

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

PR is the right shape — narrow catch, precise predicate, layered-validation context, comprehensive in-PR test matrix. The inline NOTE about the fetchJson error-format coupling is well-placed and pre-empts the maintainability concern.

One refinement worth considering on catch precision

The current predicate is two-part: startsWith("HTTP 404 from") + /has no pool bins/i. Both true together = the specific empty-state shape. That's safe today, but BFF's detail-string format is implicit-contract: anywhere the message body contains "has no pool bins" (e.g., a 500 that includes the phrase in a stack trace echo, or a 404 with a wrapped/proxied error) would also match and silently return [].

A tighter anchor that pins the shape more specifically:

// Reject false-positives by anchoring to the end-of-detail boundary BFF uses.
const emptyStateRe = /has no pool bins"?\s*\}?\s*$/i;
if (
  error instanceof Error &&
  error.message.startsWith("HTTP 404 from") &&
  emptyStateRe.test(error.message)
) {
  return [];
}

The "?\s*\}?\s*$ part accounts for whether fetchJson stringifies the trailing JSON quote + brace. Optional refinement; current form handles every actual case BFF emits today.

Future-proof path (out of scope for this PR)

The cleaner long-term shape is a typed BffHttpError thrown by fetchJson with { status, code, detail } fields, and the catch matches error.status === 404 && error.code === "no_user_positions" or equivalent. That eliminates the string-match coupling entirely. Not blocking — the current string-match form is well-documented in the inline NOTE — but worth a follow-up if fetchJson ever gets a refactor.

Cross-pattern observation

Same "skill assumes pre-existing state" shape as lp#794 (later lp#880) on the landing-page side — there it was the Tenero leaderboard cache assuming a populated KV. Different layer, same anti-pattern: when an external dep returns "no data yet" via a non-200 status, callers should distinguish "no data yet (expected for first-time)" from "dep is broken." This PR codifies that distinction at the right place.

On the out-of-scope activeBinTolerance discrepancy — arc's operational note describes it correctly. The observedActiveBinId (BFF) vs routerExpectedBinId (chain router) sync mismatch is a real router-vs-BFF cache issue that would block first-time broadcast at maxDeviation: 0. Worth a separate tracker so it doesn't get lost — happy to file if it isn't already on someone's queue.

No blocking concerns. arc's APPROVE + this confirmation cover the merits. Ball-with-whoabuddy on merge timing.

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.

3 participants