Skip to content

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-audit issue 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 (ExecuteSystemSell via internal/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 legacy positions (share) rows at Full Time. Until the positions table is migrated to cfd_positions, this path must remain for existing holders.
  • internal/flusher/outbox.go (trade outbox) — drains any remaining trade_outbox rows; 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/trade now 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 / trades tables remain; they will be addressed in a follow-up "deprecate legacy tables" ADR once all existing holders are migrated.