Skip to content

Conversation

@ducheharsh
Copy link
Contributor

This pull request introduces support for tracking and updating user credit balances when interacting with agents, as well as a new endpoint for running agents. The main changes include schema updates for user credits, a new API route and controller method for running agents, and logic to decrement user credits based on agent usage.

User credit management:

  • Added creditBalance and creditBalanceLastUpdated fields to the user table and Prisma schema, with defaults set for new users. [1] [2]

Agent execution and credit deduction:

  • Implemented runAgent method in AgentService to execute an agent, calculate credit usage based on agent costs and token usage, and update the user's credit balance accordingly.
  • Added runAgent method to AgentController and exposed a new POST endpoint /agents/:id/run in agent.route.ts for running agents. [1] [2]

Agent CRUD improvements:

  • Updated agent deletion and lookup logic to ensure operations are only performed by the agent's owner by matching both id and userId. [1] [2]

Type consistency and validation:

  • Improved handling of agent cost fields by ensuring numeric conversion and string consistency in controller logic. [1] [2]… user credit balance

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a credit-based usage system for agent execution, enabling users to run agents with automatic cost calculation and balance deduction. It introduces a new /agents/:id/run endpoint, schema changes to track user credit balances, and updates to agent ownership validation in deletion operations.

  • Added credit balance tracking with creditBalance (default 100) and creditBalanceLastUpdated fields to the user table
  • Implemented runAgent functionality with cost calculation based on agent base cost plus input/output token costs
  • Enhanced agent deletion security by attempting compound key filtering (though this has implementation issues)

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
prisma/schema.prisma Added creditBalance and creditBalanceLastUpdated fields to user model for tracking credit usage
prisma/migrations/20251127123756_ag_credits/migration.sql Database migration adding credit balance columns with default values
src/services/agent.service.ts Added runAgent method for agent execution and credit deduction; updated deleteAgent with compound where clauses
src/controllers/agent.controller.ts Added runAgent controller method with basic error handling; improved type handling for agentCost
src/routes/agent.route.ts Exposed new POST endpoint /agents/:id/run for running agents

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

try {
const { deployedUrl, default_agent_name } =(await c.req.json()) as IAgentNameVerification;
const agent_id = c.req.param('id');
const { message } = await c.req.json();
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for required parameters. The agent_id and message parameters are not validated before use. Add validation to ensure these required fields exist and are non-empty strings, similar to other endpoints like deleteAgent:

if (!agent_id || typeof agent_id !== 'string' || agent_id.trim().length === 0) {
    return c.json(
        api_response({
            message: "agent_id is required and must be a non-empty string",
            is_error: true
        }),
        400
    );
}

if (!message || typeof message !== 'string' || message.trim().length === 0) {
    return c.json(
        api_response({
            message: "message is required and must be a non-empty string",
            is_error: true
        }),
        400
    );
}
Suggested change
const { message } = await c.req.json();
const { message } = await c.req.json();
// Validate agent_id
if (!agent_id || typeof agent_id !== 'string' || agent_id.trim().length === 0) {
return c.json(
api_response({
message: "agent_id is required and must be a non-empty string",
is_error: true
}),
400
);
}
// Validate message
if (!message || typeof message !== 'string' || message.trim().length === 0) {
return c.json(
api_response({
message: "message is required and must be a non-empty string",
is_error: true
}),
400
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +132
await prisma.user.update({
where: { id: user_id },
data: {
creditBalance: {
decrement: Number(agent.agentCost) + (agent.inputTokenCost > 0 ? Number(agent.inputTokenCost) * (response.input_tokens || 0) : 0) + (agent.outputTokenCost > 0 ? Number(agent.outputTokenCost) * (response.output_tokens || 0) : 0),
},
creditBalanceLastUpdated: new Date(),
},
});

const user = await prisma.user.findUnique({
where: { id: user_id },
});
if (!user) {
throw new Error("User not found");
}

return {
response: response.response_content,
creditBalance: user.creditBalance,
};
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk of race condition in credit balance update. The credit balance is read after the agent call and update operation, which could lead to stale data if multiple concurrent requests are made. The balance could be decremented multiple times before the user query returns the updated value. Consider using an atomic transaction or returning the updated user data from the update operation:

const updatedUser = await prisma.user.update({
    where: { id: user_id },
    data: {
        creditBalance: {
            decrement: /* credit calculation */,
        },
        creditBalanceLastUpdated: new Date(),
    },
});

return {
    response: response.response_content,
    creditBalance: updatedUser.creditBalance,
};

Copilot uses AI. Check for mistakes.
where: { id: user_id },
data: {
creditBalance: {
decrement: Number(agent.agentCost) + (agent.inputTokenCost > 0 ? Number(agent.inputTokenCost) * (response.input_tokens || 0) : 0) + (agent.outputTokenCost > 0 ? Number(agent.outputTokenCost) * (response.output_tokens || 0) : 0),
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex credit calculation embedded in update statement reduces maintainability. The inline credit calculation is difficult to read, debug, and test. Extract this calculation into a separate, testable function:

const calculateAgentCost = (
    agent: { agentCost: string; inputTokenCost: number; outputTokenCost: number },
    response: { input_tokens: number | null; output_tokens: number | null }
): number => {
    const baseCost = Number(agent.agentCost);
    const inputCost = agent.inputTokenCost > 0 
        ? Number(agent.inputTokenCost) * (response.input_tokens || 0) 
        : 0;
    const outputCost = agent.outputTokenCost > 0 
        ? Number(agent.outputTokenCost) * (response.output_tokens || 0) 
        : 0;
    return baseCost + inputCost + outputCost;
};

// Then use it in the update:
const totalCost = calculateAgentCost(agent, response);
await prisma.user.update({
    where: { id: user_id },
    data: {
        creditBalance: { decrement: totalCost },
        creditBalanceLastUpdated: new Date(),
    },
});

Copilot uses AI. Check for mistakes.
});
if (!agent) {
throw new Error("Agent not found");
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing authorization check for agent ownership. The method doesn't verify that the user is authorized to run this agent. If the agent is private (not public), only the owner should be able to run it, or there should be proper access control. Add a check similar to deleteAgent:

const agent = await prisma.agent.findUnique({
    where: { id: agent_id },
});
if (!agent) {
    throw new Error("Agent not found");
}

// Check if user can access this agent
if (!agent.isPublic && agent.userId !== user_id) {
    throw new Error("Unauthorized to run this agent");
}
Suggested change
}
}
// Check if user can access this agent
if (!agent.isPublic && agent.userId !== user_id) {
throw new Error("Unauthorized to run this agent");
}

Copilot uses AI. Check for mistakes.
const agent_id = c.req.param('id');
const { message } = await c.req.json();
const user = await c.get('user');
const agent = await AgentService.runAgent(agent_id.trim(), message.trim(), user?.id);
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing user authentication check. The user object retrieved from context may be undefined, but there's no validation before accessing user?.id. This could result in undefined being passed to AgentService.runAgent(). Add a check similar to other endpoints (e.g., createAgent, deleteAgent) that verifies the user is authenticated before proceeding:

const user = await c.get('user');
if (!user || !user.id) {
    return c.json(
        api_response({
            message: "User authentication required",
            is_error: true
        }),
        401
    );
}
Suggested change
const agent = await AgentService.runAgent(agent_id.trim(), message.trim(), user?.id);
if (!user || !user.id) {
return c.json(
api_response({
message: "User authentication required",
is_error: true
}),
401
);
}
const agent = await AgentService.runAgent(agent_id.trim(), message.trim(), user.id);

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +110
public static readonly runAgent = async (agent_id: string, message: string, user_id: string) => {
const agent = await prisma.agent.findUnique({
where: { id: agent_id },
});
if (!agent) {
throw new Error("Agent not found");
}
const session_id = crypto.randomUUID();

const response = await callProxiedAgent(
agent.deployedUrl,
agent.default_agent_name || "",
(agent.framework_used as AgentFrameWorks) || AgentFrameWorks.google_adk,
message,
session_id,
user_id
);
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No check for sufficient credit balance before running the agent. The service will decrement the user's credit balance without first verifying that they have sufficient credits. This could result in negative credit balances. Add a check to ensure the user has enough credits before calling the agent:

const user = await prisma.user.findUnique({
    where: { id: user_id },
});
if (!user) {
    throw new Error("User not found");
}

const estimatedCost = Number(agent.agentCost) + 
    (agent.inputTokenCost > 0 ? Number(agent.inputTokenCost) * 1000 : 0) + // Estimate max tokens
    (agent.outputTokenCost > 0 ? Number(agent.outputTokenCost) * 1000 : 0);

if (user.creditBalance < estimatedCost) {
    throw new Error("Insufficient credit balance");
}

Copilot uses AI. Check for mistakes.
// First check if the agent exists and user has permission
const existingAgent = await prisma.agent.findUnique({
where: { id: agent_id },
where: { id: agent_id, userId: user_id },
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid Prisma where clause. Prisma's where clause on findUnique requires a unique constraint, but the agent model only has id as a unique field, not a compound (id, userId) constraint. This will cause a runtime error. The authorization check on line 319 is already sufficient, so the where clause should only use id:

const existingAgent = await prisma.agent.findUnique({
    where: { id: agent_id },
});

Copilot uses AI. Check for mistakes.

const agent = await prisma.agent.delete({
where: { id: agent_id },
where: { id: agent_id, userId: user_id },
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid Prisma where clause. Same issue as in findUnique above - Prisma's delete requires a unique constraint, but (id, userId) is not a compound unique key. Use only id:

const agent = await prisma.agent.delete({
    where: { id: agent_id },
});

Copilot uses AI. Check for mistakes.
@ducheharsh
Copy link
Contributor Author

ducheharsh commented Nov 27, 2025

resolves #1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants