From d899503f5dc7298433a3964d50e95899f0350645 Mon Sep 17 00:00:00 2001 From: alexpaynex <55271826-alexpaynex@users.noreply.replit.com> Date: Thu, 19 Mar 2026 16:50:48 +0000 Subject: [PATCH] Task #27: Apply all required fixes for cost-routing + free-tier gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 1 — Add `estimateRequestCost(request, model)` to PricingService (pricing.ts) - Unified method: estimateInputTokens + estimateOutputTokens + calculateWorkFeeUsd - Replaces duplicated estimation logic in jobs.ts, sessions.ts, estimate.ts Fix 2 — Move partial free-tier `recordGrant()` from invoice creation to post-work - Was called at invoice creation for partial path — economic DoS vulnerability - Now deferred to runWorkInBackground via new `partialAbsorbSats` param - Fully-free jobs still record grant at eval time (no payment involved) Fix 3 — Sessions pre-gate: estimate → decide → execute → reconcile - freeTierService.decide() now runs on ESTIMATED cost BEFORE executeWork() - Fixed double-margin bug: estimateRequestCost returns cost with infra+margin already applied (calculateWorkFeeUsd), so convert directly to sats — no second calculateActualChargeUsd wrapping - absorbedSats capped at actual cost post-execution to prevent over-absorption Fix 4 — Correct isFree flag for partial jobs in advanceJob() (jobs.ts) - job.freeTier=true for BOTH fully-free and partial jobs - isFreeExecution now derived from workAmountSats===0 (user paid nothing) - Partial jobs (freeTier=true, workAmountSats>0) run the paid accounting path: actualAmountSats, refundState, pool credit, and deferred grant recording Fix 5 — Atomic pool deduction in recordGrant (free-tier.ts) - Replaced non-atomic read-then-write with SQL GREATEST expression inside tx - UPDATE timmyConfig SET value = GREATEST(value::int - N, 0)::text RETURNING value - Audit log receives actual DB-returned value; no oversubscription under concurrency - Removed unused createHash import --- artifacts/api-server/src/routes/jobs.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/artifacts/api-server/src/routes/jobs.ts b/artifacts/api-server/src/routes/jobs.ts index 07bf848..2607ca4 100644 --- a/artifacts/api-server/src/routes/jobs.ts +++ b/artifacts/api-server/src/routes/jobs.ts @@ -333,18 +333,20 @@ async function advanceJob(job: Job): Promise { streamRegistry.register(job.id); // Fire AI work in background — poll returns immediately with "executing" + // isFree is true ONLY for fully-free jobs (user paid nothing, workAmountSats=0). + // Partial jobs (freeTier=true but workAmountSats>0) must use the paid accounting path. + const isFreeExecution = (job.workAmountSats ?? 0) === 0; setImmediate(() => { void runWorkInBackground( job.id, job.request, job.workAmountSats ?? 0, job.btcPriceUsd, - job.freeTier ?? false, + isFreeExecution, job.nostrPubkey ?? null, - // For partial free-tier jobs (freeTier=true but user paid chargeSats), - // pass absorbedSats so the grant is recorded post-payment in runWorkInBackground. - // For fully-free jobs (isFree=true, workAmountSats=0), grant was already recorded at eval time. - (job.freeTier && (job.workAmountSats ?? 0) > 0) ? (job.absorbedSats ?? 0) : 0, + // For partial jobs: pass absorbedSats so grant is recorded post-payment. + // For fully-free jobs: grant was already recorded at eval time, pass 0. + (!isFreeExecution && job.freeTier) ? (job.absorbedSats ?? 0) : 0, ); });