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.10column lives inmigrations/000003_create_instruments.up.sql.instruments.net_shares_sold INTEGER NOT NULL DEFAULT 0column tracks running buy/sell imbalance since the last post-match reset.trade_execute.luais the only place the formula runs in the hot path; pure-Go mirrors ininternal/amm/amm.goandinternal/instrument/pricing.goexist for tests and out-of-band quoting and must stay in sync with the Lua.base_priceis 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
kgives 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-pricingspec.
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.