Is SmartSolve - AI Homework Tutor safe?
SmartSolve captures screenshots and selected text from any tab and transmits them to api.smartsolve.ai, while suppressing browser events to conceal itself from exam platforms.
When triggered by a hotkey or right-click menu, the extension captures either a visible screenshot or selected text from the active tab and forwards it to its AI answer service at api.smartsolve.ai. It also injects a message listener into every page that relays arbitrary text to the same endpoint without validating the message origin, meaning any page can silently submit content to the AI on the user's behalf. Additionally, the extension blocks visibilitychange, blur, and focus events on all pages to prevent online exam platforms from detecting its presence.
Who publishes itNIKNEA, LLC - no other listings under this identity
NIKNEA, LLC - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Visible tab screenshot sent after SmartSolve snapshot
Choosing SmartSolve Snapshot, or Shift+S+1, captures the visible tab as a JPEG data URL, feeding it into the chat flow.
The popup opens a server-sent-events POST to api.smartsolve.ai/api/answer, carrying the signed-in authorization token.
What actually happens
You ask SmartSolve to take a snapshot of the current page.
The trigger is either Shift+S+1 or the SmartSolve Snapshot context-menu item.
The extension captures the visible tab and sends the image into an answer request.
The captured value is a JPEG data URL that is added as image input.
Data placed into the answer request
- Visible tab imagedata:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD9k= (illustrative)
The image can include whatever text, account information, or page content is visible when you trigger the snapshot.
- SmartSolve session tokenAuthorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature (illustrative)
The request is tied to the signed-in SmartSolve account used by the extension.
Captured request
Server-sent event stream used for SmartSolve answer generation.
- content-type
- application/json
- Authorization
- Z1.getSnapshot().token
The code that does this
Snapshot capture and answer submission path
Readable background listener captures and forwards the snapshot
src/pages/background/index.jschrome.runtime.onMessage.addListener((t, e, n) => { if (typeof t == "object" && t.type === "tab-capture") { if (typeof t.data == "string") x("/answers", { type: "init-chat", dataType: "image", data: t.data }); else if (t.start === !0) chrome.tabs.query({ active: !0, currentWindow: !0 }).then(s => { _(s[0]) }), n(!0); else if (e.tab) return chrome.tabs.captureVisibleTab(e.tab.windowId).then(s => { n(s) }), !0 }});Readable snapshot helper injects the capture UI into the tab
src/pages/background/index.jsfunction _(t, e) { !t.id || chrome.scripting.executeScript({ target: { tabId: t.id, frameIds: e != null && e.frameId ? [e.frameId] : void 0 }, func: A, args: [t.id + 261] })}Readable menu and keyboard triggers call the snapshot path
src/pages/background/index.jsfunction I() { chrome.contextMenus.create({ contexts: ["selection"], id: "text-select", title: "SmartSolve It!" }, () => { chrome.runtime.lastError }), chrome.contextMenus.create({ contexts: ["page"], id: "snapshot-select", title: "SmartSolve Snapshot" }, () => { chrome.runtime.lastError })}g.addEventListener("change", () => { var t, e, n, s; ((t = g.currentSettings) == null ? void 0 : t.rightClick) === !1 || !((e = g.currentAuth) != null && e.token) ? chrome.contextMenus.removeAll() : (((n = g.currentSettings) == null ? void 0 : n.rightClick) === !0 || ((s = g.currentAuth) == null ? void 0 : s.token)) && I()});chrome.runtime.onInstalled.addListener(async () => { var t, e; ((t = g.currentSettings) == null ? void 0 : t.rightClick) !== !1 && ((e = g.currentAuth) == null ? void 0 : e.token) && I()});chrome.contextMenus.onClicked.addListener(async (t, e) => { !e || (t.menuItemId === "text-select" ? q(e, t) : t.menuItemId === "snapshot-select" && _(e, t))});const j = { snapshot: ["ShiftLeft", "KeyS", "Digit1"], highlightedText: ["ShiftLeft", "KeyS", "Digit2"] },Readable popup sends non-client chat messages to the answer API
src/pages/popup/index.jsM2 = { BASE_URL: "https://smartsolve.ai/api", AI_BASE_URL: "https://api.smartsolve.ai/api" }, e0 = () => t3({ mutationFn: ({ message: s, chatId: o, visual: n, forceVisual: x, clearChat: H }) => new Promise(L => { const m = j2(), g = { id: m, message: F3(s), type: "user", streaming: !1, clientOnly: !1, isLoading: !n && s.startsWith("data:image/jpeg;base64,"), visual: !!n, forceVisual: !!n && !!x }; A1.addMessage(g, o); const f = A1.getChat(o); H && (f.messages = f.messages.filter(D => D.id !== m ? (A1.removeMessage(D.id, o), !1) : !0)); const k = j2(), u = new J6.SSE(`${M2.AI_BASE_URL}/answer`, { payload: JSON.stringify({ chat: f.messages.filter(D => !D.clientOnly), id: o }), headers: { "content-type": "application/json", Authorization: Z1.getSnapshot().token }, start: !1 }); A1.addMessage({ id: k, message: "", type: "ss", clientOnly: !1, streaming: !0, isLoading: !0 }, o), u.addEventListener("ocr_result", D => { const V = JSON.parse(D.data).message; A1.addMessage({ ...g, message: F3(V), isLoading: !1 }, o) }); let y = ""; u.addEventListener("work", async D => { var G; const V = JSON.parse(D.data).message, Q = (G = A1.getChat(o)) == null ? void 0 : G.messages.find(v => v.id === k); !Q || (Q.isLoading && (Q.isLoading = !1), Q.message += V, y += V, A1.addMessage(Q, o)) }); let M = ""; const d = j2(); u.addEventListener("answer", async D => { const V = JSON.parse(D.data).message; M += V, M.length > 200 && A1.addMessage({ id: d, message: "", type: "ss", clientOnly: !0, streaming: !0, isLoading: !0 }, o) }), u.addEventListener("tickets_update", D => { const V = JSON.parse(D.data); typeof V.message == "number" && (w2.saveTickets(V.message), w2.setShowTicketBanner(!0)) }), u.addEventListener("ocr_error", () => { y = "error", A1.removeMessage(m, o), A1.addMessage({ id: k, message: x9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !0 }, o) }), u.addEventListener("change_type", D => { JSON.parse(D.data).blocked && b3.setVisionBlocked(Date.now()), y = "error" }), u.addEventListener("answer_error", () => { y = "error", A1.removeMessage(m, o), A1.addMessage({ id: k, message: h9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !0 }, o) }), u.addEventListener("trial_limit", () => { y = "error", A1.removeMessage(m, o), Z1.setHitTrialLimit(!0) }), u.addEventListener("custom_error", D => { const V = JSON.parse(D.data); y = "error", A1.addMessage({ id: k, message: V.message, streaming: !!V.s, clientOnly: V.c !== !1, type: "ss", isLoading: !1 }, o) }), u.addEventListener("error", D => { D.data === "Unauthorized" && Z1.clear(), y = "error", A1.removeMessage(m, o), A1.addMessage({ id: k, message: L9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !1 }, o) }), u.addEventListener("readystatechange", D => { var V; if (D.readyState === 2) { const Q = y.toLowerCase().replace(/\s\s+/g, " "), G = M.toLowerCase().replace(/\s\s+/g, " "); !y && M.length < 200 ? A1.addMessage({ id: k, message: m9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !0 }, o) : M.length < 200 ? A1.setAnswer(F3(M), o) : Q.includes(G.toLowerCase()) ? G.includes(Q.toLowerCase()) && A1.removeMessage(d, o) : A1.addMessage({ id: d, message: F3(M), type: "ss", clientOnly: !!y, streaming: !0, isLoading: !1 }, o), y !== "error" && A1.lockAnswer(o); const v = (V = A1.getChat(o)) == null ? void 0 : V.messages.find(P => P.id === k); v && (v.message = F3(v.message), A1.addMessage(v, o)), setTimeout(() => { A1.stopStreaming(d, o), A1.stopStreaming(k, o) }, 100), L(1) } }), u.stream() }) }),Network destination for the captured image
- api.smartsolve.ai
Receives the SmartSolve answer request that includes the image chat message.
Page focus and visibility events blocked on load
On every page load, SmartSolve injects code into all frames before page scripts finish registering.
That code blocks visibility and focus events from reaching site handlers, so sites may miss normal tab-focus signals.
What actually happens
You open or reload a page while the extension is installed.
The behavior is tied to the tab loading state, not to a menu item.
The extension injects code that prevents page focus and visibility events from reaching website handlers.
The code runs immediately in every frame in the page context.
Browser signals affected by the injected listeners
- Tab visibility changesvisibilitychange
A site may not receive the normal signal when your tab becomes visible or not visible.
- WebKit visibility changeswebkitvisibilitychange
A site using the browser-specific visibility event may not receive that signal either.
- Top-window blurblur on the top window
A site may not receive the normal signal when the page window loses focus.
- Top-window focusfocus on the top window
A site may not receive the normal signal when the page window regains focus.
The code that does this
Immediate page-context event interception
Readable service worker injects capture-phase event blockers on page load
src/pages/background/index.jschrome.tabs.onUpdated.addListener((t, e, n) => { e.status === "loading" && chrome.scripting.executeScript({ target: { tabId: t, allFrames: !0 }, func: () => { window.addEventListener("visibilitychange", s => { s.stopImmediatePropagation() }, { capture: !0 }), window.addEventListener("webkitvisibilitychange", s => { s.stopImmediatePropagation() }, { capture: !0 }), window.addEventListener("blur", function(s) { if (this.window === s.target) return s.stopImmediatePropagation(), !1 }, { capture: !0 }), window.addEventListener("focus", function(s) { this.window === s.target && s.stopImmediatePropagation() }, { capture: !0 }) }, injectImmediately: !0, world: "MAIN" })});Selected page text sent to SmartSolve answer API
Highlighting text and pressing SmartSolve It (or Shift+S+2) sends the page selection to SmartSolve.
The extension reads the selection and POSTs it via SSE to https://api.smartsolve.ai/api/answer for the signed-in session.
What actually happens
You select text and invoke SmartSolve on the page.
The trigger is Shift+S+2 or the SmartSolve It context-menu item.
The extension reads the highlighted text and sends it as a SmartSolve answer request.
The text is sent as the user message in the chat payload.
Data placed into the answer request
- Selected page textSolve for x in 2x + 3 = 11 (illustrative)
The highlighted text can include page content you selected before invoking SmartSolve.
- SmartSolve session tokenAuthorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature (illustrative)
The request is tied to the signed-in SmartSolve account used by the extension.
Captured request
Server-sent event stream used for SmartSolve answer generation.
- content-type
- application/json
- Authorization
- Z1.getSnapshot().token
The code that does this
Selected-text capture and answer submission path
Readable text-capture listener reads window selection
src/pages/background/index.jschrome.runtime.onMessage.addListener((t, e) => { typeof t == "object" && t.type === "text-capture" && q(e.tab)});async function q(t, e) { var s; if (!t.id) return; const n = await chrome.scripting.executeScript({ target: { tabId: t.id, frameIds: e != null && e.frameId ? [e.frameId] : void 0 }, func: () => { var a; return (a = window.getSelection()) == null ? void 0 : a.toString() } }).catch(() => {}); (typeof n < "u" || (e == null ? void 0 : e.selectionText)) && x("/answers", { type: "init-chat", dataType: "text", data: ((s = n == null ? void 0 : n[0]) == null ? void 0 : s.result) || (e == null ? void 0 : e.selectionText) })}Readable menu and keyboard triggers call the text path
src/pages/background/index.jsfunction I() { chrome.contextMenus.create({ contexts: ["selection"], id: "text-select", title: "SmartSolve It!" }, () => { chrome.runtime.lastError }), chrome.contextMenus.create({ contexts: ["page"], id: "snapshot-select", title: "SmartSolve Snapshot" }, () => { chrome.runtime.lastError })}g.addEventListener("change", () => { var t, e, n, s; ((t = g.currentSettings) == null ? void 0 : t.rightClick) === !1 || !((e = g.currentAuth) != null && e.token) ? chrome.contextMenus.removeAll() : (((n = g.currentSettings) == null ? void 0 : n.rightClick) === !0 || ((s = g.currentAuth) == null ? void 0 : s.token)) && I()});chrome.runtime.onInstalled.addListener(async () => { var t, e; ((t = g.currentSettings) == null ? void 0 : t.rightClick) !== !1 && ((e = g.currentAuth) == null ? void 0 : e.token) && I()});chrome.contextMenus.onClicked.addListener(async (t, e) => { !e || (t.menuItemId === "text-select" ? q(e, t) : t.menuItemId === "snapshot-select" && _(e, t))});const j = { snapshot: ["ShiftLeft", "KeyS", "Digit1"], highlightedText: ["ShiftLeft", "KeyS", "Digit2"] },Readable hotkey listener sends text-capture for Shift+S+2
src/pages/background/index.jschrome.tabs.onUpdated.addListener(async (t, e, n) => { var s, a; if (e.status === "loading") { const C = (a = (s = g.currentSettings) == null ? void 0 : s.keys) != null ? a : j; chrome.scripting.executeScript({ target: { tabId: t, allFrames: !0 }, func: (m, c, o) => { let i = Math.sin(o) * 1e4; if (i = (i - Math.floor(i)).toString().replace(".", ""), window[`_s${i}`]) return; window[`_s${i}`] = !0; function d(p) { if (Array.isArray(p)) return p.map(h => { var y; return (y = c[h]) != null ? y : h }).sort().join() } let r = d(m.highlightedText), u = d(m.snapshot); chrome.runtime.onMessage.addListener(p => { var h, y; typeof p == "object" && p.type === "hotkey-update" && ((h = p.keys) != null && h.highlightedText && (r = d(p.keys.highlightedText)), (y = p.keys) != null && y.snapshot && (u = d(p.keys.snapshot))) }); let l = []; function w() { var p, h; if (l.length === 2 || l.length === 3) { const y = d(l); y === u ? (chrome.runtime.sendMessage({ type: "tab-capture", start: !0 }), l = []) : y === r && ((h = (p = window.getSelection()) == null ? void 0 : p.toString()) == null ? void 0 : h.length) > 0 && (chrome.runtime.sendMessage({ type: "text-capture" }), l = []) } } window.addEventListener("blur", () => { l = [] }), window.addEventListener("focus", () => { l = [] }), window.addEventListener("keydown", p => { var h, y; l.includes((h = c[p.code]) != null ? h : p.code) || l.push((y = c[p.code]) != null ? y : p.code), w() }), window.addEventListener("keyup", p => { var y; const h = l.indexOf((y = c[p.code]) != null ? y : p.code); h !== -1 && l.splice(h, 1) }) }, args: [C, $, n.id + 263] }).then(() => { v.push(t) }) }});Readable popup sends non-client chat messages to the answer API
src/pages/popup/index.jsM2 = { BASE_URL: "https://smartsolve.ai/api", AI_BASE_URL: "https://api.smartsolve.ai/api" }, e0 = () => t3({ mutationFn: ({ message: s, chatId: o, visual: n, forceVisual: x, clearChat: H }) => new Promise(L => { const m = j2(), g = { id: m, message: F3(s), type: "user", streaming: !1, clientOnly: !1, isLoading: !n && s.startsWith("data:image/jpeg;base64,"), visual: !!n, forceVisual: !!n && !!x }; A1.addMessage(g, o); const f = A1.getChat(o); H && (f.messages = f.messages.filter(D => D.id !== m ? (A1.removeMessage(D.id, o), !1) : !0)); const k = j2(), u = new J6.SSE(`${M2.AI_BASE_URL}/answer`, { payload: JSON.stringify({ chat: f.messages.filter(D => !D.clientOnly), id: o }), headers: { "content-type": "application/json", Authorization: Z1.getSnapshot().token }, start: !1 }); A1.addMessage({ id: k, message: "", type: "ss", clientOnly: !1, streaming: !0, isLoading: !0 }, o), u.addEventListener("ocr_result", D => { const V = JSON.parse(D.data).message; A1.addMessage({ ...g, message: F3(V), isLoading: !1 }, o) }); let y = ""; u.addEventListener("work", async D => { var G; const V = JSON.parse(D.data).message, Q = (G = A1.getChat(o)) == null ? void 0 : G.messages.find(v => v.id === k); !Q || (Q.isLoading && (Q.isLoading = !1), Q.message += V, y += V, A1.addMessage(Q, o)) }); let M = ""; const d = j2(); u.addEventListener("answer", async D => { const V = JSON.parse(D.data).message; M += V, M.length > 200 && A1.addMessage({ id: d, message: "", type: "ss", clientOnly: !0, streaming: !0, isLoading: !0 }, o) }), u.addEventListener("tickets_update", D => { const V = JSON.parse(D.data); typeof V.message == "number" && (w2.saveTickets(V.message), w2.setShowTicketBanner(!0)) }), u.addEventListener("ocr_error", () => { y = "error", A1.removeMessage(m, o), A1.addMessage({ id: k, message: x9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !0 }, o) }), u.addEventListener("change_type", D => { JSON.parse(D.data).blocked && b3.setVisionBlocked(Date.now()), y = "error" }), u.addEventListener("answer_error", () => { y = "error", A1.removeMessage(m, o), A1.addMessage({ id: k, message: h9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !0 }, o) }), u.addEventListener("trial_limit", () => { y = "error", A1.removeMessage(m, o), Z1.setHitTrialLimit(!0) }), u.addEventListener("custom_error", D => { const V = JSON.parse(D.data); y = "error", A1.addMessage({ id: k, message: V.message, streaming: !!V.s, clientOnly: V.c !== !1, type: "ss", isLoading: !1 }, o) }), u.addEventListener("error", D => { D.data === "Unauthorized" && Z1.clear(), y = "error", A1.removeMessage(m, o), A1.addMessage({ id: k, message: L9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !1 }, o) }), u.addEventListener("readystatechange", D => { var V; if (D.readyState === 2) { const Q = y.toLowerCase().replace(/\s\s+/g, " "), G = M.toLowerCase().replace(/\s\s+/g, " "); !y && M.length < 200 ? A1.addMessage({ id: k, message: m9, clientOnly: !0, isLoading: !1, type: "ss", streaming: !0 }, o) : M.length < 200 ? A1.setAnswer(F3(M), o) : Q.includes(G.toLowerCase()) ? G.includes(Q.toLowerCase()) && A1.removeMessage(d, o) : A1.addMessage({ id: d, message: F3(M), type: "ss", clientOnly: !!y, streaming: !0, isLoading: !1 }, o), y !== "error" && A1.lockAnswer(o); const v = (V = A1.getChat(o)) == null ? void 0 : V.messages.find(P => P.id === k); v && (v.message = F3(v.message), A1.addMessage(v, o)), setTimeout(() => { A1.stopStreaming(d, o), A1.stopStreaming(k, o) }, 100), L(1) } }), u.stream() }) }),Network destination for the selected text
- api.smartsolve.ai
Receives the SmartSolve answer request that includes the selected text chat message.
What it can do
Permissions this extension asks for, as declared in version 1.0.11. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
<all_urls>
Store data in your browser
storage
Run its own code inside the pages you visit
scripting
Add items to the right-click menu
contextMenus
Where it sends data
Destinations our analysis observed SmartSolve - AI Homework Tutor contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- api.smartsolve.ai
SmartSolve - AI Homework Tutor sends data to api.smartsolve.ai. No other extension we have analyzed sends data here.