adr: 0012 title: "Retire POST /api/trade; adopt CFD margin trading as the sole mechanism" status: accepted implementation-status: implemented date: 2026-06-01 implemented-date: 2026-06-01 implemented-in: - ftl-backend bug/retire-legacy-trade - ftl-frontend bug/cfd-only-relabel deciders: [@amalkrsihna] affects-specs: [amm-pricing, cfd-pricing-and-wallet] affects-code: - ftl-backend/internal/handler/routes.go - ftl-frontend/src/lib/api.ts - ftl-frontend/src/pages/Squad.tsx supersedes: null superseded-by: null
ADR-0012: Retire POST /api/trade; CFD as sole trading mechanism¶
Context¶
The platform launched with a buy-and-hold trading model (POST /api/trade via trade.Service.Execute + trade_execute.lua). ADR-0004 introduced CFD margin trading alongside the legacy path. The CFD path (POST/PATCH/DELETE /api/positions/*) has been the sole user-facing trading surface since the CFD rollout (BuySellModal.tsx was removed, all trade buttons now call api.openPosition).
The legacy route remained live as a safety net during rollout. With CFD validated in staging and CFD being the only entry point in the frontend, keeping the legacy route active creates:
- Attack surface: the route can still be called directly, bypassing CFD margin enforcement.
- Developer confusion: two trade mechanisms coexist; engineers working on margin/positions code need to maintain mental awareness of both.
- Audit noise: the
trading-auditissue series (#68) explicitly requested retirement.
Decision¶
Remove POST /api/trade as an HTTP endpoint. CFD positions (/api/positions/*) are now the sole mechanism for opening and closing leveraged positions.
Preserved intentionally:
internal/trade/package — still used by the bot engine (internal/bot/engine.go) and the FT auto-seller (ExecuteSystemSellviainternal/sportmonks/postmatch.go). Bot migration to CFD is a separate ADR.GET /api/trades— transaction history endpoint for displaying past trades to users. Reads only; no new writes.GET /api/portfolio— legacy share-holdings endpoint; still serves tutorial/tutorial-mode users with sandbox positions.liquidateHolders/ExecuteSystemSell— needed for auto-selling legacypositions(share) rows at Full Time. Until thepositionstable is migrated tocfd_positions, this path must remain for existing holders.internal/flusher/outbox.go(trade outbox) — drains any remainingtrade_outboxrows; safe to remove after confirming the table is empty post-cutover.
UI relabel: The last user-visible "LONG"/"SHORT" text in Squad.tsx changed to "BUY"/"SELL", aligning with all other CFD surfaces.
Dead code removed: api.executeTrade(), TradePayload, and TradeResult types in src/lib/api.ts — no callers existed after BuySellModal.tsx was deleted.
Known gap¶
PayoutOnFirstTrade (referral payout trigger) was wired inside ExecuteTrade. The CFD open-position handler does not call it. A follow-up issue tracks wiring PayoutOnFirstTrade into the CFD positions path so the referral incentive works for the first CFD trade.
Consequences¶
- Positive: The legacy attack vector is closed;
curl POST /api/tradenow returns 404. - Positive: New engineers see a single trading mechanism.
- Negative (temporary): Referral payout on first CFD trade is broken until the follow-up fix ships.
- Neutral: Legacy
positions/tradestables remain; they will be addressed in a follow-up "deprecate legacy tables" ADR once all existing holders are migrated.