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 (directionin/out, µUSDC, optional clienttransfer_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 ondata.status. - Added:
x-internal: trueon all 8 operator (/admin/*) operations, and a generatedopenapi.external.yaml(gen_openapi.py--externallogic, 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.--checknow 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 intowebsockets.md: the mass-cancel field issequencer_correlation_id(the TS union saidcorrelation_id), and the fill arm gainsfee_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/wsstub's channel list now names all seven public/ private channels (trade,mark,accountwere missing); private channels corrected tofill+account. NewMargintag 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), returnsSessionResponse(account, one-timesession_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. Uniform401for 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 gets400 invalid_request(revoke keys viaDELETE /v1/credentials/{credential_id}). - Added: shared
Conflict(409) andRateLimited(429) response components;Authtag.
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):
ContractSpecgainshas_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}/orderbookaddsyes_asks(side-native margined SELLS — signed ticks; the §11.9 no_bids complement remains event-class only).side=nocandles 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 tooltools/candle-backfillsources 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'sfunding_ratestable. Closes the docs'GET /v1/funding_historypromise (that flat route was never implemented; the nested house pattern won). - Added:
fundingWS channel — public, per-instrument, one frame per funding boundary (egress 241), previously documented but not relayed by Edge.skippeduses the indexer's rule so stream and backfill agree. - Added:
GET /v1/instrument/margin_metadata— the margined-instrument metadata registry (identity/display: uniquesymbol,display_name,base/quote/expiry_datederived from the listing'sprice_feed_id). Recorded by the instrument service at listing time; completes the two-views model for margined instruments. - Added: optional
symbol/display_nameonPOST /admin/margin/instruments(metadata-registry overrides; derived when omitted; symbol collisions rejected 400 pre-sequencing). - Fixed:
reduce_only/post_only— documented onPlaceOrderRequestsince 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 theRejectReasoncode table. Map completed; theOrderRejectReasonstring enum now lists all 52. - Fixed:
GET /v1/instruments/{id}/candlesemitted"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/instrumentsnow 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.mdstill documented kebab-case/admin/custody/simulate-depositafter the F4 rename (code servessimulate_deposit);MarginListingResult.requiredstill named the pre-F6correlation_id.
2026-07-19 — API surface audit (pre-frontend), HIGH+MEDIUM findings applied
- Added: CORS support (F1) —
EDGE_CORS_ORIGINSallowlist; 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) onPOST /v1/orders; same-key retry replays the original ack within a ~15-min Edge window; conflicts → 409client_order_id_reuse/order_outcome_unknown. - Added:
GET /v1/fillspagination (F3) —cursorparam; 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 stringcorrelation_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 REQUIREDprice_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/submit→POST /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/snapshotmargin block — per-sub-accountequity,im_held,mm_required,free_collateral,margin_withdrawable,reserved_funding, at the sameengine_seq_appliedas 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 bymanifest_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 sequencedMarginTransferevent; spec §14, §11).
Added — WebSocket
- public
mark— engine-derived mark (from the signed hourlyIndexPriceSet, spec §4) + RC-1 admission band, per instrument. - public
funding— perpetual funding rate + index (perps only). accountchannel (private) gainsmargin_warning,liquidation_started,liquidation_complete(spec §14.2).
Added — order surface
reduce_only/post_onlyorder flags onPOST /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-serveyes_position/no_positionalongside the YES-collapsedsigned_qtyfor 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 spurious400 invalid_api_fieldson 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
SequencerClientreuses a single SBE encode buffer and is documented not-thread-safe, but Edge drove it from many concurrent HTTP handler goroutines through oneForwarderwith 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 torncorrelation_id.Fix: a
Forwarder.publishMumutex held around every publish into the client (encode + Offer is sub-µs). Added a-raceregression test (services/edge/internal/forwarder/forwarder_race_test.go) + amake test-racetarget. No public API change; the JSON request/response shapes ofPOST /v1/ordersare unchanged.info.versionbumped to0.2.2per 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
SnapshotStategRPC race that produced internally-inconsistent snapshots under fill load. The handler readlast_seq_appliedfirst, then iteratedCollateralTable/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 reportedseq_applied = Nwhile the iterated tables reflected state atseq_applied ≥ N+1. Replay Verifier flaggedcollateral mismatchdivergences 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::mutexonCollateralTableandPositionTable(closes the data-race UB on the underlyingstd::unordered_map), plus a newApplyLoop::snapshot_read_lock()— astd::shared_mutexthe apply thread holds UNIQUE around the fullon_eventbody, andSnapshotStateholds 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 testservices/trading_engine/test/test_snapshot_consistency.cpp.No public API change — pure backend correctness; the JSON shape of
GET /v1/snapshotis unchanged.info.versionbumped to0.2.1per 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_idrequired; no all-instruments form, no kill-switch). Backed by aMassCancelSBE event; asynchronous — returns202 Acceptedwith acorrelation_id, then the engine emits oneOrderCanceledper affected order (audited +orderbook_delta) and a terminatingMassCancelCompletesurfaced as amass_cancel_completeevent on theaccountWS channel.GET /v1/snapshot— atomic per-account snapshot: balances + positions + open orders + pending withdrawals, all at the sameengine_seq_applied. Replaces multi-poll reconciler pattern.GET /v1/instruments/{instrument_id}/trades— paginated, anonymised public trade tape.
Added — WebSocket
accountchannel (private) — non-fill account events emitted in V0.1:deposit_credited,withdrawal_state,account_state_change,mass_cancel_complete. (Anentitlement_changeevent shape is specified for V1 forward-compat but is not emitted in V0.1 — seews.md§accountchannel.)tradechannel (public) — anonymised public tape, one event per match.
Changed
orderbook_deltaWS push extended from signal-only to a payload carrying the changed levels (changes: [{side, price_pips, new_count}];side∈yes_bid/no_bid,new_count = 0removes 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/ordersresponses now include three timing-breadcrumb fields:edge_received_at_ns,sequencer_committed_at_ns,engine_applied_at_ns(Unix ns;sequencer_committed_at_nscarries millisecond precision — Aeron cluster time ×1e6 — seeorders.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 inmarket_maker/PLAN.md §16.3. Workaround for V0.1 clients is cancel-and-replace.GET /v1/fees,GET /v1/risk_limits— V1 polish.Idempotency-Keyheader onPOST /v1/orders— V1 (existing backlog inorders.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}/candleswithresolution∈ 1d. - 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 byX-Reporter-Key). - WebSocket:
GET /v1/wswith channelsorderbook_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_nsis sourced from Aeron Cluster'sonSessionMessagetimestamp, which is Unix milliseconds, not nanoseconds. The schema name is retained for backward compatibility with the cluster wire format; seemarket-data.md.