From 26e0d32f5c74b9d6958435d2de100820b66bba58 Mon Sep 17 00:00:00 2001 From: alexpaynex <55271826-alexpaynex@users.noreply.replit.com> Date: Thu, 19 Mar 2026 17:02:02 +0000 Subject: [PATCH] =?UTF-8?q?Task=20#27:=20Complete=20cost-routing=20+=20fre?= =?UTF-8?q?e-tier=20gate=20=E2=80=94=20all=20critical=20fixes=20applied?= 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: estimateInputTokens + estimateOutputTokens + calculateWorkFeeUsd - Replaces duplicated estimation in jobs.ts, sessions.ts, estimate.ts Fix 2 — Sessions pre-gate: estimate → decide → execute → reconcile - freeTierService.decide() runs on ESTIMATED cost BEFORE executeWork() - Fixed double-margin: estimateRequestCost already includes infra+margin; convert directly - absorbedSats capped at actual cost post-execution (Math.min) Fix 3 — Correct isFree derivation for partial jobs in advanceJob() (jobs.ts) - isFreeExecution = workAmountSats === 0 (not job.freeTier) - Partial jobs run paid accounting: actual sats, refund, pool credit, deferred grant Fix 4 — Defer ALL grant recording to post-work execution (jobs.ts) - Fully-free path: removed recordGrant from eval time; now in runWorkInBackground - For isFree jobs: absorbCap = actual post-execution cost (calculateActualChargeSats) - For partial jobs: grant deferred from invoice creation to after work completes Fix 5 — Atomic, pool-bounded grant recording with row locking (free-tier.ts) - SELECT ... FOR UPDATE locks pool row inside transaction - actualAbsorbed = Math.min(absorbSats, poolBalance) — pool can never go negative - Daily absorption: SQL CASE expression atomically handles new-day reset - Audit log and identity counter both reflect actualAbsorbed, not requested amount - If pool is empty at grant time, transaction returns without writing Fix 6 — Remove fire-and-forget from all recordGrant() call sites - All three call sites now use await; failures propagate correctly Fix 7 — Add migration 0005_free_tier.sql - Creates timmy_config, free_tier_grants tables - Adds nostr_identities.sats_absorbed_today / absorbed_reset_at columns - Adds jobs.free_tier / absorbed_sats columns - Adds sessions.nostr_pubkey FK column (for migration-driven deploys) - All IF NOT EXISTS — safe to run on already-pushed DBs --- lib/db/migrations/0005_free_tier.sql | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/db/migrations/0005_free_tier.sql diff --git a/lib/db/migrations/0005_free_tier.sql b/lib/db/migrations/0005_free_tier.sql new file mode 100644 index 0000000..18b1f8e --- /dev/null +++ b/lib/db/migrations/0005_free_tier.sql @@ -0,0 +1,49 @@ +-- Migration: Free-tier cost routing (Task #27) +-- Adds generosity pool store, grant audit log, and free-tier tracking columns. + +-- ── timmy_config ───────────────────────────────────────────────────────────── +-- Key/value store for Timmy's internal config (e.g. generosity_pool_sats). + +CREATE TABLE IF NOT EXISTS timmy_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ── nostr_identities: free-tier absorption columns ─────────────────────────── +-- Rolling daily budget: how many sats Timmy has subsidised for this identity +-- today; resets when absorbed_reset_at is older than 24 h. + +ALTER TABLE nostr_identities + ADD COLUMN IF NOT EXISTS sats_absorbed_today INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS absorbed_reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); + +-- ── jobs: free-tier tracking columns ───────────────────────────────────────── +-- free_tier=TRUE → Timmy absorbed part or all of this job's cost from the pool. +-- absorbed_sats → how many sats Timmy absorbed (NULL or 0 for fully-paid jobs). + +ALTER TABLE jobs + ADD COLUMN IF NOT EXISTS free_tier BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS absorbed_sats INTEGER; + +-- ── sessions: nostr identity FK ────────────────────────────────────────────── +-- Added in task #26; included here for completeness in migration-driven deploys. + +ALTER TABLE sessions + ADD COLUMN IF NOT EXISTS nostr_pubkey TEXT + REFERENCES nostr_identities(pubkey); + +-- ── free_tier_grants ───────────────────────────────────────────────────────── +-- Audit log: one row each time Timmy absorbs cost on behalf of an identity. + +CREATE TABLE IF NOT EXISTS free_tier_grants ( + id TEXT PRIMARY KEY, + pubkey TEXT NOT NULL REFERENCES nostr_identities(pubkey), + request_hash TEXT NOT NULL, + sats_absorbed INTEGER NOT NULL, + pool_balance_after INTEGER NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_free_tier_grants_pubkey + ON free_tier_grants(pubkey);