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:
| Binary | Role | Keys |
|---|---|---|
nexus-solana-oracle (:8090) | read-only price plane: Pyth primary, Metis and Coinbase legs, quorum/outlier/divergence guard, guard-gated index per asset | none |
nexus-solana-exec (:8091) | signing plane: lending, swaps, staking, LP positions, Jupiter Trigger orders, Squads ceremony builders, reconciliation, catalogs | EXEC 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:
| Revision | What it changes | Verification |
|---|---|---|
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 — Reconciler | live-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 |
4804e26 | rent is rent, never the coin leg: rent_lamports is Option<i64> with rent_unattributed_lamports / rent_note and a sanity bound — Rent | the 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 |
7046ba8 | one send path: blockhash, preflight and confirmation poll all at confirmed — The send path | the -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 |
0641ea5 | a preflight-rejected send settles re-armable, not replayed — The send path | unit-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 bounds | 15: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)
| Group | Routes |
|---|---|
| status | GET /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) |
| intents | GET /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) |
| health | GET /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 / sleeve | POST /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 |
| lending | GET /v1/lending/rates, /position, /discover?venue=&mint=, /catalog; POST /supply, /withdraw, /withdraw-all, /catalog/refresh |
| swaps | POST /v1/swap/quote, /build, /execute |
| staking | POST /v1/stake/quote, /deposit, /withdraw — Jito pool DepositSol/WithdrawSol vs Metis, route: auto|pool|metis |
| reserves | PUT /v1/reserves (worker pushes the decision officer's float/gas), GET /v1/reserves |
| LP | GET /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 |
| 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; 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:
| Guard | Rule |
|---|---|
| discount from index | a 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 ceiling | one 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 direction | the total ladder budget may shrink, never grow: moving where the ladder buys is not permission to spend more |
| ordering | rungs 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 state | Armed, Firing and Fired / Cancelled rungs are refused (decided, committed, spent); only Idle and Arming may move |
| arming reset | an 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):
| Field | Meaning |
|---|---|
slippage_bps | tolerance on the USDC leg (and on the coin leg when coin_slippage_bps is absent) |
coin_slippage_bps | tolerance 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:
| Field | Meaning |
|---|---|
intent_id | the 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 |
fence | monotonic 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:
| Condition | Answer |
|---|---|
| different params hash (the economically relevant fields, canonicalized) | 409 intent:mismatch |
fence below the stored fence | 409 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_SECS | 409 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:
| Action | Receipt | Enforcement |
|---|---|---|
| Kamino supply | cToken (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 position | read 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 / withdraw | JitoSOL / SOL | receives 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 cancel | the mint back | receives legs, pre-sign, unchanged |
| LP open | position 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 / close | the coin + USDC back | the 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 collect | accrued fees | no 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:
| Verdict | Effect |
|---|---|
mode: "enforce" and available: false, or family headroom + the ladder's standing ladder_rung reservation (reservations[], ref: ladder:SOL|BTC) < rung budget | rung 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=0 | proceed 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 point | Fields on the 502 |
|---|---|
finish_versioned_tx, finish_tx | error, maybe_broadcast, never_broadcast, signature (only when maybe_broadcast), unsent_signature (only when never_broadcast), blockhash, last_valid_block_height |
lp::finish_ix_set | the same plus action, batch, batches_sent; no signature key at all |
POST /v1/submit-signed | error, 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[]:
kind | Keys | Meaning |
|---|---|---|
orphan_filled_sell | order, qty (negative), sold_qty, note | a filled sell with no parent lot, up to the inventory the ledger still counts — repairable by FIFO linking |
core_sell | order, qty (literal 0.0), sold_qty, cost_basis_usdc, note | new: 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_sell | order, qty, note | a resting sell whose escrow is not ledger inventory |
held_in_lp | position, venue, qty (-used), held_qty, principal_qty, fee_qty, note | new, 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_dust | qty, lots, note | the 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:
| Field | Type | Meaning |
|---|---|---|
rent_lamports | Option<i64> (was i64) | net rent locked (+) / released (−) across the tx boundary; null when it could not be attributed |
rent_unattributed_lamports | Option<i64> | the rejected figure, kept for the operator, when rent_lamports is null |
rent_note | serialised key, computed | why 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:
GET /v1/lp/positionsscanned 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.GET /v1/positionsserved 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 whileGET /v1/lending/positionalready 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:
| Field | Meaning |
|---|---|
as_of | RFC 3339. For the aggregate it is the older of the lending and LP halves, so it never claims freshness one half does not have |
stale | older than 3 refresh intervals, or a half has never been read, or any venue is unreadable |
venues_unreadable | the 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)
| Var | Default | Meaning |
|---|---|---|
EXEC_OUTBOX | unset (prod: 0) | 1 routes ladder execution through accum_outbox; unset keeps the legacy spawn-per-rung path |
EXEC_OUTBOX_MAX_ATTEMPTS | 3 | definite failures before a row is abandoned |
EXEC_OUTBOX_LEASE_SECS / _POLL_MS / _CONFIRM_POLL_MS | 120 / 1000 / 2000 | lease (heartbeat every lease/4), worker loop period (heartbeat staleness 3×), chain-status poll spacing |
EXEC_OUTBOX_STUCK_SECS | 1800 | a submitted / leased row older than this degrades /v1/health/outbox |
EXEC_OUTBOX_FILL_TOLERANCE_FRAC | 0.01 | tx-meta fill vs quote: USDC spent within ± tol, coin received ≥ quote × (1 − slippage − tol); beyond ⇒ confirmed_unverified |
EXEC_OUTBOX_META_MAX_ATTEMPTS | 10 | getTransaction misses on a landed signature before the fill is confirmed unverified from the quote |
EXEC_OUTBOX_ADOPT_LEGACY | unset | 1 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_SECS | 604800 (min 60) | retention of accum_intents idempotency records (audit F2, a1eb8f3); unrelated to EXEC_INTENT_TTL_SECS (90 s freshness) |
EXEC_RECEIPT_TOLERANCE_FRAC | 0.01 (0..=1) | receipt bound (audit S1, a1eb8f3): fungible receipt min = expected × (1 − tol); non-fungible receipts verified post-state within tol |
EXEC_BUDGET_CHECK | 1 | 0 disables the pre-fire budget coverage hook (a1eb8f3) |
NEXUS_CORE_URL | http://nexus-core | Core base URL for GET /v1/desk/budget (3 s timeout, verdict cached 5 s) |
NEXUS_CORE_API_KEY | unset | optional X-Nexus-Api-Key presented to Core — unset in prod: Core reads are open, so it is not needed |
EXEC_POSITIONS_REFRESH_SECS | 30 (floor 5) | position-cache refresh cadence (ea96739 — Position cache); a full scan is ≈ 10 s of RPC |
LENDING_CACHE_SECS | 120 | hydration cadence of the rates cache only, since ea96739 — GET /v1/lending/rates. It no longer governs positions |
SLEEVE_RENT_SANITY_LAMPORTS | 100 000 000 (0.1 SOL) | bound past which a rent figure is refused, not booked (4804e26 — Rent) |
SLEEVE_SOL_GAS_FLOAT | 1.0 | native SOL the EXEC keeps as gas float; forgiven as a surplus in the reconciler's residual (9c9d19f — Reconciler) |
SLEEVE_RECON_DUST_USDC | 10.0 | residual under which a mint is explained |
SLEEVE_TRIGGER_FEE_FRAC | 0.001 | Jupiter 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'sprochain-external-clientwas replaced by a slim RPC-only shim. - The Orca SDK compiles unmodified against the workspace pins; only the
optional
anchorfeature was stripped and the e2e harness's dev-dependencies dropped. Its global config (try_lockstatics) is set once at boot; owner and slippage are passed explicitly per call. It does not build Token-2022 transfer-hook accounts —solana-clmmrefuses 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.1re-exports (must be re-checked on every solana bump). All position logic — PDAs, bit-exact bin math including a first-partybin_id_from_pricewith round-trip property tests, bin-array coverage, position resize, chunked add/remove/claim, transaction packing — lives incrates/solana-clmm/src/meteora/, verified against the live SOL/USDC pool. - CI (
.github/workflows/build.yml):cargo test --workspaceexcludes the vendored CLMM crates (their inline tests need a local validator); clippy-D warningsruns on first-party crates only.
Gaps
- Squads spending limit — builder written, never simulation-verified or created on-chain; no VAULT→EXEC automation, no destination whitelist.
- RPC failover —
solana-rpcpool is unwired; one endpoint everywhere. - MarginFi supply — deposit builder fails on mainnet (discriminator); read/withdraw only.
- Dead crates —
harvest-allocatorand the RPC pool compile but are never called (harvest-guardsis called by the policy gate since0913342, 2026-09-05); the live guards are the oracle guard, bid-below-index, the worker's caps, the signer's policy gate and simulation. - Clip engine — rungs fill as one 75 bps Metis swap; no clip loop.
- Vault executor — single-asset vaults are catalogued, not enterable.
- LP v0 transactions / lookup tables — LP is legacy-only with split points; Token-2022 hooks unsupported.
- 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/outboxanswers{enabled: false}. Froma1eb8f3(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. - 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):2c0be5aaddedGET /v1/sleeve/reconcile(explains ledger vs wallet per asset,all_explained,unresolved) andPOST /v1/sleeve/reconcile/repair(dry-run / apply; operator-only, never automatic). The repair was applied at 16:20 UTC — orphan sellENJURa…linked to lot4HAnsRgd…, 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 whileunresolvedis set.NexusSleeveUnresolvedalerts on the flag once the exporter ships. 2026-09-06 follow-up: the reconciler produced a falseunresolvedon a core sell and an LP position and blocked every new swing buy for the morning; fixed in9c9d19fand live-verified ≈ 09:3x UTC (Reconciler explanations). The residual is still tolerance-based under this reconciler's reference prices, and historical lots still reportqty_is_actual: false. - Signer trust boundary — closed (audit 2026-09-05 S1): caller
authentication (
EXEC_API_KEYS,X-Nexus-Exec-Key) enforced since 16:27 UTC (nexus-gitops29c983a; 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, thesubmit_signedrelay. - Lending positions expose no principal —
/v1/positionsreportssupplied_uiandapyper lending position, not the cToken / receipt quantity and its exchange rate. The worker's P&L-by-source ledger (nexus-platform2c79b0e, 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 asunexplained. 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 ofea96739(2026-09-06): the cache made the figure consistent and current, it did not add a principal. costs_todayhas 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 asunbooked_sends_todayinstead. Recorded follow-up:costs_today.entries[]with signatures, and aGET /v1/chain/transfersroute so NAV's residual flow detection can be signature-confirmed rather thandetected_unconfirmed(Measuring success → Costs).- 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.