Skip to content

adr: 0001 title: Linear bonding-curve AMM (price = base + k × net_shares_sold) status: superseded date: 2026-05-22 deciders: [@vaishnav-s-01, @amalkrsihna] affects-specs: [amm-pricing] affects-code: - ftl-backend/internal/redis/lua/trade_execute.lua - ftl-backend/internal/amm/amm.go - ftl-backend/internal/instrument/pricing.go supersedes: null superseded-by: 0004


Superseded by ADR-0004 on 2026-05-24. The CFD-style margin trading redesign replaces buy-and-hold shares with bidirectional leveraged positions. The hybrid pricing curve in ADR-0004 (base + k_mod × Σ direction) is the spiritual successor of this ADR's bonding curve — same linear-in-imbalance shape, but the imbalance counter now signs longs vs shorts instead of summing share holdings. The live system runs the CFD model; the historical record below is kept for context.

ADR-0001: Linear bonding-curve AMM

Context

FTL needs a market-making algorithm that prices a player's contract in real time as users buy and sell. The constraints that shaped the choice:

  • Atomic, low-latency pricing under high contention. Peak load is ~50K concurrent users with bursts during goals — we cannot afford a price computation that takes >1 ms.
  • Predictable, explainable price moves. Users need to look at a player and understand why the price changed. "It went up because more people bought" is the floor; anything more esoteric (curve shape, liquidity tiers) makes the game harder to learn.
  • A single tunable per instrument so individual players can be made more or less price- sensitive without a code change. (A star player should react faster than a benchwarmer.)
  • No external liquidity provider — every trade clears against the curve, not against a counterparty pool we'd have to seed and rebalance.
  • Audit-friendly math. The accountant inside us wants to be able to reproduce any fill price from (base_price, k, net_shares_sold) alone, after the fact.

The retired serverless plan (see archive/old-stack/) had picked a similar shape but never shipped. The containerized rewrite was the right moment to lock it in formally.

Decision

We will use a linear bonding curve of the form price = base + k × net_shares_sold, with k stored per-instrument in the database (default 0.10), and price the curve atomically inside a Redis Lua script that walks one share at a time for multi-share orders (arithmetic-series fill pricing).

Concrete changes encoded:

  • instruments.k DECIMAL(10,4) NOT NULL DEFAULT 0.10 column lives in migrations/000003_create_instruments.up.sql.
  • instruments.net_shares_sold INTEGER NOT NULL DEFAULT 0 column tracks running buy/sell imbalance since the last post-match reset.
  • trade_execute.lua is the only place the formula runs in the hot path; pure-Go mirrors in internal/amm/amm.go and internal/instrument/pricing.go exist for tests and out-of-band quoting and must stay in sync with the Lua.
  • base_price is recomputed post-match from EWMA form (see ADR-0003) and clamped to [FloorPrice=50.0, CeilingPrice=500.0].

Consequences

  • Positive. The math is trivially explainable: linear, two named inputs. The arithmetic-series fill is a one-line closed form (N × base + k × N × (2M + N + 1) / 2). Pricing is O(1) per share, O(N) per multi-share order — fast enough for the hot path.
  • Positive. Per-instrument k gives us a runtime knob without redeploys. We can tune a player's responsiveness during the tournament from a migration or admin endpoint.
  • Negative. Linear curves have no built-in resistance to manipulation. A coordinated group could push the price into the ceiling or floor with a series of trades — we depend on the post-match reset and on slippage limits to bound this, not on curve shape.
  • Negative. The formula is duplicated in three code locations (Lua, internal/amm, internal/instrument/pricing.go). A future ADR may consolidate; for now, the spec's Code References section is the single source of truth listing all three.
  • Maintenance burden. Any change to the formula has to land synchronously across all three code locations + this ADR + the amm-pricing spec.

Alternatives considered

Alternative A: Constant-product (x × y = k) — Uniswap-style

A pool of two assets (cash + shares) with a constant-product invariant. Self-balancing, manipulation-resistant.

Rejected because: it requires bootstrapping a liquidity pool per instrument with both cash and shares, the marginal price formula isn't a one-liner anyone can mentally compute, and the slippage curve is steeper at low liquidity — punishing early traders during the first few minutes of a match. Too much surface area for a game we want players to learn in 5 minutes.

Alternative B: LMSR (Logarithmic Market Scoring Rule)

The standard for prediction markets. Provably bounded loss for the market maker.

Rejected because: we are not running a prediction market — players hold positions through a match and liquidate at FT. LMSR's bounded-loss guarantee doesn't help us; its log/exp math is heavier than linear; and the price interpretation ("the market's probability estimate") doesn't map cleanly onto a player's perceived value.

Alternative C: Order book with central limit matching

A real exchange-style matching engine, no curve.

Rejected because: order books need depth to function, which means we'd need market-maker bots seeding both sides on every player — a meaningful operational burden. The AMM gives us a guaranteed counterparty at a deterministic price with zero seed liquidity.