Skip to main content

Credit System Cleanup - Complete

Date: January 4, 2025 Status: ✅ COMPLETE Previous Work: CREDIT_SYSTEM_AUDIT.md, ADMIN_CREDIT_CONFIG_FIX.md


Executive Summary

Successfully streamlined the credit system from 5 theoretical actions to 2 ACTIVE credit actions that actually provide value and charge correctly.

Before Cleanup (5 Actions)

  1. signal_view_details (5 credits) - Dead code, frontend never sends startupId
  2. signal_pool_request (10 credits) - Dead code, frontend never sends startupId
  3. ⚠️ connection_request (15 credits) - Poor naming, doesn't differentiate signal types
  4. warm_intro_request (20 credits) - No frontend UI, feature not built
  5. priority_matching (25 credits) - Didn't provide actual priority functionality

After Cleanup (2 Actions)

  1. public_signal_request (10 credits) - Request contact from public marketplace signal
  2. assigned_signal_request (15 credits) - Request contact from admin-assigned signal

Changes Made

1. Removed Dead Code

A. signal_view_details - REMOVED

Why: Frontend fetches all signals in bulk, never calls individual view endpoint with startupId

Files Changed:

B. warm_intro_request - REMOVED

Why: No frontend UI exists for this feature, endpoint not accessible

Files Changed:

C. priority_matching - REMOVED (Previous Session)

Why: Charged for standard matching algorithm, no actual priority functionality


2. Renamed and Fixed Actions

A. signal_pool_requestpublic_signal_request

Why Renamed:

  • "Signal pool" is internal jargon
  • "Public signal" clearly indicates marketplace signals
  • Better contrast with "assigned signal"

Why Fixed:

  • Original code charged for saving signals (dead code path)
  • Real behavior: Charges when creating contact requests for public signals
  • Frontend was NOT sending startupId when saving signals

Implementation: govintel.controller.ts:4255-4367

// Check if this is a public signal request
if (signal.isPublic) {
const creditCost = await this.creditService.getActionCost(
"public_signal_request",
);
const hasCredits = await this.creditService.hasCredits(
dto.startupId,
creditCost,
);

if (!hasCredits) {
const credits = await this.creditService.getStartupCredits(dto.startupId);
throw new BadRequestException(
`Insufficient credits for public signal request. Required: ${creditCost}, Available: ${credits.creditsRemaining}`,
);
}
}

// ... after creating contact request ...

// Consume credits for public signal requests only
if (signal.isPublic) {
await this.creditService.consumeCredits({
startupId: dto.startupId,
actionType: "public_signal_request",
amount: creditCost,
description: `Public signal contact request for signal ${contactRequest.signal.signalIdentifier}`,
entityId: contactRequest.id,
entityType: "contact_request",
metadata: {
signalId: dto.signalId,
contactRequestId: contactRequest.id,
governmentName: contactRequest.signal.government?.governmentName,
},
});
}

Files Changed:

B. connection_requestassigned_signal_request

Why Renamed:

  • Both public and assigned signals create "connection requests" or "contact requests"
  • Old name was ambiguous
  • New name clearly indicates this is for admin-assigned signals

Implementation: connection-management.service.ts:220-297

// Check if startup has sufficient credits for assigned signal request
const creditCost = await this.creditService.getActionCost(
"assigned_signal_request",
);
const hasCredits = await this.creditService.hasCredits(startup.id, creditCost);

if (!hasCredits) {
const credits = await this.creditService.getStartupCredits(startup.id);
throw new BadRequestException(
`Insufficient credits for assigned signal request. Required: ${creditCost}, Available: ${credits.creditsRemaining}`,
);
}

// ... later ...

// Consume credits for the assigned signal connection request
await this.creditService.consumeCredits({
startupId: startup.id,
actionType: "assigned_signal_request",
amount: creditCost,
description: `Assigned signal connection request to ${signal.governmentName} for signal ${signal.signalIdentifier}`,
entityId: connection.id,
entityType: "connection",
metadata: {
signalId: signal.id,
matchScore: request.matchScore,
priority: request.priority,
},
});

Files Changed:


Final Credit Configuration

const configurations = [
{
actionType: "public_signal_request",
creditCost: 10,
description: "Request contact from a public signal in the marketplace",
isActive: true,
},
{
actionType: "assigned_signal_request",
creditCost: 15,
description: "Request contact from a signal assigned to your startup",
isActive: true,
},
];

Credit Action Type Definition

export type CreditActionType =
| "public_signal_request"
| "assigned_signal_request"
| "admin_adjustment"
| "monthly_reset"
| "tier_upgrade"
| "initial_allocation";

Signal Flow Differentiation

Flow 1: Public Signal (Marketplace)

  1. User browses public marketplace signals
  2. User clicks "Request Contact" on a public signal
  3. System checks signal.isPublic === true
  4. Charges 10 credits via public_signal_request
  5. Creates ContactRequest record

Code: govintel.controller.ts:4255-4367

Flow 2: Assigned Signal (Admin-Curated)

  1. Admin assigns signal to specific startup
  2. Startup views assigned signals dashboard
  3. Startup clicks "Request Connection"
  4. System creates connection
  5. Charges 15 credits via assigned_signal_request
  6. Creates Connection record

Code: connection-management.service.ts:220-297


Why Assigned Signals Cost More (15 vs 10 Credits)

  1. Higher Quality: Admin-curated and pre-vetted for specific startup
  2. Better Match: Signal specifically assigned based on startup profile
  3. Higher Conversion: Admin believes this is a good fit
  4. Premium Service: Involves human curation and matching

Verification & Testing

TypeScript Compilation

pnpm --filter=@civstart/backend typecheck

Files Modified (Complete List)

Seed Scripts:

  1. scripts/full-reset-and-seed.ts
  2. apps/backend/src/scripts/seed-credit-data.ts

Type Definitions: 3. packages/database/index.ts

Controller: 4. apps/backend/src/govintel/controllers/govintel.controller.ts

  • Removed signal_view_details credit logic (lines 497-529)
  • Removed warm intro endpoint entirely
  • Added public_signal_request credit logic (lines 4255-4367)
  • Removed old signal_pool_request logic (lines 3363-3383)

Service: 5. apps/backend/src/govintel/services/connections/connection-management.service.ts

  • Updated connection_request to assigned_signal_request (lines 220-297)
  • Removed initiateWarmIntroduction method (line 532-534)

Tests: 6. apps/backend/src/govintel/controllers/govintel.controller.spec.ts

  • Removed warm intro tests (line 633)
  1. apps/backend/src/govintel/services/connections/connection-management.service.spec.ts
    • Removed warm intro tests (line 446)

Database Migration Required

After deploying these changes, run the seed script to update credit configurations:

# Local development
pnpm reset:dev

# Or run seed script directly
DATABASE_URL="postgresql://civstart:password@localhost:5433/civstart_dev" pnpm tsx scripts/full-reset-and-seed.ts

Note: This will:

  • Remove old credit configurations (signal_view_details, signal_pool_request, connection_request, warm_intro_request, priority_matching)
  • Create new configurations (public_signal_request, assigned_signal_request)
  • Preserve existing credit transactions (history intact)

Admin Configuration Issue (Separate)

The admin authentication error you're experiencing is unrelated to these credit changes. This is a Clerk session/token issue with the admin app.

Error:

[AdminClerkAuthGuard] Admin token verification failed: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined

Potential Causes:

  1. Not signed in to admin app
  2. Clerk session expired
  3. Missing or incorrect NEXT_PUBLIC_ADMIN_CLERK_PUBLISHABLE_KEY in admin .env.local
  4. Token not being sent from frontend

Recommended Actions:

  1. Check if you're signed in to admin app
  2. Try signing out and back in
  3. Check admin app .env.local has correct Clerk keys
  4. Check browser console for auth errors
  5. Verify admin app is using correct Clerk instance

Note: We improved error handling in admin-clerk-auth.guard.ts to provide better diagnostics, but the underlying issue is likely on the admin frontend or Clerk configuration side.


Benefits of This Cleanup

1. Simpler System

  • Reduced from 5 to 2 active credit actions
  • Removed ~400 lines of dead code
  • Clearer naming conventions

2. Correct Charging

  • Fixed public signals now actually charging credits
  • Removed code that never executed
  • Clear differentiation between signal types

3. Better Maintainability

  • Type-safe credit action types
  • Dead code removed from tests
  • Clearer business logic

4. User Clarity

  • "Public signal request" vs "Assigned signal request" is self-explanatory
  • Clear value proposition (why assigned costs more)
  • No confusing unused features

Summary

Removed 3 credit actions that were dead code or not providing value:

  • signal_view_details - Frontend never calls this with startupId
  • warm_intro_request - No frontend UI exists
  • priority_matching - Didn't provide actual priority (removed previously)

Fixed and renamed 2 credit actions to match actual behavior:

  • signal_pool_requestpublic_signal_request (now actually charges!)
  • connection_requestassigned_signal_request (clearer naming)

All TypeScript compilation passing

Tests updated and passing

Ready for deployment - just need to run database seed


Cleanup completed successfully. The credit system now accurately reflects actual functionality and provides clear value differentiation between public marketplace signals (10 credits) and admin-curated assigned signals (15 credits).