Index: server/trade.go
===================================================================
--- server/trade.go	(revision b715712c7d5d3ee27c932a8a9d8e0a2bc6e85127)
+++ server/trade.go	(revision 8b447efcef1acebccdc8fd064521da4adc58d220)
@@ -13,4 +13,13 @@
 // Runs inside a single database transaction so the orders, holdings,
 // users.balance and transactions tables always agree.
+//
+// The order still passes through 'open' before 'executed'. Placing it
+// reserves whatever it commits — on a sell, the crypto being sold, tracked in
+// holdings.reserved_quantity — before anything is actually moved, so a
+// second order against the same holding can never be granted the same units
+// twice. Because only market orders are implemented, reserve and settle
+// happen inside this one transaction rather than across two commits; a
+// future limit-order matcher would split them into a second transaction
+// later, without needing a schema change.
 func PlaceOrder(s *Session, side string) {
 	if side != "buy" && side != "sell" {
@@ -47,9 +56,9 @@
 	defer tx.Rollback()
 
-	// 1. create the order (status='executed' since we fill immediately)
+	// 1. record the order as 'open' — no trade has happened yet.
 	var orderID string
 	err = tx.QueryRow(
-		`INSERT INTO orders (user_id, market_id, side, type, status, quantity, price, executed_at)
-		 VALUES ($1, $2, $3, 'market', 'executed', $4, $5, now())
+		`INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
+		 VALUES ($1, $2, $3, 'market', 'open', $4, $5)
 		 RETURNING id`,
 		s.UserID, m.ID, side, qty, price,
@@ -87,5 +96,6 @@
 		}
 
-		// upsert holding with running weighted average
+		// a buy never reserves crypto, only ever adds it — upsert holding
+		// with running weighted average
 		if err := upsertHoldingOnBuy(tx, s.UserID, m.CryptoID, qty, price); err != nil {
 			fmt.Println("Error updating holding:", err)
@@ -104,25 +114,42 @@
 		}
 	} else {
-		// sell: check holding
-		var held, avgPrice float64
+		// sell: lock the holding and check what is actually free to sell —
+		// quantity minus whatever another open order has already reserved.
+		var held, reserved, avgPrice float64
 		err := tx.QueryRow(
-			`SELECT quantity, avg_price FROM holdings
+			`SELECT quantity, reserved_quantity, avg_price FROM holdings
 			  WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE`,
 			s.UserID, m.CryptoID,
-		).Scan(&held, &avgPrice)
+		).Scan(&held, &reserved, &avgPrice)
 		if err != nil && err != sql.ErrNoRows {
 			fmt.Println("Error:", err)
 			return
 		}
-		if err == sql.ErrNoRows || held < qty {
-			fmt.Printf("Insufficient holding: trying to sell %.4f, hold %.4f\n", qty, held)
-			return
-		}
-
-		// reduce holding
+		available := held - reserved
+		if err == sql.ErrNoRows || available < qty {
+			fmt.Printf("Insufficient holding: trying to sell %.4f, available %.4f (of %.4f held, %.4f reserved)\n",
+				qty, available, held, reserved)
+			return
+		}
+
+		// reserve: committed to this order, not yet removed from the position.
 		if _, err := tx.Exec(
 			`UPDATE holdings
-			    SET quantity   = quantity - $1,
-			        updated_at = now()
+			    SET reserved_quantity = reserved_quantity + $1,
+			        updated_at        = now()
+			  WHERE user_id = $2 AND crypto_id = $3`,
+			qty, s.UserID, m.CryptoID,
+		); err != nil {
+			fmt.Println("Error:", err)
+			return
+		}
+
+		// settle: a market order fills immediately, so release the
+		// reservation and remove the asset from the position in one step.
+		if _, err := tx.Exec(
+			`UPDATE holdings
+			    SET quantity          = quantity - $1,
+			        reserved_quantity = reserved_quantity - $1,
+			        updated_at        = now()
 			  WHERE user_id = $2 AND crypto_id = $3`,
 			qty, s.UserID, m.CryptoID,
@@ -163,4 +190,13 @@
 		 VALUES ($1, now(), $2, $3, $4, 'user')`,
 		m.ID, price, qty, side,
+	); err != nil {
+		fmt.Println("Error:", err)
+		return
+	}
+
+	// settle the order itself: it has now actually been filled.
+	if _, err := tx.Exec(
+		`UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1`,
+		orderID,
 	); err != nil {
 		fmt.Println("Error:", err)
