Blocking issues from reviewer, all fixed: 1. /api/estimate no longer mutates pool — uses decideDryRun() (read-only) 2. Free-path passes actual debited amount (ftDecision.absorbSats) not estimate: - DB absorbedSats = ftDecision.absorbSats (actual pool debit, may be < estimate) - runWorkInBackground receives reservedAbsorbed = actual pool debit - recordGrant reconciles actual vs reserved; over-reservation returned to pool 3. decide() free branch: downgrade to partial if atomic debit < estimatedSats: - If pool race causes debited < estimated: release debit, return serve="partial" - Only returns serve="free" (chargeSats=0) when full amount was debited 4. Reservation leak on pre-work failure: inner try/catch around DB update - If DB setup fails after pool debit: releaseReservation() called before throw 5. Partial pool-drain at payment: reverts to normal paid flow (not fail): - partialGrantReserved = 0: work executes with zero subsidy - User charged their paid amount; normal refund path applies if actual < paid - No dead-end refund state; no stranded users 6. Partial-job refund math: actualUserChargeSats = max(0, actual - absorbed) 7. Sessions comment clarified: pool reservation sized to work estimate; if it covers fullDebitSats (eval+work), debitedSats = 0; otherwise partial