HARVEST — as built (2026-09-05)
The HARVEST target spec, the accumulation desk and the monitoring pages are the design documents HARVEST was built from. Where the build diverged from them, this page wins. It is regenerated from the code, not from the plans; the last audit against the code was 2026-09-05 (its status register is at audits → 2026-09-05). Live capital, ladder triggers and LAN endpoints are owner inputs and stay in the internal module spec.
Running revisions at 2026-09-06 ≈ 10:1xZ (the tags in nexus-gitops
environments/prod/values.yaml at gitops 91232c7): nexus-platform
63a2de3 (core / worker / telegram — P&L correctness, window
coverage, lending reconciliation, the earning block, LP as legs),
nexus-solana ea96739 (exec + oracle — sleeve reconciler, rent
attribution, one send path, re-armable intents, position cache;
§10), nexus-ui ca1d75e (LP positions, Treasury
truthfulness pass, 25 s exec GET proxy, two crash fixes and the
canonical-host redirect — all deployed 2026-09-06 10:19 UTC).
Earlier the same week — running revisions at 2026-09-05 ≈ 22:05Z: nexus-platform
fdd7912 (core/worker/telegram, rolled 21:54Z — confirmed lending
hygiene; on the way there ac38aa1 17:12Z USDT NAV, 7e1adcb 17:50Z
portfolio budget gate in log mode, 7d6ed59 ≈ 20:3xZ P&L by source),
nexus-solana a74cc87 (exec, live-verified 17:46Z — outbox phase 2 +
cost capture, EXEC_OUTBOX still 0; caller auth enforced since
16:27Z via nexus-gitops 29c983a), nexus-ui f8327f1, nexus-gitops
08ba526 (latest; 05c3ffd Nexus Treasury dashboard + nexus.financial
rules, 441291a / 078055c / d848adf / a5912c7 worker Recreate,
POD_NAME, worker.budget, dashboard label, b5b6079 P&L row,
d5318ad / 3f90adb hygiene knobs + prompt). The owner's independent
re-verification of 19:29–19:34Z and its residual checks are in the
status register.
1. What runs today
HARVEST is a live, armed, self-custodial treasury desk on Solana. Every 30 minutes an analyst panel reads the market; every 6 hours a decision officer turns that into a concrete plan; every 30 minutes an executor sweep carries the plan out on-chain within mechanical caps, keeps capital working, and checks that the cycles themselves are alive.
┌─────────────── nexus-accum-host (meerkat mob, 8 souls) ───────────────┐
60 min accum-cycle 5 parallel analysts ─► thesis officer brief (digest + machine block)
2 h accum-dream dream judge scores briefs/forecasts, distils lessons, flags idle capital
6 h accum-decision decision officer: posture, orders, treasury, reserves, LP, invalidation
└──────────────────────────────────────────────────────────────────────────┘
│ decision JSON (Mongo operator_digests)
▼
30 min accum-execute (nexus-worker) hygiene sweep ─► execute the latest unexecuted decision
│
▼
nexus-solana-exec (:8091, EXEC key) simulate on our node ─► sign ─► send
nexus-solana-oracle (:8090, no keys) Pyth + Metis + Coinbase, divergence guard
daily accum-fallback (nexus-worker) §5 "bottom never came" evaluation, breakout tranches,
DCA program runner, weekly digest
Realms and repos: nexus-solana (chain plane, two binaries),
nexus-platform (worker jobs, mob host, core API, Telegram), nexus-gitops
(mob prompts + souls, deployment), nexus-ui (operator pages).
2. Custody — as built
| Address role | Holds | Who signs | |
|---|---|---|---|
| VAULT | Squads multisig (1-of-2, both signers are the owner's hardware wallets) | cold destination for incoming capital and filled coins; today only dust | owner, Squads UI + Ledger |
| EXEC | hot wallet, the only key any pod mounts (nexus-solana-exec) | the working treasury: USDC float, Kamino receipts, JitoSOL, LP positions, Jupiter Trigger escrows | the exec plane, when armed |
The design spec's "EXEC holds at most one tranche" model was superseded on 2026-08-29 by an automation-first custody decision: working capital lives on EXEC so lending, staking, orders and LP can be automated; VAULT is the cold destination. Consequences the reader must not miss:
- Every automated position is EXEC-owned. Armed sends refuse any payer or authority other than the EXEC key.
- There is no Squads spending limit yet and therefore no VAULT→EXEC
automation, no on-chain destination whitelist, and no EXEC→VAULT sweep.
VAULT refills are owner-signed Sends; the exec plane can build Squads
ceremonies (
build-ceremony,build-vault-lending,build-vault-stake) and relay owner-signed transactions (submit-signed) but never signs for the vault. - The hard operating rule, enforced by the operator and every assistant, is that funds move only between VAULT and EXEC; any other destination is refused (see the custody rule in the project instructions).
Arming is two-valued and lives in gitops: mode: live and arm: true.
Unarmed, every endpoint simulates and returns the unsigned transaction.
3. The cycles
| Job | Cadence | Model(s) | Output |
|---|---|---|---|
accum-cycle | 60 min, at :00 | 4× Sonnet analysts (structure, flow, venue, narrative) + Codex challenger → Opus thesis officer | accum digest + accum_thesis machine block (phase, forecasts 1d/1w/1m, target probability, stance, ladder alert, swing zones, scenarios, standing issues) |
accum-dream | 2 h, at :20 | Sonnet dream judge | accum_dream (+_json): scorecard of briefs/scenarios, ≤3 lessons, bias notes incl. capital left sleeping |
accum-decision | 4 h, at :40 (was 6 h), and within the hour after two consecutive analysis briefs name a regime change (worker regime_flip_trigger, 2026-09-24) | Fable decision officer | accum_decision digest + accum_decision_json plan (incl. the binding mandate); paged to Telegram |
accum-execute | 30 min, at :20 / :50 | none (deterministic) | hygiene + execution reports (accum_execution), health pages |
accum-fallback | daily | none | §5 evaluation with weekly semantics, breakout checks, DCA clips, weekly digest |
treasury-nav | 15 min (was 60) | none | NAV snapshot + benchmarks; the budget's NAV never goes stale |
session-rollover | 6 h, at :55 | none | restarts the accum host in the idle window (the store stays small) |
The phases are deliberate (owner, 2026-09-24): the decision always reads a
brief that is 40 minutes old and a dream that is 20 minutes old, and the
:50 sweep executes a :40 decision within ten minutes. The scheduler only
knows intervals, so a phase is set by writing next_run_at once
(PATCH /v1/schedules/{id}); since nexus-platform 7f67fbe the next run
is the scheduled time plus one interval, so the phase no longer slips by
the 15-second tick latency on every fire.
Since 2026-09-24 every voting cycle (analysis, dream, decision) also gets
the live ladder (params.ladder, the exec's rungs with distance from
the index) and the score's dry-powder figures are computed at those live
rungs — until then only the decision saw the ladder, and the 08:02Z /
09:02Z analysis kept voting against the $60/$50/$40 rungs an hour after the
desk had moved them. The ladder panel's CONSENSUS line now says
· applied HH:MM / · refused HH:MM.
Every cycle prompt carries the owner thesis verbatim, the sleeping
money block (§6), and — for the decision officer — the live treasury,
portfolio, ladder, on-chain orders, LP/vault catalogs and prior decisions.
The thesis officer is told which specialists actually reported
(panel_missing) and must cap confidence on an incomplete panel.
Per-step timeouts: analysts 240 s, thesis/dream 300 s, decision 600 s; the host's CLI cap is 900 s (it must exceed the longest step — a 220 s cap silently killed every decision on 2026-09-04). At most 3 analysts run concurrently (SQLite checkpoint contention on the shared session file was the root cause of the 2026-09-03 wedge).
4. The decision contract (what the desk can actually do)
The decision officer's fenced JSON is the executable contract. Everything below the line is auto-executed on Solana shortly after the brief, within the caps in §5; everything above requires a human.
{
"posture": {"BTC": "hold", "SOL": "hold", "HYPE": "informational"},
"ladder_proposal": {"proposed": false, "changes": [], "rationale": ""}, // changes[]: {rung, trigger, budget_usdc?} — a vote, never auto-executed (item 16)
"swing_plan": {"BTC": {"buy_zone": [0,0], "sell_zone": [0,0]}, "SOL": {...}},
"order_plan": [ {"action": "place|cancel|amend|keep", "sleeve": "core|swing",
"exchange": "Solana|Binance|Kucoin", "pair": "SOL/USDC", "side": "Buy|Sell",
"price": 0, "quantity": 0, "paired_exit": 0, "order_id": "", "reason": ""} ],
"treasury_plan": [ {"action": "lending_supply|lending_withdraw|pull_float|stake_sol|unstake_sol",
"venue": "kamino", // LENDING ONLY — the staking actions never read it
"amount_usdc": 0, "amount_sol": 0, "amount_jitosol": 0, "reason": ""} ],
"reserves": {"exec_float_usd": 0, "exec_gas_sol": 0, "reason": ""}, // the officer sets these — nothing hardcoded
"limits": {"sol_exposure_frac": 0.75, "lp_total_frac": 0.30, "reason": ""}, // the SOL-exposure and LP caps, decided each cycle (2026-09-07)
"lp_plan": [ {"action": "open|close|collect|remove|keep", "venue": "orca|meteora", "pool": "<address>",
"pair": "SOL/USDC", "position": "<id>", "bps": 10000,
"coin_side": {"asset": "SOL", "qty": 0}, "usdc_side": 0, "range": [0,0],
"apr_measured_pct": 0, "apr_7d_pct": 0, "unwind_if": "", "reason": ""} ],
"swing_state": {"budget_frac": 0.2, "committed_usd": 0, "inventory": {}, "vwap": {}, "realized_usd_total": 0, "frozen": false},
"treasury_actions": ["free-text operator proposals — e.g. single-asset vaults"],
"invalidation": ["..."]
}
Typed contract (audit H4, nexus-platform e3d7c4c, deployed 2026-09-05).
The block is parsed by nexus-decision as AccumDecisionV1
("version": 1, defaulted when absent) with deny_unknown_fields, typed
enums and bounds; the host persists only contract-valid decisions and the
worker skips an invalid_decision. The reserves are the officer's, but two
code-owned floors clamp them upward and report a 🧯 floor: line:
ACCUM_MIN_EXEC_GAS_SOL 0.2 SOL and ACCUM_MIN_FLOAT_USDC 5000 USDC.
The SOL-exposure cap follows the market (owner policy 2026-09-07). The
budget gate's max_sol_exposure_frac was a fixed env constant (0.60), and
with the ladder's resting rungs reserving $37.5k of SOL buys it read every
SOL-side LP opening as a would-block at 69–70 % of NAV — under enforce, the
allowance would have been unusable. The owner's answer was not a bigger
constant but a cap the desk sets from the market read. Each decision now
carries limits.sol_exposure_frac with a reason; the contract clamps it into
code-owned bounds (ACCUM_LIMIT_SOL_EXPOSURE_MIN_FRAC 0.40 /
ACCUM_LIMIT_SOL_EXPOSURE_MAX_FRAC 0.85, prod-set) and records any clamp as a
🧯 floor: adjustment; the worker overlays the decided value onto
portfolio_limits (source: decision, the override table names the decision
and its reason) every sweep, and ACCUM_LIMIT_MAX_SOL_EXPOSURE_FRAC (prod
0.75) is only the bootstrap until the first decision sets one. The officer
reads TREASURY.budget — the gate's verdict and headroom, the cap in force
and who set it, the bounds — and the challenger ends its brief with one line,
cap: raise|hold|lower — reason. Doctrine: raise when the thesis wants SOL
working in above-basis LPs and the deep ladder is far from filling; lower as
a capitulation setup approaches so the ladder's SOL buys can fill inside the
cap. What counts as SOL exposure is unchanged: SOL-family holdings including
JitoSOL and LP coin legs, plus the ladder's reserved SOL buys, plus the
action being judged.
The objective the four instruments serve (owner, 2026-09-08). "with ladder, swing orders, lps, and kamino, you need to find the balance to have the maximum sol btc during the low market 40, 50, 60$ sol price, and during the fall accumulate USDT as much as possible with those instrument same with BTC". NAV is the scoreboard — it is how many USD the book holds, and under this thesis executed well it should rise through the fall: stables earning, bounces sold, fees collected. A falling NAV on a stable-heavy book means the instruments are not paying for the coin they carry, and that is a failure to explain rather than something to excuse as "it's a bear market".
What NAV cannot do alone is tell two books apart at the bottom:
| Book | NAV | What it buys at $60 |
|---|---|---|
| all USDT | $91k | 1,517 SOL |
| all SOL, bought averaging $100 | $91k | 910 SOL — already spent |
Owner mandate 2026-09-24 — supersedes the downside thesis below. The score is dollar NAV and the goal is to grow it while SOL and BTC rise. There is no standing downside thesis and no target zone: the $60/$50/$40 stack and the 40/40/20 split are history, not targets. The hourly analysis names the regime (climb / chop / fall) and the regime dictates the book: two consecutive briefs naming a regime different from the standing mandate's make the decision due within the hour. Pools are the default home for capital while their measured mature-window APR beats lending; Kamino holds operating cash and money waiting for a named use, never a standing share of NAV. The ladder is a tool, not insurance: rungs sit where a pullback in the regime can fill them, rungs more than 30 % under spot outside a fall are mis-placed, and the cycle moves them itself (analysis + dream votes, decision numbers,
POST /v1/ladder/reprice, no owner approval). Code caps raised the same day: LP total 0.60 of NAV, Orca 0.60. The paragraphs that follow describe the 2026-09-08 doctrine and are kept as the record of how the desk was reasoning until then.
The coin book ends at 75 % SOL / 25 % BTC (owner, 2026-09-08). "the goal
as well is to have 75% SOL and 25% BTC, my exposure on SOL should be bigger" —
a share of the coin book, not of NAV; stables are dry powder outside the
mix. There is no urgency on the BTC side ("when the bottom comes there is time
to buy some btc"), so today's ~0 cbBTC book is a rung waiting to fill, not a
miss to correct by buying at spot. The ladder already encodes the split — its
rungs reserve $37,500 of SOL buys against $12,500 of BTC, exactly 75/25 —
and score.coin_mix now carries the current mix, the target and the gap, so
the desk maintains it as rungs fill rather than letting it drift to whichever
side happened to fill. If one side runs far ahead, the desk re-weights the
remaining rungs rather than buying the laggard outside the target zone. Knob:
ACCUM_COIN_MIX_SOL_FRAC (0.75).
So NAV is read with composition, and a second number travels beside it: the quantity of SOL and cbBTC held (units, not dollars) and the average price each book was bought at. Maximise NAV through the fall, then convert it into the maximum number of units in the $40-60 zone.
The four instruments serve those two numbers: the ladder converts the stack into core coin at the target, the swings and the near-spot LP ranges grow the stack by round-tripping the high brackets, and Kamino holds the stack liquid between those events. The allocation below is the current best-known balance between them, not the goal — the officer reports both numbers every decision, says which instrument moved them, and proposes a different balance with evidence when one would serve them better.
The sleeve targets are shares of NAV: 40 / 40 / 20 (owner policy 2026-09-08). The owner asked for percentages explicitly — "80% of my portfolio should be LPs / Kamino, 20% for orders swings. work in percentage because next month i should add more funds" — with the earning 80 % split evenly, so of NAV: 40 % in LP positions, 40 % in Kamino USDC lending, 20 % backing the ladder orders and the swing sleeve. Because the targets are shares, a deposit re-prices every one of them automatically.
The 20 % is the swing sleeve alone — the ladder gets no share of its own. A rung is funded when it is about to fill, from the swing sleeve, from the Kamino reserve, or by unwinding an LP; that is why Kamino is held as the withdraw-fast reserve and why an open LP counts as available ammunition rather than locked capital. Two owner constraints bound every sizing decision: the accumulation target is SOL at $60 / $50 / $40 ("i see the market down"), and a filled rung is core inventory — "the ladder should buy sol and btc at low price completing the portfolio to hold them so after when the price will go up again to sell them" — never re-deployed into a near-spot LP, never sold back on the first bounce; it is sold into the recovery, which is what the swing sleeve and the above-basis ranges do.
That looks like it collides with a 40 % LP sleeve — an Orca range earns fees only while price is inside it, so a range bracketing spot earns and converts USDC into SOL all the way down, apparently spending the ladder's ammunition 40-60 % above the target. What reconciles them is the owner's own distinction: there are two books of SOL, and they are not the same book.
| Where it comes from | What happens to it | |
|---|---|---|
| Swing inventory | SOL a near-spot range accumulates on the way down | Sold on the next bounce through swing_plan sell zones — "the SOL you buy with LPs during the high brackets should be sold during the swing orders". A near-spot range is a round trip, so it does not spend ladder ammunition. |
| Core inventory | SOL (and cbBTC) a ladder rung buys at $60 / $50 / $40 | Held for the recovery. Never sold on a bounce, never re-deployed into a near-spot range. |
So a range is legitimate in exactly two places: bracketing spot, where it earns fees and works the high brackets, or inside the $40-60 target zone, where it is a bid that earns once price arrives (nothing until then — no worse than the resting rung it replaces). The officer must say which book every coin leg belongs to, and for a near-spot range, name the swing sell zone its inventory exits through.
The book on the day the policy was set was a long way from it: Kamino 52.4k (57 % of a 91.2k NAV), LP 6.9k (7.6 %), JitoSOL 12.2k, and the ladder reserving 50k — 55 % of NAV against a 20 % ceiling. Two standing rules changed to allow the correction:
- There is no staked floor any more. "i want to use all my sol if
possible from jitosol because it s not earning enough like 5% only" and
"i would avoid staking jitosol if possible, only if i have sleeping sol
jitosol would be the answer". JitoSOL is now a parking place for SOL with
no range to go to this cycle, never a destination; the old "leave ≥ 40
SOL-eq staked as the liquid core" clause is gone, and
ACCUM_LP_CORE_SOL_MAXwent 100 → 250 SOL so the whole SOL family (≈149 SOL) fits inside the allowance with room for a deposit. - "Never drain Kamino" became "Kamino is sized to the rungs you keep". Kamino is the ladder's withdraw-fast reserve — "it s easy to withdraw and buy sol or btc if the price narrows the ladder" — not a warehouse for stables at 4 % while an LP range earns ten times that. The old rule survives in one form, and it is the guard rail on the whole policy: a below-spot range is only legitimate where it replaces a ladder rung — same price zone, same intent, so the USDC buys SOL exactly where the desk had already committed to bid and earns fees while it waits. Shallowest rungs first, never the deep capitulation rungs, and never a below-spot range at a price that would not have carried a rung. That is what separates this from the premature accumulation the owner named as the primary failure mode.
The officer reads TREASURY.sleeping.allocation — each sleeve's share of NAV
now, its target, and the dollar gap, computed from the budget gate's own NAV
snapshot and reservation figures so the two blocks cannot disagree — and
names the tranche that closes the biggest gap every cycle, or why not.
limits.lp_total_frac is how it holds the LP side: set at or just above the
0.40 target while the gap closes, inside the code-owned bounds (0.05–0.50).
Knobs: ACCUM_ALLOC_EARNING_FRAC (0.80) and ACCUM_ALLOC_LP_SHARE (0.50).
LP USDC legs are decoupled from the swing sleeve, and there is no standing
stables reserve (owner policy 2026-09-09). Deployed as nexus-platform
944a30a + 9835e03 and nexus-gitops f9d8965.
The measured book that prompted it (live, 2026-09-09 ≈ 08:00 UTC, NAV $91,306): Kamino lending $49,435 = 54.1 % of NAV, LP positions $17,823 = 19.5 %, the swing sleeve $14,322 = 15.7 %, float and idle $9,749 = 10.7 % — against the 40 / 40 / 20 target above. Lending had fallen from 73.6 % to 54.1 % in a day and the LP share had not moved at all: the withdrawn capital went to swing escrow and float, not into ranges.
The blocker was a doctrine rule, not code. The owner policy of 2026-09-07
charged every LP position's USDC leg against the same 20 % swing sleeve
budget as resting swing bids. With $14,322 of resting bids (four orders,
none filled since Aug 31) plus $3,797 of existing LP legs, that left roughly
$140 of headroom against an $18,724 LP gap, while $12,913 sat over target
in Kamino. The LP cap itself (limits.lp_total_frac = 0.40) was nowhere near
binding. Nothing in code coupled the two: lp_core::open_lp_value_usd
already counts both legs toward the LP share, and the worker's sleeve cap
only gates swing order placement against the exec's committed_usdc, which
excludes LP legs. The coupling lived entirely in the accum-desk prompts.
The owner: "i want to decouple lp usdc legs so 40/40 for kamino and lps and a 20% for the swings" and "i don t need reserve because for me kamino is the reserve and it s easy to withdraw or to cancel orders to buy the ladder".
What shipped:
- An LP proposal's USDC leg is charged to the LP's own 40 % share of NAV,
never to the swing sleeve. The 20 % sleeve is for resting swing orders
and swing inventory only. The LP sleeve's only cap is
limits.lp_total_frac. - Closing the LP gap is a standing per-cycle instruction, with a stated
funding order: first from Kamino's excess whenever lending sits above
its own 40 % share (a
lending_withdrawand an LP open belong in the same plan), then by unstaking JitoSOL, then from the shallowest ladder rungs. Below-spot ranges are preferred because they hold USDC and convert to SOL only as price falls — the gap closes without buying SOL at a level the thesis rejects. - There is no standing stables reserve any more. Kamino is the reserve; a
lending_withdrawor cancelling a resting bid is the funding path. The code-owned floorACCUM_MIN_FLOAT_USDCdropped $2,000 → $750 (worker.reserves.minFloatUsdc, Float floor), and a float above a use the officer can name is now explicitly counted as sleeping money in all four cycle prompts (analysis venue read, thesis brief, dream, decision). worker.budget.maxLpTotalFracbootstrap 0.30 → 0.40, which would have contradicted the owner's 40 % target if it were ever fallen back to.
Verification. The code and config are deployed and the worker is running the
new image, but the desk host had not yet been restarted at the time of
writing, so the running desk was still reading the previous prompts — the
new doctrine takes effect at the next host restart (the scheduled
session_rollover, cooldown 1 500 s, runs every 6 h). No rebalancing action
has been taken, and no LP position has been opened or closed as a result of
this change.
Prose is trimmed, never fatal (2026-09-08, 868e8d9). Every free-text
field is bounded at 4,000 characters, and until this change one long one
rejected the whole decision. At 09:16 UTC the officer wrote a
ladder_proposal.rationale of 4,366 characters and the contract threw
away a complete six-hourly decision — three orders, three LP entries, the
reserves and the limits — over prose that executes nothing; the desk kept
running on the 03:16 decision, and the cycles page showed that stale one
under a "Decision officer · ok" flow card with the reason only in the host
log. (That instance cost nothing — every row was a keep and the limits it
carried, 0.70 / 0.20, were already in force from brief 48 — but the same 366
characters would have discarded a decision that opened an LP or moved the
ladder.)
trim_prose now cuts each over-long field to the bound, appends … and
records a ContractNote, exactly as the venue rewrite below does, and it
runs before validate so the bound is already satisfied. Identifiers are
not trimmed — pair, order_id, pool and position still reject over
their own bounds, because a truncated identifier names the wrong thing. The
officer prompt carries the bound explicitly (nexus-gitops 5139770): keep
each field well under 4,000 and put the long argument in the digest, not in a
JSON string.
The desk was blind to this by construction, so /v1/desk/accum-health gained
decision_rejected — populated only when a refusal is newer than the
decision in force — and the cycles page shows
it as a red card naming the reason, retitling the panel below to "In force
(older than the last cycle)". A rejected decision is never retried: the next
scheduled decision re-decides.
venue is a lending field (2026-09-06, 87ca5f8). stake_sol /
unstake_sol have exactly one destination — the executor quotes the Jito
stake pool against a Metis swap and takes the better fill — and the worker
never reads venue on them. A venue named there is therefore not a routing
instruction but a false claim in an audit record, which is how the desk
came to emit {"action": "stake_sol", "venue": "kamino", "amount_sol": 0.27}
and the operator reasonably read it as staking SOL into a lending venue that
holds only USDC and cannot stake SOL at all. The SOL would have gone to Jito
regardless: the only place all treasury rows get a venue label is the
same-venue lending round-trip detector, which filters on the two lending
action names and on a positive amount_usdc, so a staking row fails both.
The contract now rewrites the field to jito and records a ContractNote
(field, found, applied, reason) beside the numeric Adjustments, in
the accum_decision_contract audit digest and as a 📝 contract: line in the
execution digest. It is deliberately not a rejection: the field steers
nothing, and discarding a six-hourly cycle — every order, the LP plan, the
treasury actions — over a mislabel costs far more than the mislabel. Lending
venues are never rewritten, because there the field really does route money,
and jito on a lending action remains a hard rejection. Root cause was
upstream: the desk prompt's own return schema listed venue as
kamino|marginfi on every treasury action, contradicting its sleeping
doctrine, which shows stake_sol with no venue (fixed in nexus-gitops
ecc17b0).
A blank run is not a success (2026-09-06, 87ca5f8). On 14:29Z the
decision officer returned an empty string. The flow logged
status: Completed, steps_completed: 2, output_keys: ["final"], and the
contract then rejected the blank with not valid JSON: EOF while parsing a value at line 1 column 0 [sha256 e3b0c442…] — the hash of the empty string.
Nothing executable was persisted, so the cycle moved no money, but it was
lost: the first rejection in 12 consecutive cycles since 2026-09-04.
The host was testing contains_key("final") — presence, not content — so a
blank output satisfied the desk's final_ready short-circuit, ended the wait
early and read as success at every step before the contract. The dream flow
already had a watchdog for exactly this silent failure; the desk had a single
attempt. Now terminal_output_ready requires the terminal step's member text
to be non-blank, the watchdog covers the desk too (2 attempts), and a run that
ends without its terminal output logs at ERROR — carrying delivered
into Langfuse beside the executor's own status, because the executor may
still be claiming Completed and that disagreement is the point.
Incident 2026-09-07, caused by the fix above, corrected the same
morning. The watchdog had been given final as the terminal key for
every non-dream flow. The analysis cycle has five peer analysts and no
final step, so every cycle was judged empty and re-run; the re-run resumed
the same member sessions, the flow analyst produced nothing new, the health
check counted two consecutive missing outputs and the liveness probe
restarted the host. At 02:21Z the worker dispatched the 02:29Z decision into
the draining host, which refused it with {"ok": false, "error": "host draining"} — and the worker's dispatch helper read the missing run_id as
"", returned success, and logged "accum decision dispatched" for a
decision that never ran. That decision carried the officer's first use of
the LP core allowance (close the fee-starved position, reopen 25 SOL
bracketing the price). Three corrections, each tested: the terminal key is
per flow (report for the dream, final for the decision, none for peer
flows, which get no emptiness verdict and no re-run); a /run_flow reply
without ok: true and a run_id is an error the job fails on, naming the
host's reason; and the host answers a draining refusal with HTTP 503 rather
than a 200 wearing ok: false.
The same morning, the first manually triggered decision on the corrected
host emitted exactly its gated plan — close the fee-starved out-of-range
10 SOL position, reopen 25 SOL + 640 USDC at 104–112, unstake_sol and
lending_withdraw in the same plan — and three of the four actions landed
on-chain, fees collected. The lp_open was refused on a stale balance:
the LP section had fetched /v1/reconcile once before its loop, so the
funding check predated the same-plan unstake (15.2 SOL) and the close
(10 SOL back) that had just landed, and read "EXEC holds 0.0023". The check
now re-reads the wallet at the moment it decides (the exec's position cache
is write-invalidated), falling back to the loop's snapshot only if the fresh
read fails. Until that deployed, 25.7 SOL and the 640 USDC sat idle with the
decision marked done — the sleeping-money state the policy exists to prevent.
The re-run on the corrected worker (07:22Z) passed the funding check and
reached the chain, where Orca refused it in simulation with 0x1781
(TokenMaxExceeded). Quantified: SOL sat at 104.95, under 1 % above the
104 floor; the executor sizes a two-sided deposit from the coin side and
bounds the USDC leg at its estimate × (1 + 50 bp), and that leg is
hypersensitive that close to the floor — 373 USDC for 25 SOL, +33 % for a
0.25 % price move, +68 % for 0.5 % — so any upward tick between quote and
simulation exceeded the bound. The officer had offered 700 USDC, which the
executor discarded. Worker fix (no signer restart, which would trip the
desk's own six-hour post-restart LP hold): lp_core::usdc_needed_for_coin
and open_slippage_bps derive the bound so estimate × (1 + s) ≈ usdc_side
(floor 50 bp, cap 100 %) — usdc_side is the cap the chain enforces, and
the officer's prompt now says so and asks for ≥ 2× the mid estimate near a
floor. The proper executor-side fix — size liquidity as the minimum over both
offered legs and set token_max_a / token_max_b to the offered amounts
exactly — is recorded for a quiet window. Also seen on that run: the F6
budget gate's log-mode would-block ("SOL exposure after = 69 % of NAV, cap
60 %: $24.9k held + $37.5k reserved by the ladder rungs"); under enforce this
opening would have been blocked — the owner decision on the cap is still
open.
The fourth run (08:00Z, with the owner's confirmation) landed the unstake and
was refused again with "EXEC holds 0.0009" — with the fresh-read fix in
place. That is what exposed the primary cause: /v1/reconcile is a live read,
but reconcile() calls rpc.get_balance on the exec's default-commitment
client, which is finalized (the exec's own send.rs documents this), so a
confirmed unstake becomes visible to it 15–20 s later. Both refusals ran inside
that window; the one that passed ran minutes after. The worker now waits up to
60 s, polling every 5 s, when the plan itself moved coin toward the wallet
(unstake_sol, lending_withdraw, pull_float) and the wallet still reads
short; a plan with no funding leg fails fast as before. The signer-side fix —
get_balance_with_commitment(confirmed) for wallet reads, and a stake or
unstake marking the wallet dirty in the position cache the way lending and LP
writes already do — is recorded for a quiet window, since a signer restart
trips the desk's own six-hour LP hold.
Why a symmetric bound cannot open near a range floor, and what was done
(2026-09-07 08:12Z). The fifth desk attempt passed every worker check and
the signer refused it in simulation with no detail through the intents
layer. The read-only /v1/lp/quote showed the shape of the problem: at a
100 % bound the SDK's coin bound is twice the input (50 SOL against 25.21
usable, so the wSOL wrap fails on lamports); at 50 bp the coin bound fits but
the USDC bound (619 against an estimate of 616) fails on any up-tick. The
worker-side workaround had traded the USDC-side failure for a coin-side one.
The proper fix is signer-side and asymmetric — token_max_a / token_max_b
set to the offered amounts exactly, liquidity sized as the minimum over both
legs — and is recorded for a quiet window. At the owner's instruction the
opening was sent by hand from the worker key, each attempt guarded by a quote
asserting the coin bound fits the wallet: 17.5 SOL @ 40 % (position
Ff4o45UC…, 17.5 SOL + 429.68 USDC) and a top-up of 6.5 SOL @ 18 %
(2crYXNWA…, 6.5 SOL + 153.09 USDC), both at 103–112, in range, ≈ 24.17 SOL
and ≈ $3,090 in the pool. The two lp_add internal-flow rows the manual
route skips were written by hand in the worker's shape so the P&L books the
deposits as transfers; the forcing clause was withdrawn from the desk prompt
and replaced by a note of record, so from the next cycle the LP decision is
the desk's again inside the allowance.
An empty decision is rejected, not executed as nothing (2026-09-07
afternoon). Two consecutive decisions persisted as the literal {} while
their prose was complete: the officer's digest contained the phrase
ledger inventory {} well before its fenced JSON block, the extractor's
first-balanced-block fallback returned that {}, and the contract accepted
it because every field defaults — the executor reported "book already
matches the plan — nothing to do" twice, and the first cycle under the
LP-as-primary-channel policy was lost. The extractor now prefers a fenced
non-empty object in every fallback; the contract rejects an empty object
outright, so "nothing to do" has to be said with at least a posture.
**Auto-executed:** Solana buy placements/cancels/amends (Jupiter Trigger
limit orders, escrow on-chain), lending supply/withdraw (Kamino), the USDC
float pull, SOL↔JitoSOL staking, LP open/close/collect/remove (Orca
Whirlpools, Meteora DLMM), and the decided reserves.
**Human-gated (Telegram `/approve <id>` / `/reject <id>` / `/pending`):**
every **sell** or sell amend *proposed by the desk*, all CEX actions,
fallback-DCA activation, breakout tranches. Approvals are typed
`accum_approvals` records since 2026-09-05 (audit H3: 22-char ids, 6 h TTL,
CAS claim, decision-hash revalidation — see
[nexus-telegram](/docs/technical/nexus-telegram#security)). **Swing paired exits (audit H2,
fixed 2026-09-05):** `sleeve.rs` auto-places the paired sell only when the exit
is ≥ basis × (1 + `SLEEVE_MIN_EDGE` 0.006 + `SLEEVE_FEE_FRAC` 0.002); otherwise
the fill is held as `filled_unpaired` (inventory, no sell) and an operator
places a reviewed exit with `POST /v1/sleeve/{order}/exit {"price"}` on the
exec (`:8091`), which re-runs the same check. Fallback-DCA activation cancels
the unfired rungs first (`POST /v1/ladder/cancel`, audit H13) and creates the
program only if that succeeds. CEX actions are recorded as skipped rather than
paged (audit **M4**). `/basis <asset> <price>` attributes a cost basis; the desk
refuses to sell or LP inventory without one.
This is a deliberate change from the design spec's "the agent layer never
executes": deterministic code now supplies **caps and guards**, the desk
supplies **decisions**. The doctrine that makes this safe is that nothing the
desk can emit moves funds to any owner other than EXEC.
## 5. Executor — caps, guards, hygiene
`accum-execute` runs every 30 minutes and does, in order:
1. **Hygiene (every sweep, decision or not)** — since 2026-09-05 the
*confirmed* rules of the [owner lending policy](#owner-lending-policy)
(nexus-platform `4fd5c4c` + `fdd7912`, live since 21:54 UTC; the
knobs and the desk prompt are live since ≈ 21:15 UTC). One read never
moves money; the sweep keeps memory in `accum_hygiene_state`.
- *Recall* (withdraw-all to EXEC; risk-reducing, `blackout_exempt`):
utilisation ≥ 95 % on **2 consecutive sweeps**, or available liquidity
below **10× our position** on 2 consecutive sweeps; a single read
≥ 98.5 % recalls at once.
- *Rotation* (APY chasing; risk-increasing, budget-gated): only when the
spread ≥ 2 pp has held for **48 consecutive sweeps (≈ 24 h)**, ≥ 7 days
have passed since the money last rotated, the position is older than
48 h, and the destination sits under 88 % utilisation with liquidity
≥ 10× the amount. A rotation that is wanted but withheld is reported
with its reason.
- *Repark*: EXEC USDC above the **decided float** is supplied every
sweep — never into a reserve over 88 % or thinner than 10× the amount,
preferring the reserve that already holds the position — after
reserving the USDC that the pending decision's own placements will
escrow (a 2026-09-04 fix: the sweep once swept the float the officer
had sized for a bid).
- *Officer round trips*: a `lending_withdraw` + `lending_supply` of ≈ the
same amount on the same venue in one plan is a rotation request, judged
by the rules above — **both legs are skipped** and the digest carries
the hygiene verdict. Other officer `lending_supply` / `lending_withdraw`
actions run as before.
Between ≈ 21:15 and 21:54 UTC the previous worker (`7d6ed59`) applied
the new lines — recall 0.95, repark 0.88, spread 2 pp — on a single
read; the confirmation windows, cooldown and minimum hold arrived with
`fdd7912` (first sweep after the roll: no movement). (Before
2026-09-05: recall above 92 %, repark below 88 %, rotation ≥ 150 bps,
all single-read.)
- *Reserves push*: the officer's `reserves` are published to the exec
plane so the UI, the prompts and the sweep net the same numbers.
- *Cycle health* (§8): stale-flow pages, host restart at 3× cadence.
2. **Blackout gate** (audit H12, deployed 2026-09-05) — since nexus-platform
`eb1c464` (16:02 UTC, audit **F3**) it is evaluated **once per sweep,
before hygiene and fallback**: 18 actions are classified, risk-increasing
ones are blocked, risk-reducing ones run and are logged
`blackout_exempt`; `nexus_accum_actions_gated_total{class,reason}` counts
the decisions. (Until `eb1c464` the gate sat after step 1 and protected
the decision pass only.)
3. **Portfolio budget gate** (audit **F6** / H5 — deployed in log mode
2026-09-05 17:50 UTC, nexus-platform `15ebb97` … `7e1adcb`, nexus-gitops
`d848adf`; log mode never denies since `fda55f4`, live with
`fdd7912`): after the blackout check, every risk-increasing site —
hygiene rotate / repark, swing buy, lending supply, stake, LP open, the
fallback DCA clip and proposals — calls `GateLedger::check_budget` with
the movement's size, target asset and venue. The check projects the
movement against the strategy-wide caps below with every open
reservation already counted and, on pass, **reserves** the amount
(id `<kind>:<ref>`, idempotent; kinds `ladder_rung`, `swing_buy`,
`lp_add`, `lending_supply`, `repark`, `stake`; `held → consumed` when
the money moved, `held → released` when it did not or the TTL
`ACCUM_LIMIT_RESERVATION_TTL_SECS` 21 600 s expired). Up to `fdd7912`
(running) the reservation is one upserted `accum_reservations` row and
the headroom is a sweep-local snapshot; from `ee0cd80` (deployed
2026-09-05, live-verified 23:4xZ) it is projected against the **aggregate
ledger** `accum_budget_ledger` under a CAS and **claimed** with a
fence before the money moves — [Budget ledger](#budget-ledger). The
armed ladder's budgets are a standing `ladder_rung` reservation
refreshed every sweep (the rungs themselves fire on the exec plane; the
exec consults Core's `verdict` before firing since nexus-solana
`b0a00f1`, and the signer's per-action / daily caps stay the last
line). A stale or absent NAV snapshot (older than
`ACCUM_LIMIT_NAV_MAX_AGE_SECS` 7 200 s) ⇒ `budget_unavailable`: in
enforce every risk-increasing action is denied. See
[Portfolio budget](#portfolio-budget) for the limits, metrics and the
live dry run.
4. **Execution of the latest unexecuted decision** — idempotent against the
live book (cancels skip orders already gone, places skip orders already
resting); treasury and LP actions carry a once-per-decision marker so a
deferred order pass can rerun without replaying money movements.
Mechanical guards (no policy — policy is the desk's): armed plane; oracle
guard `ok`; bids below index only; per-transaction USD cap
(`ACCUM_MAX_ACTION_USD`, chunks lending, rejects oversized orders and LP
opens); cumulative 20% swing-sleeve cap over wallets + earning + committed —
it gates swing *order placement* against the exec's `committed_usdc`, which
excludes LP legs, and since 2026-09-09 an LP's USDC leg is charged to the LP's
own 40 % share instead (§4);
LP coin side must already sit in EXEC above the gas reserve (the executor
**never swaps to fund a position** — the officer emits `unstake_sol` in the
same plan); USDC float pulled via the spending limit when configured, else
"operator refill required".
### Owner lending policy (2026-09-05) \{#owner-lending-policy}
**Decision (owner, 2026-09-05 ≈ 20:55 UTC): yield-first, rare moves.** The
USDC lending stack stays in the highest-yielding *holdable* Kamino reserve
— today the JLP-market reserve at ≈ 11 % — and is **not** rotated back to
the main reserve for utilisation alone. 85–95 % utilisation is the accepted
operating band; the tripwire is the **liquidity floor** (free liquidity in
the reserve ≥ 10× our position), not the utilisation number. Rotations
happen on a sustained spread after a cooldown, or not at all.
**What it corrected.** At 17:37 UTC the hygiene sweep rotated the whole
Kamino float (≈ 56 k USDC) main → JLP on a 2.1 pp spread with JLP at 87 %
utilisation. JLP then read 89–94 %, and the desk's own "≥ 90 % utilisation
→ full withdraw" rule ordered the round trip back in the 20:29 UTC decision
(`lending_withdraw` from JLP + `lending_supply` to main — a ping-pong the
desk itself priced at ≈ $2.9/day of spread). The owner vetoed it ("keep
JLP + floor rule"). That decision carries its `accum_exec_done` marker
(written by the executor after the withdraw leg failed, `acted=0`), so it
does not re-run; the same marker is the
[veto procedure](/docs/operations/#lending-hygiene) for any future decision.
**Status.** Deployed: nexus-platform `4fd5c4c` (`accum_hygiene_state`) and
`fdd7912` (the rules, `mob_jobs/lending_hygiene.rs`, 25 unit tests),
worker rolled **21:54 UTC**; first sweep confirmed at ≈ 22:05 UTC — no
lending movement, JLP utilisation 90.3 %, liquidity 34.7× our position,
Kamino position unchanged at 56,446 USDC in JLP. Configured: nexus-gitops
`d5318ad` (chart `worker.hygiene`, live ≈ 21:15 UTC; prod inherits the
chart values). Prompted: nexus-gitops `3f90adb` (the `OWNER LENDING
POLICY` block of the decision officer in
`files/mobs/accum-desk/mob.definition.json`, live after the accum-host
restart ≈ 21:15 UTC). The policy is in force end to end (worker rules,
prod knobs, desk prompt). Not live-verified on a real move yet — by
design there should be none.
| Rule | Condition (defaults = prod values) | Knobs |
|---|---|---|
| Recall (withdraw-all) | utilisation ≥ **0.99** (prod; was 0.95) on 2 consecutive sweeps, **or** available liquidity below 10× our position on 2 consecutive sweeps. **Since 2026-09-24 the utilisation lines are exempt while free liquidity ≥ 20× our position** (`ACCUM_UTIL_RECALL_LIQ_EXEMPT_X`): Kamino main lived at 93–99.9 % every night and the old lines pulled the whole stack out and back eleven nights running (2026-09-12 → 09-23) with $1–8 M free against $60 k | `ACCUM_UTIL_RECALL`, `ACCUM_UTIL_RECALL_CONFIRM_READS`, `ACCUM_LENDING_LIQ_FLOOR_X` |
| Emergency recall | utilisation ≥ **0.995** (prod; was 0.985) on a single read, same liquidity exemption | `ACCUM_UTIL_RECALL_HARD` |
| Rotation | spread ≥ 0.02 held on 48 consecutive sweeps (≈ 24 h) **and** ≥ 604 800 s (7 d) since the last rotation into the reserve **and** position older than 172 800 s (48 h) **and** destination utilisation below 0.88 with liquidity ≥ 10× the amount | `ACCUM_ROTATE_MIN_SPREAD`, `ACCUM_ROTATE_CONFIRM_SWEEPS`, `ACCUM_ROTATE_COOLDOWN_SECS`, `ACCUM_LENDING_MIN_HOLD_SECS`, `ACCUM_UTIL_REPARK` |
| Repark of idle float | every sweep; destination below 0.88 with liquidity ≥ 10× the amount; prefers the reserve already holding the position | `ACCUM_UTIL_REPARK`, `ACCUM_LENDING_LIQ_FLOOR_X` |
| Officer same-venue round trip | never executed — both legs skipped, digest carries the hygiene verdict; single supply / withdraw actions untouched | — |
Held moves are named in the `🧹 ACCUM HYGIENE` digest with the failing
gate — `spread 2.1 pp held 5/48 sweeps`, `cooldown 6d 3h left`,
`dest liquidity 8× < 10×` — so "no move" is a stated decision, not
silence.
**State** — `nexus.accum_hygiene_state` (one document, `_id = lending`):
per reserve the last 96 reads (utilisation, available liquidity, our
position, APY, timestamp), first sight of the position, the last
supplied / rotated-in / recalled timestamps, and the running spread streak
against the best destination. A position that predates the document counts
from first sight, so the minimum hold applies to it too. If the save fails
the digest says so (`⚠️ hygiene state not saved`) and that sweep's
confirmation progress is lost — the safe direction.
**Metrics** (worker exporter, since `fdd7912`, 21:54 UTC):
`nexus_accum_lending_moves_total{kind=recall|rotate|repark|held}`,
`nexus_lending_reserve_utilization{venue,label}`,
`nexus_lending_reserve_liquidity_x_position{venue,label}`,
`nexus_lending_rotation_cooldown_seconds_left{venue}` — reading guide
under [Operations → Lending hygiene](/docs/operations/#lending-hygiene).
**Dry run on the 21:11 UTC live snapshot** (unit test
`live_snapshot_2026_09_05_2111z_is_a_no_op`): JLP 90.4 % utilisation,
liquidity 34.4× our position → hold; main is −2.1 pp → no rotation; no idle
float → no repark. **No movement** — which is the policy.
**Related fixes the same evening**
- Lending calls follow the market holding the position (nexus-platform
`fdc6758`, deployed 2026-09-06): the 20:36 UTC `lending_withdraw kamino
12 000` had hit the empty *home* reserve (main) after the 17:37 rotation
and failed with an empty reason. `lending_reserve_for(venue, asset)`
now reads the exec's `GET /v1/positions`, filters on venue + asset and
picks the reserve with the **largest `supplied_ui`**, naming it on the
call; `None` (an unreachable or empty answer) falls back to the exec's
home reserve as before. Two call sites: the decision pass's lending
supply / withdraw chunk loop and the fallback DCA float recall. FAILED
report lines now carry the exec's own error text (`exec_error_text` —
`error`, then `error.message`, `detail`, `message`, else the answer
truncated to 160 characters), in both the `⚡ ACCUM AUTOPILOT` and
`🧹 ACCUM HYGIENE` digests.
### Float floor, and a supply that cannot breach it (2026-09-06) \{#float-floor}
**Owner decision, 2026-09-06:** `ACCUM_MIN_FLOAT_USDC` **5 000 → 2 000**
(nexus-gitops `2c8d76a`, `worker.reserves.minFloatUsdc`). The $5 000 floor
was sized for the now-retired cbBTC arm; with no armed BTC buy, operations
plus a confirm-window hedge need $2 000, and a rung or an arm funds itself
with a `lending_withdraw` under the owner lending policy above. The **gas
floor is unchanged** at the 0.2 SOL code default (`ACCUM_MIN_EXEC_GAS_SOL`;
the desk asks for 0.5 and is clamped **up**, never down) —
[nexus-gitops → `worker.reserves`](/docs/repositories/nexus-gitops#worker-reserves).
The floors are code-owned clamps on the officer's proposal, and on
2026-09-06 02:30 UTC that turned out to be only half a guard: the officer
argued the float down to 2 000 and sized a `lending_supply` of 2 937 USDC
from *its* figure, while the H4 clamp had already put the reserve back to
5 000 — the supply would have left the exec at exactly the floor it was
meant to protect. Since nexus-platform `3ce6c78` the executor caps every
decision-pass `lending_supply` at `exec USDC − float floor` and reports
the cap (`sized 2937 but only 995 USDC sits above the 5000 float floor —
capped`); below 1 USDC it skips the action and releases its budget
reservation (`nothing above the float floor — skipped`). Hygiene repark
already netted the float; this closes the decision path.
**Owner decision, 2026-09-09: `ACCUM_MIN_FLOAT_USDC` 2 000 → 750**
(nexus-gitops `f9d8965`, same `worker.reserves.minFloatUsdc` key). The
standing stables reserve was removed with it — *"i don t need reserve because
for me kamino is the reserve and it s easy to withdraw or to cancel orders to
buy the ladder"* — so the float is sized to operations alone and anything
above a use the officer can name reads as sleeping money in every cycle
prompt (§4, §6). The gas floor is still the 0.2 SOL code default.
- The missing Squads spending limit means the vault cannot top up the
float: with 4 937 USDC on EXEC the sweep could only report "operator
refill required" (§12 item 1, still open).
- The F6 single-action cap flagged the 56 k rotation as
`would_block:single_action_cap` (log mode). A rotation of existing
lending capital is not net new exposure; rotations must be exempted, or
the cap measured on net new exposure, before the gate goes to enforce
(§12 item 12, TODO).
### Portfolio budget (audit F6 / H5) \{#portfolio-budget}
Limits live in code with `ACCUM_LIMIT_*` overrides (the values file
exposes the first three as `worker.budget.*`) and are persisted to
`accum_portfolio_limits` whenever they change:
| Limit | Default | Env | Counts |
|---|---|---|---|
| `max_sol_exposure_frac` | 0.60 | `ACCUM_LIMIT_MAX_SOL_EXPOSURE_FRAC` | SOL + JitoSOL + LP SOL legs + SOL buy escrow, over NAV |
| `max_btc_exposure_frac` | 0.40 | `ACCUM_LIMIT_MAX_BTC_EXPOSURE_FRAC` | cbBTC + BTC + LP BTC legs + cbBTC buy escrow |
| `max_protocol_frac[kamino]` / others | 0.70 / 0.25 | `ACCUM_LIMIT_MAX_PROTOCOL_FRAC_<NAME>` (`…_FRAC` for the default) | per-protocol supplied + LP value |
| `max_lp_total_frac` | 0.15 | `ACCUM_LIMIT_MAX_LP_TOTAL_FRAC` | all LP positions |
| `min_liquid_reserve_usdc` | 5 000 | `ACCUM_LIMIT_MIN_LIQUID_RESERVE_USDC` | wallet stables + lending stables × (1 − haircut), net of open claims — the tail reserve |
| `lending_recall_haircut_frac` | 0.10 | `ACCUM_LIMIT_LENDING_RECALL_HAIRCUT_FRAC` | how much of supplied stables is *not* counted liquid |
| `max_single_action_frac_of_nav` | 0.30 | `ACCUM_LIMIT_MAX_SINGLE_ACTION_FRAC_OF_NAV` | one movement |
| `nav_max_age_secs` | 7 200 | `ACCUM_LIMIT_NAV_MAX_AGE_SECS` | older ⇒ `budget_unavailable` |
| `reservation_ttl_secs` | 21 600 | `ACCUM_LIMIT_RESERVATION_TTL_SECS` | a `held` row not consumed by then is released |
| `claim_lease_secs` | 900 | `ACCUM_LIMIT_CLAIM_LEASE_SECS` | exclusive execution lease on a claimed reservation; after it another executor may take the claim over (`ee0cd80`, deployed 2026-09-05 — [Budget ledger](#budget-ledger)) |
| `mode` | `enforce` in code; **`log` in prod** | `ACCUM_LIMIT_MODE` = `log` \| `enforce` | log = evaluate, reserve, count, allow |
Denials write a `⛔ budget: …` line into the hygiene digest and count on
`nexus_accum_actions_gated_total{class="budget",reason}` with `reason` ∈
`budget_unavailable | single_action_cap | sol_exposure_cap |
btc_exposure_cap | liquid_reserve | protocol_cap | lp_total_cap |
reservation_error` (a reservation that cannot be written is a denial —
fail closed). In log mode the same evaluation and the same reservation
happen, but the movement is allowed and counted as
`reason="would_block:<reason>"` with a `🟡 budget (log mode) would block`
line. Gauges: `nexus_portfolio_exposure_after_frac{asset}` (SOL / BTC /
STABLE, including open reservations), `nexus_portfolio_reserved_usdc{kind}`,
`nexus_portfolio_cap_headroom_usdt{cap}`, `nexus_portfolio_budget_available`
(0 = the last sweep had no fresh, complete NAV); since `ee0cd80` also
`nexus_portfolio_ledger_version`, `nexus_portfolio_reserve_conflicts_total`,
`nexus_portfolio_fence_rejections_total`. Core exposes the same
arithmetic on `GET /v1/desk/budget` ([API reference](/docs/apis/#desk-treasury)).
#### Budget ledger and fenced claims (audit F6 concurrency / F2 fencing) \{#budget-ledger}
Built in nexus-platform `1e6a080` (ledger) / `b900de9` (worker) /
`cf219fa` (Core routes, Telegram), all in `ee0cd80` (deployed 2026-09-05,
ledger live-verified 23:4xZ — seeded on the first sweep at version 2,
`verdict` available, anonymous reserve → 401); exec side nexus-solana `b0a00f1` in `a1eb8f3` (live-verified 2026-09-05 23:33Z: `budget_check`
enabled against `http://nexus-core`, no verdict effect in log mode).
*Why.* `Db::reserve` upserted **one** row atomically, but the headroom
it had been checked against was a sweep-local snapshot: the worker runs
four jobs concurrently, a second replica exists during a rollout, and
the Core API reserves on behalf of the Telegram gateway — two of them
could read the same headroom, both pass, and together go over the cap
(the re-verification's "not an atomic global budget").
*What.* One document, `accum_budget_ledger` `{_id: "portfolio"}`, is the
authority for what is already promised: `version` (CAS token, +1 per
write), `nav_snapshot_id` / `nav_at_ms` and `limits_version` (the view
the last reserve was projected on), `totals_usdc{sol_usdc, btc_usdc,
stable_out_usdc, by_protocol_usdc{}, lp_usdc, liquid_claims_usdc,
by_kind_usdc{}, count}`, and `reservations[]` — entries `{id, kind, ref,
asset, venue, amount_usdc, stable_out_usdc, state, lease{owner, since_ms,
until_ms}, fence, created_at_ms, expires_at_ms, updated_at_ms, note,
released_reason, last_owner}`. Every reservation is projected against the
aggregate totals plus the request and written back with a compare-and-set
on `version`; a writer that loses the CAS re-reads and re-evaluates
(`MAX_CAS_TRIES` 8, counted on `nexus_portfolio_reserve_conflicts_total`,
then `reservation_error` — fail closed). The first writer that finds no
document seeds it from the live `held` rows (`rebuilt_from_rows_at_ms`);
`accum_reservations` stays as the write-through audit trail and no
longer supplies headroom. Released entries are dropped on the next write,
consumed ones after 6 h (once the hourly NAV job has valued the money
where it went).
*Fenced execution claim.* Holding a reservation is a promise on the
float, not a right to move it. Entries go `held → executing → consumed |
released`: before a call site sends money it **claims** the entry
(`held → executing`, `lease{owner: worker:<pod> | telegram:<user> |
core:<principal>, until}`, `fence += 1`; lease length `claim_lease_secs`)
and keeps the fence token; `consume` / `release` of an executing entry
require that token. A claim whose lease has run out can be **taken over**
by another executor (the fence moves on) — the crashed sweep's claim is
recoverable, and the old executor's later settlement is refused
`StaleFence` and counted on `nexus_portfolio_fence_rejections_total`
(alert `NexusBudgetFenceRejection`, critical: look for a duplicated
movement). Consuming a never-claimed `held` entry is refused
`NotClaimed` — money must never move on an unclaimed reservation; a
`held` entry is released without a token. A paged proposal hands its
claim back (`GateLedger::keep_for_later`, `executing → held`) and is
re-claimed with a fresh fence when the operator approves. In the worker
every `check_budget` pass reserves **and** claims for the sweep
(`worker:<pod>`), then consumes / releases with the fence.
*Coverage.* Telegram-approved buys (a `swap` / `create_order` whose input
mint is USDC) reserve + claim on the same ledger before the action is
sent and consume / release after (`BudgetGate` in the gateway; sells,
cancels and digest writes are not budgeted); Core exposes `verdict` and
`ledger` on `GET /v1/desk/budget` and the mutations
`POST /v1/desk/budget/reserve`, `/reservations/{id}/claim | consume |
release` for movements made outside the worker
([API reference](/docs/apis/#desk-budget-mutations)); the exec reads
`verdict` before a rung fires on both ladder paths and **holds** the
rung `Armed` when `mode: enforce` says no headroom (never re-reserving —
the standing `ladder_rung` reservation already counts it; Core
unreachable ⇒ fire with a warning; `EXEC_BUDGET_CHECK`, `/v1/status.budget_check`
— [nexus-solana](/docs/repositories/nexus-solana#budget-hook)). Direct
execute-role requests to the signer remain outside the ledger. Runbook
(inspecting the document, what a fence rejection means, releasing a stuck
reservation): [Operations → Budget ledger](/docs/operations/#budget-ledger).
**Dry run on live data, 2026-09-05 16:57 UTC** (why it ships in log mode):
NAV ≈ 91.0k USDT; the SOL family holds 25.6k, and the standing ladder
reservations add 37.5k SOL / 12.5k BTC ⇒ projected SOL exposure **69 %
against the 60 % cap** (headroom −8.5k); liquid free 6.1k against the 5k
floor. In enforce, every swing buy and LP add would be denied
(`sol_exposure_cap`, then `liquid_reserve`) for as long as the ladder is
fully armed — which is the cap doing what it says, on a ladder the owner
sized before there was a cap. **Owner decision pending**: trim the deep
rungs, raise the SOL cap (≈ 0.75 clears today's ladder), or accept that the
sleeve pauses while the ladder is armed. The flip and what to watch first
are under [Operations → Portfolio budget gate](/docs/operations/#budget-gate-flip).
Every mutation on the exec plane is **simulated on our own node first**;
a simulation error refuses the send. Since nexus-solana `0913342`
(deployed and live-verified 2026-09-05 via `/v1/status.policy`) the signing
key is owned by a **pre-sign `PolicyGate`** (audit H1) that every signing
site must pass: control store readable (fail closed), kill switch and
runtime pause (`bitview_desk.accum_control`, or `EXEC_PAUSED=1`), the
supply-only invariant, a known USD notional, a USDC depeg band of 100 bps
(fails closed when the reference is unreadable — an unreadable oracle denies
USDC actions including rung fires), per-clip slippage and venue-vs-index
guards for market swaps, below-basis sells denied at the boundary
(min_edge 0.006 + fee 0.002), and caps `EXEC_MAX_ACTION_USDC` 25 000 /
`EXEC_MAX_DAILY_USDC` 250 000 (daily spend tracked in Mongo). The gate is
additional to the GitOps disarm.
Since 2026-09-05 (audit **S1**, critical, formerly C2 — **closed**) the
gate also decodes what it signs and the exec API authenticates its callers:
`nexus-solana` `2c0be5a` (15:55 UTC) added address-lookup-table resolution, a
single-signer check, a program allowlist, delegate / authority / close /
transfer decoding and simulated token-delta bounds against a 90 s
`TxIntent` on every signing path; caller keys (`X-Nexus-Exec-Key`, roles
worker / telegram = execute, core / ui = read, `self` for the exec's own
loops) run in **`EXEC_AUTH_MODE=enforce` since 16:27 UTC** (nexus-gitops
`29c983a`, live-verified: no key → 401, read key on a mutation → 403, 0
caller denials). Not enforced by design: inner CPI programs, Token-2022
balances, LP batch sums, the Ledger-signed `submit_signed` relay. Roles,
counters and key rotation are under
[Security → Signer caller authentication](/docs/security/#signer-caller-auth).
Network containment is the NetworkPolicy union
described under [Security → Signer ingress](/docs/security/#signer-ingress-nexus-solana-exec)
— until 2026-09-05 that union also admitted the monitoring namespace
(audit **S2**). Reports go to Telegram as
`⚡ ACCUM AUTOPILOT` (decision execution) and `🧹 ACCUM HYGIENE` (sweeps);
quiet sweeps are recorded but do not page.
## 6. Sleeping money and reserves (the "nothing idle" rule as built)
The design spec's treasury allocator (§15) exists as a crate but is **not
wired**. What implements "nothing idle" today:
- **Definition** (shared by the UI card and every cycle prompt): sleeping =
dollars earning 0% — idle stables + unstaked SOL/cbBTC as plain wallet
balances in EXEC and VAULT, **net of two reserves that are meant to sit
hot** (EXEC SOL gas reserve, EXEC USDC float) — and since 2026-09-09 the
float is netted only up to a use the officer can name; anything above that
is sleeping money in all four cycle prompts. JitoSOL (yield accrues in
its redemption rate; the balance never grows), lending receipts and LP
positions (valued with unclaimed fees) are *earning*. Order escrows are
**reserved for execution** — USDC sitting in a Jupiter Trigger order
account earns nothing and may never fill; the UI and prompts still count
them on the "earning" side of the sleeping-money split, which the audit
flags as a labelling error to fix (2026-09-05 §7: label available,
reserved, yield-bearing and deployed-risk capital separately).
- **Reserves are a decision, not a config.** The decision officer sets
`reserves.exec_float_usd` and `exec_gas_sol` every cycle with reasoning;
the repark, the stake action, the sleeping block and the portfolio card all
use the decided numbers. Env defaults apply only until the first decision
sets them (`source: bootstrap` → `decision`).
- **Doctrine in the prompts**: idle SOL is a defect, not a neutral state; its
default home is JitoSOL; a held-coin LP or a single-asset vault is
preferred only on a *measured* APY that beats staking after impermanent
loss; a cycle that leaves money sleeping must state the amount and the
failing gate. The thesis officer carries idle capital as a standing issue;
the dream judge scores "capital left sleeping" as a bias.
- **LP core allowance (owner policy 2026-09-06)** — nexus-gitops `bf7aae4`,
nexus-platform (same day). Until then core JitoSOL was **operator-only**
for LPs, so the desk could open a held-coin LP only from what already sat
idle on EXEC: the first Orca position was 10 SOL against a ~160 SOL core,
and the operator asked why not more when it out-earned staking many times
over. The owner's reasoning for opening it up: an LP with its floor above
the attributed basis *sells SOL into strength continuously*, which is the
bearish thesis executing itself, and its measured fee rate beats staking by
a wide margin. So the desk now has a **bounded allowance**:
`values.worker.lp.coreSolMax` → `ACCUM_LP_CORE_SOL_MAX`, the ceiling in SOL
on the total coin side across every open LP position (**prod: 60**; code
default 30; `0` = core stays operator-only). The **decision officer sizes
each opening inside `lp_core_allowance.remaining_sol` every cycle** — the
block rides in SLEEPING MONEY as `{max_sol, in_lp_sol, remaining_sol,
remaining_usd, rule}` — emitting `unstake_sol` and the `lp_plan` open in
the same plan; the venue analyst surfaces one concrete range with a SOL
size when allowance remains and a pool qualifies. Rules that still bind:
floor at or above basis; bracket the *current* price (an out-of-range
position earns nothing, so never open into a band the price is already
outside of, and never pile SOL beside one); the per-transaction cap. The
worker **enforces** the ceiling on `lp_open` against the exec's live
position list (`mob_jobs/lp_core.rs`, 6 tests), so a position the officer
forgot still counts, and a breach is refused with the allowance, the
amount deployed and the remainder in the line.
- **LP is the primary yield channel for held SOL (owner policy 2026-09-07,
gitops `312caa7` + `9aa1783`).** The owner: *"the SOL/USDC pool works much
better than the yield from kamino — make it the main source of revenue and
increase the amount exposed to it."* Measured: ≈ 47 % realised on the
position's first day against 4 % staking and 4–5 % realised on Kamino.
Scaled the way the thesis allows, not by draining lending: the **SOL side**
comes from the JitoSOL core inside the LP core allowance (60 → **100 SOL**
of a ~147 SOL core, ≥ 40 staked as the liquid core) — every SOL in an
above-basis range is a continuous sale into strength; the **USDC leg** is
sized to what the range needs (≈ 25 % of the position or less) from the
float or an exact `lending_withdraw`, because below its range an LP converts
USDC into SOL on every dip, which is the premature accumulation the thesis
names as its primary failure mode — Kamino stays the home for stables. The
desk scales in **tranches across two or three ranges above basis** and
states each cycle what is deployed, the USDC leg, measured APR per position
and the tranche it adds or why not. LP total cap 15 → **30 % of NAV** as the
bootstrap (`values.worker.budget.maxLpTotalFrac` →
`ACCUM_LIMIT_MAX_LP_TOTAL_FRAC`; raised again to **0.40** on 2026-09-09,
nexus-gitops `f9d8965`, because a 0.30 bootstrap contradicted the owner's
40 % LP target if it were ever fallen back to) — and, the same afternoon (*"make this
more dynamic from the decision cycle"*), **decided each cycle** like the
exposure cap: `limits.lp_total_frac`, clamped into
`ACCUM_LIMIT_LP_TOTAL_{MIN,MAX}_FRAC` (prod 0.05 / 0.50), overlaid onto
`portfolio_limits` with `source: decision`, and shown to the officer as
`TREASURY.budget.lp_total_bounds` beside the cap in force.
Sleeve accounting changed to match: a held-coin LP's coin side counts
against the allowance, its USDC leg against the 20 % sleeve — **the USDC
leg clause was superseded on 2026-09-09** (next bullet), which charges it to
the LP's own 40 % share of NAV and never to the swing sleeve; idle SOL's
default home is an above-basis LP when a pool qualifies, JitoSOL the
fallback. The venue analyst surfaces up to three ranges with sizes.
**SOL the LP accumulated is sellable** (owner, same day): below its range
the position holds converted SOL as sleeve inventory at the conversion
price; when LP-held SOL exceeds the coin put in, or exposure runs past the
decided cap, the desk emits paired swing sells above that cost (and a core
sell proposal for more) — sells still page the operator (H3) — and never
sells what the pool will sell back itself once the price re-enters the
range.
- **LP USDC legs decoupled from the swing sleeve, and no standing stables
reserve (owner policy 2026-09-09)** — nexus-platform `944a30a` + `9835e03`,
nexus-gitops `f9d8965`. The owner: *"i want to decouple lp usdc legs so
40/40 for kamino and lps and a 20% for the swings"* and *"i don t need
reserve because for me kamino is the reserve and it s easy to withdraw or to
cancel orders to buy the ladder"*. An LP proposal's USDC leg is charged to
the **LP's own 40 % share of NAV**, never to the 20 % swing sleeve, which
budgets resting swing orders and swing inventory alone; the LP sleeve's only
cap is `limits.lp_total_frac`. Closing the LP gap is a **standing per-cycle
instruction** with a funding order: Kamino's excess first whenever lending
sits above its own 40 % share (the `lending_withdraw` and the LP open belong
in the same plan), then unstaking JitoSOL, then the shallowest ladder rungs
— and below-spot ranges are preferred, because they hold USDC and convert to
SOL only as price falls, closing the gap without buying SOL at a level the
thesis rejects. **Reserves are a float, not a stash:** there is no standing
stables reserve any more (Kamino is the reserve; a `lending_withdraw` or a
cancelled resting bid is the funding path), `ACCUM_MIN_FLOAT_USDC` went
$2,000 → **$750** ([Float floor](#float-floor)), and a float above a use the
officer can name counts as sleeping money in all four cycle prompts. The
measured book that prompted it and the full reasoning are in §4.
**Not yet in force at the time of writing:** the code and config are
deployed and the worker runs the new image, but the desk host had not been
restarted, so the running desk was still reading the previous prompts; the
doctrine takes effect at the next host restart (the scheduled
`session_rollover`, cooldown 1 500 s, every 6 h). No rebalancing action has
been taken and no LP position opened or closed because of it.
- **Staking routes**: `stake_sol` / `unstake_sol` quote the Jito stake pool
directly (`DepositSol` — 0% fee, no slippage; instant `WithdrawSol` — 0.1%
fee, bounded by reserve liquidity) **and** a Metis swap, and take the
better fill per chunk. Deposits keep the gas floor; everything lands in
EXEC's own accounts.
The first live outcome: on 2026-09-02 the decision after deployment moved
170.75 idle SOL into JitoSOL in two chunks and re-supplied Kamino; sleeping
money went from ~$78k (mid-recall) to vault dust.
## 7. Yield venues as built
| Venue | Status | Notes |
|---|---|---|
| **Kamino Lend** (USDC main + JLP reserves) | live, auto | supply-only cTokens; multi-reserve, discovered on-chain and scored (APY, depth, utilization); since 2026-09-05 the confirmed hygiene of the [owner lending policy](#owner-lending-policy) — recall at 95 % / liquidity below 10× on 2 reads (98.5 % hard), rotation only on a 2 pp spread sustained ≈ 24 h with a 7-day cooldown and 48 h hold; the position stays in the JLP reserve (≈ 11 %). Before: 92/88 single-read hysteresis, rotation ≥ 150 bps |
| **MarginFi** | read + withdraw only | deposit builder failed on mainnet (wrong discriminator); not a repark destination until its supply path is simulation-verified |
| **JitoSOL** (SPL stake pool) | live, auto | pool vs Metis dual route; balance never grows — value does |
| **Orca Whirlpools** | live, auto | official Rust SDK vendored; open/increase/decrease/collect/close; coin/USDC pools only |
| **Meteora DLMM** | live, auto | anchor-free codama client + hand-written position layer (bin math, bin-array coverage, resize, chunked add/remove/claim); venue stats API is down, pools are on-chain-verified but unranked |
| **Meteora single-asset vaults** | catalog only | scored from virtual price; **no executor** — operator proposals |
| **Jupiter Trigger** | live, auto (buys) / gated (sells) | self-custodial limit orders, escrow in an on-chain order account, re-verified against our node |
| **Metis** (self-hosted Jupiter) | live | quotes + swap transactions for clips, staking fallback, fallback DCA |
LP execution details: positions are opened as **one atomic legacy
transaction** when the instruction set fits (verified on mainnet: Orca
~974 bytes / 114k CU, Meteora ~1,134 bytes / 678k CU), otherwise split at
adapter-defined points and sent sequentially; every batch is simulated; the
position keypair the SDK mints is signed alongside EXEC. Ranges are aligned
to ticks/bins and the aligned range is reported. LP capital is folded into
`/v1/positions` as *earning* (value incl. unclaimed fees, APY from the
catalog), so it counts against the **LP sleeve budget** — the LP's own 40 %
share of NAV, not the 20 % swing sleeve, which since the owner policy of
2026-09-09 is charged for resting swing orders and swing inventory only (§4)
— and never reads as sleeping, as a *reported* number. There is no enforced
cumulative LP / protocol cap in the executor, only the per-transaction cap (audit **H5**);
the worker-side LP / protocol fractions (F6, §5) run in log mode, and the
audit's **L7** residuals — allowed-pool validation, fee return net of
impermanent loss vs hold, range-health alarms, inventory drift,
deterministic unwind — are not replaced by a fraction cap
([register → crosswalk](/docs/audits/2026-09-05-platform-audit#legacy-crosswalk)).
Five Orca SOL/USDC positions are open as of 2026-09-09 08:12Z — $17,823
total, all in range, 19.5 % of NAV — so the open and collect paths have run
on mainnet; the deterministic unwind has not been exercised, since no
position has yet been closed by an `unwind_if` clause firing. The LP catalog
measures Orca fee growth on-chain over a rolling ~24 h window
(`apr_onchain_pct`) beside the venue's 7d/24h APR.
## 8. Cycle health and self-healing
Built after the 2026-09-03 wedge (20 hours of half-blind analysis with an
empty brief every cycle while every status page said "ok"):
- The mob host records every terminal cycle (specialists expected/ok/missing,
brief ok); after **2 consecutive degraded cycles** its `/health` returns 503
and the Kubernetes liveness probe restarts it (fresh member sessions are the
known repair for a quarantined meerkat session). One Telegram page at the
threshold. Health rows persist as `accum_health` digests.
- The worker sweep compares each flow's newest persisted output with the
job's own cadence: **stale = older than 2× interval** → one page per
outage (`⏱ ACCUM CYCLE STALE`) and one on recovery; the analysis desk
silent for 3× cadence → pod restart. A Langfuse-trace watchdog covers the
silent case independently.
- `GET /v1/desk/accum-health` (core) aggregates flows, host self-report,
cycle records, pages and the decided reserves; the UI `/trading/cycles`
page and a **cycles pill in the header** read it.
- The blackout rule is enforced in code since 2026-09-05 (audit **H12**):
`accum_execute` refuses to execute a decision when the cycle-health verdict
is FAIL or the decision is older than `ACCUM_MAX_DECISION_AGE_SECS`
(default 43200 s = 2× the 6 h cadence), records a `⛔ BLACKOUT` digest and
re-judges on the next sweep. Scope since nexus-platform `eb1c464` (16:02
UTC, audit **F3**): evaluated once per sweep, before the standing hygiene
and the decision pass, by action class — risk-increasing movements
(rotate, repark, supply) are blocked, risk-reducing ones (recall) run as
`blackout_exempt` (§5). Until `eb1c464` it protected the decision pass
only.
The fleet-wide monitoring redesign (component health store, rule engine,
fleet grid, alert history) is **not built**; only this accumulation-specific
stack exists.
## 9. Operator surface
**Telegram:** `/pending`, `/approve <id>`, `/reject <id>`, `/basis <asset>
<price>`, `/cycles`, `/dreams`, `/decision`, `/portfolio`, `/history`; pages
for decisions, autopilot/hygiene reports, approvals, cycle health, sleeve
fills and the weekly digest.
**UI (`nexus-ui`):** `/portfolio` (treasury across venues, yield positions,
swing sleeve, on-chain orders, **sleeping money** net of the decided
reserves), `/trading/harvest`, `/trading/goals` (ladder + rung states),
`/trading/analysis` (briefs, theses, dreams, decisions), `/trading/lending`,
`/treasury` (NAV, benchmarks, the **What is earning** panel, P&L by
source with covered windows and disputed-figure badges —
[nexus-ui](/docs/repositories/nexus-ui#treasury-page)),
`/trading/liquidity` (LP + vault catalogs, **our LP positions** with range
bar, uncollected fees and vs-hold —
[nexus-ui](/docs/repositories/nexus-ui#lp-page)),
`/trading/oracle`, `/trading/oracle-health`, `/trading/cycles`,
`/trading/inspector`, `/trading/scope`, `/trading/ceremony` (Squads ceremony
builder — reachable by URL, not in the nav). The header shows treasury,
earning, next cycle, cycle health and the ARMED state.
## 10. API surface (nexus-solana)
`nexus-solana-exec` (`:8091`, in-cluster only):
| Group | Routes |
|---|---|
| status | `GET /health`, `/v1/status` (incl. `.policy`: gate caps, depeg band, control store, pause state — since `0913342`; `.intents` and `.budget_check` since `a1eb8f3`, live-verified 2026-09-05 23:33Z), `/v1/goals`, `/v1/ladder`, `/v1/reconcile`, `/v1/positions` and `/v1/lp/positions` (cache-served with `as_of` / `stale` / `venues_unreadable` and `?fresh` since `ea96739`), `/v1/sleeve` (`unresolved` + `unresolved_reasons[]` with `residual_usdc` / `summary` since `9c9d19f`); `GET /v1/intents/{id}` (the idempotency record of a caller-keyed action, `a1eb8f3`; `unsent_signatures[]` since `0641ea5`) |
| ladder | `POST /v1/ladder/cancel` — cancels idle/armed rungs (`Cancelled` terminal, persisted); `Firing` rungs are refused so capital in flight is never hidden (audit H13, deployed 2026-09-05); `POST /v1/ladder/reprice` — the only way rung triggers (and optionally rung budgets) change after boot, applied all-or-nothing and persisted before the reply: `Idle` / `Arming` rungs only (`Armed`, `Firing`, `Fired`, `Cancelled` are refused), each new trigger at least `LADDER_REPRICE_MIN_DISCOUNT_FRAC` (0.05) below the live oracle index, no single call raising a trigger by more than `LADDER_REPRICE_MAX_RAISE_FRAC` (0.25), the total ladder budget allowed to shrink but never grow, rungs still strictly descending afterwards, and an unreadable or suspended index refusing the whole request rather than repricing blind. Built and unit-tested (10 tests); the five refusal guards were exercised against the live SOL ladder 2026-09-09 03:3xZ (nexus-solana `6906f70`), each refusing with the ladder unchanged — the **accepting** path is still unexercised; `GET /v1/ladder/outbox` (+ `retry` / `abandon`) — off-flag it lists the legacy journal since `a1eb8f3` |
| every mutating route | optional `intent_id` + `fence` (audit F2, `a1eb8f3`, live-verified 2026-09-05 23:33Z; every platform caller sends them since `2d8eef1`): exactly-once under the caller's id, 409 `intent:mismatch` / `intent:stale_fence` / `intent:in_flight`, stored outcomes replayed with `replayed: true`. Since `0641ea5` a **preflight-rejected** send answers `{never_broadcast: true, maybe_broadcast: false, signature: null, unsent_signature}` and the record re-arms instead of replaying — [nexus-solana](/docs/repositories/nexus-solana#send-path) |
| sleeve | `POST /v1/sleeve/{order}/exit {"price"}` — operator-placed exit for a `filled_unpaired` fill; re-runs the basis+edge check, armed-only, CAS-locked (audit H2, deployed 2026-09-05) |
| lending | `GET /v1/lending/rates`, `/position`, `/discover`, `/catalog`; `POST /supply`, `/withdraw`, `/withdraw-all`, `/catalog/refresh` |
| swaps | `POST /v1/swap/quote`, `/build`, `/execute` |
| staking | `POST /v1/stake/quote`, `/deposit`, `/withdraw` (route `auto\|pool\|metis`) |
| reserves | `PUT /v1/reserves`, `GET /v1/reserves` |
| LP | `GET /v1/lp/pool`, `/positions`, `/catalog`; `POST /v1/lp/quote`, `/open`, `/remove`, `/collect`, `/close`, `/catalog/refresh` |
| vaults | `GET /v1/vaults/catalog`; `POST /catalog/refresh` |
| orders | `GET /v1/orders/open`; `POST /create`, `/cancel` (Jupiter Trigger) |
| squads | `GET /v1/squads/preflight`; `POST /fund-exec`, `/build-ceremony`, `/build-vault-lending`, `/build-vault-stake`, `/v1/submit-signed` |
`nexus-solana-oracle` (`:8090`): `GET /health` (feed freshness — readiness
only, since 2026-09-05, audit H8), `/v1/status`, `/v1/prices`
(per-asset index, Pyth/Metis/Coinbase legs, guard state and reason).
Background loops in the exec plane: **position cache** (every
`EXEC_POSITIONS_REFRESH_SECS`, default 30 s, floor 5 — since `ea96739`),
lending **rates** cache (`LENDING_CACHE_SECS`, 2 min — rates only now),
reconciliation (1 h), LP + vault catalog refresh (1 h), sleeve
fill-watcher (1 min), ladder trigger tick.
### What changed on the exec plane on 2026-09-06 (`ea96739`) \{#exec-2026-09-06}
Four money-path corrections, all deployed and live-verified that day. The
field-level detail is on
[nexus-solana](/docs/repositories/nexus-solana); this is what an operator
of HARVEST needs to know.
- **The sleeve reconciler stops inventing inventory** (`9c9d19f`, fixtures
`9a2f3b5`; live-verified ≈ 09:3x UTC). A **core sell** — an
operator/treasury sell of coin the sleeve ledger never held — and coin
**held in an LP position** were both being counted as sleeve inventory
gone missing from the wallet, so `unresolved` went true and the worker
refused **every new swing buy** for the morning. Two new explanation
kinds: `core_sell` (contributes 0 — it changes no expectation) and
`held_in_lp` (per position, releasing at most the ledger inventory that
position holds). New per-mint `held_elsewhere_qty` and
`non_sleeve_allowance_qty` (the EXEC gas float on the native-SOL pair,
forgiven only as a *surplus*), a top-level `lp_unreadable`, and
`unresolved_reasons[]` that carry `residual_usdc` and a one-line
`summary` instead of a bare flag. **An unreadable LP scan is
`unresolved`, never a silent zero** — an LP position the reconciler
cannot see is indistinguishable from coin missing from the wallet.
After the roll: `unresolved: false`, `all_explained: true`, SOL/USDC
explained by `core_sell` + `held_in_lp` (8.51 held elsewhere), residual
0.
- **Rent is rent, never the coin leg** (`4804e26`). The Jupiter Trigger
escrow is a wrapped-SOL account, so closing it at the fill of a 10 SOL
sell put the *sold coin* into the tx-boundary rent delta: −10.0055 SOL,
−$1,068, which the platform booked as a negative cost and turned a
+159.44 round trip into +1,226.46. `rent_lamports` is now nullable with
`rent_unattributed_lamports` + `rent_note` beside it, the known coin leg
is subtracted when doing so moves the figure toward zero, and anything
past `SLEEVE_RENT_SANITY_LAMPORTS` (0.1 SOL) is refused rather than
booked. The live core sell now reports −5,519,280 lamports of real
escrow + order-account rent, with `realized_pnl_usdc` unchanged.
- **One send path** (`7046ba8`). Blockhash, the node's send-time preflight
and the confirmation poll all run at `confirmed`. The client's default
was `finalized`, which cannot see a blockhash minted one slot ago at
`confirmed` — that is the `-32002 Transaction simulation failed:
Blockhash not found` that rejected the 02:37 UTC and 08:29 UTC lending
supplies. Applies to `finish_tx`, `finish_versioned_tx`,
`lp::finish_ix_set` and `/v1/submit-signed`; the Squads ceremony
builders mint their blockhash at the same commitment.
- **A preflight rejection settles the intent re-armable** (`0641ea5`). A
`-32002` / `SendTransactionPreflightFailure` is the one answer that
*proves* nothing was broadcast, so the route reports
`{never_broadcast: true, maybe_broadcast: false, signature: null,
unsent_signature}`, the intent record moves the signature to
`unsent_signatures` and re-arms, and the legacy ladder journal writes
`failed` instead of `submitted_unknown`. Everything else is still
ambiguous and stays `submitted`. **LP batches from index 1 on keep the
old replay behaviour** — an earlier batch has landed, so replaying the
set would spend twice.
- **A position cache with write invalidation** (`ea96739` + `d232263`).
`/v1/positions` and `/v1/lp/positions` are served from a snapshot
refreshed every 30 s and carry `as_of`, `stale` and
`venues_unreadable`; `?fresh` (`1` / `true` / `yes` / bare) forces a
synchronous scan. A **confirmed** lending supply / withdraw /
withdraw-all or LP open / remove / collect / close marks its venue dirty,
so the next read refreshes that venue before answering — NAV and the
budget gate read the aggregate, and it had been serving a pre-supply
figure while `/v1/lending/position` already showed the new one. Since
`d232263` every lending figure comes from one accessor, so the two can
no longer disagree. `/v1/lp/positions` went 9.5 s → **≈ 2 ms**, which is
also what unblocked the UI's LP page.
**Ladder execution path (audit C4 / F1).** As deployed the ladder runner
still fires a triggered rung from a detached task: the durable outbox is
**implemented through phase 2 but off (`EXEC_OUTBOX=0`)** in prod.
`db421a7` (deployed 2026-09-05 15:14 UTC) carries the hotfix (180 s
self-call client; ambiguous outcomes persisted as `submitted_unknown` *with
the signature*) and **phase 1** (`accum_outbox` rows keyed per rung and arm
epoch, one serial consumer, record-before-send CAS, boot reconciliation by
signature, cancel coordination). **Phase 2** (`12c9dc3`, docs `a74cc87`,
deployed 2026-09-05 17:46 UTC, flag off) adds: fill verification from tx meta —
`confirmed_unverified` when the realized amounts are off the quote by more
than `EXEC_OUTBOX_FILL_TOLERANCE_FRAC` (0.01) or the meta is still
unreadable after `EXEC_OUTBOX_META_MAX_ATTEMPTS` (10); `finish_tx` /
`finish_versioned_tx` answer a confirm error with
`{maybe_broadcast, signature, blockhash, last_valid_block_height}`; a rung short of float
recalls lending first with each recall signature recorded on the row before
broadcast; operator routes `GET /v1/ladder/outbox?state=&rung=&limit=`,
`POST /v1/ladder/outbox/{id}/retry {reason, force}` and
`…/abandon {reason}` (never from `leased`; retry from `submitted` needs a
two-endpoint unlandable proof, from `abandoned` / `confirmed_unverified`
needs `force`; every action lands in the row's `operator_actions[]`);
`/health` stays 200 with `{status, degraded, reasons, outbox}` while
`GET /v1/health/outbox` returns 503 when a row is `submitted` / `leased`
older than `EXEC_OUTBOX_STUCK_SECS` (1 800), any row is
`confirmed_unverified`, or the worker heartbeat is stale;
`EXEC_OUTBOX_ADOPT_LEGACY=1` adopts legacy-path `Firing` rungs for one
boot; the `accum_tranche_queue` writer is deleted (read-only for adoption).
**Regression while the flag is off, fixed in code** (owner
re-verification, F1): with the writer gone and `EXEC_OUTBOX=0`, the
legacy branch that fires rungs today had **no durable per-tranche outcome
journal** on `a74cc87` — its trace was the log line; `accum_ladder_state`
persists the machine state but not signatures / outcomes. **`918b8fb`**
(in `a1eb8f3`, live-verified 2026-09-05 23:33Z) restores it: the
flag-off path writes a `pending` row into `accum_outbox` with `source:
"legacy"` under the rung's fire key (`attempts: 1`, `max_attempts: 1`)
**before** the self-call, and settles it from the route's answer —
`confirmed` (signature, blockhash, `last_valid_block_height`, quote, fill
+ costs verified within `EXEC_OUTBOX_FILL_TOLERANCE_FRAC`),
`confirmed_unverified`, `failed`, or `submitted` with `worker_note:
"submitted_unknown"` when the answer was ambiguous. The signature is
recorded after the route returned (the note says so); a failed insert is
logged loudly and the fire goes ahead unchanged. **Behaviour change:** a
confirmed legacy fire now marks its rung `Fired` with the realized fill —
before, the spawn path never called `mark_fired`, so every legacy fire
left the rung `Firing` (the reason `EXEC_OUTBOX_ADOPT_LEGACY` exists); a
failed / unknown outcome still leaves it `Firing`, never re-fired. Off-flag
the journal is visible on `GET /v1/ladder/outbox` (`enabled: false,
journal: true`) and in the `/v1/ladder` / `/v1/status` outbox summary
(`{enabled: false, journal: "legacy", counts, legacy_rows,
submitted_unknown, unreconciled[], rows[], fired[]}`); retry / abandon
stay 503 (no worker). Turning the flag on needs nothing special: boot
reconciliation settles journal rows like worker rows (`confirmed` ⇒
`Fired`, `submitted` ⇒ chain verdict; a dead signature ends `abandoned`,
never auto-retried). Until the roll is verified, treat the legacy path's
outcome evidence as non-durable
([register → Known regressions](/docs/audits/2026-09-05-platform-audit#known-regressions)).
The pre-fire **budget hook** (`b0a00f1`) runs before `mark_firing` on
both paths — [§5 Budget ledger](#budget-ledger).
**Phase 3, open**: in-process prepare / broadcast / confirm split (no
self-call), crash / timeout / duplicate-submission rehearsal,
`EXEC_OUTBOX=1` after a dry-run soak (unarmed rows end `simulated`),
observed durable rows and restart recovery, retire the legacy spawn path
and the queue reader. Env
reference: `apps/nexus-solana-exec/README.md`; the cost fields those rows
carry are on [Measuring success → Costs](/docs/roadmap/measuring-success#costs).
## 11. Where the state lives
- `nexus.operator_digests` (kind-tagged rows): `accum`, `accum_thesis`,
`accum_dream[_json]`, `accum_decision[_json]`, `accum_execution`,
`accum_health`, `accum_health_alert`, `accum_approval[_done]`,
`accum_exec_done[_treasury]`, `accum_cost_basis`, `accum_dca_program`,
`accum_weekly`, `accum_trade`.
- `nexus.accum_approvals` (typed approval records, audit H3, since
2026-09-05; the gateway reads `NEXUS_MONGODB_DB`).
- `nexus.accum_nav_snapshots`, `accum_external_flows`, `accum_benchmarks`,
`accum_benchmark_config`, `accum_costs` (USDT NAV, audit §7, since
2026-09-05 17:12 UTC — [Measuring success](/docs/roadmap/measuring-success#as-built-2026-09-05);
since `ee0cd80` cost rows are keyed `_id = <kind>:<signature>:<field>`,
insert-once across the sleeve / outbox / route / `costs_today` feeds —
[Costs](/docs/roadmap/measuring-success#costs));
`accum_pnl_sources`, `accum_pnl_events`, `accum_internal_flows` (P&L by
source, deployed 2026-09-05 `3a91236` … `7d6ed59`, first rows at the
21:12 UTC snapshot —
[P&L by source](/docs/roadmap/measuring-success#pnl-by-source)).
- `nexus.accum_reservations`, `accum_portfolio_limits` (portfolio budget
gate, audit F6, since 2026-09-05 — [§5](#portfolio-budget));
`accum_budget_ledger` (`_id: "portfolio"` — the one CAS document with
`version`, `totals_usdc`, `reservations[]` with `state` / `lease` /
`fence`; nexus-platform `ee0cd80`, deployed 2026-09-05, live-verified 23:4xZ; seeded from
the live `held` rows on the first sweep — [§5](#budget-ledger)).
- `nexus.accum_hygiene_state` (`_id = lending`: per-reserve read history —
last 96 reads — and the move clocks of the confirmed lending hygiene;
nexus-platform `4fd5c4c` / `fdd7912`, live since 21:54 UTC —
[§5](#owner-lending-policy)).
- `bitview_desk.accum_*` (written by the exec plane): `accum_ladder_state`,
`accum_tranche_queue` (read-only since outbox phase 2 — the writer is
gone, the reader serves `EXEC_OUTBOX_ADOPT_LEGACY` and goes in phase 3;
on the running `a74cc87` this leaves the legacy fire path without a
durable outcome journal — fixed by the `accum_outbox` legacy journal in
`a1eb8f3`, live-verified 2026-09-05 23:33Z — [§10](#10-api-surface-nexus-solana)),
`accum_trades` (sleeve ledger), `accum_control` (pause / kill switch /
daily spend for the policy gate, since `0913342`), `accum_outbox`
(worker rows when `EXEC_OUTBOX=1` — still off in prod; from `a1eb8f3`
also the flag-off path's journal rows, `source: "legacy"`),
`accum_intents` (exec-side idempotency records `{_id: intent_id, kind,
params_hash, state, fence, signature, signatures[], unsent_signatures[],
attempts, outcome, …}`, Mongo TTL `EXEC_INTENT_RETENTION_SECS` 604 800 s;
`a1eb8f3`, live-verified 2026-09-05 23:33Z — audit F2;
`unsent_signatures[]` since `0641ea5`, the audit trail of a signature
that was minted and provably never broadcast — it survives the re-arm),
`accum_lending_catalog`, `accum_lp_catalog`, `accum_vault_catalog`.
- **In-process, not persisted:** the exec's position cache (`ea96739`) is
an in-memory snapshot with a dirty set — nothing to inspect in Mongo. It
is rebuilt at boot and refreshed every `EXEC_POSITIONS_REFRESH_SECS`;
after a restart the first read is a full synchronous scan
([§10](#exec-2026-09-06)).
- `nexus.scheduled_jobs`: the five `accum-*` jobs (interval, last/next run).
- Prompts and souls: `nexus-gitops/charts/nexus/files/mobs/accum-desk/`
(`mob.definition.json`, `registry-bundle.json`), mounted into the host as
a ConfigMap; the host loads the pack at start, so a prompt change needs a
host restart (the image bump does it).
## 12. What is missing
Grouped by what it would take. Each item is verified against the code.
**Custody and safety (highest value)**
1. **Squads spending limit** — never created; consequently no VAULT→EXEC
automation, no on-chain destination whitelist, no EXEC→VAULT sweep, no
on-chain approval gate for large tranches. Until then the worst case of an
EXEC-key compromise is the whole EXEC-side book, not one tranche.
2. ~~**Runtime pause**~~ — *closed 2026-09-05* (audit H1): pause and kill
switch live in `bitview_desk.accum_control` (or `EXEC_PAUSED=1`) and are
checked by the pre-sign policy gate; GitOps disarm remains the second,
independent stop.
3. ~~**USDC depeg halt, price bands, slippage caps**~~ — *closed 2026-09-05*
(audit H1): `harvest-guards` is now invoked at the signer boundary
(depeg band 100 bps, clip slippage / venue deviation, supply-only,
per-action and daily USD caps). **Still missing** behind the gate: swing
action cap/cooldown, bottom lock, a SOL↔JitoSOL clip guard. Tier / family
caps now exist on the worker side (item 12) — in log mode. These are
the audit's **L6** residual checks (with: schema validity never
authorizes a financial action; effective reserve floors — code 0.2 SOL /
5 000 USDC vs the desk's 0.5 SOL — verified across a restart) —
[register → crosswalk](/docs/audits/2026-09-05-platform-audit#legacy-crosswalk).
4. ~~**Sleeve ledger vs chain**~~ — *closed 2026-09-05* (audit **F5**;
**F4** deployed). Was: inventory/VWAP from the trade ledger plus an
"order account vanished ⇒ keeper filled" heuristic, no reconciliation
against on-chain balances; live-verified at the audit snapshot as 0.0521
cbBTC in the ledger vs ≈ 0.0000479 in EXEC (a filled 0.052 sell with no
parent buy link). Deployed 15:55 UTC (`2c0be5a`): fills proven from
transaction / token-delta data (`fill_proof: tx_meta`,
`reconciliation_required`, record-before-send), `GET /v1/sleeve/reconcile`
and a dry-run / apply repair route. The operator applied the repair at
16:20 UTC after reviewing the dry-run: orphan sell `ENJURa…` linked to lot
`4HAnsRgd…`, lot closed with 0.0001 cbBTC dust. Now: inventory empty,
`committed_usdc` = 10,537 (the open buy escrow only),
`all_explained: true`, `unresolved: false`. The worker keeps refusing new swing buys
whenever `unresolved` is set (nexus-platform `eb1c464`). The first
fill / cancel under the F4 proof path is still to be observed.
- ~~**Signer caller authentication and semantic transaction
validation**~~ — *closed 2026-09-05* (audit **S1**): `2c0be5a` +
`EXEC_AUTH_MODE=enforce` live since 16:27 UTC; see §5.
**Execution breadth**
5. **Vault executor** — single-asset vaults are catalogued but cannot be
entered; they remain operator proposals.
6. **LP on VAULT** — VAULT positions are read but not creatable (armed sends
are EXEC-only by design).
7. **MarginFi supply path** — read/withdraw only until the deposit builder
is fixed and simulation-verified.
8. **Clip engine** — rungs execute as a single 75 bps swap; no clip loop,
jitter, retry/backoff or quote-vs-received verification.
9. **v0 / lookup-table transactions for LP** — LP is legacy-only with adapter
split points; swaps and orders already use versioned transactions.
10. **Token-2022 transfer hooks** — unsupported in the Orca SDK; mitigated by
refusing non-treasury mints.
11. **Bottom lock** — no explicit freeze when deep rungs arm; the officer
enforces it in doctrine only. Since 2026-09-05 the budget gate (§5)
counts the armed ladder as a standing `ladder_rung` reservation, so with
`ACCUM_LIMIT_MODE=enforce` a fully armed ladder that exhausts the SOL
cap *does* pause swing buys and LP adds — the live dry run shows exactly
that (69 % vs 60 %). That is a side effect of the exposure cap, not a
bottom-lock rule, and it is **not enforced yet** (log mode). The
deep-bottom freeze stays an explicit **L6** residual check.
12. **Tail reserve, yield-tier caps** — *implemented 2026-09-05, log mode*
(audit **F6** / H5, nexus-platform `7e1adcb`): `min_liquid_reserve_usdc`
5 000 with a 10 % lending-recall haircut, per-protocol fractions
(kamino 0.70 / others 0.25), LP total 0.15, SOL / BTC family caps
0.60 / 0.40, single action 0.30 of NAV. **Enforce pending** an owner
decision on the ladder vs the SOL cap, **and** a fix for the
single-action cap: on 2026-09-05 20:36 UTC it flagged the hygiene
rotation of the existing 56 k Kamino float as
`would_block:single_action_cap` — same-funds rotations must be
exempted or the cap measured on net new exposure first
([§5](#owner-lending-policy)). The owner's re-verification adds that
the gate is **not an atomic global budget**: exec-plane rung fires,
direct execute-role requests and Telegram approvals sit outside the
worker-only projection, and two independent sweeps can reserve the
same headroom (no aggregate CAS across reservation ids) — aggregate
atomicity + exec-side coverage before enforce (register F6). LP
governance beyond the fraction cap is **L7** (§7). **Monthly yield
sweep** — still not implemented.
**Intelligence and learning**
13. **Forecast resolution and Brier scoring** — forecasts ship in every brief
but no resolver scores them and no calibration record is fed back; the
dream judge scores qualitatively.
14. **Benchmarks and NAV** — *largely closed 2026-09-05* (audit **§7**,
nexus-platform `ac38aa1`, first snapshot 17:25 UTC: 91,138 USDT): hourly
USDT NAV series, TWR / MWR, attribution, four frozen benchmarks (cash,
hold 50/50, weekly DCA, passive ladder), `GET /v1/desk/nav*`, the Nexus
Treasury dashboard and `nexus.financial` rules. **Still missing**: the
no-AI counterfactual (benchmark 5), external-flow auto-detection,
historical reconstruction, scenario tests, a coins-per-dollar KPI; and
the series has no history yet. The sleeve ledger's USDC PnL remains
**not** a portfolio return. See
[Measuring success](/docs/roadmap/measuring-success).
**P&L by source** — *deployed 2026-09-05 evening, first live rows at
the 21:12 UTC snapshot*
(nexus-platform `3a91236` … `7d6ed59`, gitops `b5b6079`): the owner's
"where are we winning money" ledger — per period
`ΔNAV − external flows = Σ sources + unexplained` with JitoSOL staking,
lending per venue, LP fees, swing round trips (realized / unrealized),
market, costs and an explicit residual; own movements recorded in
`accum_internal_flows` at the gate call sites; `GET /v1/desk/pnl*`;
dashboard row "Where the money comes from".
**Corrected 2026-09-06** (nexus-platform `2553748` … `63a2de3`,
deployed): a filled sell takes the exec's own `realized_pnl_usdc` when
the exec read the confirmed transaction, costs are positive and small
by construction, `rent_*` is skipped whole on native-SOL pairs, a
repair pass purges the poisoned stored rows, rolling windows say what
they cover instead of resolving to a flat zero, lending interest
reconciles against its own balances, an LP position is emitted as its
legs, and a window-independent **`earning`** block answers "what is
making money right now". The round trip that read +1,226.46 reads
**+159.40**; `costs:tx` +1,068 reads **−0.0005**.
**Still missing**: a
cumulative lending-interest figure (the exec exposes no cToken qty /
exchange rate — nexus-solana gap), a `costs:slippage` source and a
pre-ledger bucket for the periods before the flow ledger existed (both
**specified only**, no code — [the residual](/docs/roadmap/measuring-success#residual-2026-09-06)),
an alert rule on the residual, and enough
live periods to judge anything (the first periods keep a known gap
from the 17:37 UTC Kamino rotation). The owner's re-verification
qualifications —
in-scope NAV, manual flows, TWR period-end approximation, cost
ingestion incomplete — are on
[Measuring success → Measurement qualifications](/docs/roadmap/measuring-success#measurement-qualifications).
See [P&L by source](/docs/roadmap/measuring-success#pnl-by-source).
15. **Two-speed cadence** — one fixed 30-minute analysis cadence on one model
set; no light/deep tiering or event-driven wake.
16. **Ladder proposals** — *partly shipped 2026-09-09* (nexus-solana
`apps/nexus-solana-exec/src/ladder.rs`; nexus-platform
`apps/nexus-worker/src/mob_jobs/ladder_watch.rs` and
`apps/nexus-telegram/src/approval.rs`; the accum-desk mob definition in
nexus-gitops). Was: `ladder_alert` / `ladder_proposal` had been emitted
for months and consumed by no code. Now all three terminal cycles vote —
analysis and dream through `ladder_alert.triggered` / `assets` /
`reason`, the decision officer through `ladder_proposal.proposed` /
`changes[]` (`{rung, trigger, budget_usdc?}`), the only source of
concrete rung numbers. The scheduled `accum_ladder` job posts one
`📊 ACCUM LADDER` panel per terminal cycle (idempotent on the newest
`accum_health` digest) and counts the votes under
`ConsensusPolicy::from_env`: `ACCUM_LADDER_CONSENSUS_WINDOW_H` (24),
`ACCUM_LADDER_CONSENSUS_MIN_ANALYSIS` (**2** since 2026-09-24, was 3),
`ACCUM_LADDER_CONSENSUS_NEED_DREAM` (true),
`ACCUM_LADDER_CONSENSUS_NEED_DECISION` (true). When every gate passes
*and* concrete numbers exist for the asset, the job **applies the moves
itself** against `POST /v1/ladder/reprice` (owner 2026-09-24: "fully
auto"; `ACCUM_LADDER_AUTO_APPLY=0` restores the `ladder_raise` Telegram
approval and `/approve <id>`), idempotent on the rung+price set
(`accum_ladder_applied` digest) and paged as `🪜 LADDER MOVED` /
`LADDER MOVE REFUSED`. The exec still re-checks its own bounds (§10) —
≥ 5 % under the index, a raise of at most **95 %** per call
(`LADDER_REPRICE_MAX_RAISE_FRAC`, was 25 %), no added capital,
descending rungs — and applies all-or-nothing. Because a rung parked
at $48 cannot reach $100 in one call, the job **stages** a proposal:
each rung is clamped to current × (1 + `ACCUM_LADDER_STAGE_RAISE_FRAC`,
0.95), rungs are kept descending, and the desk's next proposal takes
the next step (the first live run, 2026-09-24 07:30Z, moved the BTC
rungs to 79,000 / 76,800 / 74,100 / 70,000 and had the SOL request
refused over S3 +109 % before staging existed).
**Still open**: the policy is a flat consensus count, not the
stance-weighted "weeks at `at_risk`" graduated ladder the design
describes; forecasts remain unscored by a resolver (item 13), so a
vote carries no calibration weight; and nothing shifts deep-rung budget
toward DCA as the design's `at_risk` row proposes. The whole path is
unit-tested (10 + 12 tests). Live state on deploy day (nexus-solana
`6906f70`, nexus-platform `d8fbf68` + `85ea914`, nexus-gitops
`c8718e0`): the panel is **verified in production** — the first one
posted 2026-09-09 03:30Z for the 02:59Z analysis cycle — and all five
refusal guards were exercised against the live SOL ladder at 03:3xZ
(too close to spot, over the raise cap, crossing rungs, adding
capital, missing reason), each refused with the ladder unchanged.
**No rung has actually been moved**: no consensus has yet formed, so
no approval has been raised, and the accepting path — an owner
`/approve` that reprices real rungs — is still unexercised.
17. **Agent tools** — the desk sees pre-fetched JSON only; no `accum.*` MCP
tools to query state mid-cycle.
**Infrastructure**
18. **RPC failover** — *partial 2026-09-05* (audit **O2** / H8): the exec
runs two endpoints since the 15:5x UTC gitops batch (`208f20a`) — the
dedicated node plus `public=https://api.mainnet-beta.solana.com` in
`SOLANA_RPC_URLS`, priority failover in `RpcChain`; the
`solana-rpc-pool` crate stays unwired (item 21). Failover has never
been exercised (no outage induced), and the **L9** oracle-integrity
checks — account owner, discriminator, expected feed id, full
verification, confidence, slot lag — remain open; a second endpoint or
an `ok` price agreement proves none of them
([register → crosswalk](/docs/audits/2026-09-05-platform-audit#legacy-crosswalk)).
19. **Fleet monitoring** — no component-health store, rule engine, fleet
grid or alert history; the cycle-health stack covers only the accumulation
flows.
20. **Meteora stats** — the public DLMM stats API is down and DLMM has no
fee-growth counters, so Meteora pools cannot be ranked by measured APR.
21. **Dead crates** — `harvest-allocator` and `solana-rpc-pool` are compiled
but never called; either wire or delete (`harvest-guards` is live via the
policy gate since 2026-09-05).
22. **Meerkat session store** — the SQLite lock → quarantine failure is in an
upstream dependency; the host self-heals around it (§8) rather than
fixing it.
## 13. Doctrine changes since the design spec
| Spec said | As built | Why |
|---|---|---|
| VAULT holds all yield positions; EXEC holds one tranche | EXEC holds the working treasury and every automated position | automation-first custody (2026-08-29); spending limit not yet created |
| The agent layer never executes | The desk's decision JSON is auto-executed within caps; desk-proposed sells/CEX/fallback are human-gated; sleeve paired exits are code-gated by the basis+edge rule (audit H2) | the desk is the decision-maker; code is the guard |
| Tier 2 LP = SOL/cbBTC, JitoSOL/SOL, capped, opt-in | coin/USDC pools (SOL/USDC, cbBTC/USDC) on Orca + Meteora, decided per cycle by doctrine, no hard fraction cap | an LP is an automated swing; since the owner policy of 2026-09-09 it counts inside the LP's own 40 % share of NAV (`limits.lp_total_frac`), not the 20 % swing sleeve |
| Allocator crate with hot/warm/cold ladder and recall-before-act | Executor hygiene (recall/rotate/repark) + **decided reserves** + prompt doctrine; since 2026-09-05 confirmed and rate-limited under the [owner lending policy](#owner-lending-policy) (yield-first, rare moves) | the officer sizes the float; code sweeps the rest; one read never moves money |
| Fallback DCA: default-on, silence = proceed | Activation requires `/approve`; the approval cancels the unfired rungs first (audit H13), then clips run on schedule | approve-in is the safer default for a market buy program |
| Hourly light / daily deep cadence, 1d–3m Brier-scored forecasts | 30 min / 2 h / 6 h fixed cadence; 1d/1w/1m forecasts, qualitatively scored | scoring pipeline not built |
| Squads 2-of-3 | 1-of-2 (both owner hardware wallets) | ceremony outcome |
| Accumulate for a $40–60 SOL bottom; ladder = insurance; owner approves every rung move | **2026-09-24:** grow dollar NAV while SOL/BTC rise; the hourly regime dictates the book (regime flip ⇒ decision within the hour); pools are the default capital home; the ladder is a tool whose rungs the desk consensus moves itself; LP cap 0.60; Kamino recall exempt above 20× liquidity; an unknown `mandate` key is a contract *note*, not a rejection | owner instruction 2026-09-24 ("fully auto … NAV is now the main goal … the cycle dictates the behavior of the portfolio") |
## Related
- [HARVEST target spec](/docs/roadmap/harvest) — the design this was built from
- [Accumulation desk](/docs/roadmap/accumulation-desk) — desk design
- [Monitoring & alerting](/docs/roadmap/monitoring) — fleet monitoring design (cycle health built, rest not)
- [nexus-solana](/docs/repositories/nexus-solana) — the chain plane
- [Solana DeFi](/docs/integrations/solana-defi) — protocols
- [Measuring success](/docs/roadmap/measuring-success) — the USDT objective, NAV and benchmarks (deployed 2026-09-05; benchmark 5 and scenario tests not built)
- [2026-09-05 platform audit — status register](/docs/audits/2026-09-05-platform-audit)