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.
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.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
Visible tab image | data: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 token | Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature (illustrative) | The request is tied to the signed-in SmartSolve account used by the extension. |
| content-type | application/json |
| Authorization | Z1.getSnapshot().token |
Snapshot capture and answer submission path
chrome.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
}
});function _(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]
})
}function 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"]
},M2 = {
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()
})
}),- 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.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
Tab visibility changes | visibilitychange | A site may not receive the normal signal when your tab becomes visible or not visible. | |
WebKit visibility changes | webkitvisibilitychange | A site using the browser-specific visibility event may not receive that signal either. | |
Top-window blur | blur on the top window | A site may not receive the normal signal when the page window loses focus. | |
Top-window focus | focus on the top window | A site may not receive the normal signal when the page window regains focus. |
Immediate page-context event interception
chrome.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.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
Selected page text | Solve for x in 2x + 3 = 11 (illustrative) | The highlighted text can include page content you selected before invoking SmartSolve. | |
SmartSolve session token | Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature (illustrative) | The request is tied to the signed-in SmartSolve account used by the extension. |
| content-type | application/json |
| Authorization | Z1.getSnapshot().token |
Selected-text capture and answer submission path
chrome.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)
})
}function 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"]
},chrome.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)
})
}
});M2 = {
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()
})
}),- api.smartsolve.ai
Receives the SmartSolve answer request that includes the selected text chat message.