everydaycats commited on
Commit
f899730
·
verified ·
1 Parent(s): bf51995

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +124 -92
app.js CHANGED
@@ -16,6 +16,7 @@ try {
16
  if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
 
 
19
  const bucketName = `shago-web.firebasestorage.app`;
20
 
21
  if (admin.apps.length === 0) {
@@ -40,47 +41,64 @@ try {
40
 
41
  const app = express();
42
  const PORT = process.env.PORT || 7860;
43
- const sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
44
 
 
 
 
 
 
 
 
45
 
46
  app.use(cors());
47
  app.use(bodyParser.json({ limit: '50mb' }));
48
 
49
- // --- CREDIT HELPERS ---
50
- const MIN_CREDITS_REQUIRED = 20;
 
 
 
 
 
 
51
 
52
  /**
53
- * Checks if user has enough credits to START an operation.
54
- * Throws if < 20.
 
55
  */
56
- async function checkMinimumCredits(userId) {
57
  if (!db) return;
58
- const snap = await db.ref(`users/${userId}/credits`).once('value');
 
 
59
  const credits = snap.val() || 0;
60
 
61
- if (credits < MIN_CREDITS_REQUIRED) {
62
- throw new Error(`Insufficient credits. You have ${credits}, need minimum ${MIN_CREDITS_REQUIRED} to proceed.`);
 
 
63
  }
64
  }
65
 
66
  /**
67
- * Deducts exact tokens from the user's account.
 
 
 
68
  */
69
- async function deductUserCredits(userId, amount) {
70
  if (!db || !amount || amount <= 0) return;
71
 
72
  try {
73
- const ref = db.ref(`users/${userId}/credits`);
74
  await ref.transaction((current_credits) => {
75
  const current = current_credits || 0;
76
- // Allow going negative if the bill is huge, or cap at 0?
77
- // Usually valid to go slightly negative on final bill, or cap at 0.
78
- // Here we assume we just subtract.
79
  return Math.max(0, current - amount);
80
  });
81
- console.log(`[Credits] Deducted ${amount} tokens from User ${userId}`);
82
  } catch (err) {
83
- console.error(`[Credits] Failed to deduct ${amount} from ${userId}:`, err);
84
  }
85
  }
86
 
@@ -139,19 +157,17 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
139
  if (!description) return res.status(400).json({ error: "Description required" });
140
 
141
  try {
142
- // 1. Pre-flight Check
143
- await checkMinimumCredits(userId);
144
 
145
  console.log(`[Onboarding] Analyzing idea...`);
146
 
147
- // 2. AI Call
148
- // Result is { questions: [...], usage: { totalTokenCount: ... }, ... }
149
  const result = await AIEngine.generateEntryQuestions(description);
150
 
151
  const usage = result.usage?.totalTokenCount || 0;
152
 
153
- // 3. Billing (Only on success)
154
- if (usage > 0) await deductUserCredits(userId, usage);
155
 
156
  if (result.status === "REJECTED") {
157
  return res.json({
@@ -163,7 +179,7 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
163
 
164
  } catch (err) {
165
  console.error(err);
166
- if (err.message.includes("Insufficient credits")) {
167
  return res.status(402).json({ error: err.message });
168
  }
169
  res.status(500).json({ error: "Analysis failed" });
@@ -172,20 +188,20 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
172
 
173
  app.post('/onboarding/create', validateRequest, async (req, res) => {
174
  const { userId, description, answers } = req.body;
175
- let totalTokens = 0;
176
 
177
  try {
178
- // 1. Pre-flight Check
179
- await checkMinimumCredits(userId);
180
 
181
  const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
182
  const projectId = `proj_${Date.now()}_${randomHex(7)}`;
183
 
184
  console.log(`[Onboarding] Grading Project ${projectId}...`);
185
 
186
- // 2. Grading
187
  const gradingResult = await AIEngine.gradeProject(description, answers);
188
- totalTokens += (gradingResult.usage?.totalTokenCount || 0);
189
 
190
  const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
191
 
@@ -195,11 +211,13 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
195
  console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
196
  const imagePrompt = `Game Title: ${gradingResult.title}. Core Concept: ${description}`;
197
 
198
- // 3. Image Generation
199
  const imgResult = await AIEngine.generateImage(imagePrompt);
200
  if (imgResult) {
201
  thumbnailBase64 = imgResult.image;
202
- totalTokens += (imgResult.usage?.totalTokenCount || 0);
 
 
203
  }
204
  }
205
 
@@ -222,7 +240,7 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
222
  const timestamp = Date.now();
223
  const status = isFailure ? "rejected" : "Idle";
224
 
225
- // Save Data
226
  if (!isFailure) {
227
  const memoryObject = {
228
  id: projectId,
@@ -242,7 +260,6 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
242
  await StateManager.updateProject(projectId, memoryObject);
243
  }
244
 
245
- // DB Updates
246
  if (firestore && !isFailure) {
247
  await firestore.collection('projects').doc(projectId).set({
248
  id: projectId,
@@ -274,8 +291,8 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
274
  await db.ref().update(updates);
275
  }
276
 
277
- // 4. Billing (Only on success)
278
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
279
 
280
  res.json({
281
  success: !isFailure,
@@ -287,7 +304,7 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
287
 
288
  } catch (err) {
289
  console.error("Create Error:", err);
290
- if (err.message.includes("Insufficient credits")) {
291
  return res.status(402).json({ error: err.message });
292
  }
293
  res.status(500).json({ error: "Creation failed" });
@@ -298,39 +315,42 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
298
 
299
  async function runBackgroundInitialization(projectId, userId, description) {
300
  console.log(`[Background] Starting initialization for ${projectId}`);
301
- let totalTokens = 0;
 
 
 
302
 
303
  try {
304
  const pmHistory = [];
305
 
306
- // 1. Generate GDD
307
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
308
  const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
309
 
310
- totalTokens += (gddResult.usage?.totalTokenCount || 0);
311
  const gddText = gddResult.text;
312
 
313
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
314
  pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
315
 
316
- // 2. Generate First Task
317
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
318
  const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
319
 
320
- totalTokens += (taskResult.usage?.totalTokenCount || 0);
321
  const taskText = taskResult.text;
322
 
323
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
324
  pmHistory.push({ role: 'model', parts: [{ text: taskText }] });
325
 
326
- // 3. Initialize Worker
327
  const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
328
  const workerHistory = [];
329
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
330
 
331
  const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
332
 
333
- totalTokens += (workerResult.usage?.totalTokenCount || 0);
334
  const workerText = workerResult.text;
335
 
336
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
@@ -345,8 +365,8 @@ async function runBackgroundInitialization(projectId, userId, description) {
345
  failureCount: 0
346
  });
347
 
348
- // 5. Queue commands (Handle assets + their costs)
349
- // We pass userId to deduct image costs immediately inside this function
350
  await processAndQueueResponse(projectId, workerText, userId);
351
 
352
  // 6. Update Status
@@ -355,15 +375,15 @@ async function runBackgroundInitialization(projectId, userId, description) {
355
  lastUpdated: Date.now()
356
  });
357
 
358
- // 7. Deduct accumulated text tokens
359
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
 
360
 
361
- console.log(`[Background] Init complete. Text Tokens: ${totalTokens}`);
362
 
363
  } catch (err) {
364
  console.error(`[Background] Init Error for ${projectId}:`, err);
365
  if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
366
- // No deduction on failure
367
  }
368
  }
369
 
@@ -371,7 +391,9 @@ app.post('/new/project', validateRequest, async (req, res) => {
371
  const { userId, projectId, description } = req.body;
372
 
373
  try {
374
- await checkMinimumCredits(userId);
 
 
375
 
376
  if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
377
 
@@ -383,7 +405,7 @@ app.post('/new/project', validateRequest, async (req, res) => {
383
  runBackgroundInitialization(projectId, userId, description);
384
 
385
  } catch (err) {
386
- if (err.message.includes("Insufficient credits")) {
387
  return res.status(402).json({ error: err.message });
388
  }
389
  res.status(500).json({ error: "Failed to start project" });
@@ -392,15 +414,17 @@ app.post('/new/project', validateRequest, async (req, res) => {
392
 
393
  app.post('/project/feedback', async (req, res) => {
394
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
395
- let totalTokens = 0;
 
 
396
 
397
  try {
398
  const project = await StateManager.getProject(projectId);
399
  if (!project) return res.status(404).json({ error: "Project not found." });
400
  if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
401
 
402
- // 1. Pre-flight Check
403
- await checkMinimumCredits(userId);
404
 
405
  if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
406
 
@@ -408,8 +432,9 @@ app.post('/project/feedback', async (req, res) => {
408
  console.log(`[${projectId}] ✅ TASK COMPLETE.`);
409
  const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
410
 
 
411
  const pmResult = await AIEngine.callPM(project.pmHistory, summary);
412
- totalTokens += (pmResult.usage?.totalTokenCount || 0);
413
  const pmText = pmResult.text;
414
 
415
  project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
@@ -420,8 +445,8 @@ app.post('/project/feedback', async (req, res) => {
420
  await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
421
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() });
422
 
423
- // Deduct what we used so far
424
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
425
 
426
  return res.json({ success: true, message: "No further tasks. Project Idle." });
427
  }
@@ -429,8 +454,9 @@ app.post('/project/feedback', async (req, res) => {
429
  const newWorkerHistory = [];
430
  const newPrompt = `New Objective: ${nextInstruction}`;
431
 
 
432
  const workerResult = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
433
- totalTokens += (workerResult.usage?.totalTokenCount || 0);
434
  const workerText = workerResult.text;
435
 
436
  newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
@@ -447,25 +473,20 @@ app.post('/project/feedback', async (req, res) => {
447
 
448
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() });
449
 
450
- // Billing
451
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
452
- return res.json({ success: true, message: "Next Task Assigned" });
453
- }
454
 
455
- // Logic for Failure Handling (PM Guidance) or Standard Feedback
456
- let isFailure = false;
457
- if (logContext?.logs) {
458
- const errs = ["Error", "Exception", "failed", "Stack Begin", "Infinite yield"];
459
- if (errs.some(k => logContext.logs.includes(k))) {
460
- isFailure = true;
461
- project.failureCount = (project.failureCount || 0) + 1;
462
- }
463
  }
464
 
 
465
  if (project.failureCount > 3) {
466
- const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs);
 
 
467
  const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
468
- totalTokens += (pmResult.usage?.totalTokenCount || 0);
469
  const pmVerdict = pmResult.text;
470
 
471
  if (pmVerdict.includes("[TERMINATE]")) {
@@ -473,8 +494,9 @@ app.post('/project/feedback', async (req, res) => {
473
  const resetHistory = [];
474
  const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
475
 
 
476
  const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt, []);
477
- totalTokens += (workerResult.usage?.totalTokenCount || 0);
478
 
479
  resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
480
  resetHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
@@ -483,12 +505,15 @@ app.post('/project/feedback', async (req, res) => {
483
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
484
  await processAndQueueResponse(projectId, workerResult.text, userId);
485
 
486
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
 
487
  return res.json({ success: true, message: "Worker Terminated." });
488
  } else {
489
  const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
 
 
490
  const workerResult = await AIEngine.callWorker(project.workerHistory, injection, []);
491
- totalTokens += (workerResult.usage?.totalTokenCount || 0);
492
 
493
  project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
494
  project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
@@ -496,23 +521,26 @@ app.post('/project/feedback', async (req, res) => {
496
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
497
  await processAndQueueResponse(projectId, workerResult.text, userId);
498
 
499
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
 
500
  return res.json({ success: true, message: "PM Guidance Applied." });
501
  }
502
  }
503
 
504
- // Standard Interaction
505
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
506
 
507
  let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
508
- totalTokens += (workerResult.usage?.totalTokenCount || 0);
509
  let responseText = workerResult.text;
510
 
 
511
  const pmQuestion = extractPMQuestion(responseText);
512
  if (pmQuestion) {
 
513
  const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
514
  const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
515
- totalTokens += (pmResult.usage?.totalTokenCount || 0);
516
  const pmAnswer = pmResult.text;
517
 
518
  project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
@@ -522,9 +550,9 @@ app.post('/project/feedback', async (req, res) => {
522
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
523
  project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
524
 
525
- // Re-call worker with answer
526
  workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
527
- totalTokens += (workerResult.usage?.totalTokenCount || 0);
528
  responseText = workerResult.text;
529
 
530
  project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
@@ -545,13 +573,14 @@ app.post('/project/feedback', async (req, res) => {
545
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() });
546
 
547
  // Final Billing
548
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
 
549
 
550
  res.json({ success: true });
551
 
552
  } catch (err) {
553
  console.error("AI Error:", err);
554
- if (err.message.includes("Insufficient credits")) {
555
  return res.status(402).json({ error: err.message });
556
  }
557
  res.status(500).json({ error: "AI Failed" });
@@ -585,10 +614,10 @@ app.post('/project/ping', async (req, res) => {
585
 
586
  app.post('/human/override', validateRequest, async (req, res) => {
587
  const { projectId, instruction, pruneHistory, userId } = req.body;
588
- let totalTokens = 0;
589
 
590
  try {
591
- await checkMinimumCredits(userId);
592
 
593
  const project = await StateManager.getProject(projectId);
594
  const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
@@ -598,8 +627,9 @@ app.post('/human/override', validateRequest, async (req, res) => {
598
  project.workerHistory.pop();
599
  }
600
 
 
601
  const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
602
- totalTokens += (workerResult.usage?.totalTokenCount || 0);
603
 
604
  project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
605
  project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
@@ -607,18 +637,18 @@ app.post('/human/override', validateRequest, async (req, res) => {
607
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
608
  await processAndQueueResponse(projectId, workerResult.text, userId);
609
 
610
- if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
611
 
612
  res.json({ success: true });
613
  } catch (err) {
614
- if (err.message.includes("Insufficient credits")) {
615
  return res.status(402).json({ error: err.message });
616
  }
617
  res.status(500).json({ error: "Override Failed" });
618
  }
619
  });
620
 
621
- // Helper to handle Asset Parsing & Billing
622
  async function processAndQueueResponse(projectId, rawResponse, userId) {
623
  const imgPrompt = extractImagePrompt(rawResponse);
624
  if (imgPrompt) {
@@ -628,10 +658,12 @@ async function processAndQueueResponse(projectId, rawResponse, userId) {
628
  const imgResult = await AIEngine.generateImage(imgPrompt);
629
 
630
  if (imgResult && imgResult.image) {
631
- // 2. Bill for image tokens immediately
632
  const imgTokens = imgResult.usage?.totalTokenCount || 0;
633
- if (userId && imgTokens > 0) {
634
- await deductUserCredits(userId, imgTokens);
 
 
635
  }
636
 
637
  // 3. Queue creation command
 
16
  if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
 
19
+ // Specific bucket as requested
20
  const bucketName = `shago-web.firebasestorage.app`;
21
 
22
  if (admin.apps.length === 0) {
 
41
 
42
  const app = express();
43
  const PORT = process.env.PORT || 7860;
 
44
 
45
+ // Load prompts safely
46
+ let sysPrompts = {};
47
+ try {
48
+ sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
49
+ } catch (e) {
50
+ console.error("Failed to load prompts.json:", e);
51
+ }
52
 
53
  app.use(cors());
54
  app.use(bodyParser.json({ limit: '50mb' }));
55
 
56
+ // --- CREDIT CONFIGURATION ---
57
+
58
+ // Minimums required to start a request
59
+ const MIN_BASIC_REQUIRED = 50;
60
+ const MIN_DIAMOND_REQUIRED = 50;
61
+
62
+ // Fixed cost for images (Basic credits)
63
+ const IMAGE_COST_BASIC = 1000;
64
 
65
  /**
66
+ * Checks if user has enough credits of a specific type.
67
+ * @param {string} userId
68
+ * @param {'basic'|'diamond'} type
69
  */
70
+ async function checkMinimumCredits(userId, type = 'basic') {
71
  if (!db) return;
72
+
73
+ // Path: users/{uid}/credits/basic OR users/{uid}/credits/diamond
74
+ const snap = await db.ref(`users/${userId}/credits/${type}`).once('value');
75
  const credits = snap.val() || 0;
76
 
77
+ const required = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
78
+
79
+ if (credits < required) {
80
+ throw new Error(`Insufficient ${type} credits. You have ${credits}, need minimum ${required} to proceed.`);
81
  }
82
  }
83
 
84
  /**
85
+ * Deducts exact tokens from the user's specific wallet.
86
+ * @param {string} userId
87
+ * @param {number} amount
88
+ * @param {'basic'|'diamond'} type
89
  */
90
+ async function deductUserCredits(userId, amount, type = 'basic') {
91
  if (!db || !amount || amount <= 0) return;
92
 
93
  try {
94
+ const ref = db.ref(`users/${userId}/credits/${type}`);
95
  await ref.transaction((current_credits) => {
96
  const current = current_credits || 0;
 
 
 
97
  return Math.max(0, current - amount);
98
  });
99
+ console.log(`[Credits] Deducted ${amount} ${type} credits from User ${userId}`);
100
  } catch (err) {
101
+ console.error(`[Credits] Failed to deduct ${amount} ${type} from ${userId}:`, err);
102
  }
103
  }
104
 
 
157
  if (!description) return res.status(400).json({ error: "Description required" });
158
 
159
  try {
160
+ // Analyst uses BASIC models (Flash)
161
+ await checkMinimumCredits(userId, 'basic');
162
 
163
  console.log(`[Onboarding] Analyzing idea...`);
164
 
 
 
165
  const result = await AIEngine.generateEntryQuestions(description);
166
 
167
  const usage = result.usage?.totalTokenCount || 0;
168
 
169
+ // Bill Basic
170
+ if (usage > 0) await deductUserCredits(userId, usage, 'basic');
171
 
172
  if (result.status === "REJECTED") {
173
  return res.json({
 
179
 
180
  } catch (err) {
181
  console.error(err);
182
+ if (err.message.includes("Insufficient")) {
183
  return res.status(402).json({ error: err.message });
184
  }
185
  res.status(500).json({ error: "Analysis failed" });
 
188
 
189
  app.post('/onboarding/create', validateRequest, async (req, res) => {
190
  const { userId, description, answers } = req.body;
191
+ let basicTokens = 0; // Create flow uses Grader (Basic) + Image (Basic)
192
 
193
  try {
194
+ // Pre-flight check
195
+ await checkMinimumCredits(userId, 'basic');
196
 
197
  const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
198
  const projectId = `proj_${Date.now()}_${randomHex(7)}`;
199
 
200
  console.log(`[Onboarding] Grading Project ${projectId}...`);
201
 
202
+ // 1. Grading (Basic)
203
  const gradingResult = await AIEngine.gradeProject(description, answers);
204
+ basicTokens += (gradingResult.usage?.totalTokenCount || 0);
205
 
206
  const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
207
 
 
211
  console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
212
  const imagePrompt = `Game Title: ${gradingResult.title}. Core Concept: ${description}`;
213
 
214
+ // 2. Image Generation (Basic)
215
  const imgResult = await AIEngine.generateImage(imagePrompt);
216
  if (imgResult) {
217
  thumbnailBase64 = imgResult.image;
218
+ // Add token usage for text + fixed cost
219
+ basicTokens += (imgResult.usage?.totalTokenCount || 0);
220
+ if (imgResult.image) basicTokens += IMAGE_COST_BASIC;
221
  }
222
  }
223
 
 
240
  const timestamp = Date.now();
241
  const status = isFailure ? "rejected" : "Idle";
242
 
243
+ // Save Data Logic
244
  if (!isFailure) {
245
  const memoryObject = {
246
  id: projectId,
 
260
  await StateManager.updateProject(projectId, memoryObject);
261
  }
262
 
 
263
  if (firestore && !isFailure) {
264
  await firestore.collection('projects').doc(projectId).set({
265
  id: projectId,
 
291
  await db.ref().update(updates);
292
  }
293
 
294
+ // Deduct Basic Credits
295
+ if (basicTokens > 0) await deductUserCredits(userId, basicTokens, 'basic');
296
 
297
  res.json({
298
  success: !isFailure,
 
304
 
305
  } catch (err) {
306
  console.error("Create Error:", err);
307
+ if (err.message.includes("Insufficient")) {
308
  return res.status(402).json({ error: err.message });
309
  }
310
  res.status(500).json({ error: "Creation failed" });
 
315
 
316
  async function runBackgroundInitialization(projectId, userId, description) {
317
  console.log(`[Background] Starting initialization for ${projectId}`);
318
+
319
+ // Separate Counters
320
+ let diamondUsage = 0; // For PM
321
+ let basicUsage = 0; // For Worker
322
 
323
  try {
324
  const pmHistory = [];
325
 
326
+ // 1. Generate GDD (PM -> Diamond)
327
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
328
  const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
329
 
330
+ diamondUsage += (gddResult.usage?.totalTokenCount || 0);
331
  const gddText = gddResult.text;
332
 
333
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
334
  pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
335
 
336
+ // 2. Generate First Task (PM -> Diamond)
337
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
338
  const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
339
 
340
+ diamondUsage += (taskResult.usage?.totalTokenCount || 0);
341
  const taskText = taskResult.text;
342
 
343
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
344
  pmHistory.push({ role: 'model', parts: [{ text: taskText }] });
345
 
346
+ // 3. Initialize Worker (Worker -> Basic)
347
  const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
348
  const workerHistory = [];
349
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
350
 
351
  const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
352
 
353
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
354
  const workerText = workerResult.text;
355
 
356
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
 
365
  failureCount: 0
366
  });
367
 
368
+ // 5. Queue commands (Handle assets + basic costs)
369
+ // We pass userId to deduct image costs (Basic) immediately inside this function
370
  await processAndQueueResponse(projectId, workerText, userId);
371
 
372
  // 6. Update Status
 
375
  lastUpdated: Date.now()
376
  });
377
 
378
+ // 7. Deduct Accumulated Credits
379
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
380
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
381
 
382
+ console.log(`[Background] Init complete. Diamond: ${diamondUsage}, Basic: ${basicUsage}`);
383
 
384
  } catch (err) {
385
  console.error(`[Background] Init Error for ${projectId}:`, err);
386
  if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
 
387
  }
388
  }
389
 
 
391
  const { userId, projectId, description } = req.body;
392
 
393
  try {
394
+ // Init requires BOTH types to be safe
395
+ await checkMinimumCredits(userId, 'diamond');
396
+ await checkMinimumCredits(userId, 'basic');
397
 
398
  if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
399
 
 
405
  runBackgroundInitialization(projectId, userId, description);
406
 
407
  } catch (err) {
408
+ if (err.message.includes("Insufficient")) {
409
  return res.status(402).json({ error: err.message });
410
  }
411
  res.status(500).json({ error: "Failed to start project" });
 
414
 
415
  app.post('/project/feedback', async (req, res) => {
416
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
417
+
418
+ let diamondUsage = 0;
419
+ let basicUsage = 0;
420
 
421
  try {
422
  const project = await StateManager.getProject(projectId);
423
  if (!project) return res.status(404).json({ error: "Project not found." });
424
  if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
425
 
426
+ // Basic Check (most interaction is worker)
427
+ await checkMinimumCredits(userId, 'basic');
428
 
429
  if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
430
 
 
432
  console.log(`[${projectId}] ✅ TASK COMPLETE.`);
433
  const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
434
 
435
+ // PM Call -> Diamond
436
  const pmResult = await AIEngine.callPM(project.pmHistory, summary);
437
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
438
  const pmText = pmResult.text;
439
 
440
  project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
 
445
  await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
446
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() });
447
 
448
+ // Deduct what we used
449
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
450
 
451
  return res.json({ success: true, message: "No further tasks. Project Idle." });
452
  }
 
454
  const newWorkerHistory = [];
455
  const newPrompt = `New Objective: ${nextInstruction}`;
456
 
457
+ // Worker Call -> Basic
458
  const workerResult = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
459
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
460
  const workerText = workerResult.text;
461
 
462
  newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
 
473
 
474
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() });
475
 
476
+ // Final Bill
477
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
478
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
 
479
 
480
+ return res.json({ success: true, message: "Next Task Assigned" });
 
 
 
 
 
 
 
481
  }
482
 
483
+ // Logic for Failure Handling (PM Guidance)
484
  if (project.failureCount > 3) {
485
+ const pmPrompt = (sysPrompts.pm_guidance_prompt || "Analyze logs: {{LOGS}}").replace('{{LOGS}}', logContext?.logs);
486
+
487
+ // PM -> Diamond
488
  const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
489
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
490
  const pmVerdict = pmResult.text;
491
 
492
  if (pmVerdict.includes("[TERMINATE]")) {
 
494
  const resetHistory = [];
495
  const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
496
 
497
+ // Worker -> Basic
498
  const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt, []);
499
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
500
 
501
  resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
502
  resetHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
 
505
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
506
  await processAndQueueResponse(projectId, workerResult.text, userId);
507
 
508
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
509
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
510
  return res.json({ success: true, message: "Worker Terminated." });
511
  } else {
512
  const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
513
+
514
+ // Worker -> Basic
515
  const workerResult = await AIEngine.callWorker(project.workerHistory, injection, []);
516
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
517
 
518
  project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
519
  project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
 
521
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
522
  await processAndQueueResponse(projectId, workerResult.text, userId);
523
 
524
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
525
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
526
  return res.json({ success: true, message: "PM Guidance Applied." });
527
  }
528
  }
529
 
530
+ // Standard Interaction (Worker Only = Basic)
531
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
532
 
533
  let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
534
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
535
  let responseText = workerResult.text;
536
 
537
+ // PM Consultation Check
538
  const pmQuestion = extractPMQuestion(responseText);
539
  if (pmQuestion) {
540
+ // PM -> Diamond
541
  const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
542
  const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
543
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
544
  const pmAnswer = pmResult.text;
545
 
546
  project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
 
550
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
551
  project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
552
 
553
+ // Re-call Worker -> Basic
554
  workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
555
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
556
  responseText = workerResult.text;
557
 
558
  project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
 
573
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() });
574
 
575
  // Final Billing
576
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
577
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
578
 
579
  res.json({ success: true });
580
 
581
  } catch (err) {
582
  console.error("AI Error:", err);
583
+ if (err.message.includes("Insufficient")) {
584
  return res.status(402).json({ error: err.message });
585
  }
586
  res.status(500).json({ error: "AI Failed" });
 
614
 
615
  app.post('/human/override', validateRequest, async (req, res) => {
616
  const { projectId, instruction, pruneHistory, userId } = req.body;
617
+ let basicUsage = 0;
618
 
619
  try {
620
+ await checkMinimumCredits(userId, 'basic');
621
 
622
  const project = await StateManager.getProject(projectId);
623
  const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
 
627
  project.workerHistory.pop();
628
  }
629
 
630
+ // Worker -> Basic
631
  const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
632
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
633
 
634
  project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
635
  project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
 
637
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
638
  await processAndQueueResponse(projectId, workerResult.text, userId);
639
 
640
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
641
 
642
  res.json({ success: true });
643
  } catch (err) {
644
+ if (err.message.includes("Insufficient")) {
645
  return res.status(402).json({ error: err.message });
646
  }
647
  res.status(500).json({ error: "Override Failed" });
648
  }
649
  });
650
 
651
+ // Helper to handle Asset Parsing & Billing (Basic Credits)
652
  async function processAndQueueResponse(projectId, rawResponse, userId) {
653
  const imgPrompt = extractImagePrompt(rawResponse);
654
  if (imgPrompt) {
 
658
  const imgResult = await AIEngine.generateImage(imgPrompt);
659
 
660
  if (imgResult && imgResult.image) {
661
+ // 2. Bill for image tokens immediately (Basic)
662
  const imgTokens = imgResult.usage?.totalTokenCount || 0;
663
+ const totalCost = imgTokens; // imgTokens + IMAGE_COST_BASIC;
664
+
665
+ if (userId && totalCost > 0) {
666
+ await deductUserCredits(userId, totalCost, 'basic');
667
  }
668
 
669
  // 3. Queue creation command