diff --git a/artifacts/api-server/src/routes/jobs.ts b/artifacts/api-server/src/routes/jobs.ts index 69079b0..4abeea5 100644 --- a/artifacts/api-server/src/routes/jobs.ts +++ b/artifacts/api-server/src/routes/jobs.ts @@ -90,6 +90,12 @@ async function runEvalInBackground(jobId: string, request: string): Promise { } const { request } = parseResult.data; - // Optionally bind a Nostr identity + // Optionally bind a Nostr identity — ensure row exists before FK insert const nostrPubkey = resolveNostrPubkey(req); + if (nostrPubkey) await trustService.getOrCreate(nostrPubkey); try { const evalFee = pricingService.calculateEvalFeeSats(); @@ -316,7 +335,7 @@ router.post("/jobs", jobsLimiter, async (req: Request, res: Response) => { jobId, createdAt: createdAt.toISOString(), ...(nostrPubkey ? { nostrPubkey } : {}), - ...(trust ? { trust_tier: trust.tier } : {}), + trust_tier: trust ? trust.tier : "anonymous", evalInvoice: { paymentRequest: lnbitsInvoice.paymentRequest, amountSats: evalFee, @@ -346,7 +365,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => { const trustTier = job.nostrPubkey ? await trustService.getTier(job.nostrPubkey) - : undefined; + : "anonymous"; const base = { jobId: job.id, @@ -354,7 +373,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => { createdAt: job.createdAt.toISOString(), completedAt: job.state === "complete" ? job.updatedAt.toISOString() : null, ...(job.nostrPubkey ? { nostrPubkey: job.nostrPubkey } : {}), - ...(trustTier ? { trust_tier: trustTier } : {}), + trust_tier: trustTier, }; switch (job.state) { diff --git a/artifacts/api-server/src/routes/sessions.ts b/artifacts/api-server/src/routes/sessions.ts index 28a168b..b0a1f83 100644 --- a/artifacts/api-server/src/routes/sessions.ts +++ b/artifacts/api-server/src/routes/sessions.ts @@ -50,7 +50,7 @@ function sessionView(session: Session, includeInvoice = false, trustTier?: strin expiresAt: session.expiresAt?.toISOString() ?? null, minimumBalanceSats: MIN_BALANCE_SATS, ...(session.nostrPubkey ? { nostrPubkey: session.nostrPubkey } : {}), - ...(trustTier ? { trust_tier: trustTier } : {}), + trust_tier: trustTier ?? "anonymous", ...(session.macaroon && (session.state === "active" || session.state === "paused") ? { macaroon: session.macaroon } : {}), @@ -160,8 +160,9 @@ router.post("/sessions", sessionsLimiter, async (req: Request, res: Response) => return; } - // Optionally bind a Nostr identity + // Optionally bind a Nostr identity — ensure row exists before FK insert const nostrPubkey = resolveNostrPubkey(req); + if (nostrPubkey) await trustService.getOrCreate(nostrPubkey); try { const sessionId = randomUUID(); @@ -187,7 +188,7 @@ router.post("/sessions", sessionsLimiter, async (req: Request, res: Response) => sessionId, state: "awaiting_payment", ...(nostrPubkey ? { nostrPubkey } : {}), - ...(trust ? { trust_tier: trust.tier } : {}), + trust_tier: trust ? trust.tier : "anonymous", invoice: { paymentRequest: invoice.paymentRequest, amountSats, @@ -377,8 +378,8 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => { if (session.nostrPubkey) { if (finalState === "complete") { void trustService.recordSuccess(session.nostrPubkey, debitedSats); - } else if (finalState === "rejected") { - void trustService.recordFailure(session.nostrPubkey, reason ?? "rejected"); + } else if (finalState === "rejected" || finalState === "failed") { + void trustService.recordFailure(session.nostrPubkey, reason ?? errorMessage ?? finalState); } } diff --git a/lib/db/src/schema/jobs.ts b/lib/db/src/schema/jobs.ts index d293e4a..5c67172 100644 --- a/lib/db/src/schema/jobs.ts +++ b/lib/db/src/schema/jobs.ts @@ -1,6 +1,7 @@ import { pgTable, text, timestamp, integer, real } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod/v4"; +import { nostrIdentities } from "./nostr-identities"; export const JOB_STATES = [ "awaiting_eval_payment", @@ -36,8 +37,8 @@ export const jobs = pgTable("jobs", { actualOutputTokens: integer("actual_output_tokens"), actualCostUsd: real("actual_cost_usd"), - // Optional Nostr identity bound at job creation - nostrPubkey: text("nostr_pubkey"), + // Optional Nostr identity bound at job creation (FK → nostr_identities.pubkey) + nostrPubkey: text("nostr_pubkey").references(() => nostrIdentities.pubkey), // ── Post-work honest accounting & refund ───────────────────────────────── actualAmountSats: integer("actual_amount_sats"), diff --git a/lib/db/src/schema/sessions.ts b/lib/db/src/schema/sessions.ts index 231d765..f2aff6e 100644 --- a/lib/db/src/schema/sessions.ts +++ b/lib/db/src/schema/sessions.ts @@ -1,6 +1,7 @@ import { pgTable, text, timestamp, integer, boolean, real } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod/v4"; +import { nostrIdentities } from "./nostr-identities"; // ── Session state machine ───────────────────────────────────────────────────── @@ -43,8 +44,8 @@ export const sessions = pgTable("sessions", { // Auth token — issued once when session activates; required for requests macaroon: text("macaroon"), - // Optional Nostr identity bound at session creation - nostrPubkey: text("nostr_pubkey"), + // Optional Nostr identity bound at session creation (FK → nostr_identities.pubkey) + nostrPubkey: text("nostr_pubkey").references(() => nostrIdentities.pubkey), // TTL — refreshed on each successful request expiresAt: timestamp("expires_at", { withTimezone: true }),