Skip to main content

nexus-solana

Status: live (armed on mainnet since 2026-08-29). The Solana venue project for the HARVEST accumulation module. Everything Solana lives here — protocol bindings (Kamino, MarginFi, Jupiter Metis + Trigger, the Jito stake pool, Orca Whirlpools, Meteora DLMM, Squads), the price plane (Pyth + Metis + Coinbase with a divergence guard), and transaction signing. nexus-platform keeps zero chain code; its worker drives this service over HTTP and owns scheduling, the desk, execution policy caps and Telegram.

One repo, two deployables — the security boundary is which pod mounts the key secret, not which git repo the code lives in:

BinaryRoleKeys
nexus-solana-oracle (:8090)read-only price plane: Pyth primary, Metis and Coinbase legs, quorum/outlier/divergence guard, guard-gated index per assetnone
nexus-solana-exec (:8091)signing plane: lending, swaps, staking, LP positions, Jupiter Trigger orders, Squads ceremony builders, reconciliation, catalogsEXEC hot key only (nexus-exec-key secret)

Both deploy via nexus-gitops as cluster-internal Deployments (no ingress); CI builds sha- tagged images and bumps the prod values file (CI-only, .cargo and *.md changes no longer roll the armed executor — paths-ignore since db421a7). Arming is two-valued (mode: live + arm: true); unarmed, every mutating endpoint simulates and returns the unsigned transaction.

Deployed 2026-09-05: 0913342 (live-verified via /v1/status.policy) — the pre-sign PolicyGate (audit H1: the key is owned by the gate and every signing site calls authorize(); runtime pause/kill via bitview_desk.accum_control, USDC depeg band 100 bps fail-closed, caps EXEC_MAX_ACTION_USDC 25 000 / EXEC_MAX_DAILY_USDC 250 000, exit policy min_edge 0.006 + fee 0.002, clip guards), POST /v1/ladder/cancel (H13), the paired-exit rule with filled_unpaired and POST /v1/sleeve/{order}/exit (H2), oracle /health = feed freshness (H8), RPC-error-is-never-a-fill and CAS-gated fills (C3). Rolling: db421a7 — the C4 hotfix (180 s self-call client, ambiguous outcomes persisted as submitted_unknown with signature) plus the durable ladder outbox behind EXEC_OUTBOX=0 (implemented and tested, off by default). Deployed 15:55 UTC: 2c0be5a — caller authentication (EXEC_API_KEYS / X-Nexus-Exec-Key, EXEC_AUTH_MODE) and semantic transaction validation in the gate (audit S1), sleeve fill proof and the ledger reconciler + repair (F4/F5). EXEC_AUTH_MODE=enforce live since 16:27 UTC (nexus-gitops 29c983a, live-verified: no key → 401, read key on POST → 403, 0 caller denials); the F5 repair was applied at 16:20 UTC and the sleeve reconciles cleanly. Deployed 2026-09-05 17:46 UTC, live-verified: c483bbd (execution-cost capture for the NAV model — fee_lamports / priority_fee_lamports / rent_lamports / realized quantities on every confirmed send, /v1/status.costs_today), 12c9dc3 (ladder outbox phase 2: tx-meta fill verification, operator routes, /v1/health/outbox, legacy adoption) and a74cc87 (the exec README). EXEC_OUTBOX is still 0 in prod — the phase-2 code is deployed, the outbox path is not enabled. Env reference for both subsystems: apps/nexus-solana-exec/README.md in the repo.

Deployed 2026-09-05, live-verified 23:33 UTC as a1eb8f3: 918b8fb — the flag-off ladder path journals every fire into accum_outbox (source: "legacy") and marks a confirmed rung Fired (audit F1 regression, below); 23c083b — exec-side idempotency + fencing on every mutating route (accum_intents, intent_id / fence, audit F2, below); 83bb7d8 — receipt bounds for Kamino cTokens, JitoSOL and LP remove / close, post-state verification for MarginFi and LP open (audit S1, below); b0a00f1 — the pre-fire budget hook reading Core's GET /v1/desk/budget.verdict (audit F6, below); a1eb8f3 — the README. Two behaviour changes to know before the roll: a confirmed legacy fire now ends Fired instead of sitting in Firing forever, and the budget hook runs before mark_firing (log-only until Core's verdict.mode says enforce). That batch was live-verified 2026-09-05 23:33 UTC: auth enforce 14/0/0, not_enforced = the seven documented checks, accum_intents on mongo:accum_intents (TTL 90 s, retention 7 d), budget_check enabled against http://nexus-core, outbox off with journal: legacy (0 rows), 9 rungs idle, 0 restarts.

Deployed 2026-09-06 as ea96739 (exec + oracle; gitops 2e9decf writes the tag). Five changes, each written against a defect that had been observed live that morning — the money-path detail is in the sections below:

RevisionWhat it changesVerification
9c9d19f (+ 9a2f3b5, fixtures)the sleeve reconciler stops inventing inventory the sleeve never bought: explanation kinds core_sell and held_in_lp, per-mint held_elsewhere_qty / non_sleeve_allowance_qty, top-level lp_unreadable — Reconcilerlive-verified 2026-09-06 ≈ 09:3x UTC: unresolved: false, all_explained: true, SOL/USDC explained by core_sell + held_in_lp (8.51 held elsewhere), residual 0 — swing buys unblocked
4804e26rent is rent, never the coin leg: rent_lamports is Option<i64> with rent_unattributed_lamports / rent_note and a sanity bound — Rentthe live core sell's reported rent went from −10.0055 SOL / −$1,068 to −5,519,280 lamports (escrow + order-account rent); realized_pnl_usdc unchanged at 159.4367
7046ba8one send path: blockhash, preflight and confirmation poll all at confirmed — The send paththe -32002 … Blockhash not found that rejected two lending supplies (02:37 UTC, 08:2x UTC) does not recur; a 995.55 USDC supply confirmed at 09:0x UTC
0641ea5a preflight-rejected send settles re-armable, not replayed — The send pathunit-pinned; no live preflight rejection observed since
ea96739 (+ d232263)background-refreshed, write-invalidated position cache; one lending accessor — Position cache/v1/lp/positions 9.5 s → ≈ 2 ms; the aggregate can no longer serve a pre-write lending figure
HEAD (2026-09-07)asymmetric LP deposit bounds: POST /v1/lp/open takes coin_slippage_bps (coin leg only; not in the intent params hash), the vendored Orca SDK gains open_position_instructions_with_bounds — LP deposit bounds15:14 UTC: a symmetric 100 % bound wrapped 60 SOL for a 30 SOL leg against 30.29 held and the open failed simulation; the same intent re-armed under the split bound

Layout (as of 2026-09-04)​

nexus-solana/
apps/
nexus-solana-oracle/ # price plane
nexus-solana-exec/ # REST signing plane: main.rs (routes, lending, swaps,
# squads, reconcile, positions), stake.rs (Jito pool),
# lp.rs (LP endpoints + tx batching), ladder.rs, sleeve.rs,
# outbox.rs (durable ladder outbox, EXEC_OUTBOX), costs.rs
# (TxCosts vocabulary) — README.md is the env reference
crates/
harvest-domain/ # config (ladders, swing), treasury + ladder types
harvest-engine/ # ladder trigger state machine (confirm window, fire-once)
harvest-guards/ # deploy caps, bands, depeg, swing caps — COMPILED, NOT CALLED
harvest-allocator/ # §15 invariants — COMPILED, NOT CALLED
solana-keys/ # EXEC keypair custody (mounted secret, exec pod only)
solana-oracle/ # Pyth PriceUpdateV2 reader + plane/guard
solana-swap/ # Metis quote/build client; trigger.rs = Jupiter Trigger orders
solana-lending/ # Kamino/MarginFi via the vendored Lending trait; on-chain discovery
solana-lp/ # LP + single-asset vault CATALOGS, on-chain APR measurement (read-only)
solana-clmm/ # LP POSITION EXECUTION: LpVenue trait, orca.rs, meteora/{pda,bins,
# commons,open,plan,state}.rs — Orca Whirlpools + Meteora DLMM
solana-squads/ # Squads v4: vault address, ceremony builders (spending-limit
# path written, never verified on mainnet)
solana-rpc/ # multi-endpoint pool — UNWIRED (both binaries use one endpoint)
vendor/
prochain-lending-* # Kamino/MarginFi lending family (prochain-amm-server 16e170d)
prochain-external-client# SLIM RPC-only shim replacing the fleet original
orca-whirlpools/ # official Orca Rust SDK 5.0.1 (client/core/macros/whirlpool)
meteora-dlmm/codama # anchor-free codama DLMM client (solana-* pinned to 2.x)
prochain-{transaction,ata-cache,jup-client,solana-service} # copied, unwired

What each plane does​

Exec plane background loops: lending rates + positions cache (2 min), reconciliation of EXEC + VAULT balances and prices (1 h), LP + vault catalog refresh with rolling on-chain APR (1 h), sleeve fill-watcher for Trigger orders (1 min), ladder trigger tick against the oracle plane.

Execution pipeline (every mutation): build instructions → simulate on our own node (sigVerify off, fresh blockhash) → refuse on any simulation error → when armed, sign with EXEC (plus any keypair the venue SDK minted, e.g. an Orca position NFT) → send_and_confirm. Swaps and Trigger orders use versioned transactions from Metis/Jupiter; lending, staking and LP build legacy transactions (LP opens run as one atomic transaction when they fit the 1232-byte limit, otherwise split at adapter-defined points and sent sequentially).

Custody invariant: every account the exec plane creates or funds — lending receipts, JitoSOL, LP positions, Trigger escrows, token accounts, rent refunds — is owned by EXEC. Armed sends refuse any other payer.

REST surface (nexus-solana-exec)​

GroupRoutes
statusGET /health (always 200 while serving: {status: "ok"|"degraded", degraded, reasons[], outbox{enabled, degraded}} — the kubelet probe), /v1/status (incl. .policy since 0913342, .auth since 2c0be5a, .costs_today since c483bbd; .intents and .budget_check since a1eb8f3, live-verified 2026-09-05 23:33Z), /v1/goals, /v1/ladder (machines + outbox{counts, head, rows[], fired[] with realized fill + costs, health}; off-flag since a1eb8f3 outbox{enabled: false, journal: "legacy", counts, legacy_rows, submitted_unknown, unreconciled[], rows[], fired[]}), /v1/reconcile (balances, prices, decided reserves), /v1/positions (lending + LP, totals; since ea96739 cache-served with as_of / stale / venues_unreadable and ?fresh — Position cache), /v1/sleeve (incl. unresolved + unresolved_reasons[] carrying residual_usdc / summary since 9c9d19f; filled recent_trades[] carry the cost fields, with rent_lamports nullable since 4804e26)
intentsGET /v1/intents/{id} (read role) — the accum_intents record of a caller-keyed action: {_id, kind, params_hash, state: prepared|submitted|confirmed|failed, fence, signature, signatures[], attempts, outcome{status, body}, error, created_at, updated_at, submitted_at, settled_at} (a1eb8f3, live-verified 2026-09-05 23:33Z — Idempotency + fencing)
healthGET /v1/health/outbox (read role) — 503 {status: "degraded", reasons[]} when, with EXEC_OUTBOX=1, a row is submitted / leased older than EXEC_OUTBOX_STUCK_SECS (1 800), any row is confirmed_unverified, or the worker heartbeat is older than 3 × EXEC_OUTBOX_POLL_MS; 200 {enabled: false} while the flag is off. Alert on this, not on /health, which never takes the pod out of Service for a degraded outbox (phase 2, 12c9dc3, rolling)
ladder / sleevePOST /v1/ladder/cancel (audit H13); POST /v1/ladder/reprice (2026-09-09 — the only way a rung's trigger changes after boot; Ladder reprice); POST /v1/sleeve/{order}/exit (audit H2); GET /v1/sleeve/reconcile, POST /v1/sleeve/reconcile/repair (dry-run / apply, audit F5) — the cancel, exit and reconcile routes deployed 2026-09-05
ladder outbox (phase 2, deployed 17:46 UTC; {"enabled": false} while EXEC_OUTBOX=0 — from a1eb8f3 {"enabled": false, "journal": true} with the legacy journal's rows, {"enabled": false, "journal": false} without Mongo)GET /v1/ladder/outbox?state=a,b&rung=S1&limit= (newest first, 400 on an unknown state; retry / abandon answer 503 while the flag is off — there is no worker to act); POST /v1/ladder/outbox/{id}/retry {"reason", "force"} → pending — from submitted only with a completed two-endpoint unlandable proof, from abandoned / confirmed_unverified only with force: true (a confirmed_unverified retry spends the rung again), never from leased, never for a Cancelled rung; POST /v1/ladder/outbox/{id}/abandon {"reason"} → abandoned from pending (or submitted with the proof), releasing the rung Firing → Cancelled. Every action appends operator_actions[{at, action, caller, reason, force, from_state, to_state}]; earlier signatures move into errors[], never dropped
lendingGET /v1/lending/rates, /position, /discover?venue=&mint=, /catalog; POST /supply, /withdraw, /withdraw-all, /catalog/refresh
swapsPOST /v1/swap/quote, /build, /execute
stakingPOST /v1/stake/quote, /deposit, /withdraw — Jito pool DepositSol/WithdrawSol vs Metis, route: auto|pool|metis
reservesPUT /v1/reserves (worker pushes the decision officer's float/gas), GET /v1/reserves
LPGET /v1/lp/pool?venue=&pool=, /positions (cache-served since ea96739: as_of / stale / venues_unreadable, ?fresh; a write on /open, /remove, /collect, /close marks the venue dirty), /catalog; POST /v1/lp/quote, /open, /remove (bps), /collect, /close, /catalog/refresh
vaultsGET /v1/vaults/catalog; POST /catalog/refresh
ordersGET /v1/orders/open; POST /create, /cancel (Jupiter Trigger)
squadsGET /v1/squads/preflight; POST /fund-exec, /build-ceremony, /build-vault-lending, /build-vault-stake; POST /v1/submit-signed (relay an owner-signed ceremony)

Every route except GET /health requires X-Nexus-Exec-Key (EXEC_AUTH_MODE=enforce since 2026-09-05 16:27 UTC): execute-role keys (worker, telegram) for the POST/PUT routes, read-role keys (core, ui) for the GETs — 401 without a key, 403 on a role mismatch.

Ladder reprice​

POST /v1/ladder/reprice (src/ladder.rs, 2026-09-09) is the first and only way rung trigger prices change after boot. Rungs come from harvestConfigYaml at boot and are then overwritten by the persisted Mongo state on hydrate, so editing values.yaml and restarting does not change them; before this route the ladder's only write path was POST /v1/ladder/cancel.

{ "asset": "SOL",
"changes": [ {"rung": "S1", "trigger": 72.0, "budget_usdc": 4500.0} ],
"reason": "...",
"expect_total_budget_usdc": 37500.0 }

budget_usdc is optional (omitted keeps the rung's current budget) and expect_total_budget_usdc is optional — it pins the total the approver agreed to. reason is required. The answer is a RepriceReport: {asset, applied[{rung, from_trigger, to_trigger, from_budget_usdc, to_budget_usdc, arming_reset}], refused[], index_price, total_budget_usdc, reason}. Auth is the standard execute-role X-Nexus-Exec-Key.

Every guard lives in the pure plan_reprice planner and the whole request is all-or-nothing — one bad row changes nothing, because a half-applied ladder is a shape nobody approved:

GuardRule
discount from indexa new trigger must sit at least LADDER_REPRICE_MIN_DISCOUNT_FRAC (default 0.05) below the live oracle index; an unreadable or suspended index refuses the whole request rather than repricing blind
raise ceilingone call may not raise a trigger by more than LADDER_REPRICE_MAX_RAISE_FRAC (default 0.25); lowering is unbounded — it only defers the spend
budget directionthe total ladder budget may shrink, never grow: moving where the ladder buys is not permission to spend more
orderingrungs must stay strictly descending after the move, counting untouched rungs; terminal (fired / cancelled) rungs are excluded from the ordering check but keep their budget in the total
rung stateArmed, Firing and Fired / Cancelled rungs are refused (decided, committed, spent); only Idle and Arming may move
arming resetan Arming rung whose trigger moves resets to Idle — the confirm window must be observed continuously against one trigger

State is persisted before the reply, so a restart cannot forget an approved move. 10 unit tests cover the guards. The caller in practice is the Telegram bot's ladder_reprice approval action (nexus-telegram), raised by the worker's accum_ladder consensus job. All five refusal guards were exercised against the live SOL ladder on 2026-09-09, each refusing with the ladder unchanged; the accepting path has not run — no rung has been moved.

LP deposit bounds​

POST /v1/lp/open sizes the position from the coin leg (coin_amount) and derives the USDC leg from the pool price; slippage_bps is the tolerance the chain enforces on both legs (token_max_a / token_max_b). Near a range floor that is the wrong shape: the USDC leg swings by tens of percent per quarter-percent of price, so the worker derives a loose USDC bound from the officer's usdc_side (up to 100 %) — but the Orca wrapper moves coin × (1 + bound) into the WSOL account before the deposit, so a loose bound applied to the coin leg asks the wallet for coin it does not hold. On 2026-09-07 15:14 UTC a 30 SOL open with a 100 % bound wrapped 60 SOL against 30.29 held and refused in simulation (simulation failed — refusing to send).

Since 2026-09-07 the request also takes coin_slippage_bps (optional):

FieldMeaning
slippage_bpstolerance on the USDC leg (and on the coin leg when coin_slippage_bps is absent)
coin_slippage_bpstolerance on the coin leg alone; the worker sends what the wallet can wrap — (held above reserves − 0.02) / coin − 1, clamped to [0, slippage_bps] (lp_core::coin_bound_bps)

The coin bound is not part of the intent's params hash (it changes the protective bound, not the action), so a failed-without-signature open re-arms under the split bound with the same intent_id (rule 5 of the intent store). The response carries bounds_bps: {usdc, coin}; on a refusal the worker now prints the signer's simulation.err and the last log lines in the digest. The worker records the fill from the response's quote (coin_est / usdc_est), never the officer's offer — the first open under the split bound (15:46 UTC, 30 SOL, offer 2 000, fill ≈ 673 USDC) had gone into the ledger at the offer, $5,099 for a $3,772 deposit; that row was corrected from the EXEC wallet delta. Implementation: the vendored Orca SDK gained open_position_instructions_with_bounds(…, (max_bps_a, max_bps_b), …), which re-derives one side's token_max_* from its estimate; the adapter maps the coin side to a/b from the pool's mint order. Meteora DLMM deposits are bin-exact on both sides and ignore the field.

Idempotency + fencing on every mutating route (audit F2)​

nexus-solana 23c083b (in a1eb8f3, live-verified 2026-09-05 23:33 UTC). /v1/swap/execute, /v1/orders/create|cancel, /v1/lending/supply|withdraw|withdraw-all, /v1/stake/deposit|withdraw, /v1/squads/fund-exec and /v1/lp/open|remove|collect|close accept two optional request fields:

FieldMeaning
intent_idthe caller's durable decision id — Telegram approval id, worker plan id, outbox row id. When present the semantic TxIntent takes this id (so the policy / semantic reports name it) and the action is exactly-once under it
fencemonotonic per intent, from the caller's lease (the budget ledger's fence is the natural source); a request whose fence is below the stored one is refused

One record per id in accum_intents (GET /v1/intents/{id}), retained EXEC_INTENT_RETENTION_SECS (Mongo TTL on expire_at; unrelated to the 90 s EXEC_INTENT_TTL_SECS freshness the semantic gate enforces). The three signing choke points (finish_tx, finish_versioned_tx, lp::finish_ix_set) drive it: begin before the transaction is inspected, the prepared → submitted CAS with the signature after authorize and before the broadcast (a refused CAS — re-armed or fenced by a newer request — drops the signed transaction unsent), settle afterwards. A request whose intent_id already exists gets, in order:

ConditionAnswer
different params hash (the economically relevant fields, canonicalized)409 intent:mismatch
fence below the stored fence409 intent:stale_fence
confirmed, or failed with a signature (an LP batch that failed after batch 0 landed)the stored outcome — same status and body, replayed: true + intent{…}; nothing is signed
submitted (signature recorded, outcome unknown / maybe_broadcast), or prepared younger than EXEC_INTENT_TTL_SECS409 intent:in_flight (with the signature when known — reconcile on-chain)
failed without a signature (refused before the key was touched), or prepared older than the TTL (a crash before the sign CAS)re-armed and executed again (attempts + 1) — nothing was ever signed; an older call still running fails its own sign CAS and drops its tx

An unarmed dry run leaves the record failed without a signature (re-executable). withdraw-all binds one id per leg ({intent_id}#{reserve}); an LP batch set keeps one claim and appends every batch signature to signatures[]. /v1/submit-signed and /v1/sleeve/{order}/exit take no fields; requests without intent_id are untouched (the ladder's outbox rows are their own record). The ambiguous submitted state is not reconciled automatically — the caller or an operator settles it from the signature on GET /v1/intents/{id}. Since nexus-platform 2d8eef1 (2026-09-05 23:5xZ) every caller sends the fields: Telegram steps as approval:<id>:<step> with the H3 claim generation as fence (bumped on claim, retry and takeover), worker sites as the F6 reservation id + claim fence (or <decision_ts>:<action>:… + sweep-start ms when unbudgeted); replayed and refusals are counted (nexus_exec_intent_replays_total{caller}, nexus_exec_intent_refusals_total{caller,code}). A live replay has not been observed yet (register F2).

Legacy journal — the flag-off ladder path (audit F1)​

nexus-solana 918b8fb (in a1eb8f3, live-verified 2026-09-05 23:33Z). Until EXEC_OUTBOX=1 the spawn-per-rung path is the live executor; from this revision every attempt it makes is journaled in accum_outbox — evidence, not a gate: a pending row with source: "legacy" (attempts: 1, max_attempts: 1) under the rung's fire key is inserted before the self-call, and settled from the route's answer afterwards (confirmed with 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"). The signature is recorded after the route returned, unlike the outbox path (the row's note says so); a failed insert is logged loudly and the fire goes ahead as before. A confirmed legacy fire now marks the rung Fired with the realized fill — before, the spawn path never called mark_fired and every legacy fire stayed Firing (which is what EXEC_OUTBOX_ADOPT_LEGACY was for). A failed or unknown outcome leaves the rung Firing, never re-fired, as before. unreconciled[] in the summary are the submitted journal rows: with a signature the chain settles them the day the flag is on; without one an operator checks the chain and deletes the row or lets retry {force: true} re-fire it. No Mongo ⇒ no journal (log lines only, loudly). Turning the flag on needs nothing special: boot reconciliation treats a journal row as a worker row (confirmed ⇒ Fired, submitted ⇒ chain verdict, a dead signature ⇒ abandoned, never auto-retried).

Receipt bounds (audit S1 qualification)​

nexus-solana 83bb7d8 (in a1eb8f3, live-verified 2026-09-05 23:33Z). The semantic gate already bounded what leaves the payer; these are the receipts:

ActionReceiptEnforcement
Kamino supplycToken (collateral supply / total liquidity read live)receives leg, min = expected × (1 − EXEC_RECEIPT_TOLERANCE_FRAC), pre-sign in simulation; an unreadable rate refuses the supply. Response receipt{kind: "token", enforced: "pre-sign", mint, expected_base, min_base}
MarginFi supply (no receipt token)the marginfi positionread before and after the send; must grow by amount × (1 − tol). receipt{kind: "position", enforced: "post-state", verified, pre_base, post_base}; unverified is logged RECEIPT UNVERIFIED, sent stays true
JitoSOL deposit / withdrawJitoSOL / SOLreceives leg from the pool or Metis quote less slippage_bps (default 30 bps, stricter than the tolerance) — pre-sign, unchanged
lending withdraw, Squads funding, order cancelthe mint backreceives legs, pre-sign, unchanged
LP openposition NFT (Orca) / position account (Meteora)after sent: true the venue is read back: the position must exist under the owner with liquidity within the tolerance of the adapter's quote. receipt{kind: "lp_position", enforced: "post-state", verified, liquidity, expected_liquidity}
LP remove / closethe coin + USDC backthe adapter's minimum out less the tolerance as receives legs when the set is one transaction (receipt.enforced: "pre-sign"); a split set reports not_enforced: multi-transaction set
LP collectaccrued feesno minimum

The /v1/status not_enforced list is accordingly: inner_programs, token2022_balances, lp_batch_sum, lp_open_receipt_presign, lp_collect_receipt, lending_receipt_untokenized, submit_signed.

Budget coverage hook (audit F6)​

nexus-solana b0a00f1 (in a1eb8f3, live-verified 2026-09-05 23:33Z). Before a rung fires — on both the legacy and the outbox path, and before mark_firing — the exec asks Core GET {NEXUS_CORE_URL}/v1/desk/budget (3 s timeout, verdict cached 5 s) and consults verdict when present:

VerdictEffect
mode: "enforce" and available: false, or family headroom + the ladder's standing ladder_rung reservation (reservations[], ref: ladder:SOL|BTC) < rung budgetrung held: not claimed, stays Armed (cancellable), re-asked every tick; digest line budget: rung S1 HELD — … once per change of verdict
mode: "log" (or no mode)log line only, the rung fires
no verdict, Core unreachable, non-2xx, unreadable, EXEC_BUDGET_CHECK=0proceed with a warning — Core availability never blocks a rung on its own; the exec's own caps (H1) remain the last line

The family is the rung's asset (SOL → sol_exposure, cbBTC → btc_exposure); the exec never reserves — the standing reservation the worker mirrors already counts the rung. Three call sites share one gate: the tick (a held rung, once cleared, is claimed at its original arm epoch so the fire key is stable), flush_intents (a held rung gets no live intent) and the outbox worker before the spend (a claimed row is deferred back to pending for 30 s, the attempt not counted). /v1/status.budget_check shows {enabled, core_url, route, timeout_secs, cache_secs, authenticated, last_fetch{at, ok, detail}, held[], held_rungs[]}. Core's reads are open (NEXUS_AUTH_READ=open), so NEXUS_CORE_API_KEY is unset in prod.

The send path — one commitment, and a provable "never broadcast" (2026-09-06)​

nexus-solana 7046ba8 + 0641ea5, in ea96739 (deployed 2026-09-06). src/send.rs is now the broadcast path for every armed signing route, and it holds two invariants that used to be spread across the choke points.

1. One commitment end to end. pub const LEVEL: CommitmentLevel = CommitmentLevel::Confirmed is used for the blockhash fetch (send::commitment()), for the node's send-time preflight (send::config() sets skip_preflight: false, preflight_commitment: Some(LEVEL)) and for the confirmation poll (send::send_and_confirm, CONFIRM_TIMEOUT_SECS 120, POLL_INTERVAL_MS 500). Before this the choke points fetched the blockhash at confirmed — phase 2 wants the exact last_valid_block_height that comes with it — and then called the client's send_and_confirm_transaction, which preflights at the client default. prochain-external-client builds the client as RpcClient::new(url), i.e. finalized, and a blockhash one slot old at confirmed is not in the finalized bank's queue yet: the node answered -32002 Transaction simulation failed: Blockhash not found and nothing was broadcast. That is what rejected the lending_supply kamino 2937 USDC at 2026-09-06 02:37 UTC and again on the 08:29 decision pass.

Callers of send::send_and_confirm (four, exhaustive): finish_versioned_tx, finish_tx, lp::finish_ix_set and POST /v1/submit-signed. A structural test pins it — no choke point may contain send_and_confirm_transaction(. The Squads ceremony builders (/v1/squads/build-ceremony, /build-vault-lending, /build-vault-stake) do not broadcast — they build an unsigned transaction for the owner's Ledger — but they mint their blockhash at send::commitment() so the relay that later sends it agrees; /v1/squads/fund-exec reaches the wire through finish_tx. The semantic gate's own pre-send simulation was never affected: it runs with replace_recent_blockhash: true, so the node substitutes its own.

2. "Never broadcast" is a proof, not a guess. send::classify returns Failure::NeverBroadcast for exactly one shape — the JSON-RPC answer the node returns from sendTransaction instead of forwarding the transaction: RpcResponseErrorData::SendTransactionPreflightFailure, or the bare code -32002 (send::PREFLIGHT_FAILURE_CODE; some providers fail to serialise the payload, and the code alone already means "rejected at preflight"). Everything else — transport errors, timeouts, an unresolved confirm — is Failure::Ambiguous: capital may be in flight.

What a failed send answers, per choke point:

Choke pointFields on the 502
finish_versioned_tx, finish_txerror, maybe_broadcast, never_broadcast, signature (only when maybe_broadcast), unsent_signature (only when never_broadcast), blockhash, last_valid_block_height
lp::finish_ix_setthe same plus action, batch, batches_sent; no signature key at all
POST /v1/submit-signederror, maybe_broadcast, never_broadcast only — no signature either way

What it means for an intent. Record-before-send is unchanged: the signature is CAS'd into the record before the broadcast. When the answer proves nothing went out, Claim::settle moves the signature (and any batch signatures) into unsent_signatures, sets the record failed without a landed signature, and clears submitted_at — so the "failed with a signature ⇒ replay the stored outcome" rule no longer matches and the intent is re-armable. rearmed() deliberately does not clear unsent_signatures: it is the audit trail. There is no never_broadcast intent state — the states remain prepared, submitted, confirmed, failed.

LP batches keep the old behaviour from batch 1 on. lp::finish_ix_set sets never = f.never_broadcast() && i == 0: only the first batch may be re-armed. A rejection on a later batch stays failed with the recorded signature — an earlier batch has already landed, and replaying the set would spend twice.

The legacy ladder journal follows the same verdict. ladder::classify_response reads never_broadcast before maybe_broadcast and returns SelfCallOutcome::DefiniteFailure, which journals the row failed (Row::legacy_failed) instead of submitted_unknown. Note that submitted_unknown is not an outbox state: it is worker_note: "submitted_unknown" (outbox::SUBMITTED_UNKNOWN_NOTE) on a row in State::Submitted.

Reconciler explanations — coin the sleeve never owned (2026-09-06)​

nexus-solana 9c9d19f (fixtures in 9a2f3b5), in ea96739. On the morning of 2026-09-06 GET /v1/sleeve.unresolved went true and the worker's gate refused every new swing buy. Nothing was actually missing: a core sell of 10 SOL (an operator/treasury sell, not a sleeve lot) and a 10 SOL Orca LP position were both being counted as sleeve inventory that had gone missing from the wallet.

The report is built with serde_json::json! literals in sleeve/recon.rs::reconcile_pairs and sleeve/mod.rs::unresolved_report — there is no serde struct to consult. Every explanation answers one question: why does the wallet differ from inventory − open-sell escrow? And no explanation may release more than the ledger expected to be there (expected_wallet_qty) — beyond that bound the coin was never sleeve inventory, and releasing it invents an expectation the wallet cannot meet.

mints["<pair>"].explanations[]:

kindKeysMeaning
orphan_filled_sellorder, qty (negative), sold_qty, notea filled sell with no parent lot, up to the inventory the ledger still counts — repairable by FIFO linking
core_sellorder, qty (literal 0.0), sold_qty, cost_basis_usdc, notenew: what that sell moved beyond the sleeve's inventory. A sell of coin the sleeve ledger never held changes no expectation, so it contributes 0 and does not make the pair unexplained
orphan_open_sellorder, qty, notea resting sell whose escrow is not ledger inventory
held_in_lpposition, venue, qty (-used), held_qty, principal_qty, fee_qty, notenew, one entry per LP position: it releases the ledger inventory that position holds, capped at what the ledger expected. Beyond that it reports qty: 0 with its held_qty so the operator still sees the coin
trigger_fee_dustqty, lots, notethe 0.1 % Jupiter Trigger output fee on lots recorded at their requested quantity

New per-mint fields: held_elsewhere_qty (present on all three wallet branches — readable, wallet_unreadable, wallet_not_read) and non_sleeve_allowance_qty (readable branch only). The allowance is a function of the pair: on the native-SOL pair it is the EXEC gas float (SLEEVE_SOL_GAS_FLOAT, default 1.0 SOL — it pays for every send and was never sleeve inventory), 0 on every other pair, and it forgives only a surplus; a shortfall is always reported. residual_qty and residual_usdc are on the per-mint entry next to difference_qty.

An unreadable LP scan is unresolved, never a silent zero. Top-level lp_unreadable carries the scan error (null when the scan was clean) and gates all_explained alongside wallet_unreadable. Three layers enforce it: LpHoldings::exec returns Err when the position cache reports any LP venue unreadable, reconcile_pairs propagates it, and unresolved_report turns it into a reason. An LP position the reconciler cannot see looks exactly like coin missing from the wallet, so it must fail rather than read zero.

GET /v1/sleeve.unresolved_reasons[] now carries the numbers, not only the flag. Kinds: wallet_ledger_mismatch, lp_unreadable, ledger_unavailable, reconciliation_required, placement_unconfirmed, cancel_unconfirmed, rpc_degraded, ledger_write_failed. The wallet_ledger_mismatch entry carries pairs[], a summed residual_usdc, a one-line summary (SOL/USDC: unexplained -10.000000 (-900.00 USDC)) and at. residual_qty is per pair — unresolved_reasons[].pairs[].residual_qty, not on the reason itself.

Reconciler knobs (ReconParams::from_env, all optional): SLEEVE_TRIGGER_FEE_FRAC (0.001), SLEEVE_RECON_DUST_USDC (10.0), SLEEVE_SOL_GAS_FLOAT (1.0). They are echoed on the report under params.

Rent attribution — rent is rent, never the coin leg (2026-09-06)​

nexus-solana 4804e26, in ea96739. The cost boundary scan counts every non-owner account that crossed the transaction (0 → funded = rent paid, funded → 0 = rent refunded). On a native-SOL pair the coin leg is lamports: the Jupiter Trigger escrow is a wrapped-SOL account, so closing it at the fill of a 10 SOL sell showed up as rent_lamports −10,005,519,280 / rent_usd −1,067.999. The platform books rent_usd as a cost, subtracted −$1,068 that was never a cost, and reported that round trip as +1,226.46 USDC instead of +159.44.

TxCosts (src/costs.rs) changed shape:

FieldTypeMeaning
rent_lamportsOption<i64> (was i64)net rent locked (+) / released (−) across the tx boundary; null when it could not be attributed
rent_unattributed_lamportsOption<i64>the rejected figure, kept for the operator, when rent_lamports is null
rent_noteserialised key, computedwhy it is null — "rent not attributable: N SOL crossed the tx boundary in accounts that opened/closed — a token leg, not rent; not booked as a cost". It is the method TxCosts::rent_note(), emitted into the JSON / BSON row only on the unattributed branch

TxCosts::attribute_rent(coin_leg_lamports, sanity_lamports) hands the known coin leg back: +making_amount for a native-SOL sell (what the escrow held), -received_base for a native-SOL buy, 0 otherwise — and only when the correction brings the figure closer to zero, so a swap that delivered native SOL straight to the owner keeps its real ATA rent. Whatever remains is bounded by SLEEVE_RENT_SANITY_LAMPORTS (RENT_SANITY_LAMPORTS, default 100 000 000 lamports = 0.1 SOL; account rent on Solana is thousandths of a SOL — an SPL token account 0.00204, a Trigger order ≈ 0.0035). Past the bound the row carries rent_lamports: null + the rejected figure instead of a number, and rent_usd() is None with it — never 0.

Three call sites: sleeve fill rows (sleeve/mod.rs, with the leg), outbox::verify_fill (with the leg when the out-mint is wSOL), and TxCosts::rent_bounded() — the bound alone — for route answers and outbox fills whose legs this layer cannot name. CostLedger::today counts the refusals as unattributed_rent_sends rather than absorbing them.

Live effect on the 2026-09-06 core sell: reported rent −10.0055 SOL / −$1,068 → −5,519,280 lamports (the escrow and order-account rent that really came back), with realized_pnl_usdc unchanged at 159.4367.

Position cache — one background-refreshed view, invalidated by writes (2026-09-06)​

nexus-solana ea96739 (+ d232263, one lending accessor), src/positions.rs. Two live defects on 2026-09-06 motivated it:

  1. GET /v1/lp/positions scanned both CLMM venues on-chain per call and took ≈ 9.5 s. The UI proxy aborted it at its 8 s budget and rendered "positions unavailable" over a live Orca position; the hourly NAV job and the sleeve reconciler paid the same cost inside their own budgets.
  2. GET /v1/positions served lending from a 2-minute cache that no write invalidated. A confirmed 995.55 USDC supply left the aggregate reporting the pre-supply 56,448.93 while GET /v1/lending/position already read 57,445.10 — and NAV and the budget gate read the aggregate.

A background task refreshes every EXEC_POSITIONS_REFRESH_SECS (DEFAULT_REFRESH_SECS 30, floored at MIN_REFRESH_SECS 5 — a full scan is ≈ 10 s of RPC, and a shorter period would keep one in flight forever), starting immediately at boot. Reads answer from the snapshot.

Both /v1/positions and /v1/lp/positions now carry three freshness fields:

FieldMeaning
as_ofRFC 3339. For the aggregate it is the older of the lending and LP halves, so it never claims freshness one half does not have
staleolder than 3 refresh intervals, or a half has never been read, or any venue is unreadable
venues_unreadablethe venue keys whose last scan failed with no value to fall back on (kamino, marginfi, orca, meteora)

/v1/lp/positions uses an LP-only view of those fields, so a down lending venue never marks it unreadable — the sleeve reconciler depends on that distinction.

A write invalidates. PositionCache::invalidate(Scope) is called on sent: true from four places: POST /v1/lending/supply and /v1/lending/withdraw (shared handler), /v1/lending/withdraw-all, POST /v1/lp/open, and the shared position handler behind /v1/lp/remove | collect | close. The next read drains the dirty set and refreshes those venues synchronously before answering, so the aggregate can no longer serve a pre-write figure. A cold cache triggers a full refresh instead.

A failed scan never becomes an empty list: the last good value is kept and stale / venues_unreadable say so.

?fresh forces a full synchronous scan — accepted values are 1, true, yes and a bare ?fresh (empty value); ?fresh=0 and absence do not. Use it for an operator read that must not be a cache hit, never in a loop.

Since d232263 there is exactly one lending accessor (positions::read_lending / read_lending_at), pinned by a structural test, so /v1/positions can never disagree with /v1/lending/position again. LENDING_CACHE_SECS (default 120) now governs only GET /v1/lending/rates.

Measured after the roll: /v1/lp/positions 9.5 s → ≈ 2 ms.

nexus-solana-oracle: GET /health, /v1/status, /v1/prices (no caller auth; read-only, no keys).

Outbox, intent, receipt and budget env (apps/nexus-solana-exec/README.md is the reference)​

VarDefaultMeaning
EXEC_OUTBOXunset (prod: 0)1 routes ladder execution through accum_outbox; unset keeps the legacy spawn-per-rung path
EXEC_OUTBOX_MAX_ATTEMPTS3definite failures before a row is abandoned
EXEC_OUTBOX_LEASE_SECS / _POLL_MS / _CONFIRM_POLL_MS120 / 1000 / 2000lease (heartbeat every lease/4), worker loop period (heartbeat staleness 3×), chain-status poll spacing
EXEC_OUTBOX_STUCK_SECS1800a submitted / leased row older than this degrades /v1/health/outbox
EXEC_OUTBOX_FILL_TOLERANCE_FRAC0.01tx-meta fill vs quote: USDC spent within ± tol, coin received ≥ quote × (1 − slippage − tol); beyond ⇒ confirmed_unverified
EXEC_OUTBOX_META_MAX_ATTEMPTS10getTransaction misses on a landed signature before the fill is confirmed unverified from the quote
EXEC_OUTBOX_ADOPT_LEGACYunset1 adopts legacy-path Firing rungs at boot from the (now read-only) accum_tranche_queue; set for one boot, then unset
SOLANA_RPC_URLS—extra label=url entries give the unlandable proof its second endpoint; with one endpoint rows stay submitted (safe direction)
EXEC_INTENT_RETENTION_SECS604800 (min 60)retention of accum_intents idempotency records (audit F2, a1eb8f3); unrelated to EXEC_INTENT_TTL_SECS (90 s freshness)
EXEC_RECEIPT_TOLERANCE_FRAC0.01 (0..=1)receipt bound (audit S1, a1eb8f3): fungible receipt min = expected × (1 − tol); non-fungible receipts verified post-state within tol
EXEC_BUDGET_CHECK10 disables the pre-fire budget coverage hook (a1eb8f3)
NEXUS_CORE_URLhttp://nexus-coreCore base URL for GET /v1/desk/budget (3 s timeout, verdict cached 5 s)
NEXUS_CORE_API_KEYunsetoptional X-Nexus-Api-Key presented to Core — unset in prod: Core reads are open, so it is not needed
EXEC_POSITIONS_REFRESH_SECS30 (floor 5)position-cache refresh cadence (ea96739 — Position cache); a full scan is ≈ 10 s of RPC
LENDING_CACHE_SECS120hydration cadence of the rates cache only, since ea96739 — GET /v1/lending/rates. It no longer governs positions
SLEEVE_RENT_SANITY_LAMPORTS100 000 000 (0.1 SOL)bound past which a rent figure is refused, not booked (4804e26 — Rent)
SLEEVE_SOL_GAS_FLOAT1.0native SOL the EXEC keeps as gas float; forgiven as a surplus in the reconciler's residual (9c9d19f — Reconciler)
SLEEVE_RECON_DUST_USDC10.0residual under which a mint is explained
SLEEVE_TRIGGER_FEE_FRAC0.001Jupiter Trigger output fee a lot recorded at its requested quantity is short by

States: pending → leased → submitted → confirmed | confirmed_unverified, plus abandoned (cancel, operator, max attempts) and simulated (dry-run). confirmed_unverified is terminal — the spend is real and the rung is Fired with the realized numbers — and degrades health until an operator looks. Phase 3, not done: in-process prepare / broadcast / confirm split (the worker signs through the gate directly, no self_url hop), EXEC_OUTBOX=1 in gitops after a dry-run soak (then EXEC_OUTBOX_ADOPT_LEGACY=1 for one armed boot), retire the queue reader and the legacy path.

Costs: every confirmed send is read back from its tx meta into one vocabulary (src/costs.rs) — fee_lamports + priority_fee_lamports partition the tx fee (tx_fee_lamports is their sum), rent_lamports is net rent across the tx boundary and is null when it could not be attributed (rent_unattributed_lamports + rent_note say why, since 4804e26 — Rent), sol_usd_mark / tx_fee_usd / rent_usd are absent when unpriced, never 0, received_qty / spent_qty are realized quantities, and fee_usd is the venue fee only (sleeve: the Jupiter Trigger output fee). They appear on filled sleeve rows, on /v1/ladder.outbox.fired[] / fill.costs, in signing-route responses and summed on /v1/status.costs_today. How the NAV job books them: Measuring success → Costs.

Vendored crates — provenance and patches​

See vendor/README.md in the repo for the commit table. Load-bearing notes:

  • The lending family comes from prochain-amm-server; the fleet's prochain-external-client was replaced by a slim RPC-only shim.
  • The Orca SDK compiles unmodified against the workspace pins; only the optional anchor feature was stripped and the e2e harness's dev-dependencies dropped. Its global config (try_lock statics) is set once at boot; owner and slippage are passed explicitly per call. It does not build Token-2022 transfer-hook accounts — solana-clmm refuses non-treasury mints.
  • The Meteora codama client's solana crates are re-resolved from 4.x to the 2.x versions solana-sdk 2.3.1 re-exports (must be re-checked on every solana bump). All position logic — PDAs, bit-exact bin math including a first-party bin_id_from_price with round-trip property tests, bin-array coverage, position resize, chunked add/remove/claim, transaction packing — lives in crates/solana-clmm/src/meteora/, verified against the live SOL/USDC pool.
  • CI (.github/workflows/build.yml): cargo test --workspace excludes the vendored CLMM crates (their inline tests need a local validator); clippy -D warnings runs on first-party crates only.

Gaps​

  1. Squads spending limit — builder written, never simulation-verified or created on-chain; no VAULT→EXEC automation, no destination whitelist.
  2. RPC failover — solana-rpc pool is unwired; one endpoint everywhere.
  3. MarginFi supply — deposit builder fails on mainnet (discriminator); read/withdraw only.
  4. Dead crates — harvest-allocator and the RPC pool compile but are never called (harvest-guards is called by the policy gate since 0913342, 2026-09-05); the live guards are the oracle guard, bid-below-index, the worker's caps, the signer's policy gate and simulation.
  5. Clip engine — rungs fill as one 75 bps Metis swap; no clip loop.
  6. Vault executor — single-asset vaults are catalogued, not enterable.
  7. LP v0 transactions / lookup tables — LP is legacy-only with split points; Token-2022 hooks unsupported.
  8. Ladder outbox not enabled (audit C4 / F1) — phases 1 and 2 are implemented, tested and deployed (17:46 UTC), but EXEC_OUTBOX=0: rungs still fire from the legacy detached task and /v1/health/outbox answers {enabled: false}. From a1eb8f3 (deployed) that path at least journals every fire with its signature, fill and costs (Legacy journal); the durable consumer, crash / duplicate-submission rehearsal and activation remain phase 3 above.
  9. Reconciliation — the balance snapshot (/v1/reconcile) computes no expectation and raises no alert on mismatch. The sleeve side is closed (audit 2026-09-05 F5): 2c0be5a added GET /v1/sleeve/reconcile (explains ledger vs wallet per asset, all_explained, unresolved) and POST /v1/sleeve/reconcile/repair (dry-run / apply; operator-only, never automatic). The repair was applied at 16:20 UTC — orphan sell ENJURa… linked to lot 4HAnsRgd…, lot closed with 0.0001 cbBTC dust — after which inventory is empty, committed_usdc = 10,537 (open buy escrow only), all_explained: true, unresolved: false. The worker refuses new swing buys while unresolved is set. NexusSleeveUnresolved alerts on the flag once the exporter ships. 2026-09-06 follow-up: the reconciler produced a false unresolved on a core sell and an LP position and blocked every new swing buy for the morning; fixed in 9c9d19f and live-verified ≈ 09:3x UTC (Reconciler explanations). The residual is still tolerance-based under this reconciler's reference prices, and historical lots still report qty_is_actual: false.
  10. Signer trust boundary — closed (audit 2026-09-05 S1): caller authentication (EXEC_API_KEYS, X-Nexus-Exec-Key) enforced since 16:27 UTC (nexus-gitops 29c983a; 401 without a key, 403 for a read-role key on a mutation) and semantic validation of every transaction before signing (2c0be5a). See Security → Signer caller authentication for roles, counters and key rotation. Still not enforced by design: inner CPI program allowlisting (delta bounds constrain instead), Token-2022 balances, LP batch sums, the submit_signed relay.
  11. Lending positions expose no principal — /v1/positions reports supplied_ui and apy per lending position, not the cToken / receipt quantity and its exchange rate. The worker's P&L-by-source ledger (nexus-platform 2c79b0e, 2026-09-05) can therefore derive lending interest only per period (Δ balance − its own supply / withdraw flows); a cumulative "interest since deposit" per venue is not derivable, and a movement the flow ledger did not see (the 17:37 UTC kamino/main → kamino/jlp rotation) shows as unexplained. Open item: expose cToken qty + exchange rate (and the same for any receipt-token venue) so the worker can restate interest from the position itself — see Measuring success → P&L by source. Still open as of ea96739 (2026-09-06): the cache made the figure consistent and current, it did not add a principal.
  12. costs_today has no per-signature entries — it is an aggregate (sends, SOL, USD, per-source counts), so the platform cannot book it per send and reconciles it as unbooked_sends_today instead. Recorded follow-up: costs_today.entries[] with signatures, and a GET /v1/chain/transfers route so NAV's residual flow detection can be signature-confirmed rather than detected_unconfirmed (Measuring success → Costs).
  13. The depeg guard has no grace window — every exec/oracle deploy produces ≈ 10–15 s of price plane unreadable (both binaries ship in the same image, so the oracle pod restarts with the exec; the exec's 5 s price fetch has no retry). During that window the H1 depeg check fails closed and any signing is denied. Known, safe direction, self-inflicted only on deploys; the queued improvement is to keep the last good USDC cross for a grace period with its age already exposed on /v1/status.policy.depeg.age_s (Operations → Deploying the exec).

The full cross-repo gap list is on HARVEST — as built §12.

Why one repo, not nexus-solana-oracle + nexus-solana-execution​

The two planes share the vendored crates, the domain types and the protocol bindings. Two repos would duplicate all of that for no isolation gain — key isolation is enforced at the pod level (which Deployment mounts the secret), and the oracle binary simply has no signing dependencies.

Why separate from nexus-platform​

  • Key isolation. nexus-platform holds no signing material; the entire agent platform keeps zero on-chain access.
  • Dependency weight. The Solana SDK tree is heavy; keeping it out of the platform workspace protects everyone else's CI.
  • Venue-service symmetry. The desk already treats venues as external services (the CEX manager for KuCoin). Solana is the second venue: REST for services, env-var wiring, no shared DB coupling.