📌Expert Workflows · Step 2: AI Chrome Extension · Edition A (Reseller) · Part 2 of 3
🎯 Why Bother With This
Inpart 1you built an extension that loads into Chrome and opens a popup. It looked like a real tool and did absolutely nothing, which was the point — a reliable foundation first.
Today it earns its place. By the end of this post, you’ll select any text on any web page, click your extension icon, and get an AI-rewritten version back in the popup. For the example user that means highlighting a rough description of a jacket she’s listing and getting a clean, appealing version after a provider-dependent request whose latency, cost, and availability can vary — without opening a chatbot, copying, waiting, and copying back.
This is the part where an extension stops being a novelty and starts saving you real time. It’s also where two problems ambush every first-timer: an AI request that gets silently blocked by Chrome’s security rules, and the question of where to put your API key so it isn’t sitting in plain text. Both are solved below.
Project context
Faster route if you code:Edition C (Solo Founder). Building with a younger learner?Edition B (Young Coder).
🛠️ Your Setup Checklist
- Part 1 complete: a
listing-helperfolder withmanifest.jsonandpopup.html, loading successfully in Chrome - An API key from an approved provider; a limited free allowance may or may not be available — if you don’t have one yet,ChatGPT vs. Claude vs. Geminicompares the major providers and their free tiers
- About 40 minutes
How to Use This Troubleshooting Tutorial
- 🔴 BEFORE shows an intentionally incomplete or flawed example so readers can reproduce and understand a representative problem.
- 🧠 CAUSE explains why the problem occurs.
- 🟢 AFTER presents the corrected implementation for this lesson.
- ✅ VERIFICATION explains how to confirm that the correction works.
📝 Step 1: Understanding the Two Places Your Code Can Live
This section is short, and it will save you an hour. Chrome extensions run code in separate compartments, and theonlything you need to remember today is which compartment is allowed to talk to the internet.
The popupis the little window that opens when you click your icon. It appears when you click and disappears the moment you click elsewhere — including everything it was holding in memory.
The background service workeris a piece of code that runs behind the scenes, with no visible window. It’s the part of your extension that’s allowed to make requests to outside services like an AI API, and it keeps working even when the popup closes.
Here’s the rule that matters:make your AI requests from the service worker, not from code injected into the web page.Code that runs inside someone else’s page is subject to that page’s security rules, and those rules will block your request to an outside service. Your extension’s own background code isn’t subject to them — provided you declare which sites you intend to contact, which we’ll do in a moment.
If that feels abstract, the Troubleshooting notes below makes it concrete. It’s the wall almost everyone hits.
📝 Step 2: Updating the Manifest
Your manifest needs three new things: permission to see the current tab, permission to store your key, and a declaration of which outside address you’ll contact. Replacemanifest.jsonwith:
{
"manifest_version": 3,
"name": "Listing Helper",
"version": "1.1",
"description": "Rewrites and summarizes selected text using AI.",
"permissions": ["activeTab", "scripting", "storage"],
"host_permissions": ["https://your-provider-url/*"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_title": "Listing Helper"
}
}
Read that as a list of declarations, because that’s what it is.activeTabandscriptinglet your extension look at the page you’re currently on — only when you click the icon, and only that tab.storagelets it remember your API key between sessions.host_permissionsis the important newcomer: it tells Chrome “this extension will contact this address,” and without it, your AI request is blocked.Replacehttps://your-provider-url/*with your provider’s actual address— the base URL from their documentation, with/*on the end.
backgroundpoints at the file we’re about to write.
📝 Step 3: The Background File That Calls the AI
Createbackground.jsin the same folder:
// Listens for a request from the popup, calls the AI, sends the answer back.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== "REWRITE") return;
handleRewrite(message.text)
.then((result) => sendResponse({ ok: true, result }))
.catch((error) => sendResponse({ ok: false, error: String(error) }));
return true; // keeps the channel open for the async reply
});
async function handleRewrite(text) {
const { apiKey, baseUrl, model } = await chrome.storage.local.get([
"apiKey",
"baseUrl",
"model",
]);
if (!apiKey) throw new Error("No API key saved yet. Open the popup and add one.");
const prompt =
"Rewrite the following product description for an online marketplace listing. " +
"Keep every factual detail exactly as given — do not invent sizes, brands, " +
"materials, or condition. Make it clear, warm and easy to skim. " +
"Reply with the rewritten description only.\n\n" +
text;
const response = await fetch(`${baseUrl}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: model,
messages: [{ role: "user", content: prompt }],
temperature: 0.4,
}),
});
if (!response.ok) {
throw new Error(`Service returned ${response.status}`);
}
const data = await response.json();
return data.choices[0].message.content.trim();
}
Two things deserve your attention rather than a copy-paste.
The prompt contains one line doing enormous work:“Keep every factual detail exactly as given — do not invent sizes, brands, materials, or condition.”For a reseller, an AI that invents “100% cotton” or upgrades “good condition” to “like new” isn’t a helpful assistant, it’s a misleading-listing complaint waiting to happen. That single sentence is your protection, and it’s the practical application of everything inAI Hallucination and Ethics.
Andtemperature: 0.4keeps it lightly creative — enough to write pleasantly, not enough to freelance.
📝 Step 4: The Popup That Ties It Together
Replacepopup.htmlwith:
<!DOCTYPE html>
<html>
<body style="width: 320px; padding: 14px; font-family: sans-serif;">
<h3 style="margin-top: 0;">Listing Helper</h3>
<div id="setup">
<input id="key" type="password" placeholder="Paste your API key" style="width: 100%;" />
<button id="save" style="margin-top: 6px;">Save key</button>
</div>
<hr />
<button id="go" style="width: 100%; padding: 8px;">Rewrite selected text</button>
<p id="status" style="color: #666; font-size: 12px;"></p>
<textarea id="out" style="width: 100%; height: 160px;"></textarea>
<script src="popup.js"></script>
</body>
</html>
And createpopup.js:
document.getElementById("save").addEventListener("click", async () => {
const apiKey = document.getElementById("key").value.trim();
await chrome.storage.local.set({
apiKey,
baseUrl: "https://your-provider-url/v1",
model: "your-model-name",
});
document.getElementById("status").textContent = "Key saved.";
document.getElementById("key").value = "";
});
document.getElementById("go").addEventListener("click", async () => {
const status = document.getElementById("status");
status.textContent = "Reading page…";
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const [{ result: selected }] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.getSelection().toString(),
});
if (!selected) {
status.textContent = "Select some text on the page first.";
return;
}
status.textContent = "Asking the AI…";
chrome.runtime.sendMessage({ type: "REWRITE", text: selected }, (reply) => {
if (reply && reply.ok) {
document.getElementById("out").value = reply.result;
status.textContent = "Done.";
} else {
status.textContent = "Error: " + (reply ? reply.error : "no response");
}
});
});
Remember to put your provider’s real base URL and model name intopopup.js.
Now go tochrome://extensions, click therefresh iconon your extension card, then try it: open any page, select a paragraph of text, click your icon, paste your key once and save, then clickRewrite selected text.
⚠️ Troubleshooting — The request that was blocked with no explanation
(Troubleshooting example — a common mistake reconstructed for learning, not a personal claim.)
🔴 BEFORE
Educational error scenario: A fetch call is placed in page-injected code, and the request is blocked or behaves differently because page and extension contexts have different security boundaries.
Access to fetch at 'https://...' from origin 'https://www.example-marketplace.com'
has been blocked by CORS policy
🤔 Cause
Two rules collided. Web pages are not allowed to make requests to arbitrary other services — a browser security protection calledCORSthat exists to stop a malicious page quietly using your logged-in accounts elsewhere. Code your extension injectsinto a pageinherits that page’s restrictions, so my request was treated as coming from the marketplace site, and blocked.
The extension must also declare the specific external origins it needs in host_permissions. Grant only the narrowest provider origin required and review Chrome’s current extension documentation.
🟢 AFTER
The fix is the architecture above, and it’s worth stating as a rule you can reuse forever:do the network call in the background service worker, and declare the address inhost_permissions.The popup asks the service worker to do the work; the service worker calls the AI and sends the answer back.
Two practical notes while you’re debugging this class of problem. To see errors from your background code, go tochrome://extensions, find your card, and click the“service worker”link — that opens the console where those messages appear, and without it you’re guessing. And whenever you changemanifest.json, click refresh on the card; manifest changes don’t take effect until you do.
Educational error scenario — repeated key entry and hard-coding. A popup loses its in-memory value when closed, but placing the key in source code would expose it through backups or sharing.
🔴 BEFORE
The corrected personal-use prototype stores configuration in the extension’s local storage rather than source files. This is convenience storage, not a hardware-backed secret vault.
🤔 Cause
Hard-coding a key places it in the extension folder and can expose it through version control, backup, or sharing. A key stored in the browser profile can still be accessed by this extension and by anyone controlling the profile or device.
🟢 AFTER
chrome.storage.local— which is what the code above already uses. It saves the key against your browser profile, not inside your extension’s files, so it survives the popup closing but doesn’t travel with the folder.
The pattern is the one you saw in earlier steps wearing different clothes:.envon a laptop, Colab Secrets in a notebook,chrome.storage.localin an extension. Same principle every time —keep the credential out of source code, minimise its permissions, monitor usage, and rotate it if exposure is suspected.
One honest limitation, because you’ll want to know before you share this with anyone. Anything stored this way is readable by someone with access to your computer and browser profile, and an extension you hand to a friend as a folder can be inspected by them. That’s fine for a personal tool on your own machine. It isnothow you’d ship something to strangers — that requires a small server standing between the extension and the AI service, holding the key where nobody can reach it, which is exactly what Phase 5 of this course builds.
Security takeaway: Treat an API key as a billable credential. Do not share the extension profile or folder as though local storage made the key universally safe.
⚠️ Loose Ends Worth Tying
Your tool works. Three things to know before you use it on real listings.
Read every rewrite before you post it.Your prompt tells the model not to invent details, and that instruction genuinely helps — but “helps” is not “guarantees.” A model can still smooth “small mark on the sleeve” into something softer, and on a marketplace that difference has consequences with buyers and with platform policy. Treat the output as a strong first draft written by an enthusiastic assistant who has never seen the item.
Know what you’re sending.Every rewrite sends the selected text to an outside company, and free tiers often reserve the right to use inputs to improve their models. For public product descriptions that’s usually unremarkable — for anything containing a buyer’s name, address, or message, it isn’t. Select deliberately.
Watch the usage.Each click is an API call. Free tiers have daily limits, and a busy listing evening can find them faster than you’d expect.
✅ What You Have Now
An extension that reads the text you’ve selected on any page, sends it to an AI service from the correct place in your extension’s architecture, and shows a rewritten version in your popup — with your API key stored safely beside the tool rather than inside it. You’ve also met the two problems that stop most first-time extension builders: the blocked request, and where secrets belong. For the authoritative details, seeChrome extensions documentation.
Highlight, click, paste. That’s an evening chore turned into four seconds.
💡 If You Want to Push It
Change one line — the prompt inbackground.js— and your tool becomes something else entirely. Ask for “a friendly 40-word version for social media” and it writes captions. Ask for “three suggested titles under 60 characters” and it does your listing headlines. The code stays identical; the instruction is the product. That’s theprompting skill from Essentials Step 6paying off inside software you built.
❓ Try This Yourself
Rewrite three real descriptions with it, then compare each output against your original word by word. Find anything the model added, softened, or dropped. Whatever you find is exactly what your prompt needs to forbid — and editing that one sentence to fix it is how this tool becomes genuinely yours.
🔗 Next in This Series
[Step 2-A-3/3] Making It Yours — Buttons, Icons and a Tool You’ll Actually Use— where we add a proper icon so it stops looking like a placeholder, give it multiple buttons for different jobs (rewrite, shorten, write a title), remember your last result, and cover honestly what would need to change before you could share it with another person.
Personal-prototype boundary
This design is for a local personal prototype, not public distribution. Selected page text is transmitted to the configured provider. Do not select messages, addresses, account data, or confidential content. Browser local storage is not a secret vault; use a restricted key, monitor usage, and move credentials behind a reviewed server before distribution.
Series status: Part 3 is not currently published. Stop at the personal prototype described here rather than treating the extension as ready to share or sell.
Verified Part 2 Build Evidence
The four tutorial files were reconstructed from this article and checked on 30 August 2026 using Windows 10 and Node.js v24.19.0. The package contains no real API key, account data, selected page text, or live provider configuration.
Reproduction input and observed output
Mock input: Blue mug, chipped rim.
Observed mocked output: Clear factual listing.
The deterministic test confirmed that the input was included in the request body, the expected endpoint and POST method were used, a missing key stopped before a network call, and a simulated HTTP 429 followed the error path.
Automated verification results
| Check | Result | Verified condition |
|---|---|---|
| Manifest JSON and V3 | PASS | The manifest parsed and declared Manifest V3. |
| Manifest file linkage | PASS | The background worker and popup point to existing files. |
| JavaScript syntax | PASS | Both JavaScript files parsed successfully. |
| Required popup elements | PASS | Key, save, rewrite, status, and output controls exist. |
| Secret scan | PASS | No credential-like token is included. |
| Provider boundary | PASS | The provider URL and model remain explicit placeholders. |
| Missing-key guard | PASS | No request occurs without an API key. |
| Mock request and response | PASS | Endpoint, method, input, and returned content were verified. |
| HTTP error path | PASS | A simulated 429 response becomes an error. |
| Popup guards | PASS | Test-only key storage and empty-selection behavior were verified. |
Download the verified Part 2 tutorial files (ZIP)
SHA-256: 710D0255168547BBD7F4D05208518B482D2B2C91829DC83481F9F804B923AFD9
Verification boundary: this is a deterministic mock test, not evidence that a named provider accepted a live request. No live API call was made. Before use, replace the provider and model placeholders using the chosen provider’s current documentation and review its availability, pricing, request format, and data terms.

![[Step 2-A-2/3] Making It Read the Page and Talk to AI (Reseller Edition)](https://lifetechhack.com/wp-content/uploads/2026/08/EW-Step2-A-2-of-3-1100x450.png)