Architecture by serve type:
serve="free" (fully-free jobs & sessions):
- decide() atomically debits pool via SELECT FOR UPDATE transaction
- Pool debit and service decision are a single atomic DB operation
- If work fails → releaseReservation() refunds pool
- Grant audit written post-work with actual absorbed (≤ reserved); excess returned
serve="partial" (partial-subsidy jobs):
- decide() advisory; pool NOT debited at eval time
(prevents economic DoS from users abandoning payment flow)
- At work-payment confirmation: reservePartialGrant() atomically debits pool
(re-validates daily limits, SELECT FOR UPDATE, cap to available balance)
- If pool is empty at payment time: work proceeds (user already paid);
bounded loss (≤ estimated partial sats); partialGrantReserved=0 means
no pool accounting error — pool was already empty
- Grant audit: actualAbsorbed = min(actualCostSats, reserved); excess returned
serve="partial" (sessions — synchronous):
- decide() advisory; reservePartialGrant() called after work completes
- Actual cost capped at advisory absorbSats; over-reservation returned
recordGrant(pubkey, reqHash, actualAbsorbed, reservedAbsorbed):
- Over-reservation (estimated > actual token usage) atomically returned to pool
- Daily counter and audit log reflect actual absorbed sats
- Pool never goes negative; no silent losses under concurrent requests
New methods added: reservePartialGrant(), releaseReservation()
New 4-arg recordGrant() signature with over-reservation reconciliation