{
  "markdown": "<div align=\"center\">\n\n<img src=\"https://nexustrade.io/logo192.jpeg\" alt=\"NexusTrade\" width=\"88\" height=\"88\">\n\n# NexusTrade TypeScript SDK\n\n**Author trading strategies in typed TypeScript. Backtest them on the engine that runs them live.**\n\n[![npm](https://img.shields.io/npm/v/nexustrade.svg)](https://www.npmjs.com/package/nexustrade)\n[![Node](https://img.shields.io/node/v/nexustrade.svg)](https://www.npmjs.com/package/nexustrade)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![Deps](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](package.json)\n\n[Quickstart](#quickstart) · [Authoring](#authoring-strategies) · [Polling](#jobs-run-on-the-engine--you-poll) · [Agents](#agent-runs) · [Lake SQL](#lake-sql) · [Auth](#authentication) · [Errors](#errors)\n\n</div>\n\n---\n\n```bash\nnpm install nexustrade\n```\n\n**Zero runtime dependencies.** ESM and CommonJS builds ship together, with types.\n\n## MCP server\n\nNexusTrade also exposes the platform as a hosted, remote Model Context Protocol\nserver. Modern MCP clients connect directly to the production Streamable HTTP\nendpoint and discover NexusTrade OAuth automatically:\n\n```bash\nclaude mcp add --transport http nexustrade https://nexustrade.io/api/mcp\n```\n\nCursor and other remote-capable clients use:\n\n```json\n{\n  \"mcpServers\": {\n    \"nexustrade\": {\n      \"url\": \"https://nexustrade.io/api/mcp\"\n    }\n  }\n}\n```\n\nFor Claude Desktop and other stdio-only clients, use the established\n`mcp-remote` bridge—no clone or local NexusTrade server is required:\n\n```json\n{\n  \"mcpServers\": {\n    \"nexustrade\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"mcp-remote@latest\",\n        \"https://nexustrade.io/api/mcp\",\n        \"--transport\",\n        \"http-only\"\n      ]\n    }\n  }\n}\n```\n\nThe live server exposes more than 120 tools across market research, portfolio\nconstruction, backtesting, optimization, walk-forward validation, managed\ncompute, Aurora agents, paper trading, and controlled brokerage operations.\nIts creator-marketplace tools cover the full strategy adoption path:\n\n- `search_creators` discovers public creators and their marketplace portfolios.\n- `subscribe_portfolio` validates a monetized listing and returns a safe\n  checkout preview; the user completes payment in NexusTrade, never through the\n  MCP tool.\n- `fork_shared_portfolio` creates a one-time editable copy of a marketplace\n  strategy in a new or existing portfolio.\n- `copy_trade_shared` continuously mirrors an accessible strategy into a paper\n  or live portfolio at an explicit allocation.\n\nSee the [developer guide](https://nexustrade.io/developers), the\n[utility tool reference](https://nexustrade.io/docs/api-reference/mcp-tools-utility),\nand the [Aurora tool reference](https://nexustrade.io/docs/api-reference/mcp-tools-aurora).\n\nResearch and historical results are not investment advice and do not guarantee\nfuture performance. Keep paper and live modes explicit. Tools that can affect\nportfolios, schedules, or brokerage orders remain subject to the authenticated\naccount's NexusTrade permissions and approval controls.\n\n## Quickstart\n\n```ts\nimport {\n  NexusTradeClient,\n  always,\n  backtest,\n  buy,\n  portfolio,\n  stockAsset,\n  strategy,\n} from \"nexustrade\";\n\nconst client = new NexusTradeClient({\n  apiKey: \"sk-...\",\n  baseUrl: \"https://nexustrade.io/api/v1\",\n});\n\nconst book = portfolio(\"Example\", [\n  strategy(\"Buy SPY\", always(), buy(stockAsset(\"SPY\"), 100)),\n]);\n\nconst operation = await client.createBacktest(\n  backtest(book, { startDate: \"2024-01-01\", endDate: \"2024-12-31\" }),\n  { idempotencyKey: \"example-v1\" }\n);\nconst result = await client.waitForBacktest(operation.id as string);\nconsole.log(result.result);\n```\n\nBacktest operations may include `warnings: string[]` immediately after\nsubmission and again in the terminal `result`. Treat them as material caveats;\nthey do not change a successful operation into a failure.\n\n## Authoring strategies\n\nEvery builder is generated from the same indicator specification the NexusTrade\nengine runs, so a book is **valid by construction** rather than by convention.\n\nTypeScript cannot overload comparison operators, so indicators compose through\n`gt` / `gte` / `lt` / `lte` / `eq` / `neq` and `and` / `or`:\n\n```ts\nimport * as nt from \"nexustrade\";\n\nconst book = nt.portfolio(\n  \"Momentum\",\n  [\n    nt.strategy(\n      \"Rotate into strength\",\n      nt.always(),\n      nt.dynamicRebalance({\n        universe: nt.universe(\"SP500\"),\n        pipeline: [\n          nt.filter(nt.gt(nt.Price(nt.CANDIDATE), nt.SMA(nt.CANDIDATE, 200))),\n          nt.selectTop(nt.RSI(nt.CANDIDATE, 14), 10),\n        ],\n        weightIndicator: nt.RSI(nt.CANDIDATE, 14),\n        limit: 10,\n        deploymentPercent: 80,\n      })\n    ),\n  ],\n  { initialValue: 100_000 }\n);\n```\n\n<details>\n<summary><b>What you can build</b> — 170+ generated builders</summary>\n\n| Group               | Examples                                                                                     |\n| ------------------- | -------------------------------------------------------------------------------------------- |\n| **Price & volume**  | `Price` `OpeningPrice` `HighOfDay` `VWAP` `Volume` `GapPercentage`                           |\n| **Technicals**      | `SMA` `EMA` `RSI` `BollingerBand` `AverageTrueRange` `CrossAbove`                            |\n| **Position state**  | `PositionValue` `PositionPercentChange` `PositionMaxDrawdown`                                |\n| **Portfolio state** | `PortfolioValue` `BuyingPower` `MaxDrawdown` `InitialValue`                                  |\n| **Fundamentals**    | `Fundamental` `Economic` `DaysUntilEarnings` `IsIndexMember` `IsIndustry`                    |\n| **Options**         | `OptionDaysToExpiration` `OptionCollateral` `OptionUnrealizedPnL` `openOption` `closeOption` |\n| **Actions**         | `buy` `sell` `alert` `dynamicRebalance` `rebalanceOption`                                    |\n| **Selection**       | `filter` `selectTop` `selectPercentile` `universe`                                           |\n| **Logic**           | `always` `atLeast` `atMost` `exactly` `fewerThan` `multi` `and` `or`                         |\n\nEvery builder is fully typed — your editor completes the whole surface.\n\n</details>\n\n## Jobs run on the engine — you poll\n\n`create*` enqueues work and returns immediately. It does **not** resolve when\nresults exist. There are no webhooks today.\n\n```mermaid\nsequenceDiagram\n    participant You\n    participant SDK\n    participant Engine\n\n    You->>SDK: createBacktest(book)\n    SDK->>Engine: POST (enqueue)\n    Engine-->>SDK: id, status=queued\n    SDK-->>You: operation (returns immediately)\n\n    loop waitForBacktest — backoff 2s→15s\n        SDK->>Engine: GET /operations/{id}\n        Engine-->>SDK: status update\n    end\n\n    SDK-->>You: result (when completed)\n\n    Note over You,Engine: Poll timeout throws operation_timeout.<br/>The job keeps running — call wait again with the same id.\n```\n\nEvery job kind reports the same envelope, so one poller serves all of them:\n\n```ts\n{\n  id: \"op_...\",\n  kind: \"backtest\",          // backtest | optimization | walk_forward\n  status: \"queued\",          // queued | running | completed | failed | cancelled\n  result: {...},             // present only once terminal\n  error: { code, message, retryable },\n}\n```\n\n```ts\nconst finished = await client.waitForBacktest(operation.id as string);\n```\n\n| Option                   | Default | Meaning                                            |\n| ------------------------ | ------- | -------------------------------------------------- |\n| `timeoutSeconds`         | `900`   | Give up waiting (the job keeps running)            |\n| `pollIntervalSeconds`    | `2`     | First interval; backs off 1.5×                     |\n| `maxPollIntervalSeconds` | `15`    | Interval ceiling                                   |\n| `throwOnFailure`         | `true`  | Throw on `failed`/`cancelled` instead of returning |\n\nA timeout throws `operation_timeout` and does **not** cancel the job — call the\nwaiter again with the same id rather than resubmitting.\n\n**Batches.** `createBacktests` submits many in one request and returns one\noperation each; `waitForBacktests(operations)` waits on all of them. Prefer it\nover a loop: one request, one idempotency key, one rate-limit slot.\n\n**Optimization and walk-forward** follow the identical shape:\n\n```ts\nconst study = await client.createWalkForward(\n  nt.walkForward(book, {\n    globalStartDate: \"2022-01-01\",\n    globalEndDate: \"2024-12-31\",\n    foldCount: 4,\n  }),\n  { idempotencyKey: \"wf-v1\" }\n);\nawait client.waitForWalkForward(study.id as string);\n```\n\n## Deploying a portfolio\n\nAuthoring and backtesting a book does not persist it. `save` writes it to your\naccount; `deploy` starts running it.\n\n```ts\nconst book = nt.portfolio(\"Momentum\", [\n  /* … */\n]);\n\nawait book.save({ idempotencyKey: \"momentum-v1\" }); // persists; sets book.id\nconst deployment = await book.deploy(); // starts paper trading\nawait book.undeploy(); // stops it\n```\n\n**`save` and `deploy` produce different ids, and the distinction matters.**\n`save` persists a _draft_ and sets `book.id` to it. `deploy` mints the real\npaper portfolio and returns its own `portfolioId` — deploying creates a\nportfolio rather than converting the draft into one, so the two ids coexist.\nHold on to `deployment.portfolioId` for anything that reads live state;\n`book.id` addresses the draft.\n\n```ts\ndeployment.portfolioId; // the running portfolio\ndeployment.deploymentType; // paper, unless you deployed an existing live one\ndeployment.outcome; // created | reactivated\n```\n\nHandle methods accept an optional `transport`; omitted, they resolve one from\nthe environment. The same operations exist on the client — `client.deploy(id)`,\n`client.undeploy(id)` — when you have an id rather than a handle.\n\n```ts\nawait client.listPortfolios({ includePaper: true, includePositions: true });\nawait client.getPortfolio(portfolioId);\n```\n\nFetched portfolio handles include a typed, read-only `policy` snapshot. Trading\npolicy changes are intentionally unavailable through the SDK; edit them in\nPortfolio Settings. `PortfolioHandle.toJSON()` omits the snapshot so a fetched\nportfolio cannot accidentally submit policy changes through an authoring call.\n\n`listPortfolios` filters with `includePaper`, `includeLive`, `includeInactive`,\n`includeChatPortfolios`, `search`, `limit`, and `page`. `includePositions`\ndefaults off when `search` is set.\n\n**A portfolio you create here is always paper**, and minting a _live_ one still\nhappens in the web app. Orders and brokerage status are reachable from here;\nsee [Live trading](#live-trading).\n\n**But `deploy` can start live trading.** Given the id of a portfolio that is\nalready deployed, it reactivates that portfolio as whatever it already is — so\n`client.deploy(id)` on a paused live portfolio resumes live trading against the connected\nbrokerage, and `includeLive: true` above will hand you such an id. Check `deployment.deploymentType` before\ntreating a deploy as simulated.\n\n## Live trading\n\nLive trading needs a brokerage linked to your account. Linking is an OAuth\nredirect, so an API key cannot complete it — a human opens the URL.\n\n```ts\nawait client.listBrokerages();\n// [{ brokerage: \"Alpaca\", connected: false,\n//    connectUrl: \"https://nexustrade.io/live-trading\" }, ...]\n\nawait client.connectBrokerage(\"Alpaca\"); // logs the URL, waits until connected\n```\n\n`connectBrokerage` waits by default **only when stdout is a TTY**. In CI, cron,\nor `run_compute` it rejects with `brokerage_not_connected` immediately, with the\nURL in the message, rather than stalling for five minutes in front of nobody.\nPass `{ wait: true }` or `{ wait: false }` to force either.\n\nA live-only listing that comes back empty rejects with the same error rather\nthan an empty array, since an empty array says nothing about why:\n\n```ts\nawait client.listPortfolios({ includeLive: true, includePaper: false });\n// NexusTradeApiError: brokerage_not_connected: No live portfolios, and no\n// brokerage is connected. Connect one at https://nexustrade.io/live-trading\n```\n\n### Orders\n\n```ts\nconst result = await client.createOrders(\n  portfolioId,\n  [\n    {\n      asset: { name: \"SPY\", type: \"STOCK\", symbol: \"SPY\" },\n      side: \"BUY\",\n      quantity: 10,\n      orderType: \"MARKET\",\n    },\n  ],\n  { idempotencyKey: \"rebalance-2024-04-01\" }\n);\n\n// Dollar notional (stock/crypto only — options require contract quantity):\nawait client.createOrders(\n  portfolioId,\n  [\n    {\n      asset: { name: \"AAPL\", type: \"STOCK\", symbol: \"AAPL\" },\n      side: \"BUY\",\n      amount: 500,\n      orderType: \"MARKET\",\n    },\n  ],\n  { idempotencyKey: \"buy-aapl-500\" }\n);\n```\n\n**Paper orders are accepted immediately. Live orders are staged for approval\nand are never sent to a broker by this call.**\n\n```ts\nif (result.requiresApproval) {\n  console.log(\"nothing has traded yet — approve at\", result.approvalUrl);\n}\n```\n\nThere is no argument, scope, or flag that submits a live order without\napproval. The brokerage boundary refuses an unapproved live order regardless of\nwhat any caller asks for, so this is a property of the system rather than a\npromise made by this method. At most 50 orders per request.\n\n## Your own data\n\nA custom data source is a time series you own — sentiment counts, a proprietary\nfactor, anything the platform does not already carry. Create one, then reference\nit from a strategy with `CustomIndicator`.\n\n```ts\nconst series = await client.createCustomIndicator(\n  {\n    name: \"WSB NVDA Mentions\",\n    scope: \"asset\",\n    description: \"Daily r/wallstreetbets mentions\",\n    pointKind: \"observation\",\n    points: [\n      { timestamp: \"2024-04-01\", value: 152, ticker: \"NVDA\" },\n      { timestamp: \"2024-04-02\", value: 90, ticker: \"NVDA\" },\n    ],\n  },\n  { idempotencyKey: \"wsb-mentions-v1\" }\n);\n\nconst busy = nt.gt(\n  nt.CustomIndicator(nt.stockAsset(\"NVDA\"), String(series.customIndicatorId)),\n  100\n);\nconst book = nt.portfolio(\"Attention\", [\n  nt.strategy(\"Buy the buzz\", busy, nt.buy(nt.stockAsset(\"NVDA\"), 25)),\n]);\n```\n\n`scope` is `\"global\"` (one series) or `\"asset\"` (one series per ticker, so every\npoint needs a `ticker`). It cannot be changed after creation.\n\nDeclare `pointKind` whenever the time semantics are known: `observation` for\npoint-in-time samples, `period_aggregate` plus `aggregatePeriod` (`1d`, `1w`,\n`1mo`, or `1q`) for closed-period values, and `disclosed` for values with an\nexplicit publication time on every row. The SDK applies this contract before\nboth inline and large-upload writes. A same-day date-only observation becomes\nan explicit same-day UTC instant instead of shifting to the next calendar day.\n\n**Size is not a constraint.** `points` is unlimited. A batch that fits the\nrequest goes with it; a larger one is uploaded to storage and validated before\nthe call resolves. Either way the returned indicator reflects what actually\nlanded, and an upload that fails validation rejects rather than reporting\nsuccess.\n\n**Growing a series.** Append to the same id every run:\n\n```ts\nawait client.appendCustomIndicatorPoints(\n  String(series.customIndicatorId),\n  [{ timestamp: \"2024-04-03\", value: 118, ticker: \"NVDA\" }],\n  { idempotencyKey: \"wsb-mentions-2024-04-03\" }\n);\n```\n\nCreating a fresh series per run splits the history into fragments no strategy\ncan read. Re-sending an identical batch is safe — the duplicate is not written\ntwice.\n\n| Call                                                          | Purpose                   |\n| ------------------------------------------------------------- | ------------------------- |\n| `createCustomIndicator(spec, { idempotencyKey })`             | Create, optionally seeded |\n| `appendCustomIndicatorPoints(id, points, { idempotencyKey })` | Add points                |\n| `replaceCustomIndicatorPoints(id, points, { idempotencyKey })` | Replace points, retain id |\n| `archiveCustomIndicator(id)` / `restoreCustomIndicator(id)`   | Reversible lifecycle      |\n| `listCustomIndicators()` / `getCustomIndicator(id)`           | Discover ids and coverage |\n\nPoints accept `timestamp`, `value`, `ticker`, `assetType`, and `availableAt`\n— camelCase or snake_case, with `Date` objects allowed. Set `availableAt` when a\nvalue became knowable later than it is dated: an earnings figure stamped to\nquarter-end but published weeks after. An unrecognized field throws rather than\nbeing silently dropped.\n\nTo hand over a file you already have on disk, `createCustomIndicatorUpload` /\n`completeCustomIndicatorUpload` / `waitForCustomIndicatorUpload` expose the\nthree steps directly. CSV, JSON, and JSONL up to 100 MB.\n\n## Agent runs\n\nEvery other job is fire-and-poll. **Agents are not** — three states\n(`pending_plan_approval`, `pending_action_approval`, `awaiting_user_input`)\ncannot advance without you. Iterate the run and answer when it blocks:\n\n```mermaid\nsequenceDiagram\n    participant You\n    participant Run as AgentRun\n    participant Engine\n\n    You->>Run: createAgent(prompt)\n    Run->>Engine: POST /agents\n    Engine-->>Run: run id\n\n    loop for await (const event of run)\n        Run->>Engine: GET events (cursor)\n        Engine-->>Run: new events\n\n        alt event.needsApproval\n            Run-->>You: plan or action awaiting approval\n            You->>Run: approve() or reject()\n            Run->>Engine: POST approval\n        else event.needsInput\n            Run-->>You: awaiting user input\n            You->>Run: say(\"...\")\n            Run->>Engine: POST message\n        else\n            Run-->>You: event.text\n        end\n    end\n\n    Run-->>You: terminal\n\n    Note over You,Engine: Without approve/say, the run stalls and bills.<br/>Reattach later with attachAgent(run.id).\n```\n\n```ts\nconst run = await client.createAgent(\"Find momentum names in the S&P 500\", {\n  idempotencyKey: \"momentum-scan-v1\",\n  costCeilingUsd: 20,\n});\nfor await (const event of run) {\n  console.log(event.text);\n  if (event.needsApproval) await run.approve();\n  if (event.needsInput) await run.say(\"Focus on tech\");\n}\n```\n\n## Lake SQL\n\nRead-only SQL over the NexusTrade market-data lake, against the server-resolved\n`lake.*` catalog. Results are durable Parquet parts rather than an implicitly\nmaterialized in-memory array.\n\n```mermaid\nflowchart LR\n    A[createLakeQuery] --> B[waitForLakeQuery]\n    B --> C[getLakeQueryManifest]\n    C --> D[downloadLakeQueryPart]\n    D --> E[Stream Parquet within your memory budget]\n```\n\n```ts\nconst query = await client.createLakeQuery(\n  {\n    query:\n      \"SELECT ticker, date, closingPrice FROM lake.daily_ohlc WHERE ticker = ?\",\n    params: [\"AAPL\"],\n    limits: { maxRows: 10_000 },\n  },\n  { idempotencyKey: \"aapl-daily-v1\" }\n);\nconst finished = await client.waitForLakeQuery(query.id as string);\nconst manifest = await client.getLakeQueryManifest(finished.id as string);\n```\n\n## Natural language\n\nDescribe the screen instead of writing the SQL. The server generates it,\nvalidates it against the same `lake.*` catalog the engine reads, executes it,\nand hands back both the rows and the statement.\n\n```ts\nconst screen = await client.createNlScreen(\n  \"technology stocks with a market cap over 100 billion and a PE under 30\"\n);\nconst done = await client.waitForNlScreen(screen.id as string);\n\nconst result = done.result as Record<string, unknown>;\nconsole.log(result.rows);\nconsole.log(result.sql); // always check the SQL — it is model-generated\n```\n\n`returnQuery` defaults to `true` because the SQL is the audit trail: without it\nthe rows are a number you cannot re-derive. It is returned on failure whatever\nyou pass, since a rejected query is the most useful thing to read.\n\nBranch on `result.outcome`, not on status alone:\n\n| `outcome`           | Meaning                                                   |\n| ------------------- | --------------------------------------------------------- |\n| `ROWS`              | Matches found                                              |\n| `EMPTY`             | Every filter ran and nothing cleared them all — an answer  |\n| `CLARIFICATION`     | The question was ambiguous; `result.clarification` asks    |\n| `GENERATION_FAILED` | The retry budget was spent — the only case worth retrying  |\n\nThis method spends LLM credits. The structured `lake` API below does not.\n\nUse the manifest plus `downloadLakeQueryPart` to stream results within your own\nmemory budget. NexusTrade picks a compatible backing engine for the referenced\ntables; your SQL does not change when it does.\n\n> The Python SDK additionally ships `nt.lake.sql(...)`, a DuckDB/pandas\n> convenience layer over these same endpoints.\n\n## Complete method reference\n\nEvery public method on `NexusTradeClient`. A test in this package fails if one\nis missing here, so this list cannot drift from the code.\n\n**Live trading and orders**\n\n| Method                                                  | Purpose                                              |\n| ------------------------------------------------------- | ---------------------------------------------------- |\n| `listBrokerages()`                                      | Every connectable brokerage and whether it is linked |\n| `getBrokerage(brokerage)`                               | Whether one brokerage is linked                      |\n| `connectBrokerage(brokerage, { wait })`                 | Log the connect URL and wait for the link            |\n| `createOrders(portfolioId, orders, { idempotencyKey })` | Stage orders; live ones need approval                |\n\n**Portfolios**\n\n| Method                                      | Purpose                                      |\n| ------------------------------------------- | -------------------------------------------- |\n| `createPortfolio(book, { idempotencyKey })` | Persist a portfolio definition               |\n| `listPortfolios(options)`                   | List portfolios, with filters and pagination |\n| `getPortfolio(portfolioId)`                 | Read one portfolio                           |\n| `deploy(portfolioId, { frequency })`        | Start paper trading it                       |\n| `undeploy(portfolioId)`                     | Stop it                                      |\n\n**Backtests**\n\n| Method                                         | Purpose                    |\n| ---------------------------------------------- | -------------------------- |\n| `createBacktest(handle, { idempotencyKey })`   | Submit one backtest        |\n| `createBacktests(handles, { idempotencyKey })` | Submit many in one request |\n| `getBacktest(backtestId)`                      | Read the operation         |\n| `waitForBacktest(backtestId, options)`         | Block until terminal       |\n| `waitForBacktests(operations, options)`        | Block on a whole batch     |\n\n**Optimization and walk-forward**\n\n| Method                                           | Purpose                     |\n| ------------------------------------------------ | --------------------------- |\n| `createOptimization(handle, { idempotencyKey })` | Submit an optimization      |\n| `getOptimization(optimizationId)`                | Read the operation          |\n| `waitForOptimization(optimizationId, options)`   | Block until terminal        |\n| `createWalkForward(handle, { idempotencyKey })`  | Submit a walk-forward study |\n| `getWalkForward(studyId)`                        | Read the operation          |\n| `waitForWalkForward(studyId, options)`           | Block until terminal        |\n\n**Custom data sources**\n\n| Method                                                                      | Purpose                                            |\n| --------------------------------------------------------------------------- | -------------------------------------------------- |\n| `createCustomIndicator(spec, { idempotencyKey })`                           | Create a series, optionally seeded                 |\n| `listCustomIndicators(options)`                                             | List owned series                                  |\n| `getCustomIndicator(id)`                                                    | Read one, with its point count and range           |\n| `appendCustomIndicatorPoints(id, points, { idempotencyKey })`               | Add points                                         |\n| `replaceCustomIndicatorPoints(id, points, { idempotencyKey, allowShrink })` | Replace the complete series while retaining its id |\n| `archiveCustomIndicator(id, { confirm })`                                   | Soft-archive a series                              |\n| `restoreCustomIndicator(id)`                                                | Restore an archived series                         |\n| `createCustomIndicatorUpload(id, options)`                                  | Open an upload slot (CSV/JSON/JSONL)               |\n| `completeCustomIndicatorUpload(id, jobId)`                                  | Start validating uploaded bytes                    |\n| `getCustomIndicatorUpload(id, jobId)`                                       | Read the upload operation                          |\n| `waitForCustomIndicatorUpload(id, jobId, options)`                          | Block until validated                              |\n\n**Agent runs**\n\n| Method                                    | Purpose                             |\n| ----------------------------------------- | ----------------------------------- |\n| `createAgent(prompt, { idempotencyKey })` | Start a run                         |\n| `getAgent(agentId)`                       | Read its status                     |\n| `attachAgent(agentId, { cursor })`        | Reattach to a run already in flight |\n\n**Lake SQL**\n\n| Method                                          | Purpose                              |\n| ----------------------------------------------- | ------------------------------------ |\n| `createLakeQuery(request, { idempotencyKey })`  | Submit read-only SQL                 |\n| `getLakeQuery(queryId)`                         | Read the operation                   |\n| `waitForLakeQuery(queryId, options)`            | Block until terminal                 |\n| `cancelLakeQuery(queryId)`                      | Cancel an owned query                |\n| `createLakeAsk(question)`                       | Ask the lake in plain language       |\n| `getLakeAsk(askId)`                             | Read the operation                   |\n| `waitForLakeAsk(askId, options)`                | Block until terminal                 |\n| `cancelLakeAsk(askId)`                          | Cancel an owned ask                  |\n| `getLakeQueryManifest(queryId)`                 | Schema, checksums, and part metadata |\n| `downloadLakeQueryPart(queryId, part, options)` | Download one Parquet part            |\n| `getLakeCatalog()`                              | List queryable tables                |\n| `describeLakeTable(table)`                      | Columns and types for one table      |\n\n**Natural language**\n\n| Method                                      | Purpose                                     |\n| ------------------------------------------- | ------------------------------------------- |\n| `createNlScreen(question, { returnQuery })` | Screen stocks from a plain-language question |\n| `getNlScreen(screenId)`                     | Read the operation                          |\n| `waitForNlScreen(screenId, options)`        | Block until terminal                        |\n| `cancelNlScreen(screenId)`                  | Cancel an owned screen                      |\n\n**Client construction**\n\n| Method                                      | Purpose                                  |\n| ------------------------------------------- | ---------------------------------------- |\n| `new NexusTradeClient({ apiKey, baseUrl })` | Explicit credentials                     |\n| `NexusTradeClient.fromEnvironment()`        | Read them from the environment or `.env` |\n\n**PortfolioHandle** — returned by the `portfolio(...)` builder and by\n`getPortfolio` / `listPortfolios`.\n\n| Method                                             | Purpose                                |\n| -------------------------------------------------- | -------------------------------------- |\n| `save({ idempotencyKey })`                         | Persist it as a draft, setting `.id`   |\n| `backtest({ startDate, endDate, idempotencyKey })` | Backtest it, preferring the saved id   |\n| `deploy({ frequency })`                            | Mint the real paper portfolio (new id) |\n| `undeploy()`                                       | Deactivate its deployment              |\n\n## Authentication\n\nCreate a key at **[nexustrade.io/developers](https://nexustrade.io/developers)**\n(Profile → API Keys). Keys start with `sk-` and are shown once.\n\n```ts\nconst client = new NexusTradeClient({\n  apiKey: \"sk-...\",\n  baseUrl: \"https://nexustrade.io/api/v1\",\n});\n// or set NEXUSTRADE_API_KEY / NEXUSTRADE_API_BASE_URL and:\nconst fromEnv = new NexusTradeClient();\n```\n\nBoth variables are also read from a **`.env` file** at or above the current\ndirectory, so a local project works with no exports, no `dotenv` dependency, and\nno `--env-file` flag:\n\n```bash\n# .env\nNEXUSTRADE_API_KEY=sk-...\nNEXUSTRADE_API_BASE_URL=https://nexustrade.io/api/v1\n```\n\nThe real environment always wins — a `.env` value is used only when the variable\nis absent, so a stale file can never override what you exported. Nothing is\nwritten back to `process.env`. Opt out with `NEXUSTRADE_DISABLE_DOTENV=1`.\n\n| Scope   | Grants                                                                            |\n| ------- | --------------------------------------------------------------------------------- |\n| `read`  | `getBacktest`, `getOptimization`, `getWalkForward`                                |\n| `write` | `createPortfolio`, `createBacktest(s)`, `createOptimization`, `createWalkForward` |\n| `lake`  | Lake catalog, query lifecycle, manifests, result parts                            |\n\nA key missing the scope gets `403 insufficient_scope`.\n\n> **OAuth is not accepted here.** NexusTrade's OAuth flow serves the MCP server.\n> These endpoints take `sk-` API keys only; a bearer JWT is rejected with\n> `401 invalid_token`.\n\n**Transport hardening.** HTTPS is required (except loopback). The client refuses\ncross-origin redirects, so the credential cannot be replayed to another host, and\nrefuses to follow a redirect on any non-GET request, so a redirect can never\nre-submit a paid job. The key is held in a `#private` field and never appears in\na stringified client.\n\n## Idempotency\n\nEvery mutation takes a key. Reusing the same key with the same request returns\nthe original resource instead of launching a second paid job — so a retry after\na network failure is free.\n\n```ts\nawait client.createBacktest(handle, { idempotencyKey: \"momentum-2024-v1\" });\n```\n\n## Errors\n\n```ts\nimport { NexusTradeApiError } from \"nexustrade\";\n\ntry {\n  await client.createBacktest(handle, { idempotencyKey: \"run-1\" });\n} catch (error) {\n  if (\n    error instanceof NexusTradeApiError &&\n    error.code === \"rate_limit_exceeded\"\n  ) {\n    // back off\n  }\n  throw error;\n}\n```\n\n| Status | Code                                   | Meaning                                                      |\n| ------ | -------------------------------------- | ------------------------------------------------------------ |\n| 401    | `invalid_token`                        | Missing, malformed, or expired key (or an OAuth JWT)         |\n| 403    | `insufficient_scope`                   | Key lacks `read`, `write`, or `lake`                         |\n| 400    | `invalid_request`, `invalid_portfolio` | Malformed input                                              |\n| 400    | `invalid_idempotency_key`              | Must match `[A-Za-z0-9._:-]{1,160}`                          |\n| 409    | `idempotency_conflict`                 | Key reused with a different payload                          |\n| 409    | `idempotency_in_progress`              | Same key, first call still running. Re-poll, do not resubmit |\n| 404    | `not_found`, `operation_not_found`     | Unknown or not yours                                         |\n| 429    | `rate_limit_exceeded`                  | Back off and retry                                           |\n\n`status` is `0` when no HTTP status describes the failure: `transport_error`\n(never reached the API), `unsafe_redirect`, or an `invalid_response` envelope\ncheck on an otherwise-successful reply.\n\n## Timeouts\n\n`new HttpTransport({ timeoutSeconds })` (default 30) is a total wall-clock\ndeadline for one request. Neither it nor the poll timeout bounds how long a\n_job_ takes.\n\n## Scope\n\nPortfolio drafting, backtesting, optimization, walk-forward studies, and\nread-only SQL over the market-data lake, versioned under `/api/v1/nexustrade`.\nThe screener and creating a live deployment remain outside this surface.\nOrders are reachable, but a live order is only ever staged for human approval —\nnever submitted. `deploy` and `undeploy` act on whatever an existing id already\nis, live included.\n\n## Requirements\n\nNode 18+ (uses the global `fetch`). Contributing: the test suite runs TypeScript\ndirectly via `node --test`, which needs Node 22.6+ for type stripping. The\npublished `dist/` is plain JavaScript and has no such requirement.\n\n## Using this SDK with a coding agent\n\nSee **[AGENTS.md](AGENTS.md)** — the conventions, invariants, and recipes an\nagent needs to write correct NexusTrade strategies on the first pass.\n\n## License\n\nMIT\n",
  "bytes": 33524,
  "sha": "9c81893306e99a6c8366b37386887de29ce7600728c5ca8309035d303d4ab4b2",
  "repo_slug": "austin-starks/nexustrade-ts",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_austin_starks_nexustrade_mcp_635118fc/readme"
}