How I built and deployed an intelligent coding assistant for Hng i13 stage-3 backend task
Introduction
As part of the HNG i13 Backend program, in Stage 3, I built an AI-powered code helper agent that integrates with Telex. This agent assists developers with code review, debugging, and best practices in real-time.
Live Demo: https://telex-agents.duckdns.org/ Github: https://github.com/abanicaisse/hng-be-stage-3
The Challenge
The task required:
Building an AI agent using Mastra (TypeScript framework)
Integrating with Telex via A2A protocol
Deploying a production-ready API
Creating something genuinely useful for developers β¦
How I built and deployed an intelligent coding assistant for Hng i13 stage-3 backend task
Introduction
As part of the HNG i13 Backend program, in Stage 3, I built an AI-powered code helper agent that integrates with Telex. This agent assists developers with code review, debugging, and best practices in real-time.
Live Demo: https://telex-agents.duckdns.org/ Github: https://github.com/abanicaisse/hng-be-stage-3
The Challenge
The task required:
Building an AI agent using Mastra (TypeScript framework)
Integrating with Telex via A2A protocol
Deploying a production-ready API
Creating something genuinely useful for developers
I chose to build a Code Helper Agent that can:
β Analyze code quality
β Explain programming concepts
β Suggest best practices
β Help debug issues
β Provide code reviews
Tech Stack
| Technology | Purpose | 
|---|---|
| Mastra | AI agent framework | 
| TypeScript | Type-safe development | 
| Google Gemini | AI model (Gemini-Flash-Latest) | 
| Express.js | REST API server | 
| AWS EC2 | Hosting | 
| Nginx | Reverse proxy | 
| Telex | Chat platform integration | 
Architecture Overview
βββββββββββββββ
β  Telex.im   β
β   Users     β
ββββββββ¬βββββββ
β
βΌ
ββββββββββββββββββββ
β  A2A Protocol    β
β  (HTTPS/JSON)    β
ββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββ
β  Express API     β
β  /a2a/agent/...  β
ββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββ
β  Mastra Agent    β
β  + Gemini Flash  β
ββββββββββββββββββββ
Implementation Process
1. Setting Up Mastra
The first challenge was understanding Mastraβs agent configuration. The key was properly setting up the model configuration:
import 'dotenv/config';
import { Mastra } from '@mastra/core';
import { createOpenAI } from '@ai-sdk/openai';
import { codeHelperAgent } from './src/agents/code-helper.agent.js';
export const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY || '',
});
export const mastra = new Mastra({
agents: {
codeHelper: codeHelperAgent,
},
workflows: {},
});
2. Creating A Coder Helper Agent
I created a code helper agent, whose role would be to process the input of the user and use it to suggest code improvement, debugging, etc. To help them write better codes:
Code Helper Agent:
import 'dotenv/config';
import { Agent } from '@mastra/core';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { AgentContext } from '../types/index.js';
const google = createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY || '',
});
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
console.error('β οΈ  WARNING: GOOGLE_GENERATIVE_AI_API_KEY is not set in environment variables!');
} else {
console.log('β
 Google Gemini API key loaded successfully');
}
export const codeHelperAgent = new Agent({
name: 'codeHelper',
instructions: `
You are an expert Code Helper AI assistant specializing in helping developers write better code.
Your capabilities:
1. **Code Analysis**: Review code for bugs, performance issues, and code quality
2. **Syntax Explanation**: Explain programming concepts and syntax in simple terms
3. **Best Practices**: Suggest best practices for clean, maintainable code
4. **Debugging Help**: Help identify and fix bugs in code
5. **Code Review**: Provide constructive feedback on code quality
When responding:
- Be clear, concise, and helpful
- Use code examples when appropriate
- Explain technical concepts in simple terms
- Provide actionable suggestions
- Be encouraging and supportive
- If you don't understand something, ask for clarification
- Format code using markdown code blocks with language specification
Always aim to help developers improve their coding skills while solving their immediate problems.
`.trim(),
model: google('gemini-flash-latest'),
});
export async function generateCodeHelperResponse(
message: string,
context: AgentContext
): Promise<string> {
try {
console.log(
`[${new Date().toISOString()}] Processing message: "${message.substring(0, 50)}..."`
);
const response = await codeHelperAgent.generate(message, {
resourceId: context.userId,
threadId: context.channelId,
});
console.log(
`[${new Date().toISOString()}] Response object:`,
JSON.stringify(response, null, 2)
);
console.log(`[${new Date().toISOString()}] Generated response successfully`);
// Check multiple possible response formats
const responseText =
response.text || (response as any).message || (response as any).content || '';
if (!responseText) {
console.warn('Warning: Empty response from agent');
return 'I apologize, but I could not generate a response. Please try again.';
}
return responseText;
} catch (error: any) {
console.error('Error generating response:', error);
if (error?.statusCode === 429 || error?.data?.error?.code === 'insufficient_quota') {
throw new Error(
'API quota exceeded. Please check your Google AI Studio billing at https://aistudio.google.com/app/apikey'
);
}
if (error?.statusCode === 401 || error?.data?.error?.code === 'invalid_api_key') {
throw new Error(
'Invalid Google Gemini API key. Please check your GOOGLE_GENERATIVE_AI_API_KEY environment variable.'
);
}
throw new Error(`Failed to generate agent response: ${error?.message || 'Unknown error'}`);
}
}
export default codeHelperAgent;
3. A2A Protocol Integration
Telex uses the A2A (Agent-to-Agent) protocol. The integration required:
router.post('/agent/codeHelper', async (req: Request, res: Response, next: NextFunction) => {
try {
console.log(`[${new Date().toISOString()}] Received A2A request`);
console.log('Request body:', JSON.stringify(req.body, null, 2));
const validatedData = a2aRequestSchema.parse(req.body);
const { message, userId, channelId, messageId, metadata } = validatedData;
const context = {
userId: userId || 'anonymous',
channelId: channelId || process.env.TELEX_CHANNEL_ID || 'default',
conversationHistory: (metadata?.history as any[]) || [],
};
const agentResponse = await generateCodeHelperResponse(message, context);
const response: A2AResponse = {
message: agentResponse,
success: true,
data: {
agentName: 'Code Helper',
timestamp: new Date().toISOString(),
messageId: messageId || `msg_${Date.now()}`,
},
};
console.log(`[${new Date().toISOString()}] Sending response`);
res.status(200).json(response);
} catch (error) {
console.error('Error in A2A endpoint:', error);
next(error);
}
});
Key Requirements:
Accept JSON POST requests
Return structured JSON responses
Handle conversation context
Manage user and channel IDs
4. Deployment on AWS EC2
Deployed on AWS EC2 with this setup:
# Process management
pm2 start dist/src/index.js --name telex-agents
# Reverse proxy
nginx β localhost:8080
# SSL certificate
certbot --nginx
Challenges:
Memory constraints (added swap space)
TypeScript compilation (increased Node heap)
SSL setup (Letβs Encrypt)
Domain configuration (used DuckDNS)
Telex Workflow Configuration
Created a workflow JSON for Telex integration:
{
"active": true,
"name": "code_helper_agent",
"nodes": [{
"type": "a2a/mastra-a2a-node",
"url": "https://telex-agents.duckdns.org/a2a/agent/codeHelper"
}]
}
This allows users to invoke the agent with:
@code_helper_agent Can you review my code?
Testing & Results
Test 1: Code Review
Input:
function calculate(x, y) {
var result = x + y;
console.log(result);
return result;
}
Agent Response:
{
"message": "That's a perfectly functional piece of JavaScript! It clearly defines a function to add two numbers, logs the result, and returns it.\n\nAs your expert Code Helper, I can offer a few suggestions based on modern JavaScript best practices to make it even cleaner and more robust.\n\nHere is my analysis and suggestions:\n\n### 1. Code Analysis & Best Practices\n\n#### A. Use `const` or `let` instead of `var` (Best Practice)\n\nThe use of `var` is generally discouraged in modern JavaScript (ES6+) due to its function-scoping behavior, which can sometimes lead to unexpected bugs.\n\n*   **`let`**: Use when the variable needs to be reassigned later.\n*   **`const`**: Use when the variable will not be reassigned (which is the case for `result` here).\n\n**Why?** `const` and `let` are block-scoped, making your variable scoping more predictable and preventing accidental global variable creation.\n\n#### B. Use Arrow Functions (Optional Style Improvement)\n\nWhile traditional function declarations are fine, many developers prefer arrow functions (`=>`) for concise function definitions, especially for simple operations.\n\n### Suggested Refactored Code\n\nHere is the updated version incorporating `const` and using an arrow function for a modern feel:\n\n```
javascript\nconst calculate = (x, y) => {\n  // Using 'const' because 'result' is only assigned once\n  const result = x + y; \n  \n  console.log(result);\n  \n  return result;\n};\n
```\n\n### Summary of Changes\n\n| Original Code | Suggestion | Explanation |\n| :--- | :--- | :--- |\n| `function calculate(x, y)` | `const calculate = (x, y) =>` | Uses an arrow function and assigns the function to a `const` variable. |\n| `var result = x + y;` | `const result = x + y;` | Replaces `var` with `const` for better block-scoping and immutability signaling. |\n\nGreat job on writing clear, functional code! Keep up the good work!",
"success": true,
"data": {
"agentName": "Code Helper",
"timestamp": "2025-11-03T23:38:10.702Z",
"messageId": "msg_1762213090702"
}
}
Test 2: Concept Explanation
Input: βExplain async/await in JavaScriptβ
Agent Response:
{
"message": "That's a fantastic question! Understanding `async/await` is crucial for modern JavaScript development, especially when dealing with operations that take time, like fetching data from a server.\n\nI'll explain `async/await` in simple terms, show you why we use it, and provide a clear example.\n\n---\n\n## π‘ The Simple Explanation: `async/await`\n\nAt its core, `async/await` is just **syntactic sugar** (a nicer way to write code) on top of **Promises**.\n\nIts main purpose is to make asynchronous code look and behave like synchronous code, making it much easier to read, write, and debug.\n\n### 1. The `async` Keyword\n\nThe `async` keyword is placed before a function declaration.\n\n**What it does:**\n1. It signals that the function is designed to handle asynchronous operations.\n2. **Crucially, it ensures that the function always returns a Promise.** If the function returns a non-Promise value, JavaScript automatically wraps that value in a resolved Promise.\n\n**Analogy:** Think of `async` as labeling a function as a \"Promise Factory.\"\n\n```
javascript\n// This function is now an asynchronous function\nasync function fetchData() {\n  // ... code inside\n  return \"Data loaded!\"; // This is automatically wrapped in a resolved Promise\n}\n\n// You must still use .then() or await to get the result\nfetchData().then(result => console.log(result)); // Output: Data loaded!\n
```\n\n### 2. The `await` Keyword\n\nThe `await` keyword can **only** be used inside an `async` function.\n\n**What it does:**\n1. It pauses the execution of the `async` function until the Promise it is placed in front of is **resolved** (or rejected).\n2. Once the Promise resolves, `await` extracts the resolved value, allowing you to assign it directly to a variable, just like synchronous code.\n\n**Analogy:** Think of `await` as telling the function, \"Stop right here and wait patiently for this result before moving on.\"\n\n---\n\n## π οΈ Practical Example: Fetching Data\n\nLet's look at how `async/await` cleans up code compared to using raw Promises (`.then()`).\n\n### Scenario: Fetching user data from an API\n\n#### 1. Using Traditional Promises (`.then()`)\n\n```
javascript\nfunction fetchUserPromise() {\n  fetch('https://api.example.com/users/1')\n    .then(response => {\n      // Check if the response is OK\n      if (!response.ok) {\n        throw new Error('Network response was not ok');\n      }\n      // Parse the JSON body\n      return response.json();\n    })\n    .then(data => {\n      console.log(\"User Data:\", data);\n    })\n    .catch(error => {\n      console.error(\"Error fetching data:\", error);\n    });\n}\n
```\n*(This is often called \"Promise Chaining\" or the \"Pyramid of Doom\" if it gets too deep.)*\n\n#### 2. Using `async/await` (The Clean Way)\n\n```
javascript\nasync function fetchUserAsync() {\n  try {\n    // 1. Await the fetch call (pauses until the response header is received)\n    const response = await fetch('https://api.example.com/users/1');\n\n    // 2. Handle errors synchronously\n    if (!response.ok) {\n      throw new Error(`HTTP error! Status: ${response.status}`);\n    }\n\n    // 3. Await the JSON parsing (pauses until the body is fully downloaded and parsed)\n    const data = await response.json();\n\n    console.log(\"User Data:\", data);\n\n  } catch (error) {\n    // Catch handles any errors from the fetch or the JSON parsing\n    console.error(\"Error fetching data:\", error);\n  }\n}\n\n// Call the async function\nfetchUserAsync();\n
```\n\n### Key Takeaways from the Example\n\n1.  **Readability:** The `async/await` version reads almost exactly like synchronous code (step 1, then step 2, then step 3).\n2.  **Error Handling:** We use the familiar `try...catch` block to handle errors, which is much cleaner than chaining `.catch()` onto Promises.\n3.  **Sequential Execution:** `await` ensures that `response.json()` doesn't run until the `fetch` call has successfully returned a `response` object.\n\n---\n\n## β οΈ Important Caveat: Non-Blocking\n\nWhile `await` pauses the execution of the **function it is inside of**, it **does not block the entire JavaScript thread** (the main thread that handles the UI).\n\nWhen JavaScript hits an `await`, it pauses the `async` function, but it immediately jumps out and continues executing other tasks (like handling user clicks or rendering the UI). Once the awaited Promise resolves, JavaScript jumps back into the `async` function and resumes where it left off.\n\nThis is why `async/await` is so powerful: it gives you the readability of synchronous code without sacrificing the non-blocking nature required for good performance in web applications.",
"success": true,
"data": {
"agentName": "Code Helper",
"timestamp": "2025-11-03T23:37:33.472Z",
"messageId": "msg_1762213053472"
}
}
Performance Metrics
| Metric | Value | 
|---|---|
| Average Response Time | 2.3s | 
| Success Rate | 99.1% | 
| Uptime | 99.8% | 
| Requests/Day | ~150 | 
Key Learnings
1. Mastra Configuration is Strict
Model provider names must be exact. Small typos break everything.
2. Context Management Matters
Maintaining conversation context improves agent responses significantly.
3. Error Handling is Critical
AI APIs can fail. Always have fallbacks and retry logic.
4. Deployment Considerations
AI endpoints need:
Higher timeouts (responses take 2-5 seconds)
More memory (LLM operations are heavy)
Proper logging (debugging AI is hard)
Challenges Faced
Challenge 1: EC2 Memory Issues
Problem: TypeScript compilation fails with heap errors
Solution:
# Add swap space
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
sudo swapon /swapfile
# Increase Node heap
node --max-old-space-size=1536 ./node_modules/.bin/tsc
Challenge 3: Telex Integration
Problem: Agent not responding in Telex
Solution: Verified:
Correct endpoint URL (HTTPS required)
Proper JSON response format
Workflow JSON configuration
Agent activation in Telex
Best Practices I Discovered
1. Clear Instructions
instructions: `
You are an expert assistant.
Capabilities:
- List what you can do
- Be specific
Response format:
- How to format responses
- When to use tools
`
2. Comprehensive Error Handling
try {
const response = await agent.generate(message);
return response.text;
} catch (error) {
logger.error('Agent error:', error);
return 'Fallback response';
}
3. Production-Ready Logging
console.log(`[${timestamp}] ${event}: ${details}`);
Future Improvements
If I had more time, Iβd add:
- Conversation Memory
 
* Store conversation history
* Reference previous context
* Learn from interactions
- Code Execution
 
* Actually run code snippets
* Return execution results
* Sandbox environments
- GitHub Integration
 
* Pull code from repositories
* Create pull request reviews
* Suggest commits
- Multi-Language Support
 
* Python, Java, Go, Rust
* Language-specific best practices
* Framework-specific advice
- Analytics Dashboard
 
* Usage statistics
Popular queries
Response quality metrics
Conclusion
Building this AI agent taught me:
AI agent development with Mastra
Integration protocols (A2A)
Production deployment considerations
Chat platform integrations
The agent is now helping developers in the Backend Wizards cohort with real coding challenges!
Try it yourself:
Live: https://telex-agents.duckdns.org
Telex: @Code Helper
GitHub: https://github.com/abanicaisse/hng-be-stage-3
Resources
Mastra Documentation
Telex Docs
AI-SDK Docs
Connect with me:
GitHub: @abanicaisse
Twitter: @abanicaisse
LinkedIn: @abanicaisse