DEV Community

Cover image for A Stock Monitoring AI Agent with Mastra
Jesse Ekoh-Ordan
Jesse Ekoh-Ordan

Posted on

A Stock Monitoring AI Agent with Mastra

Today, I created a stock monitoring AI agent using Mastra, and it was mind-blowing. I also learned about inter-agent communication and the need for a standardized protocol - A2A protocol (Agent to Agent). I'm currently in the 13th cohort of the HNG internship program and for my stage 3 task I was told to build a simple AI agent that integrates with Telex, a Slack alternative for communities and also an AI agent platform. Since my stack is mainly NodeJS, I was instructed to use Mastra AI.

Set up

Create a Mastra project using the command below

pnpm create mastra@latest
Enter fullscreen mode Exit fullscreen mode

Create the stock agent

// src/mastra/agents/stock-agent.ts
import { Agent } from "@mastra/core/agent";
import {
  getStockTool,
  getStocksTool,
  analyzeTrendTool,
} from "../tools/stock-tool";
import { Memory } from "@mastra/memory";
import { LibSQLStore } from "@mastra/libsql";

export const stockAgent = new Agent({
  model: "google/gemini-2.5-flash",
  name: "Stock Agent",
  instructions: `
  you are a stock agent. Your task is to provide accurate and up-to-date information about stocks prices. You should only provide information on MAMAA related stocks like price and summarize market trends.

  Your responsibilities:
  1. Fetch current stock prices for all MAMAA companies
  2. Analyze performance trends
  3. Provide insights on price movements
  4. Compare relative performance across MAMAA stocks
  5. Present data in a clear, actionable format
  6. Provide trend analysis for MAMAA stocks. This should include a detailed analysis of the stock's performance over a specific period, highlighting key trends and patterns and should be about 3 sentences long.

  `,
  tools: { getStockTool, getStocksTool, analyzeTrendTool },
  memory: new Memory({
    storage: new LibSQLStore({ url: "file:../mastra.db" }),
  }),
});
Enter fullscreen mode Exit fullscreen mode

Create the various tools the agent would use to fetch data from external APIs

This tools would fetch stock related data from the Alphavantage API.

//src/mastra/tools/stock-tool.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import axios from "axios";

export const getStockTool = createTool({
  id: "Get single stock",
  description: "Get current price of a Stock",
  inputSchema: z.object({
    symbol: z.string().describe("Symbol of stock"),
  }),
  outputSchema: z.object({
    symbol: z.string().describe("Stock symbol"),
    price: z.number().describe("Stock price"),
  }),
  execute: async ({ context }) => {
    return await getStockInfo(context.symbol);
  },
});

export const getStocksTool = createTool({
  id: "Get FAANG stocks",
  description: "Get current stock prices for MAMAA stocks",
  inputSchema: z.object({
    symbols: z.array(z.string()).describe("Array of stock symbols"),
  }),
  outputSchema: z.array(
    z.object({
      symbol: z.string().describe("Stock symbol"),
      price: z.number().describe("Stock price"),
    }),
  ),
  execute: async ({ context }) => {
    return await getStocksInfo(context.symbols);
  },
});

export const analyzeTrendTool = createTool({
  id: "Analyze stock trend",
  description: "Analyze stock trend",
  inputSchema: z.object({
    symbols: z.array(z.string().describe("Symbol of stock")),
  }),
  outputSchema: z.array(
    z.object({
      symbol: z.string().describe("Stock symbol"),
      trend: z.number().describe("Stock trend"),
    }),
  ),
  execute: async ({ context }) => {
    return await stockTrendAnalysis(context.symbols);
  },
});

const getStockInfo = async (symbol: string) => {
  const stocksUrl = `https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=${symbol}&apikey=${process.env.ALPHAVANTAGE_API_KEY}`;
  const response = await axios.get(stocksUrl);
  return response.data;
};

const getStocksInfo = async (symbols: string[]) => {
  console.log(symbols);
  const promises = symbols.map(async (symbol) => {
    const stockInfo = await getStockInfo(symbol);
    return stockInfo;
  });
  const results = await Promise.all(promises);
  return results;
};

const stockTrendAnalysis = async (symbols: string[]) => {
  const promises = symbols.map(async (symbol) => {
    const stockInfo = await getStockInfo(symbol);
    const data = stockInfo["Time Series (Daily)"];
    const prices = Object.values(data).map((day) =>
      parseFloat(day["4. close"]),
    );
    const trend = prices.reduce((acc, price) => acc + price, 0);
    return { symbol, trend };
  });
  const results = await Promise.all(promises);
  return results;
};
Enter fullscreen mode Exit fullscreen mode

Register the Stock agent with Mastra

//src/mastra/index.ts
import { config } from "dotenv";
import { Mastra } from "@mastra/core/mastra";
import { PinoLogger } from "@mastra/loggers";
import { LibSQLStore } from "@mastra/libsql";
import { stockAgent } from "./agents/stock-agent";
config();

export const mastra = new Mastra({
  agents: { stockAgent },
  bundler: {
    externals: ["axios"],
  },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: "Mastra",
    level: "info",
  }),
});
Enter fullscreen mode Exit fullscreen mode

Create the A2A route handler

This exposes an endpoint that allows other agents to communicate with ours using the A2A protocal

// src/routes/index.ts
import { registerApiRoute } from "@mastra/core/server";
import { randomUUID } from "crypto";

export const a2aAgentRoute = registerApiRoute("/a2a/agent/:agentId", {
  method: "POST",
  handler: async (c) => {
    try {
      const mastra = c.get("mastra");
      const agentId = c.req.param("agentId");

      // Parse JSON-RPC 2.0 request
      const body = await c.req.json();
      const { jsonrpc, id: requestId, method, params } = body;

      // Validate JSON-RPC 2.0 format
      if (jsonrpc !== "2.0" || !requestId) {
        return c.json(
          {
            jsonrpc: "2.0",
            id: requestId || null,
            error: {
              code: -32600,
              message:
                'Invalid Request: jsonrpc must be "2.0" and id is required',
            },
          },
          400,
        );
      }

      const agent = mastra.getAgent(agentId);
      if (!agent) {
        return c.json(
          {
            jsonrpc: "2.0",
            id: requestId,
            error: {
              code: -32602,
              message: `Agent '${agentId}' not found`,
            },
          },
          404,
        );
      }

      // Extract messages from params
      const { message, messages, contextId, taskId, metadata } = params || {};

      let messagesList = [];
      if (message) {
        messagesList = [message];
      } else if (messages && Array.isArray(messages)) {
        messagesList = messages;
      }

      // Convert A2A messages to Mastra format
      const mastraMessages = messagesList.map((msg) => ({
        role: msg.role,
        content:
          msg.parts
            ?.map((part) => {
              if (part.kind === "text") return part.text;
              if (part.kind === "data") return JSON.stringify(part.data);
              return "";
            })
            .join("\n") || "",
      }));

      // Execute agent
      const response = await agent.generate(mastraMessages);
      const agentText = response.text || "";

      // Build artifacts array
      const artifacts = [
        {
          artifactId: randomUUID(),
          name: `${agentId}Response`,
          parts: [{ kind: "text", text: agentText }],
        },
      ];

      // Add tool results as artifacts
      if (response.toolResults && response.toolResults.length > 0) {
        artifacts.push({
          artifactId: randomUUID(),
          name: "ToolResults",
          parts: response.toolResults.map((result) => ({
            kind: "data",
            data: result,
          })),
        });
      }

      // Build conversation history
      const history = [
        ...messagesList.map((msg) => ({
          kind: "message",
          role: msg.role,
          parts: msg.parts,
          messageId: msg.messageId || randomUUID(),
          taskId: msg.taskId || taskId || randomUUID(),
        })),
        {
          kind: "message",
          role: "agent",
          parts: [{ kind: "text", text: agentText }],
          messageId: randomUUID(),
          taskId: taskId || randomUUID(),
        },
      ];

      // Return A2A-compliant response
      return c.json({
        jsonrpc: "2.0",
        id: requestId,
        result: {
          id: taskId || randomUUID(),
          contextId: contextId || randomUUID(),
          status: {
            state: "completed",
            timestamp: new Date().toISOString(),
            message: {
              messageId: randomUUID(),
              role: "agent",
              parts: [{ kind: "text", text: agentText }],
              kind: "message",
            },
          },
          artifacts,
          history,
          kind: "task",
        },
      });
    } catch (error) {
      return c.json(
        {
          jsonrpc: "2.0",
          id: null,
          error: {
            code: -32603,
            message: "Internal error",
            data: { details: error.message },
          },
        },
        500,
      );
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

Add the routes to the mastra instance config object

src/mastra/index.ts
...
import { a2aAgentRoute } from "./routes/index";
...
export const mastra = new Mastra({
...
server: {
    build: {
      openAPIDocs: true,
      swaggerUI: true,
    },
    apiRoutes: [a2aAgentRoute],
  },
})
Enter fullscreen mode Exit fullscreen mode

Run your agent

pnpm run dev
Enter fullscreen mode Exit fullscreen mode

AI agents are a mind-blowing concept. During implementation, I was concerned about how I'd need to parse whatever data I fetch from Alphavantage for my agent to use, but I soon realized that it wasn't even a problem. All I had to do was define the input schema for the various tools using zod, and the LLM took care of everything else. Although the functionality of this agent is basic, I've really learned a lot about creating AI agents using Mastra, and I plan on going through their documentation and creating a much more interesting AI agent.

Top comments (0)