Skip to main content

Changelog

The API follows semantic versioning; the current version is 0.4.0. Breaking changes are called out explicitly, and the deploy pipeline runs a breaking-change diff on every spec change, so undeclared breaks fail CI before they can ship.

0.4.0 — 2026-08-30

Docs-site groundwork (api_docs_site_spec.md Phase B): the spec closes its two external-surface gaps and gains a public variant for the documentation site at docs.staging.sphx.io.

  • Added: POST /v1/margin/transfer — user-initiated margin sub-account cash transfer (direction in/out, µUSDC, optional client transfer_id); 202 accepted-for-sequencing semantics. The endpoint shipped with the B2 margin slice; the spec had never documented it.
  • Added: GET /healthz/venue — the public venue-functional liveness probe (two-sided books, fill-tape freshness, disk headroom). HTTP 200 for both verdicts; monitors match on data.status.
  • Added: x-internal: true on all 8 operator (/admin/*) operations, and a generated openapi.external.yaml (gen_openapi.py --external logic, emitted alongside the full spec): drops x-internal operations, prunes unreferenced schemas, unused security schemes, and empty tags. The docs site renders the external variant; the full spec remains the internal contract of record. --check now covers both files.
  • Changed: servers: — staging marked LIVE (it has been since 2026-08-01); production stays a placeholder.
  • Added: asyncapi.yaml — an AsyncAPI 3.0 document for the WebSocket surface (EDGE-085): the session envelope, all 7 external channels, and every event's exact payload, verified field-by-field against the edge's emitting code. The docs site renders it as the streaming reference and serves it at /asyncapi.yaml. Parity fixes folded back into websockets.md: the mass-cancel field is sequencer_correlation_id (the TS union said correlation_id), and the fill arm gains fee_uusdc + tag.
  • Added: operationId + tags on the five margin-era read endpoints (margin_info, insurance_fund, liquidation_metrics, contract_specs ×2) — they rendered as UNTAGGED in generated docs.
  • Fixed: the /v1/ws stub's channel list now names all seven public/ private channels (trade, mark, account were missing); private channels corrected to fill + account. New Margin tag groups the margin-domain operations.

0.3.1 — 2026-08-09

Email+password auth for outside users (auth review option A, edge d4dd5cc). Passwords are a credential factory, not a parallel auth system: signup/login mint a short-lived session credential (label session, 7-day expiry) and return the same <prefix>.<suffix> bearer API keys use, so every authenticated endpoint accepts both token kinds unchanged; logout is credential revocation.

  • Added: POST /v1/auth/signup — anonymous. Creates the account (entitlement mask 7 — no APITrading for outside users), stores the password (argon2id), returns SessionResponse (account, one-time session_token, expires_at_ns, and — on faucet-enabled dev/staging venues — faucet_uusdc, sequenced asynchronously). 409 email_taken, 429 rate_limited (10 / 5 min per IP).
  • Added: POST /v1/auth/login — anonymous. Uniform 401 for unknown-email vs wrong-password (equalized timing); 429 rate_limited (10 / 5 min per IP+email).
  • Added: POST /v1/auth/logout — Bearer. Revokes the CURRENT session credential; an API key calling it gets 400 invalid_request (revoke keys via DELETE /v1/credentials/{credential_id}).
  • Added: shared Conflict (409) and RateLimited (429) response components; Auth tag.

0.3.0 — 2026-08-05

REMOVED (breaking): the funding-rate history surface — the GET /v1/instruments/{instrument_id}/funding REST endpoint and the public funding WebSocket channel. The terminal's funding-line overlay it fed was retired (operator decision 2026-08-05); realized funding remains fully visible through the account channel's settlement events and the snapshot's reserved_funding. The engine's funding MECHANISM is unchanged.

0.2.5 — 2026-08-04

  • POWER v12 (power_products_spec §4.2): ContractSpec gains has_price_domain / min_price_ticks / max_price_ticks / exposure_floor_uusdc / funding_dollar_cap_daily_rate_bps / funding_dollar_cap_lookback_hours; the margined listing request accepts the same fields. GET /v1/instruments/{id}/orderbook adds yes_asks (side-native margined SELLS — signed ticks; the §11.9 no_bids complement remains event-class only). side=no candles are rejected on margined instruments. Zero-behavior-change for existing listings: domain fields are flag-gated, floors default 0.

2026-08-01 — 0.2.4 — candle backfill (dev/staging, additive)

  • Added: POST /admin/candles/backfill — seeds historical bars into the in-memory candle aggregator (non-sequenced projection; refuses bars at/after the first live fill). Companion tool tools/candle-backfill sources real market history per instrument (feed-grammar-driven source registry) and strike-derives binary probability paths from the underlying.

2026-07-20 — 0.2.3 — funding surface completed + margined metadata registry (additive)

Driven by the live-frontend integration (createEdgeProvider): the terminal's class mappers need a public identity record for margined instruments, and the funding surface the docs promised is now real.

  • Added: GET /v1/instruments/{instrument_id}/funding — public, paginated funding-rate history (trades-style cursor), projected from the Audit Indexer's funding_rates table. Closes the docs' GET /v1/funding_history promise (that flat route was never implemented; the nested house pattern won).
  • Added: funding WS channel — public, per-instrument, one frame per funding boundary (egress 241), previously documented but not relayed by Edge. skipped uses the indexer's rule so stream and backfill agree.
  • Added: GET /v1/instrument/margin_metadata — the margined-instrument metadata registry (identity/display: unique symbol, display_name, base/quote/expiry_date derived from the listing's price_feed_id). Recorded by the instrument service at listing time; completes the two-views model for margined instruments.
  • Added: optional symbol / display_name on POST /admin/margin/instruments (metadata-registry overrides; derived when omitted; symbol collisions rejected 400 pre-sequencing).
  • Fixed: reduce_only / post_only — documented on PlaceOrderRequest since the derivatives release but never accepted by the Edge decoder (unknown field); now plumbed through to the engine's RC-3 flags byte (idempotency body-hash covers them). Caught by the first spec-conformant client (the live frontend provider).
  • Fixed: Edge's reject-reason strings stopped at code 33 — every margin-era reject (34–52: no_mark_yet, outside_price_band, margined_requires_margin_sub, …) surfaced as "unknown" despite being documented in the RejectReason code table. Map completed; the OrderRejectReason string enum now lists all 52.
  • Fixed: GET /v1/instruments/{id}/candles emitted "bars": null (Go nil slice) instead of the documented empty array.
  • Fixed: both /v1/contract_specs* responses were documented WITHOUT the standard envelope; the server has always wrapped them.
  • Added: POST /admin/margin/instruments now accepts (and for futures REQUIRES) daily_settlement_time_utc_seconds + expiry_datetime_utc_ns; the funding block is venue-default-derived for perps (§10.2 leaves no freedom). Without these the admin path could never list a valid perp or dated future.
  • Fixed: admin.md still documented kebab-case /admin/custody/simulate-deposit after the F4 rename (code serves simulate_deposit); MarginListingResult.required still named the pre-F6 correlation_id.

2026-07-19 — API surface audit (pre-frontend), HIGH+MEDIUM findings applied

  • Added: CORS support (F1) — EDGE_CORS_ORIGINS allowlist; empty = permissive dev default. Preflights answered with explicit Authorization/X-Admin-Key/X-Reporter-Key.
  • Added: order idempotency (F2) — optional client_order_id (≤64 chars) on POST /v1/orders; same-key retry replays the original ack within a ~15-min Edge window; conflicts → 409 client_order_id_reuse / order_outcome_unknown.
  • Added: GET /v1/fills pagination (F3) — cursor param; response is now {fills, next_cursor} (BREAKING shape change from a bare array).
  • Renamed (F4, snake_case house rule): POST /admin/seed-balance/admin/seed_balance; POST /admin/custody/simulate-deposit/admin/custody/simulate_deposit.
  • Renamed (F5): GET /v1/contract_spec/{instrument_id}GET /v1/contract_specs/{instrument_id} (plural-collection rule).
  • Renamed (F6): every PAYLOAD-level integer correlation_id (order/cancel/ withdrawal/admin acks, order detail, fills, margin listing result, WS cancel_all completion) → sequencer_correlation_id. The ENVELOPE's string correlation_id (HTTP trace) is unchanged. Full audit: docs/exchange/api_surface_audit.md.

2026-07-18 — price_feed_id on margined listings

  • POST /admin/margin/instruments: new REQUIRED price_feed_id — the data-oracle index feed pricing the instrument (the manifest's authoritative feed↔contract record; format-checked against the feed grammar + index-qualifier rule; never forwarded to the 341 wire).

2026-07-17 — price/resolution split

  • Moved: POST /v1/instrument/submitPOST /v1/instrument/resolution/submit (behavior unchanged; the handler moved to the resolution component, sphinx.resolution.v1.ResolutionService).
  • Added: GET /v1/instrument/resolution/preview/{instrument_id} (reporter-gated) — the advisory b′ operator-confirm aid: last journaled selected index value + staleness.
  • Backend (not wire-visible here): index prices now enter via the price component's data-oracle ingest (sphinx.price.v1.PriceIngestService, gRPC :18205); IndexPriceSet and ResolutionFinal are signed with separate per-component keys. Decision record: docs/exchange/price_resolution_split.md.

[1.0.0] — 2026-07 — Margin-era (derivatives) surface

The margined-derivatives layer is IMPLEMENTED and ships alongside the fully-collateralized event-contract product. The margin-era surface lands as one SemVer-coherent contract change (DRV-101, API-1..API-5); partial adoption is prohibited. The full contract is in margin_era_api.md and the exchange repo's margin-era openapi.yaml (pending merge into this directory's openapi.yaml). Behavior is normative in the derivatives spec (../exchange_derivatives_engine_spec.md); this entry records only the API surface delta.

Added — REST

  • GET /v1/snapshot margin block — per-sub-account equity, im_held, mm_required, free_collateral, margin_withdrawable, reserved_funding, at the same engine_seq_applied as balances/positions (spec §14).
  • GET /v1/margin_info — same-seq read-only projection of the margin block alone.
  • GET /v1/contract_spec/{instrument_id} + GET /v1/contract_specs — per-instrument margined manifest (pinned by manifest_hash) + listing (spec §14.2).
  • GET /v1/insurance_fund — balance + itemized flow journal (spec §7).
  • GET /v1/liquidation_metrics — per-instrument hourly liquidation aggregates, T+24h delayed.
  • POST /v1/margin/transfer — free-USDC transfer across the margin sub-account boundary (the sequenced MarginTransfer event; spec §14, §11).

Added — WebSocket

  • public mark — engine-derived mark (from the signed hourly IndexPriceSet, spec §4) + RC-1 admission band, per instrument.
  • public funding — perpetual funding rate + index (perps only).
  • account channel (private) gains margin_warning, liquidation_started, liquidation_complete (spec §14.2).

Added — order surface

  • reduce_only / post_only order flags on POST /v1/orders (spec §8.4).
  • Margin-era reject codes (wire reason byte 30–49; spec §8) — see orders.md.

Changed

  • Position shape: margined positions serve signed_qty + last_settlement_price; event contracts dual-serve yes_position / no_position alongside the YES-collapsed signed_qty for at least one client-release cycle (spec §14.5).

[0.2.2] — 2026-05-28 — Edge publish-path concurrency hotfix

Fixed

  • Concurrent POST /v1/orders (and cancel / mass-cancel / withdrawal / admin publishes) could corrupt each other's wire bytes under load, surfacing as a spurious 400 invalid_api_fields on a valid order plus an orphaned-but-rested order the client never tracked (market-maker MM-3 finding — see ../exchange/bug_reports/02_engine-rest-cluster-divergent-place.md).

    Root cause was in Edge, not the engine: the shared aeron SequencerClient reuses a single SBE encode buffer and is documented not-thread-safe, but Edge drove it from many concurrent HTTP handler goroutines through one Forwarder with no publish serialization. Concurrent publishes interleaved their writes into that buffer; the engine then decoded torn frames as garbage fields (→ invalid_api_fields) or mis-routed a torn correlation_id.

    Fix: a Forwarder.publishMu mutex held around every publish into the client (encode + Offer is sub-µs). Added a -race regression test (services/edge/internal/forwarder/forwarder_race_test.go) + a make test-race target. No public API change; the JSON request/response shapes of POST /v1/orders are unchanged. info.version bumped to 0.2.2 per SemVer patch. A deeper follow-up (per-call encode buffer in the shared client) is tracked for V1.

[0.2.1] — 2026-05-28 — Snapshot consistency hotfix

Fixed

  • Engine SnapshotState gRPC race that produced internally-inconsistent snapshots under fill load. The handler read last_seq_applied first, then iterated CollateralTable / PositionTable / open orders / risk tables without synchronization against the apply thread. The apply thread could commit one or more events between the seq capture and the table iteration, so the snapshot reported seq_applied = N while the iterated tables reflected state at seq_applied ≥ N+1. Replay Verifier flagged collateral mismatch divergences on every soak that produced fills (market-maker finding — see ../exchange/bug_reports/01_engine-snapshot-collateral-race.md).

    Fix has two layers: per-table std::mutex on CollateralTable and PositionTable (closes the data-race UB on the underlying std::unordered_map), plus a new ApplyLoop::snapshot_read_lock() — a std::shared_mutex the apply thread holds UNIQUE around the full on_event body, and SnapshotState holds SHARED for the entire handler. Together they give snapshot callers a coherent (seq_applied, balances, positions, collateral, open orders, account/instrument states) tuple at one seq boundary. Adds the concurrent regression test services/trading_engine/test/test_snapshot_consistency.cpp.

    No public API change — pure backend correctness; the JSON shape of GET /v1/snapshot is unchanged. info.version bumped to 0.2.1 per SemVer patch convention.

[0.2.0] — 2026-05-28 — V0.1 MM-readiness API additions

Shipped as repo tag v0.2.0 (parent + docs). Additive overlay on the V0 baseline, scoped to support the market-maker test harness described in ../../market_maker/PLAN.md. The negotiated cut and ordering are recorded in market_maker/PLAN.md §16.

Added — REST

  • GET /v1/orders/{order_id} — look up an order's full lifecycle state, including terminal (filled/canceled/rejected). Closes the "order disappeared from /v1/orders — what happened?" reconciliation hole.
  • POST /v1/orders/cancel_all — per-instrument mass cancel (instrument_id required; no all-instruments form, no kill-switch). Backed by a MassCancel SBE event; asynchronous — returns 202 Accepted with a correlation_id, then the engine emits one OrderCanceled per affected order (audited + orderbook_delta) and a terminating MassCancelComplete surfaced as a mass_cancel_complete event on the account WS channel.
  • GET /v1/snapshot — atomic per-account snapshot: balances + positions + open orders + pending withdrawals, all at the same engine_seq_applied. Replaces multi-poll reconciler pattern.
  • GET /v1/instruments/{instrument_id}/trades — paginated, anonymised public trade tape.

Added — WebSocket

  • account channel (private) — non-fill account events emitted in V0.1: deposit_credited, withdrawal_state, account_state_change, mass_cancel_complete. (An entitlement_change event shape is specified for V1 forward-compat but is not emitted in V0.1 — see ws.md §account channel.)
  • trade channel (public) — anonymised public tape, one event per match.

Changed

  • orderbook_delta WS push extended from signal-only to a payload carrying the changed levels (changes: [{side, price_pips, new_count}]; sideyes_bid/no_bid, new_count = 0 removes a level). Subscribers apply diffs in place instead of re-snapshotting on every notification. The push is now emitted only when the book actually changes — a rejected / no-op order no longer produces a (previously empty) signal.
  • POST /v1/orders responses now include three timing-breadcrumb fields: edge_received_at_ns, sequencer_committed_at_ns, engine_applied_at_ns (Unix ns; sequencer_committed_at_ns carries millisecond precision — Aeron cluster time ×1e6 — see orders.md). Lets clients decompose round-trip latency by stage.

Deferred to V1 (recorded here so consumers don't anticipate them in V0.1)

  • PATCH /v1/orders/{order_id} (atomic modify) — locked design in market_maker/PLAN.md §16.3. Workaround for V0.1 clients is cancel-and-replace.
  • GET /v1/fees, GET /v1/risk_limits — V1 polish.
  • Idempotency-Key header on POST /v1/orders — V1 (existing backlog in orders.md §"Treating engine_timeout correctly").

[0.1.0] — 2026-05-18 — V0 baseline

Initial documented spec, captured after the V0 SHIPPED milestone. The surface as documented here is the V0 acceptance gate state, validated by the full V0 user-journey integration test and the 30-iteration soak.

Added (relative to "no spec")

  • Accounts: POST /v1/accounts, GET /v1/accounts/me.
  • Credentials: POST /v1/credentials, GET /v1/credentials, DELETE /v1/credentials/{credential_id}.
  • Orders: POST /v1/orders, DELETE /v1/orders/{order_id}, GET /v1/orders.
  • Market data: GET /v1/instruments, GET /v1/instruments/{instrument_id}, GET /v1/instruments/{instrument_id}/orderbook, GET /v1/balances, GET /v1/positions, GET /v1/fills.
  • Candles: GET /v1/instruments/{instrument_id}/candles with resolution1d.
  • Custody (customer): GET /v1/deposits/addresses, POST /v1/withdrawals, GET /v1/withdrawals/{withdrawal_id}.
  • Instrument service (read): GET /v1/instrument/instruments, GET /v1/instrument/instruments/{instrument_id}.
  • Instrument service (reporter): POST /v1/instrument/submit (gated by X-Reporter-Key).
  • WebSocket: GET /v1/ws with channels orderbook_delta, fill, market_lifecycle, candles.
  • Admin: POST /admin/seed-balance, POST /admin/accounts/{id}/state, POST /admin/instruments/{id}/state, POST /admin/instrument/instruments, POST /admin/instrument/instruments/{id}/state, POST /admin/custody/simulate-deposit.
  • Operations: GET /healthz, GET /readyz, GET /metrics (on the health port 8080).

Conventions

  • Integer-pip pricing (1–9999 = 0.0001–0.9999).
  • USDC atomic units (6 decimals).
  • Timestamps in Unix nanoseconds (*_at_ns) or seconds (*_unix_seconds).
  • Response envelope { data, correlation_id } for success, { error: { code, message, details }, correlation_id } for failure.
  • Three auth schemes: Bearer (customers), X-Admin-Key (operators), X-Reporter-Key (reporters).

Known historical artefacts (do not "fix" without a coordinated client migration)

  • Fill.ts_ns is sourced from Aeron Cluster's onSessionMessage timestamp, which is Unix milliseconds, not nanoseconds. The schema name is retained for backward compatibility with the cluster wire format; see market-data.md.