Design Log

Every decision in this design, dated, with reasoning — so it can be revisited and confirmed.

This is the record Jason asked for: what was decided, when, and why. The docs on this site are the source of truth for the API update; this page is the source of truth for how they got this way. Append-only — reversals get a new entry, not an edit.

2026-07-28 — Initial design

D1. Docs-first development. These docs are written ahead of implementation and act as the spec. The API is built to match the docs, not the other way around. Rationale: the current 5,240-line hand-written OpenAPI file proved that spec-after-code drifts; spec-before-code also gives Jason a reviewable artifact before any backend work is spent.

D2. Two pillars + auth plane. /context (market data engine) and /runtime (execution), with /account for keys. Rationale: matches the business ("infrastructure AI agents build on") and replaces version-prefixes-as-product-matrix. Framework and venue become request fields — adding either never mints a new API version.

D3. Endpoint budget ~30, down from ~138. Consolidations: status/result embedded in resource GETs; paper trading as mode, not a resource tree; one deployment create for all frameworks. The original draft used one PATCH verb for lifecycle; D34 supersedes that with explicit lifecycle endpoints after review found the loose body too ambiguous.

D4. BYOK-only draft superseded. The original draft removed custody endpoints from the public surface and treated the API as BYOK-only. D33 and D35 supersede it: managed trading wallets are the default account path, BYOK remains the self-custody option, and wallet operations live under /wallet with explicit idempotency and polling contracts.

D5. OTP returns the key in-band. Replaces the magic-link flow that emailed the plaintext key. Rationale: an agent can't read its operator's inbox; a six-digit code is human-relayable and expires in 10 minutes; a key in an inbox is a standing liability. Existing abuse controls (3/6h rate limit, disposable-domain screening) carry over unchanged.

D6. Scoped-key preference became a hard default. The original draft labeled scoped_key: false with a warning rather than rejecting master keys. D33 supersedes it: unrestricted credentials are rejected unless the request body includes explicit unsafe-credential acknowledgements. Rationale: agent-facing APIs need unsafe choices to be machine-visible and deliberate, not hidden behind a 2xx.

D7. Client-side onboarding ceremony (@superior-trade/onboard). Agent-wallet approval and builder-fee signatures move to an open-source CLI run locally, because only the user's master key can sign them and BYOK forbids us receiving it. The API checks readiness; it never signs.

D8. Datasets get an HTTP surface. /context/candles + /context/funding serve the existing GCS releases (Lighter: 1m OHLCV + mark + hourly funding, ~226 markets; Polymarket: outcome history). Rationale: the data engine already exists and is validated — it was simply unexposed. Immutable release IDs make reads reproducible and CDN-cacheable.

D9. Honest-data invariants. Venue-side history holes are served as gaps, never interpolated. Backtest results always include market_change_pct. Paper metrics carry simulated: true. Rationale: these are brand values (see the leaderboard's mandatory disclaimer today) promoted to API contract.

D10. Spec generated from code when built. Zod schemas → OpenAPI via @hono/zod-openapi (already a dependency, currently unused). The hand-written spec file is retired. These MD pages remain the narrative layer; the generated spec becomes the reference layer.

D11. Agent-native docs serving. Every page fetchable as raw markdown at /md/<path>; /llms.txt generated from the nav tree; error responses carry docs_url. Modeled on vercel.com/docs, which serves markdown+frontmatter to machine readers — verified live 2026-07-28.

D12. Docs layout = Vercel, skin = terminal v2. Three-column shell (sidebar / article / on-this-page), ⌘K search, card landing. Styling imports terminal-v2's actual tokens: lime #a3e635 on near-black, DM Sans + Space Mono, liquid-glass panels. Dark-only in this preview; the terminal's GitHub-Light remap is planned, logged here so it isn't forgotten.

D13. Error taxonomy commitments. Stable append-only codes; distinct problems get distinct codes (credentials_missingaccount_not_funded — a direct lesson from the current API rewriting "not onboarded" as "not funded"); docs_url in every error.

D14. Old API sunsets slowly. v1/v2/v3 keep working during build-out; deprecation with Sunset headers and dates; 410 + docs_url at the end. Existing st_live_ keys valid on both surfaces throughout.

2026-07-28 — Layout rebuilt against the measured Vercel docs system

D15. The first pass looked nothing like Vercel's docs (Jason's call — correct), because it was built from memory. Rebuilt after pulling vercel.com/docs HTML + CSS chunks and extracting the actual system: 64px sticky header (transparent over black, hairline on scroll); 1400px page grid with 300px sidebar / fluid article / 240px sticky TOC; search box at the top of the sidebar (not the header) with a ⌘K kbd; sidebar groups with 13px gray labels and 14px-medium h-8 items at 1px gaps; H1 48/56 semibold at −6% tracking; 16/24 body #a0a0a0 on black with #ededed headings; hairline borders at white/14%; 13/20 code in bordered rounded blocks with a 40px header row; breadcrumb + "Copy page" utility row above the H1; TOC with "Was this helpful?" pill and scroll-to-top; the docs landing rendered inside the docs shell (Vercel's index is a docs page with card grids, not a marketing hero). All glass/glow surfaces dropped except the ⌘K modal — Vercel's docs are flat; Superior identity is carried by the lime accent (links, active nav, badges), DM Sans/Space Mono, and the black background, which happens to match Vercel's dark theme exactly. Measured values are annotated in app/globals.css.

2026-07-28 — Header unified with the terminal (Jason-directed)

D16. The docs header is the terminal's header. Rebuilt to mirror terminal-v2/components/terminal/header.tsx: 56px bar, border-b white/10 over bg-black/40, the v3 logo mark + "Superior Trade" wordmark (muted "Docs" suffix), and a single Open Terminal CTA in the terminal's deposit-pill style (lime, rounded-full, mono 10px bold uppercase). Removed per Jason: the "Design preview" badge and the separate Terminal / superior.trade buttons. Rationale: the docs and the product should read as one surface; the chrome users already know is the chrome the docs should wear.

2026-07-28 — Dual-eval validation round and the honesty pass it forced

Jason reset validation with a sharper rubric: (1) does the writing work for its actual readers, (2) will the documented features work in practice. Two independent evaluators ran — a reader-experience eval (four personas walking the docs end to end) and a feasibility eval (every documented behavior graded EXISTS / BUILDABLE / HARD / FICTION against the real apps/api backend). Feasibility bottom line: ~60% of the surface is a facade over machinery that already runs; four load-bearing gaps (Nautilus paper mode, Nautilus per-deployment metrics, HL/Binance dataset releases, delete guarantees on shared runtimes); two claims that were flatly false. Fixes below — docs changed to be honest, not promises added.

D17. The quickstart must be literally executable. Rewrote it: paper deployment now started explicitly (creation never starts — the docs said so and the quickstart forgot); "flip to live" removed (mode is fixed at creation; live = new deployment); a complete 30-line runnable strategy included; content-type on every request; retitled plain "Quickstart" (the "for Agents" title repelled the human half of the audience).

D18. The human↔agent seam is documented, not implied. The two deliberate human handoffs (OTP; scoped-key after the local signing ceremony) are named as such, and the deployment GET gained a venue_account readiness probe (funded, balance_usd) so an agent checks funding instead of starting-and-hoping. Feasibility: the underlying balance checks exist.

D19. Deletion honesty. "After a 200 no process holding your key exists" was structurally untrue for Nautilus shared runtimes (reconciler-based teardown; per-wallet shared credential). New contract: DELETE202 + deleting status; deleted only after confirmed teardown + credential destruction; shared-wallet credentials destroyed with their last deployment. Confirmed-not-assumed is the strongest claim the architecture supports.

D20. Network-restriction claim retracted. The docs claimed sandbox egress restricted to the venue with violations failing "at validation" — false on both counts (no egress policy on freqtrade pods; v3 policy deliberately allows public 443 because venue IPs live behind CDNs; validation-time detection of arbitrary network calls is not honestly possible). Replaced with the truth: gVisor isolation, private networks blocked, general outbound HTTPS currently permitted and unsupported.

D21. Paper mode is freqtrade-only at launch. Nautilus sim execution has zero code today; paper_supported now reads false for Polymarket/Lighter, the canonical loop offers a bounded-live substitute, and Nautilus paper is planned work, not documented behavior.

D22. Release pinning claimed only where release machinery exists. Lighter/Polymarket: immutable releases, live today. Hyperliquid/Binance: release: null, mutable backfill store, reproducibility explicitly not claimed until their pipeline exists. "No gaps array means not-audited, never no-gaps."

D23. The npx @superior-trade/onboard block removed. A copy-pasteable command for a package that would 404 was the most checkable false claim in the docs. The two-call SDK ceremony (approveAgent + approveBuilderFee, shown inline) is the supported path; the CLI is planned. Building it is S/M — it should exist before public launch.

D24. Security page argues with mechanisms, not adjectives. Named gVisor; named the secret-store boundary (runtime cluster store, never the application DB); stated the residual risk plainly — a leaked trade-only key can destroy value up to the account balance via deliberate bad trades; scoped keys bound a leak, they don't neutralize it. Funding discipline is part of the security model. Also: scoped_key: true is a venue lookup (derived address ∈ approved agents), not an assurance.

D25. Smaller corrections from the evals: Hyperliquid market responses originally carried framework aliases as the primary symbol. D32 supersedes that: Context symbol is venue-native and framework aliases live under framework_symbols. Intelligence bucket list corrected (majors excluded by the engine's design) and the "same data as candles" claim dropped; simulated field added to the metrics example; limit_exceeded added to the error registry; leaderboard example drawdown made plausible; Free-tier pricing stated ($0) with Pro priced at launch; freqtrade guide now states the venue scope (HL/Binance only) and the hyperopt/FreqAI boundary in its opening; HIP-3 defined at first use; a Reliability section added to the runtime overview (crash-restart, state persistence, positions never auto-closed by infrastructure).

2026-07-30 — Minimal-text rework, 繁體中文, stage-1 scope (Jason-directed)

D26. Reference density, not prose. Jason: "cut down all the bullshit and text — too hard to work on." Every content page rewritten to tables/JSON/one-line notes under a strict contract: all 32 endpoints, every example, table, error code, and honesty caveat preserved; all aphorisms and philosophy paragraphs deleted. Result: −32% words / −29% lines overall, with the survivors being mandatory content (JSON, tables, code), not prose. Chrome cut to match: header is logo + wordmark + language toggle only (no nav links, no Open Terminal, no "Docs" word); TOC dropped "Was this helpful?" and "Scroll to top"; landing is one line + title-only cards.

2026-07-30 — PR review hardening

D32. Context uses canonical venue-native symbols. /context/markets, candles, funding, datasets, and URL path params use symbol as the venue-native id. Framework-native names are aliases in framework_symbols, used only inside framework config. Rationale: one primary id prevents clients from passing a freqtrade pair to a Context endpoint that expects venue data.

D33. Unsafe BYOK credentials require explicit acknowledgement. PUT /runtime/deployments/:id/credentials rejects unrestricted or withdrawal-capable credentials by default with credential_scope_unsafe. The override requires allow_unscoped_credential, acknowledge_withdrawal_risk, and acknowledge_not_recommended in the request body. Rationale: agents treat 2xx as success; unsafe custody choices must be visible in input and output.

D34. Deployment lifecycle actions use a status endpoint. Metadata stays on PATCH /runtime/deployments/:id; lifecycle transitions use PUT /runtime/deployments/:id/status with { "action": "start" | "stop" }. Delete force is a request-body acknowledgement, not ?force=true. Rationale: validation and destructive behavior should be discriminated by endpoint and body shape, not inferred from loose metadata fields or query params.

D35. Money-moving async calls require idempotency and polling. Managed-wallet withdrawal now requires Idempotency-Key, returns a withdrawal resource, and documents GET /wallet/withdrawals/:id plus list. Rationale: retry after a lost 202 must never create a second payout.

D27. Traditional Chinese. EN/繁 toggle in the header (cookie-based). Content lives in content/en/ and content/zh-Hant/ with per-page English fallback, so a missing translation never 404s. Nav, chrome strings, search index, and /md/<path>?lang=zh-Hant all localize; llms.txt stays English.

D28. Stage-1 design scope: Account + Runtime only. For first-stage design focus the sidebar, landing, search, and llms.txt show only the Account and Runtime groups (stage1 flags in lib/nav.ts). Getting Started, Market Context, Guides, and Reference remain authored and render by direct URL, but are hidden until later stages.

2026-07-30 — D26 reversed: full text restored (Jason-directed)

D29. The tersification (D26) is rolled back. Jason's call before sharing with the team: the docs should read like an established API reference (Stripe/Vercel register — complete sentences, context around every endpoint), not a bare cheat-sheet. All content pages restored to their pre-tersification (post-honesty-pass) text at content/en/. What stays from the rework: the minimal chrome (D26's header/TOC/landing cuts), the 繁體中文 toggle (D27), and the stage-1 Account+Runtime scope (D28). The terse experiment remains in git history (6cf306a3) if a condensed variant is ever wanted.

2026-07-30 — Developer-persona review applied + the two-execution model

D30. Developer-review fixes. A developer-persona evaluation (senior quant-shop dev deciding whether to integrate) surfaced seven must-fixes; all applied: the stage-1 visible scope is now self-sufficient for a first request (base URL + x-api-key + full curls in account/registration and runtime/deployments); alive_until lapse semantics are specified, including a new on_expiry: "stop" | "block_entries" field (block-entries keeps exit logic running until flat — the safe expiry for position-holding bots); code/config immutability and the live-update path are stated; image dependency contents and create-time digest pinning documented; a backtest cost-model table (fees and builder fee modeled; slippage and funding NOT — stated plainly); Idempotency-Key on all creates plus name_taken; the Polymarket paper-mode contradiction in the Nautilus guide fixed. Also: per-code HTTP status registry with x-request-id on every response, webhooks promoted from candidate to committed roadmap with an event list, list-response envelopes shown, credential verification timing (PUT + start) and stop→PUT→start rotation specified, OpenAPI location declared, UTC reset boundaries stated, and a pass trimming "honest/honesty" self-references (the reviewer: "Stripe never tells you it's being honest").

D31. Two kinds of execution (Jason-directed). The runtime now has an explicit split: one-time execution (/runtime/executions — a Python script that runs once with injected credentials and exits; place/close/rebalance/sweep; 15-min cap; execution_timeout and script_error codes) and recurring execution (deployments, as before). This also closes the reviewer's "how do I close positions via the API" gap — a close script is a one-time execution. New stage-1-visible Quick Start group (Introduction — supported platforms + the two execution types; Agent — the integration flow request-by-request) becomes the on-ramp, fixing the reviewer's blocker that stage-1 hid all authentication context. Site title fixed to "Superior Trade API"; search moved to the header center with neutral (non-lime) selection.

D32 (planned, Jason 2026-07-30). The Quick Start Agent page becomes a copy-able prompt: one block the operator pastes into their AI agent that teaches it the whole integration (base URL, auth + OTP relay, the two human handoffs, endpoint map, readiness probes, failure etiquette) — the page's docs prose becomes secondary to that block. Deferred by Jason ("can come later"); the current request-by-request page stands until then. Natural implementation: a <CopyPrompt> block at the top of quickstart/agent.md reusing the existing copy-button plumbing, with the prompt maintained as its own file so /md/ serves it to agents directly.

2026-07-30 — Credential model pivot: managed wallets primary, BYOK optional

D33. D4 amended (Jason-directed). The public API keeps Superior-managed trading wallets (Privy) as the primary credential path — deposit and deploy, no key handling, same custody that runs the Terminal — with BYOK as the option for self-custody users and existing venue accounts. Rationale: flexibility at deploy time beats purity; the managed path is the fastest onboarding and reuses the proven allocate-on-deploy machinery, while BYOK remains fully specced (toxic-material rules unchanged, scoped-key verification unchanged) for users who won't hand over custody. Spec shape: credentials on create defaults to { "type": "managed" }; BYOK payloads carry "type": "byok". What D4 removed from the public surface partially returns: managed-wallet funding/withdrawal semantics (deposit instructions via GET /account, withdrawals restricted to the verified login wallet). Supported-platforms table now ranked by typical trading volume with venue logos (Binance > Hyperliquid > Lighter > Polymarket — simple public-volume eval, not a study).

D34 (2026-07-30). Follow-ons to D33: the managed wallet gets its own account-level page (/wallet/overviewGET /wallet for address/deposit/held-vs-allocated balances, POST /wallet/withdraw to the verified login wallet only, no destination parameter by design). Site gains dark/light theme with system default (pre-paint inline script, GitHub-Light palette in light mode, cycle toggle system→light→dark in the header). Landing-section icons via the terminal's Higgsfield crystal-glass pipeline are queued (blocked on CLI re-auth).

2026-07-30 — Phi's API review applied (D35)

D35. Five findings from Truong Ma Phi's review of the spec, all accepted and applied: (1) POST /account/wallet/withdraw now requires Idempotency-Key — money movement must be retry-safe by construction, not convention. (2) PATCH's action/metadata mixing was ambiguous under partial failure — lifecycle moved to explicit POST …/start / POST …/stop endpoints; PATCH edits metadata only (this reverses D30's "fields combine" convenience — Phi's critique was correct). (3) DELETE ?force=true replaced by a two-step acknowledgement: 409 positions_open returns a single-use, state-bound confirm_token; the caller repeats DELETE with X-Confirm-Delete. (A DELETE request body was considered per Phi's suggestion and rejected only because proxies drop DELETE bodies unreliably; the token achieves the same explicit acknowledgement.) (4) Credentials are now one discriminated union everywhere — {"type": "managed"} | {"type": "byok", "venue", …}; omission ≡ managed on deployments, ≡ credential-less on executions (stated, not implied); credentials: null eliminated from all examples. (5) The withdrawal flow is complete: GET /account/wallet/withdrawals + /:id with the queued → processing → completed | failed lifecycle and tx_hash. New error codes: idempotency_key_required, invalid_confirm_token.

D36 (2026-07-30, Jason-directed). Per-endpoint subsections in the sidebar: the active page's headings (endpoint METHOD /path entries in mono, prose sections in text) render directly under its sidebar item with scroll-spy highlighting and smooth-scroll anchors. The right-rail "On this page" TOC is removed — navigation lives in one place.

2026-07-30 — Second review round from Phi's PR threads (D37)

D37. Five further findings from the PR review threads, all applied: (a) unscoped (master/withdrawal-capable) credentials are now rejected by default with 400 unscoped_credential; the override is an explicit allow_unscoped: true body field — machine-visible, deliberate, never a silent 2xx. This substantially resolves open question 1 at attach time. (b) One canonical market identifier: /context/markets' symbol is THE id for every /context endpoint; framework spellings moved to an aliases map used only in strategy config ("context reads → canonical; strategy config → alias") — the markets-vs-candles contradiction is gone. (c) Executions gained a sandbox contract table: 1 vCPU/1 GiB, ephemeral 512 MiB /tmp, package allowlist, gVisor + egress posture (consistent with D20's honesty), 1 MiB output cap, credential redaction. (d) The header no longer overflows small viewports (fixed rails now lg:-only, ⌘K hint hidden on mobile). (e) Hidden pages get full page chrome — breadcrumb/title resolve against the full nav; neighbors paginate within the visitor's context (staged list for staged pages, full list for hidden ones) — and apps/docs has a working ESLint flat config so lint runs.

2026-07-31 — Endpoint diet (43→38) + Managed Wallet section (D39)

D39 (Jason-directed). Five cuts, Account/Runtime only (Context untouched by choice): GET …/deployments/:id/credentials dropped (duplicate of the deployment GET's embedded metadata); GET /account/usage merged into GET /account (one small read for the whole account picture); PATCH /account/keys/:id dropped (names fix at mint; rotation replaces renaming); GET …/withdrawals/:id merged into the list via ?id=; DELETE /runtime/backtests/:id dropped (12-month retention; re-addable non-breakingly). A cut that was proposed and rejected, and the principle behind the rejection: folding /logs into resource GETs via ?include=logs was retracted because agent readers have context windows — resource GETs must stay small by construction, and bulk text (logs, big trade lists) is always a separate, deliberate fetch. All three /logs endpoints stay.

Also per Jason: the managed wallet is promoted from one Account page to its own stage-1 Managed Wallet section — Overview (GET /account/wallet, held vs allocated, lifecycle), Deposit (the on-chain flow, rules and edges), Withdraw (POST …/withdraw + the merged withdrawals list). account/trading-wallet is removed and all links retargeted. Crystal-glass wallet icon added to the landing via the terminal icon pipeline.

2026-08-01 — Reconciliation with the unified-api implementation (D40)

Two parallel lines of work met on this branch: the docs' D35–D39 passes and the first apps/unified-api implementation. Where they disagreed, the implementation's choices win, superseding parts of D35/D37 recorded above:

  • Lifecycle: one endpoint — PUT /runtime/deployments/:id/status with { "action": "start" | "stop" }. Supersedes D35's separate POST …/start / POST …/stop. PATCH stays metadata-only either way.
  • Delete around open positions: explicit body acknowledgement (force + acknowledge_positions_open), not the D35 confirm_token two-step. Still never a query flag.
  • Wallet surface: GET /wallet, GET /wallet/deposits, POST /wallet/withdraw, GET /wallet/withdrawals[/:id] (not /account/wallet/*). Nav group is Managed Wallet (Jason's naming); page slugs are wallet/deposits / wallet/withdrawals.
  • Unscoped-credential override: allow_unscoped_credential plus the two acknowledgement fields, error credential_scope_unsafe (supersedes allow_unscoped: true / unscoped_credential).
  • Markets: framework spellings stay under framework_symbols.
  • QA callout dropped (Jason, 2026-08-01): the boxed Q&A blockquote treatment is out; those passages render as plain blockquotes.

Open questions awaiting Jason

  1. D6 override — hard-require scoped keys for live starts?
  2. Domain — docs at docs.superior.trade with api.superior.trade/docs redirecting, or keep docs on the api host?
  3. Deposit floor amountsmin_deposit_usd values in /context/venues are design placeholders; confirm per venue.
  4. Plan limits — Free/Pro numbers on Usage & Limits mirror today's tiers; confirm they're the go-forward tiers for API-first users.
  5. Copy-trade execution — shipped as a strategy template on deployments (current design) or as a first-class resource?
  6. Delete vs open positions — today's stop path auto-closes positions; this design deliberately does NOT (409 positions_open + explicit force). Reversing live behavior is a user-safety call — confirm.
  7. Entitlements — the 3/6 deployment limits are new (current code caps at 10 deployments, 3 trading accounts); a plan concept doesn't exist in code yet. Confirm the go-forward numbers and where Pro is sold.
  8. Secret-store bar — BYOK keys land in Kubernetes Secrets today; is that the accepted bar, or do we require KMS-envelope/sealed secrets before accepting third-party keys?
  9. Where missing compliance/eligibility surface lives — "an anonymous email buys a trading runtime": geo/KYC/terms posture needs a page, and the policy behind it is not a docs decision.

How to propose a change

Edit the page in apps/docs/content/, and add a dated entry here saying what changed and why. The diff is the design review.