I read it on the internet so it must be true.
Let's be honest, not everything on the internet is true. Throughout my career, I've learned that the higher up you go, the more credible you need to be. For example, when I'm talking with the top management, they expect to-the-point accurate answers, and they expect you to back up your assumptions with citations if necessary. It's not that they don't trust you, it's just that the internet is full of junk. And if they're going to stick their neck out on something, they need to be sure.
Artificial intelligence and large language models have brought forth an incredible amount of productivity for us. But large language models are built with a snapshot in time. This means that knowledge is out of date. Paired with other problems such as hallucinations, and data poisoning, the results cannot be trusted.
Nowhere is this truer than in the financial sector. What are the repercussions of lying when it comes to money? Why just about money—how about healthcare? Or really any other field.
I thought it would be fun to write an app that acts as a stock analyst. I can give it any symbol or company to analyze, and it will give me its opinion about it. And not just an opinion; it will tell me why it thinks what it tells me, with citations. Then it's up to me to go to the citations and verify their veracity.
Tech Stack
To build this application, I intend to use the Gemini API. Calling the Gemini API requires you to have an API key. In the real world, you probably want to build this as a client-server application, so the API key can be securely stored on the server side. For this article's sake and to keep things simple, I'll build this application as a single page application using purely JavaScript and HTML. In addition to JavaScript, so that my application doesn't look super ugly, I'll use Tailwind CSS to style the application.
The first thing I need to do is to get an API key. To get a Gemini API key, visit Google AI Studio at https://aistudio.google.com, and choose to generate a new API key. Here, you'll be asked to create a new project where your API key can live. You can optionally associate billing information if you intend to put this application in production and have access to higher limits. For what I'm about to show in this article, the free tier will do just fine.
Save this API key somewhere. You'll need it shortly in the single page application. In a production application, you should always handle API keys on a secure back-end. For this single-file, client-side demo, I'll embed the key directly for simplicity, but treat it with care.
Let's Start Building
The target user interface for my application can be seen in Figure 1.

If you've followed my previous article in CODE Magazine around AI and developer productivity, you'll probably just vibe code this user interface in a matter of seconds. Just hook up VSCode to GitHub Copilot or Cline with Claude, supply the screenshot in Figure 1, and have it generate the HTML for the application.
Because I don't want to bore you with the basics of CSS and HTML, I'll keep my discussion pertinent to the main user interface elements and the actual logic of my application. To start with, my HTML's overall structure can be seen in Listing 1.
Listing 1: My application's overall structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Stock Analyst</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?
family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
</style>
</head>
<body>
<div class="container-card">
</div>
<script>
</script>
</body>
</html>
As can be seen in Listing 1, I've taken a dependency on Tailwind CSS, and certain fonts from Google APIs. Besides that, there are two main parts to my application.
- The
container-cardclass div where the UI will live. - The JavaScript in the
<script/>tag that will give this UI life
Building the UI
Let's get the UI out of the way first. Again, I'm going to omit CSS-specific details so I can focus on the main functionality.
The abbreviated code for my main UI can be seen in Listing 2. As you can see, the main elements are as follows.
Listing 2: My user interface code
<div class="container-card">
<h1>AI Stock Analyst</h1>
<p>Analyze a stock's latest performance using real-time, verifiable
data via the Gemini API.</p>
<!-- Input and Action -->
<div>
<input type="text"
id="tickerInput"
placeholder="Enter stock ticker (e.g., GOOGL, MSFT)"
value="GOOGL">
<button id="analyzeButton"
onclick="analyzeStock()">Analyze Stock</button>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator">
<div>
<svg ..>
<circle ..></circle>
<path ..></path>
</svg>
Analyzing...
</div>
</div>
<!-- Results Area -->
<div id="resultsArea">
<div id="analysisOutput">
<h3>Analysis</h3>
<p id="responseText">Analysis will appear here after you
click "Analyze Stock".</p>
</div>
<div id="citationOutput">
<h3>Source Citations</h3>
<ul id="sourcesList">
<li id="initialSourceText">Sources will be listed here,
providing links to verify the AI's claims.</li>
</ul>
</div>
</div>
</div>
- I have an input type text of ID
tickerInputwhere I expect the user to enter a stock symbol or similar. This is what we intend to do analysis on. - There is a button called
analyzeButtonthat will kick start the analysis process. - The analysis is an async job, so while the job is running, I'll show the user an animated user interface informing the user that an operation is underway.
- Then as the results come in, I'll show them a div called
analysisOutput. Within these results, there will be citations, which I'll list out in a div calledcitationOutputso the user can click on those links and verify the information themselves.
Building the Application Logic
With the user interface out of the way, now let's focus on getting the application logic done.
Start by declaring three constants at the top in your script block, as can be seen in Listing 3.
Listing 3: Global constants
const API_URL_BASE = 'https://generativelanguage.googleapis.com/v1beta/models/
gemini-2.5-flash-preview-05-20:generateContent';
const apiKey = "";
const apiUrl = `${API_URL_BASE}?key=${apiKey}`;
const maxRetries = 3;
const initialDelay = 1000;
Now let's define a core request function that will get called when the user
clicks the **Analyze Stock** button.
This is where you'd also insert the apiKey you got from Google AI Studio earlier. There are also a couple of constants to ensure that you don't hammer the service incessantly.
async function analyzeStock() {
}
This function is the heart of the application, responsible for taking the user's input, communicating with the Gemini API, and handling the results robustly.
First, you do UI management and error checks. The function first retrieves references to all necessary UI elements. It then performs basic checks, such as verifying the API key and ensuring that the user entered a ticker. This part of the code can be seen in Listing 4.
Listing 4: UI Management and Error Checks
// Get input value and UI elements
const ticker = document
.getElementById('tickerInput')
.value
.trim()
.toUpperCase();
// ... other element references ...
if (!apiKey || apiKey === "YOUR_GEMINI_API_KEY_HERE") {
// Handle API Key error
return;
}
// ... disable button, show loading spinner ...
Next, you need to define the AI's persona, aka the system prompt. This is the most critical part for ensuring quality and correct citation formatting. Here, I use a highly prescriptive systemPrompt to guide the model's behavior.
My system prompt is as follows:
“You are a world-class financial analyst. ... CRITICAL INSTRUCTION: You MUST use the numerical citation format [1], [2], [3], etc., IMMEDIATELY after every sentence that contains factual information taken from the search results to ensure verification. Every factual statement must be followed by a citation. Do not use any external knowledge.”
Note the “CRITICAL INSTRUCTION” mentioned in my system prompt, and very clear instructions to AI that I really care about the citations and accuracy of information presented. After all, it's my hard-earned money we're talking about.
Go ahead and put this in a constant, as shown below. I have omitted the actual text for brevity in the code snippet.
const systemPrompt = `..`;
With the AI persona defined, now let's give it a specific task to do, based on this persona. I'll use the following text as the task I intend to give to AI.
“What is the latest news, recent performance, and key analyst sentiment for the stock ticker ${ticker}? Summarize this in one paragraph.”
Go ahead and put this in a userQuery const as below. Again, I've omitted the actual text for brevity in the code snippet.
const userQuery = `..`;
Note that, in this application, you can tweak the AI persona and system prompt to customize the application as you wish. For instance, you could have it answer any finance question, or turn into a healthcare app, or ask it specific questions about the latest way congress is trying to spend money we don't have, etc.
With the query and persona defined, now let's define the grounding payload. The payload object tells the API which model to use, what the user asked, and what tools are available. This can be seen in Listing 5.
Listing 5: The grounding payload
const payload = {
contents: [{ parts: [{ text: userQuery }] }],
// CRITICAL: Enable Google Search grounding
tools: [{ google_search: {} }],
systemInstruction: {parts: [{ text: systemPrompt }]}
};
Specifically, in Listing 5, setting google_search: {} is what enables Grounding, instructing the Gemini model to automatically execute a Google Search, read the results, and base its response only on the information it just retrieved.
Next, you need to work on the actual network request, specifically, robust fetching with exponential backoff. Network requests can fail. To make the app resilient, use a while loop implementing exponential backoff. This retries the API call a few times, waiting longer after each failure (e.g., 1 second, then 2 seconds, then 4 seconds). This is standard practice for handling rate limiting or temporary network issues. The abbreviated code approach for this can be seen in Listing 6.
Listing 6: Making network calls with exponential backoff
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(apiUrl, fetchOptions);
// ... Check response.ok and process results ...
break; // Success! Exit the loop.
} catch (error) {
attempt++;
// ... If max retries, fail.
// Otherwise, calculate delay and wait ...
}
}
The key part in Listing 6 besides the apiURL defined earlier, is fetchOptions, which can be seen as below:
const fetchOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
};
As you can see, this is a simple POST request to Google's APIs. The other detail I've omitted here for simplicity is UI rendering details, such as disabling the Analyze Stock button if a query is active and showing a loading spinner while the query is running. Eventually, when the results show up, render them, re-enable the button, and hide the loading spinner.
With proper throttling and backoff, you should eventually see the results show up. Once the API call succeeds, the data returned must be parsed and presented correctly. This is where the magic of linking text to sources happens.
First, let's extract the grounding metadata.
The model returns the citation source information within the groundingMetadata object, specifically in the groundingChunks array. You iterate through this array to pull out the essential URI and title for each source. This can be seen in Listing 7.
Listing 7: Extracting grounding metadata
// In the analyzeStock function, after fetching the result:
const candidate = result?.candidates?.[0];
let sources = [];
const groundingMetadata = candidate.groundingMetadata;
if (groundingMetadata && groundingMetadata.groundingChunks) {
sources = groundingMetadata.groundingChunks.map(attribution => ({
uri: attribution.web?.uri,
title: attribution.web?.title,
}))
// Filter out invalid sources
.filter(source => source.uri && source.title);
}
As you can see from Listing 7, I'm looking for the groundingMetadata property, and in that, I'm looking for groundingChunks. There, I filter out the URI and title for the citations returned.
To render this, I've written a helper function called formatGroundText that can be seen in Listing 8.
Listing 8: The formatGroundedText utility function
function formatGroundedText(text, sources) {
// Regex to find citation markers like [1], [2], etc.
const citationRegex = /\[(\d+)\]/g;
return text.replace(citationRegex, (match, index) => {
const sourceIndex = parseInt(index) - 1;
const source = sources[sourceIndex];
if (source && source.uri) {
// Create an inline citation link
return `<a href="${source.uri}"
target="_blank"
title="${source.title || 'Source'}"
class="citation inline-block ml-1"><sup>[${index}]</sup></a>`;
}
// Return original text if source is invalid
return match;
});
}
This function converts the plain text response with citation markers into HTML with clickable links. The first parameter is text, which is the generated text from the model, e.g., "The stock is up [1] by 5% [2].". The second parameter is sources, which is an array of source objects. Finally, it returns an HTML string with citations as links.
At last, it displays the sources that can be seen in Listing 9.
Listing 9: Rendering the final output
// In the analyzeStock function, after formatGroundedText:
if (sources.length > 0) {
sourcesList.innerHTML = '';
sources.forEach((source, index) => {
// code to create <li> with [Index + 1]
// and the clickable URL ...
});
}
By implementing these steps—from robust network calls to aggressive prompt engineering and sophisticated regex replacements—you turn a simple AI summary into a powerful, verifiable analysis tool.
Running the Application
With the application done, load it up in a browser. I prefer to use Chrome browser for its great debugging tools, but feel free to use whatever you wish. As long as it can render the HTML file and execute the script, you're good to go.
Now, enter a stock symbol, and click Analyze Stock. Note that your results will be different than mine because you're asking Gemini to give up-to-date information.
As the application runs, you can see the user interface in Figure 2.

Once the results are processed, you can see the output, as can be seen in Figure 3.

This is quite incredible. I'm writing this article on October 8, 2025. Tesla literally introduced the “standard” Model Y and 3 yesterday and rolled out FSD 14.1 today. Also, the $7500 federal tax credit ended just a few days ago. So I know this information is up-to-date and the latest. Also, the stock prices are accurate as of today. Additionally, all the facts and figures check out, and where in doubt, I can verify all the citations listed below.
As they say, trust but verify.
I don't know about you, but I'm quite blown away with how easily I can build such a powerful time-saving application in a matter of minutes.
Summary
This article reminds me of an interesting incident I saw on TV a while ago. Sam Altman from OpenAI was asked by a congressman what the profit model was for OpenAI. He said they have no idea, and once they figure out how to make open AI, they will ask open AI how to make profit out of it.
At the time, that sounded hilarious, but maybe it isn't so? Every time I turn around and look at the capability of artificial intelligence across all the major vendors, I have to pick my jaw off the floor. Just look at what you were able to accomplish with relative ease in this article.
As I started this article, a voice in the back of my head told me that this is just a simple JavaScript post request. Is this really worth writing an article about?
You tell me: Was it worth writing an article about this?
You can so easily modify this article to do so much more. You can ask it to do generic stock analysis, for instance, analyze the stock chart for a given symbol over the last six months and explain why you feel this is a bullish or bearish pattern with explanations.
You don't have to guess. I actually tried it. The output can be seen in Figure 4.

Excuse me while I go pick my jaw up off the floor again. Why don't you try some interesting things with this? Can you have it answer questions about your car, like my Honda Civic is making this specific noise and how do I fix it, have it show answers with diagrams and links to information. Try it out. Let me know how it goes.
Until next time, happy coding.



