Skip to main content

API reference

Nexus Core exposes a single REST API, grouped into scopes. Every endpoint speaks JSON; OpenAPI is generated by utoipa and served live when Core runs.

ScopePurpose
/agents-apiAgent CRUD, clone, enable/disable, skill assignment
/skills-apiSkill CRUD, versioning, test, publish
/memory-apiMemory search, scoped reads/writes, approval queue
/goals-apiCreate goals, decompose into tasks
/tasks-apiTask lifecycle reads/writes
/runs-apiAgent runs: start, cancel, logs, results
/board-apiInternal board + sync triggers
/approvals-apiHuman-approval queue
/webhooks-apiInbound webhooks (Taiga, CI)
/v1/schedulesScheduled-job CRUD plus POST /v1/schedules/{id}/run, which makes a job due on the scheduler's next tick (it does not enqueue) — Operations → Triggering a cycle
/v1/desk/*Trading + HARVEST desk reads: digests, decisions, accum-health (cycle health), oracle health, scope; since 2026-09-05 the treasury routes nav, nav/history, nav/flows (+ one POST) and budget — below; pnl, pnl/history, pnl/events, pnl/flows (P&L by source, deployed 2026-09-05 ≈ 20:3x UTC) — below; the budget mutations budget/reserve, budget/reservations/{id}/claim|consume|release (deployed 2026-09-05, live-verified 23:4x UTC) — below

The Solana venue service has its own REST surface (in-cluster only) — see nexus-solana; its ladder-outbox operator routes (GET /v1/ladder/outbox, POST /v1/ladder/outbox/{id}/retry, …/abandon, GET /v1/health/outbox) and /v1/status.costs_today are documented there (phase 2, deployed 2026-09-05 17:46 UTC, EXEC_OUTBOX still 0), as are the exec-side idempotency fields intent_id / fence accepted by every mutating route, its 409 codes intent:mismatch / intent:stale_fence / intent:in_flight and GET /v1/intents/{id} (nexus-solana a1eb8f3, live-verified 2026-09-05 23:33 UTC — nexus-solana → Idempotency).

Exec fields added 2026-09-06 (ea96739), documented in full on nexus-solana:

WhereField / parameterNote
every armed signing route, on a failed sendnever_broadcast (bool), unsent_signature (present only when never_broadcast), beside the existing maybe_broadcast / signature / blockhash / last_valid_block_heightnever_broadcast: true proves nothing went on the wire (a -32002 / SendTransactionPreflightFailure); the intent is re-armable, not replayed. POST /v1/lp/* batch answers add action / batch / batches_sent and carry no signature key; /v1/submit-signed carries neither signature field — Send path
GET /v1/intents/{id}unsent_signatures[]signatures minted and provably never broadcast; survives a re-arm
GET /v1/positions, GET /v1/lp/positionsas_of, stale, venues_unreadable[]; query ?freshcache-served; ?fresh accepts 1, true, yes or a bare ?fresh and forces a synchronous scan — Position cache
GET /v1/sleeve, GET /v1/status.sleeveunresolved_reasons[] entries carry residual_usdc + summary (and pairs[].residual_qty); a lp_unreadable reason kindan unreadable LP scan is unresolved, never a silent zero
GET /v1/sleeve/reconcileexplanation kinds core_sell, held_in_lp; per-mint held_elsewhere_qty, non_sleeve_allowance_qty, residual_qty, residual_usdc; top-level lp_unreadableReconciler
every cost payload (recent_trades[], costs_today, route answers, outbox fills)rent_lamports is now nullable; rent_unattributed_lamports + rent_note appear when it isRent

Desk treasury routes (/v1/desk/nav*, /v1/desk/budget)​

Deployed with nexus-platform ac38aa1 (2026-09-05 17:12 UTC, audit §7) and 71b9a44 / 7e1adcb (budget, audit F6; log mode). All go through the Core auth middleware like every /v1 route.

RouteReturns
GET /v1/desk/navthe latest accum_nav_snapshots row: NAV in USDT and USD, per-component valuations (known / unknown / stale), unknown_components, TWR windows, MWR, HWM / drawdown, exposure, attribution, the four benchmarks. Since nexus-platform 0a58575 (in ee0cd80, deployed 2026-09-05, live-verified 23:4x UTC) also: scope: "strategy", in_scope_nav_usdt (= nav_usdt, kept for compatibility), out_of_scope{value_usdt_estimate, unpriced_entries, entries[{exchange, asset, qty, usdt_estimate | null, priced, pricing}], note}, twr{method, period_return, period_return_end_of_period, sub_periods[]} (method labels), attribution_method: "pnl_ledger", costs{accrued_off_portfolio_usd, on_chain_usd, by_kind_usd, exec_today, rule}, and since 72967bd (deployed 2026-09-06) returns_coverage{"1d"|"7d"|"30d": {covered, truncated_to_series, series_starts_at, covered_days}} — a truncated window returns the since-inception figure with covered: false, never null; these per-window objects carry no note (Window coverage), flows{in_usdt, out_usdt, net_usdt, detection, detected_unconfirmed[], last_detection} (detected_unconfirmed = accum_external_flows rows with source: detected_unconfirmed, newest first, up to 50), notes[] (the scope / cost / TWR / attribution rules, one sentence each)
GET /v1/desk/nav/history?from=&to=&step=the NAV and benchmark series for charts
GET /v1/desk/nav/flowsrecorded external flows, newest first
POST /v1/desk/nav/flowsrecord a deposit / withdrawal — body and semantics on Measuring success; also how the operator confirms or offsets a detected_unconfirmed flow. Service keys only since NEXUS_AUTH_MODE=enforce (22:37 UTC)
GET /v1/desk/budgetthe strategy-wide caps in force and the exposure the worker gates on — same arithmetic as the worker's check_budget, from the latest NAV snapshot. Since cf219fa (in ee0cd80, deployed 2026-09-05) also verdict (the compact block a non-worker executor reads before moving money) and ledger (the accum_budget_ledger document: version, entries[] with state / lease / fence)

GET /v1/desk/budget shape (nexus-core api.rs):

{
"mode": "log" | "enforce", // ACCUM_LIMIT_MODE on the worker; log = would-blocks counted, movements allowed
"available": true | false, // false ⇒ the worker denies risk-increasing actions (budget_unavailable)
"unavailable_reason": "..." | null, // stale / absent / incomplete NAV
"limits": { "max_sol_exposure_frac": 0.6, "max_btc_exposure_frac": 0.4, "max_protocol_frac": {"kamino": 0.7}, "max_protocol_default_frac": 0.25, "max_lp_total_frac": 0.15, "min_liquid_reserve_usdc": 5000, "lending_recall_haircut_frac": 0.1, "max_single_action_frac_of_nav": 0.3, "nav_max_age_secs": 7200, "reservation_ttl_secs": 21600, "mode": "log", "..." : "..." },
"limits_source": "persisted" | "code defaults (...)",
"snapshot": { "id", "at", "age_seconds", "nav_usdt", "stale": bool } | null,
"exposure": { "...ExposureView: nav_usdt, sol_usdt, btc_usdt, stable_usdt, per-protocol, LP, escrow..." } | null,
"verdict": { // since cf219fa / ee0cd80 (deployed 2026-09-05): what the exec / a service reads
"version": 41, // accum_budget_ledger version (CAS token)
"available": true, "mode": "log" | "enforce",
"headroom_usdt": { "sol_exposure": 0, "btc_exposure": 0, "liquid_reserve": 0, "protocol:<name>": 0, "lp_total": 0, "single_action": 0 } | null,
"reserved_usdc": { "sol_usdc", "btc_usdc", "stable_out_usdc", "by_protocol_usdc": {}, "lp_usdc", "liquid_claims_usdc", "by_kind_usdc": {}, "count" }
},
"reserved": { "by_kind_usdc", "sol_usdc", "btc_usdc", "by_protocol_usdc", "lp_usdc", "liquid_claims_usdc", "count" }, // from the ledger's counting set since ee0cd80
"reservations": [ "...open rows, or consumed since the snapshot: id <kind>:<ref>, kind, state, amount_usdc, ..." ],
"ledger": { "version", "updated_at", "nav_snapshot_id", "limits_version", "rebuilt_from_rows_at_ms", // since ee0cd80
"entries": [ { "id", "kind", "ref", "asset", "venue", "amount_usdc", "stable_out_usdc",
"state": "held" | "executing" | "consumed" | "released",
"lease": { "owner", "since_ms", "until_ms" } | null, "fence", "expires_at_ms", "..." } ] },
"recent": [ "...newest 50 audit rows of any state..." ],
"headroom": [ { "cap", "used_usdt", "limit_usdt", "headroom_usdt", "used_frac", "limit_frac" } ] | null,
"env_table": [ ["ACCUM_LIMIT_MAX_SOL_EXPOSURE_FRAC", "max_sol_exposure_frac"], "..." ],
"note": "families, ladder_rung semantics, liquid definition"
}

Families: SOL = SOL + JitoSOL + LP SOL legs + SOL buy escrow; BTC = cbBTC + BTC + LP BTC legs + cbBTC buy escrow. ladder_rung reservations mirror the armed exec-plane ladder (the rungs fire outside this gate; the signer's per-action / daily caps remain the last line). Liquid = wallet stables + lending stables × (1 − lending_recall_haircut_frac), net of open claims. On 2026-09-05 16:57 UTC this route showed SOL headroom −8.5k USDT (69 % projected vs the 60 % cap) with the ladder fully armed — the reason the gate runs in log mode; see Operations → Portfolio budget gate.

Contract for the exec plane (verdict, read by nexus-solana b0a00f1 before a rung fires): a rung's budget is already counted by the standing ladder_rung:ladder:<SOL|BTC> reservation the worker mirrors from /v1/ladder every sweep, so the exec must not reserve again. It consults verdict.available (false ⇒ the NAV behind the caps is stale / incomplete), verdict.headroom_usdt["sol_exposure" | "btc_exposure"] and ["liquid_reserve"] (< 0 ⇒ already over counting every promise): in mode: "enforce" the rung is held, in "log" it fires and logs. A read failure is treated as available == false by the contract; the exec's own rule is that Core availability never blocks a rung on its own (nexus-solana → Budget hook).

Desk budget mutations (POST /v1/desk/budget/*)​

Built in nexus-platform cf219fa (in ee0cd80, deployed 2026-09-05 23:00 UTC). For money movements made outside the worker (a Telegram-approved buy, a manual execute-role request): the same CAS ledger and fence rules the worker uses (HARVEST §5 → Budget ledger). Under NEXUS_AUTH_MODE=enforce only a service key — plain or acting for an operator — may call them; the read key and the UI proxy get 403 budget mutations require a service key (X-Nexus-Api-Key).

RouteBodyAnswers
POST /v1/desk/budget/reserve{ "kind": "swing_buy" | "lp_add" | "lending_supply" | "repark" | "stake", "ref": "approval:<id>:<n>", "asset": "SOL" | "CBBTC" | "SOL-USDC" | …, "venue"?: "kamino", "amount_usdc", "stable_out_usdc"? (default = amount for swing_buy / lp_add, else 0), "owner"?: "telegram:<user>" (default core:<principal>), "claim"?: true (default), "lease_secs"?, "ttl_secs"? } — kind: ladder_rung is refused (400)200 { "outcome": "reserved" | "existing", "id": "<kind>:<ref>", "fence": n | null, "lease_until_ms", "version", "mode", "would_block": { "reason", "detail" } | null } — proceed, then consume / release with the fence (existing = an open entry with that id already existed, nothing new promised; would_block only in log mode). 409 { "outcome": "denied", "reason", "detail", "version", "mode" } — do not move money (never in log mode). 409 { "outcome": "lease_held", "id", "detail" } — another executor holds a live claim
POST /v1/desk/budget/reservations/{id}/claim{ "owner", "lease_secs"? }200 { "id", "fence", "lease_until_ms", "took_over_from": "<owner>" | null } (held → executing, or a takeover of a dead lease — the fence moves on). 404 unknown id. 409 { "reason": "expired" | "settled" | "lease_held", "detail" }
POST /v1/desk/budget/reservations/{id}/consume{ "fence": n } — required (400 without: "fence is required to consume (claim first)")200 { "id", "state": "consumed" } — the money moved. 409 { "reason": "stale_fence" | "not_claimed" | "already_settled", "current_fence"?, "detail" } — stale_fence means the claim was taken over after the lease died and is counted on nexus_portfolio_fence_rejections_total; not_claimed means nobody claimed before moving money
POST /v1/desk/budget/reservations/{id}/release{ "fence"?: n, "reason"?: "…" } — the fence is required for a claimed (executing) entry, not for a held one200 { "id", "state": "released" } — the money did not move. 409 as consume

Denial reason values are the gate's: budget_unavailable, single_action_cap, sol_exposure_cap, btc_exposure_cap, liquid_reserve, protocol_cap, lp_total_cap, reservation_error (a lost CAS eight times in a row — fail closed). The Telegram gateway does not go through these routes — it uses the ledger directly through nexus-db with owner telegram:<user> — but the semantics are the same; the routes exist for the exec plane and operators. Operator runbook: Operations → Budget ledger.

Desk P&L routes (/v1/desk/pnl*)​

Deployed 2026-09-05 (nexus-core d76eb11, in the same push as the worker's accrual 2c79b0e; pushed ≈ 20:1x UTC). Read-only; Core auth middleware like every /v1 route. What they mean and how each source is derived: Measuring success → P&L by source.

RouteReturns
GET /v1/desk/pnl?window=1d|7d|30d|inception|allwhere the money was made, by source, over the window (default inception); all = inception plus the sleeve round trips that predate the NAV series (historical: true) — shape below
GET /v1/desk/pnl/history?from=&to=&step=per-snapshot source amounts for stacked charts; from / to RFC 3339 or epoch ms (default last 30 d); step in seconds buckets the periods (0 = per snapshot)
GET /v1/desk/pnl/events?source=&from=&to=&limit=&historical=the raw ledger (accum_pnl_events), newest first; source exact, or a prefix when it ends with : (sleeve_realized: = every pair); historical=false hides pre-inception rows; limit default 200, max 5 000
GET /v1/desk/pnl/flows?kind=&from=&to=&limit=the worker's own movements between venues (accum_internal_flows), newest first

GET /v1/desk/pnl shape (from the handler doc comment in nexus-core api.rs; the UI is built from it). Every figure is USDT unless suffixed; the identity the view maintains is delta_usdt = Σ sources[].amount_usdt where sources includes the unexplained residual (check restates it):

{
"window": "inception", "from": "rfc3339", "to": "rfc3339", "days": 0.07, "periods": 2,
"coverage": { "requested_days": 30, "covered": false, "truncated_to_series": true,
"series_starts_at": "rfc3339", "covered_days": 0.6, "note": "…" },
"covered": false, "truncated_to_series": true, "series_starts_at": "rfc3339",
"nav_start_usdt": 0, "nav_end_usdt": 0, "external_flows_usdt": 0, "delta_usdt": 0,
"sources": [ { "source": "jito_staking", "group": "yield", "kind": "accrual", "venue": "jito",
"pair": null, "amount_usdt": 0, "amount_usd": 0, "share_of_delta": 0,
"detail": { "...": "latest period's working for this source" } } ],
"groups": { "yield": 0, "trading": 0, "market": 0, "costs": 0, "unexplained": 0 },
"check": { "explained_usdt": 0, "unexplained_usdt": 0, "identity": "delta_usdt = explained_usdt + unexplained_usdt" },
"sleeve": {
"realized_total_usdt": 0,
"realized_in_window_usdt": 0, "realized_by_pair": { "<pair>": 0 },
"round_trips": [ { "buy_order": "", "sell_order": "", "pair": "", "asset": "", "qty": 0, "entry_price": 0, "exit_price": 0,
"cost_usdc": 0, "proceeds_usdc": 0, "fees_usdc": 0, "profit_usdc": 0, "exec_realized_usdc": 0,
"profit_source": "exec_realized" | "derived",
"notes": [ "...fee fields the sanity rules refused..." ],
"opened_at": "", "closed_at": "", "holding_hours": 0, "historical": false, "fees_unknown": true, "amount_usdt": 0 } ],
"open_positions": [ { "order": "", "pair": "", "asset": "", "qty": 0, "vwap": 0, "cost_usdc": 0, "mark_usdt": 0, "unrealized_usdt": 0,
"escrow_qty": 0, "orphan": false, "opened_at": "" } ],
"unrealized_usdt": 0, "win_rate": 0, "avg_profit_usdt": 0, "avg_holding_hours": 0, "fees_unknown_count": 0,
"profit_source_counts": { "exec_realized": 0, "derived": 0 },
"exec_realized_total_usdc": 0, "exec_inventory": {}
},
"staking": { "jitosol_qty": 0, "rate_start": 0, "rate_end": 0, "yield_sol": 0, "yield_usdt": 0, "apr_annualized": 0 },
"lending": [ { "venue": "", "asset": "", "balance_start": 0, "balance_end": 0,
"balance_delta_usdt": 0, "net_flows_usdt": 0, "net_flows": 0, "interest_usdt": 0, "unattributed_usdt": 0,
"identity": "balance_delta_usdt = net_flows_usdt + interest_usdt + unattributed_usdt",
"periods": 0,
"contaminated_periods": [ { "at": "", "snapshot_id": "", "delta_qty": 0, "implied_qty": 0, "hours": 0, "reason": "" } ],
"negative_periods": 0, "interest_clean_usdt": 0, "clean_hours": 0,
"apr_reported": 0, "apr_realized": 0, "apr_realized_clean": 0, "method": "", "notes": [] } ],
"lp": [ { "pool": "", "venue": "", // pool is the exec's own base58 case, never re-cased
"fees_usdt": 0, // fee income = accrued + collected
"fees_uncollected_usdt": 0, // balance still in the pool at window end
"fees_collected_usdt": 0, "fees_accrued_usdt": 0,
"drift_usdt": 0, // position legs only; fee legs excluded
"split": "", "renamed_from": null } ],
"lp_unattributed_drift_usdt": 0, // LP value change whose component named a protocol and no pool
"lp_rule": "Σ lp[].drift_usdt + lp_unattributed_drift_usdt = market:LP",
"earning": [ { "kind": "lending" | "staking" | "lp", "venue": "", "asset": "",
"principal_qty": 0, "principal_usdt": 0, "rate_reported": 0,
"earned_window_usdt": 0, "earned_inception_usdt": 0,
"apr_realized_inception": 0, // null under one day of age; note says why
"age_days": 0,
"status": "earning" | "idle" | "out_of_range" | "contaminated",
"since": "rfc3339", "note": null,
"earned_inception_clean_usdt": 0, // lending only
"reconciliation": { "balance_delta_usdt": 0, "net_flows_usdt": 0, "unattributed_usdt": 0 },
"uncollected": { "coin_ui": 0, "usdc_ui": 0, "usd": 0 }, // lp only
"fees_uncollected_usdt": 0, "fees_collected_usdt": 0, // lp only
"range": {}, "price": 0 } ],
"ladder": { "fired": [ { "ladder": "", "id": "", "asset": "", "trigger": 0, "budget_usdc": 0, "qty": 0, "cost_usdc": 0, "fill_price": 0,
"mark_usdt": 0, "unrealized_usdt": 0, "filled_at": "" } ], "unrealized_usdt": 0 },
"costs": { "tx_usdt": 0, "venue_usdt": 0, "infra_usdt": 0, "total_usdt": 0, "unattributed_usdt": 0,
"repair": { "costs_deleted": [], "events_fixed": [], "rows_fixed": [], "notes": [], "sanity_usd": 25, "rule": "" } },
"snapshot": { "id": "", "at": "", "age_seconds": 0 },
"notes": [ "...what could not be derived in the latest period..." ],
"numeraire": "USDT"
}

sleeve.realized_total_usdt is Σ every round trip ever, historical included; realized_in_window_usdt only the fills inside the window. /pnl/history returns { from_ms, to_ms, covered_from_ms, step_secs, coverage: {...}, sources: [..], series: [ { at, at_ms, periods, sources: { "<source>": usdt }, groups: { yield, trading, market, costs, unexplained } } ] } — a requested from older than the series is clamped to the series' own start and coverage says so, never an empty chart; /pnl/events and /pnl/flows return { count, items: [..] } with the stored record plus id. The numbers in the sample above are placeholders (the doc comment shows types, not values).

Contract notes for consumers (all deployed 2026-09-06, nexus-platform 2553748 … 63a2de3):

  • earning[].note is string | string[] | null. As built the server emits null or an array of sentences (a lending row passes its notes array through; staking and LP emit a one-element array). Do not call string methods on it — an array here crashed /treasury on 2026-09-06 (nexus-ui 068ef0f normalises string | string[] | object | null). The union is documented rather than narrowed so a future single-sentence note is not a breaking change. Earning block.
  • round_trips[].profit_usdc is the exec's own realized figure (profit_source: "exec_realized") when the exec read the confirmed transaction — i.e. the fill carried realized_pnl_usdc and proceeds_source: "tx_meta"; otherwise "derived" (proceeds − lot cost − known fees), and only that path ever sets fees_unknown. Profit source.
  • Costs are positive and small by construction. A figure ≤ 0, or above PNL_COST_SANITY_USD (default 25), is in no bucket: it is summed into costs.unattributed_usdt, said in notes, and counted on nexus_pnl_cost_anomalies_total{field}. rent_* is skipped entirely on a native-SOL pair. Cost rules.
  • Coverage before conclusions. covered: false means the series does not reach back over the requested window — the figures are the since-inception ones, not a flat period. coverage.note exists on /v1/desk/pnl and /pnl/history; /v1/desk/nav.returns_coverage carries the same four flags without a note.
  • Lending ties or says so: balance_delta_usdt = net_flows_usdt + interest_usdt + unattributed_usdt, with contaminated_periods[] and the clean figures (interest_clean_usdt, apr_realized_clean) beside the raw ones. PNL_LENDING_SANITY_MULT (default 10) is the contamination threshold. Contamination sentences appear on lending[].notes and earning[].note — not in the top-level notes. Lending reconciliation.
  • There is no /v1/desk/earning route — earning is a field of /v1/desk/pnl.

Live OpenAPI​

When Core is running:

  • Swagger UI — https://api.nexus.dev/swagger-ui/
  • Raw JSON — https://api.nexus.dev/api-docs/openapi.json

Base URL​

Production: https://api.nexus.dev · Local dev: http://localhost:8080

Authentication​

SchemeHeaderUsed by
Admin JWTAuthorization: Bearer <jwt>UI / operators
API keyx-nexus-api-key: <key>machine clients
Agent tokenAuthorization: Bearer <agent-token>run pods reporting back

This is the intended scheme. As built (2026-09-05): every /v1 route passes through Core's auth middleware, which recognises the service key (x-nexus-api-key, checked against NEXUS_API_KEY) and a proxy-asserted operator (X-Nexus-Operator-Sub / -Email, honoured only behind a valid key and checked against NEXUS_OPERATOR_ALLOWLIST). Production runs NEXUS_AUTH_MODE=enforce since 2026-09-05 22:37 UTC (nexus-gitops c95071e, audit S3 closed) with NEXUS_AUTH_READ=open: reads (GET) are open, every mutation needs a key — an anonymous POST is 401 no_key; decisions are counted in nexus_core_auth_decisions_total. The ui-proxy principal (NEXUS_UI_API_KEY, X-Nexus-Proxy-Origin attestation on mutations, optional read-only NEXUS_API_KEY_READ) shipped first (audit S4) — see Identity propagation. Admin JWT and agent tokens are still not implemented (audit H11 partial).

Common shapes​

Errors:

{ "error": "human-readable message" }

Status codes: 200 OK · 201 Created · 400 validation · 401 auth · 403 forbidden · 404 not found · 409 conflict · 429 rate-limited · 5xx server/upstream.

Write endpoints​

The Admin UI editors for the four pillars map onto these concrete routes (server fills _id, timestamps, and version):

PillarCreateUpdate
SOUL (agents)POST /v1/agentsPATCH /v1/agents/{id}
SKILLSPOST /v1/skillsPATCH /v1/skills/{id}
MEMORYPOST /v1/memoryPATCH /v1/memory/{id}
HISTORY (runs)read-only (GET /v1/runs, GET /v1/runs/{id})—

PATCH is a shallow merge: send only the keys you want to change. The merged document is re-validated against the domain schema before it is persisted.

Examples​

Create an agent​

curl -X POST https://api.nexus.dev/v1/agents \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "Backend Implementer",
"slug": "backend-implementer",
"soul": {
"role": "developer",
"persona": "You are a meticulous Rust backend engineer.",
"objectives": ["Ship correct, well-tested services"],
"principles": ["Prefer small, reviewable changes"],
"guardrails": ["Never force-push to main"]
},
"backend": "openai-api",
"model": { "provider": "openai", "model": "gpt-5.5-thinking", "temperature": 0.2 },
"runtime": { "image": "nexus-agent-runner:latest", "cpu": "2", "memory": "4Gi", "timeout_minutes": 60 },
"skills": ["rust-backend-development"],
"memory_policy": { "read_scopes": ["global","project","agent_private"], "write_scopes": ["project","agent_private"], "auto_save": true },
"tools": ["git","shell","cargo"],
"permissions": { "can_commit": true, "can_open_pr": true, "can_merge": false }
}'

Create a goal​

curl -X POST https://api.nexus.dev/goals-api \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{ "project_id": "nexus", "title": "Build authentication system" }'

Returns the goal plus drafted tasks (status draft) awaiting approval.

Start a run​

curl -X POST https://api.nexus.dev/runs-api \
-H "Authorization: Bearer $JWT" \
-d '{ "agent_id": "agent_backend_implementer", "task_id": "task_456" }'

Stream run logs​

curl -N https://api.nexus.dev/runs-api/run_123/logs

What's not in the API yet​

  • Cursor-based pagination (current limits are generous defaults)
  • GraphQL (REST suffices)
  • Vector memory search (text + tags first)
  • Jira board adapter (Taiga first)

Tracked on the roadmap.