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.
| Scope | Purpose |
|---|---|
/agents-api | Agent CRUD, clone, enable/disable, skill assignment |
/skills-api | Skill CRUD, versioning, test, publish |
/memory-api | Memory search, scoped reads/writes, approval queue |
/goals-api | Create goals, decompose into tasks |
/tasks-api | Task lifecycle reads/writes |
/runs-api | Agent runs: start, cancel, logs, results |
/board-api | Internal board + sync triggers |
/approvals-api | Human-approval queue |
/webhooks-api | Inbound webhooks (Taiga, CI) |
/v1/schedules | Scheduled-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:
| Where | Field / parameter | Note |
|---|---|---|
| every armed signing route, on a failed send | never_broadcast (bool), unsent_signature (present only when never_broadcast), beside the existing maybe_broadcast / signature / blockhash / last_valid_block_height | never_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/positions | as_of, stale, venues_unreadable[]; query ?fresh | cache-served; ?fresh accepts 1, true, yes or a bare ?fresh and forces a synchronous scan — Position cache |
GET /v1/sleeve, GET /v1/status.sleeve | unresolved_reasons[] entries carry residual_usdc + summary (and pairs[].residual_qty); a lp_unreadable reason kind | an unreadable LP scan is unresolved, never a silent zero |
GET /v1/sleeve/reconcile | explanation kinds core_sell, held_in_lp; per-mint held_elsewhere_qty, non_sleeve_allowance_qty, residual_qty, residual_usdc; top-level lp_unreadable | Reconciler |
every cost payload (recent_trades[], costs_today, route answers, outbox fills) | rent_lamports is now nullable; rent_unattributed_lamports + rent_note appear when it is | Rent |
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.
| Route | Returns |
|---|---|
GET /v1/desk/nav | the 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/flows | recorded external flows, newest first |
POST /v1/desk/nav/flows | record 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/budget | the 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).
| Route | Body | Answers |
|---|---|---|
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 one | 200 { "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.
| Route | Returns |
|---|---|
GET /v1/desk/pnl?window=1d|7d|30d|inception|all | where 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[].noteisstring | string[] | null. As built the server emitsnullor an array of sentences (a lending row passes itsnotesarray through; staking and LP emit a one-element array). Do not call string methods on it — an array here crashed/treasuryon 2026-09-06 (nexus-ui068ef0fnormalisesstring | 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_usdcis the exec's own realized figure (profit_source: "exec_realized") when the exec read the confirmed transaction — i.e. the fill carriedrealized_pnl_usdcandproceeds_source: "tx_meta"; otherwise"derived"(proceeds − lot cost − known fees), and only that path ever setsfees_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 intocosts.unattributed_usdt, said innotes, and counted onnexus_pnl_cost_anomalies_total{field}.rent_*is skipped entirely on a native-SOL pair. Cost rules. - Coverage before conclusions.
covered: falsemeans the series does not reach back over the requested window — the figures are the since-inception ones, not a flat period.coverage.noteexists on/v1/desk/pnland/pnl/history;/v1/desk/nav.returns_coveragecarries the same four flags without a note. - Lending ties or says so:
balance_delta_usdt = net_flows_usdt + interest_usdt + unattributed_usdt, withcontaminated_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 onlending[].notesandearning[].note— not in the top-levelnotes. Lending reconciliation. - There is no
/v1/desk/earningroute —earningis 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
| Scheme | Header | Used by |
|---|---|---|
| Admin JWT | Authorization: Bearer <jwt> | UI / operators |
| API key | x-nexus-api-key: <key> | machine clients |
| Agent token | Authorization: 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):
| Pillar | Create | Update |
|---|---|---|
| SOUL (agents) | POST /v1/agents | PATCH /v1/agents/{id} |
| SKILLS | POST /v1/skills | PATCH /v1/skills/{id} |
| MEMORY | POST /v1/memory | PATCH /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.