Problem Statement
The recurring-deposit scheduler (src/jobs/recurringDeposits.ts) fires a fixed amount on a fixed calendar cadence. That is a serviceable DCA baseline, but it ignores everything the platform already knows: the user's portfolio value, current volatility, APY dispersion across protocols, and whether the market is in a drawdown. The result is that contributions land at the worst times (buying at peaks into a single protocol), and users have no visibility into what the plan will actually do before committing to it. This issue upgrades DCA from "cron that moves money" into an adaptive, portfolio-aware investment engine with transparent preview and honest failure semantics.
Current State
RecurringDepositPlan (prisma/schema.prisma): amount, assetSymbol, cadence (WEEKLY/BIWEEKLY/MONTHLY), nextRunAt, status (ACTIVE/PAUSED/CANCELLED), lastRunStatus. No volatility knobs, no allocation rules, no plan history, no pause-on-drawdown.
src/jobs/recurringDeposits.ts sweeps due plans and deposits a fixed amount; src/routes/recurring-deposits.ts + src/validators/recurring-deposit-validators.ts expose create/update/cancel with fixed-amount semantics only.
- The agent already has the pieces this needs:
ProtocolRate history (for volatility), src/agent/riskScoring.ts (for per-protocol risk), src/jobs/alertRules.ts's valueByInstant aggregation (for drawdown detection), and the deposit path that turns a contribution into a real on-chain deposit.
Proposed Solution
1. Volatility-aware contribution sizing (src/jobs/recurringDeposits.ts)
- A new contribution policy on the plan:
FIXED (current behavior, default, backward compatible) or ADAPTIVE.
- For
ADAPTIVE plans, the per-run amount is scaled from a baseline by a documented volatility regime computed from recent ProtocolRate history and/or the user's own portfolio series:
- Regime scaling (e.g. more units when the asset is cheap relative to its trailing range; fewer when it is expensive) — mean-reversion-inspired, but the policy must be explicitly stated, unit-tested, and bounded (configurable floor/ceiling as fractions of baseline so no run can exceed a user-approved range).
- Pause-on-drawdown: if the user's portfolio is in a drawdown beyond a configurable threshold (reusing the
POSITION_DRAWDOWN peak-tracking approach from src/jobs/alertRules.ts), the run is skipped and rescheduled (documented) rather than executed — or optionally doubled (documented, user-configurable) — never silently dropped.
- Every run, adaptive or fixed, records a run ledger row (new
RecurringDepositRun model): baseline amount, applied amount, regime snapshot (metrics used), result, and the reasoning for any deviation — so a user can see exactly what the engine did and why. AgentLog stays the audit trail for the deposit itself; the run ledger is the policy audit.
2. Multi-protocol allocation
- Extend plans with an optional allocation map (
{ protocol: weight% }) validated against the same rule set as User.strategyConfig.targetAllocations (weights sum to 100, protocols in the supported set). A single-protocol plan is just a single-entry map — no schema break.
- Each run splits the (possibly scaled) contribution across allocations, and each leg is a separately tracked deposit. All-or-nothing semantics must be explicit: define whether a failed leg rolls the whole run back (safe default) or partial-executes with a
PARTIAL run status that is visibly retried.
- Interaction with the agent: a contribution that lands while a
SavingsGoal is active must respect the goal's riskCeiling when allocating across protocols (the allocation map must pass the same applyRiskCeiling / fail-closed treatment as the strategy engine in src/agent/strategies.ts).
3. Plan preview & simulation
GET /api/v1/deposit/recurring/preview — a deterministic simulation of the next N runs under the plan's policy: projected contributions, regime assumptions, projected portfolio trajectory (using current ProtocolRate data and the same non-compounding APY convention as calculateApy so numbers are consistent with the rest of the product). Explicitly labeled simulation, not a guarantee.
- The preview must render the policy's inputs (regime math, drawdown state, allocation math) so the user can audit the adaptive logic before it touches money.
4. Scheduler hardening
- Catch-up policy (documented, configurable): a plan whose run was skipped (drawdown pause, provider outage, insufficient balance) — retry next window vs. skip-and-continue vs. accumulate. This must be an explicit, tested state machine, not an emergent bug.
- Cancellation semantics: cancelling a plan mid-window must not race an in-flight deposit (idempotency: a run already recorded must not double-execute; the deposit path already has idempotency anchors — reuse them, don't invent new ones).
- Backoff + alerting: repeated failures must emit an operational alert (deduplicated, via
alertingService) and eventually auto-pause the plan with a user-visible reason, mirroring the retriable-failure philosophy of src/jobs/referralPayout.ts and src/jobs/fiatReconciliation.ts.
5. API surface (src/routes/recurring-deposits.ts, src/validators/recurring-deposit-validators.ts, docs/openapi.yaml)
- Extend create/update with
policy: FIXED | ADAPTIVE, optional allocation map, regime bounds, pause-on-drawdown threshold, and catch-up mode. All new fields validated with the existing zod-validator style and rejected with 400 + named reason when inconsistent (e.g. adaptive policy without bounds).
GET /api/v1/deposit/recurring/:id/runs — run ledger, paginated.
GET /api/v1/deposit/recurring/preview — as above.
Edge Cases & Failure Modes
- Balance insufficient at run time: skip vs. partial vs. fail — explicit, tested, and reflected in
lastRunStatus + run ledger; no silent gaps.
- Protocol delisted / risk score collapses mid-plan: the allocation map fails closed (no contribution to a delisted/unrated protocol), the leg is retried or re-allocated per the documented policy, and the user is notified.
- Drawdown threshold crossed exactly at run time: deterministic ordering — drawdown state is sampled at run start, not mid-run.
- Catch-up accumulation exploding: a paused plan with accumulated runs must have a documented cap (e.g. max N accumulated) to avoid a single monster deposit.
- Adaptive policy with zero history: must fall back to
FIXED baseline with a visible flag, never guess from no data.
- Clock/DB timezone drift:
nextRunAt scheduling stays UTC-consistent with the existing job; DST-aware cadences documented.
Security & Privacy Considerations
- The run ledger and previews are owner-scoped (
enforceUserAccess), including parent/child sub-account semantics (actingAsUserId) consistent with src/routes/sub-accounts.ts.
- Adaptive policy math must not infer/leak data about other users; volatility is computed from public
ProtocolRate and the user's own series only.
- No new secrets: the engine reuses the existing deposit/agent-signed path; the job must never touch
WALLET_ENCRYPTION_KEY or user key material.
Out of Scope
- Recurring withdrawals (a scheduler for selling is a separate feature).
- ML price forecasting — the adaptive policy is rules-based, documented, and auditable.
- Changing the underlying deposit mechanism (smart-contract interaction stays as-is).
Suggested Implementation Plan
RecurringDepositPlan schema extensions + RecurringDepositRun model + migration.
- Pure policy module (
src/jobs/ or src/agent/) for regime scaling, drawdown state, and allocation — unit-tested with fixture rate/value series.
- Scheduler rework: run ledger, all-or-nothing legs, catch-up state machine, backoff/auto-pause.
- Preview simulation endpoint.
- Validators, routes,
docs/openapi.yaml, integration tests (including a full run cycle with a real deposit path mocked).
Acceptance Criteria
Problem Statement
The recurring-deposit scheduler (
src/jobs/recurringDeposits.ts) fires a fixed amount on a fixed calendar cadence. That is a serviceable DCA baseline, but it ignores everything the platform already knows: the user's portfolio value, current volatility, APY dispersion across protocols, and whether the market is in a drawdown. The result is that contributions land at the worst times (buying at peaks into a single protocol), and users have no visibility into what the plan will actually do before committing to it. This issue upgrades DCA from "cron that moves money" into an adaptive, portfolio-aware investment engine with transparent preview and honest failure semantics.Current State
RecurringDepositPlan(prisma/schema.prisma):amount,assetSymbol,cadence(WEEKLY/BIWEEKLY/MONTHLY),nextRunAt,status(ACTIVE/PAUSED/CANCELLED),lastRunStatus. No volatility knobs, no allocation rules, no plan history, no pause-on-drawdown.src/jobs/recurringDeposits.tssweeps due plans and deposits a fixedamount;src/routes/recurring-deposits.ts+src/validators/recurring-deposit-validators.tsexpose create/update/cancel with fixed-amount semantics only.ProtocolRatehistory (for volatility),src/agent/riskScoring.ts(for per-protocol risk),src/jobs/alertRules.ts'svalueByInstantaggregation (for drawdown detection), and the deposit path that turns a contribution into a real on-chain deposit.Proposed Solution
1. Volatility-aware contribution sizing (
src/jobs/recurringDeposits.ts)FIXED(current behavior, default, backward compatible) orADAPTIVE.ADAPTIVEplans, the per-run amount is scaled from a baseline by a documented volatility regime computed from recentProtocolRatehistory and/or the user's own portfolio series:POSITION_DRAWDOWNpeak-tracking approach fromsrc/jobs/alertRules.ts), the run is skipped and rescheduled (documented) rather than executed — or optionally doubled (documented, user-configurable) — never silently dropped.RecurringDepositRunmodel): baseline amount, applied amount, regime snapshot (metrics used), result, and the reasoning for any deviation — so a user can see exactly what the engine did and why.AgentLogstays the audit trail for the deposit itself; the run ledger is the policy audit.2. Multi-protocol allocation
{ protocol: weight% }) validated against the same rule set asUser.strategyConfig.targetAllocations(weights sum to 100, protocols in the supported set). A single-protocol plan is just a single-entry map — no schema break.PARTIALrun status that is visibly retried.SavingsGoalis active must respect the goal'sriskCeilingwhen allocating across protocols (the allocation map must pass the sameapplyRiskCeiling/ fail-closed treatment as the strategy engine insrc/agent/strategies.ts).3. Plan preview & simulation
GET /api/v1/deposit/recurring/preview— a deterministic simulation of the next N runs under the plan's policy: projected contributions, regime assumptions, projected portfolio trajectory (using currentProtocolRatedata and the same non-compounding APY convention ascalculateApyso numbers are consistent with the rest of the product). Explicitly labeled simulation, not a guarantee.4. Scheduler hardening
alertingService) and eventually auto-pause the plan with a user-visible reason, mirroring the retriable-failure philosophy ofsrc/jobs/referralPayout.tsandsrc/jobs/fiatReconciliation.ts.5. API surface (
src/routes/recurring-deposits.ts,src/validators/recurring-deposit-validators.ts,docs/openapi.yaml)policy: FIXED | ADAPTIVE, optional allocation map, regime bounds, pause-on-drawdown threshold, and catch-up mode. All new fields validated with the existing zod-validator style and rejected with 400 + named reason when inconsistent (e.g. adaptive policy without bounds).GET /api/v1/deposit/recurring/:id/runs— run ledger, paginated.GET /api/v1/deposit/recurring/preview— as above.Edge Cases & Failure Modes
lastRunStatus+ run ledger; no silent gaps.FIXEDbaseline with a visible flag, never guess from no data.nextRunAtscheduling stays UTC-consistent with the existing job; DST-aware cadences documented.Security & Privacy Considerations
enforceUserAccess), including parent/child sub-account semantics (actingAsUserId) consistent withsrc/routes/sub-accounts.ts.ProtocolRateand the user's own series only.WALLET_ENCRYPTION_KEYor user key material.Out of Scope
Suggested Implementation Plan
RecurringDepositPlanschema extensions +RecurringDepositRunmodel + migration.src/jobs/orsrc/agent/) for regime scaling, drawdown state, and allocation — unit-tested with fixture rate/value series.docs/openapi.yaml, integration tests (including a full run cycle with a real deposit path mocked).Acceptance Criteria
FIXEDplans behave byte-for-byte like today (backward-compat regression tests)ADAPTIVEpolicy scales contributions by a documented, bounded, unit-tested regime model; a no-history plan falls back to baseline with a visible flagSavingsGoalriskCeilingdocs/openapi.yamlupdated; unit + integration tests green