Is StayFocusd – Website Blocker & Focus Timer & Shorts Blocker safe?
StayFocusd is high risk. StayFocusd uploads session data to api-pm.stayfreeapps.com/Ajax0001/IPD after you browse. Confirmed: 14 POSTs in two runs. Each ~28-33 byte payload carries install ID, session duration, device type, timezone. Run by SensorTower/StayFree.…
Who publishes itSensor Tower - 5 other listings from the same operator, 3 of them carrying a finding
Sensor Tower - 5 other listings from the same operator, 3 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 200k+ users between them. 1 of them carries a finding.
Same operator - 4 listings
Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.
Shared hosts - 4 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Session Data Uploaded to SensorTower Panel via Ajax0001/IPD
StayFocusd uploads session data to api-pm.stayfreeapps.com/Ajax0001/IPD after you browse.
Confirmed: 14 POSTs in two runs.
Each ~28-33 byte payload carries install ID, session duration, device type, timezone.
Run by SensorTower/StayFree.
You browse the web with StayFocusd installed and accept its terms of service.
StayFocusd automatically sends your session data to a SensorTower analytics server, 14 POST requests were captured in a single analysis session.
Each request uses the opaque Ajax0001/IPD endpoint with a binary (likely protobuf) body that cannot be read in plain text.
| User-Agent | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 |
| Content-Type | application/octet-stream |
<binary protobuf payload, ~28-33 bytes — not human-readable>
| Field | Value | Why it matters | |
|---|---|---|---|
Your install ID | vw7asxlnvaqsj | A persistent identifier unique to your extension install. Ties every session upload to you across browser restarts. | |
Session duration | 847 seconds | How long your browsing session lasted. | |
Session timestamp | 1744640000 (Unix epoch) | When the browsing session started and ended. | |
Browser and OS | Chrome / Linux | Which browser (Chrome) and operating system you are using. | |
Birth year (if provided) | 1992 | If you entered your birth year in StayFocusd settings, it is included in every upload. | |
Panel partner ID | 33 | Hardcoded identifier (33) marking these uploads as belonging to the SensorTower/StayFree panel research programme. |
The endpoint constants and Ko() ad-finder setup from background.js:
// Hardcoded SensorTower panel endpoints
const SESSIONS_UPLOAD_URL = "https://api-pm.stayfreeapps.com/Ajax0001/IPD";
const REMOTE_CONFIG_URL = "https://api-pm.stayfreeapps.com/Ajax0001/Config";
const PANEL_PARTNER_ID = 33; // SensorTower/StayFree panel ID
function setupAdFinderSDK() {
const settings = getUserSettings();
const { processAdFinderMessage } = createAdFinder({
async isEnabled() { return await settings.canUploadData(); },
getInstallId: () => getInstallId(),
extensionVersion: "4.5.2",
extensionId: "laankejkbhbdhmipfmgcngdelahlfoji",
crawlUploadUrl: SESSIONS_UPLOAD_URL, // Ajax0001/IPD
pmExternalConfigUrl: REMOTE_CONFIG_URL, // Ajax0001/Config
panelPartnerId: PANEL_PARTNER_ID,
logger: logger
});
// Forward ad-finder messages from content scripts
onMessage("@sensortower/ad-finder#MESSAGE", ({ data }) => processAdFinderMessage(data));
}The SDK upload payload assembly (from ad-finder.js):
// Injects panel metadata into every upload before compression
prepareData(rawPayload) {
return {
...rawPayload,
PartnerVersion: chrome.runtime.getManifest().version, // "4.5.2"
ExtensionId: chrome.runtime.id,
ExtensionVersion: this.config.extensionVersion,
UserAgent: navigator.userAgent,
BrowserLanguage: navigator.language,
BlankZys: null,
PanelPartnerId: this.config.panelPartnerId // 33
};
}Intercepts StayFocusd's Ajax0001/IPD upload in Chrome DevTools and decodes the LZ-String compressed payload so you can read what was sent.
// decode-ipd-payload.js
// Decodes a captured Ajax0001/IPD upload from StayFocusd.
// The payload is LZ-String compressed JSON posted as application/octet-stream.
//
// Install: npm i lz-string
// Usage: node decode-ipd-payload.js <base64-of-body>
const LZString = require('lz-string');
// Paste your captured binary body as base64 here, or pass as argv
const base64Body = process.argv[2] || '<paste-base64-body-here>';
try {
// Convert base64 to Uint8Array
const buf = Buffer.from(base64Body, 'base64');
const uint8 = new Uint8Array(buf);
// Decompress with LZ-String (decompressFromUint8Array)
const json = LZString.decompressFromUint8Array(uint8);
if (!json) {
console.error('Decompression failed — ensure you are using the correct LZ-String variant');
process.exit(1);
}
const obj = JSON.parse(json);
console.log('Decoded payload:');
console.log(JSON.stringify(obj, null, 2));
// Highlight key fields
console.log('\n--- Key identifiers ---');
if (obj.PanelPartnerId) console.log('PanelPartnerId:', obj.PanelPartnerId);
if (obj.ExtensionId) console.log('ExtensionId:', obj.ExtensionId);
if (obj.UserAgent) console.log('UserAgent:', obj.UserAgent);
} catch (err) {
console.error('Error decoding payload:', err.message);
process.exit(1);
}
- 1Capture a POST to api-pm.stayfreeapps.com/Ajax0001/IPD in Chrome DevTools.
- 2Copy the request body: right-click → Copy → Copy as base
- 36
- 4
- 5Run: node decode-ipd-payload.js <paste-base64-here>
- api-pm.stayfreeapps.com
SensorTower/StayFree panel analytics backend. Ajax0001/IPD receives compressed session and ad-crawl data; stayfreeapps.com is run by SensorTower, an app market intelligence firm.
Browsing History Uploaded Every 5 Minutes via SensorTower SDK
StayFocusd bundles a SensorTower/StayFree SDK that records every site you visit, hostname, path, time on page, referrer, and UTM tags, without notifying you.
Data batches and uploads every 5 minutes.
Two 7-minute tests didn't see it fire.
You visit any web page while StayFocusd is installed.
StayFocusd records the hostname, path, how long you stayed, and where you came from, then uploads a batch to a SensorTower analytics server every five minutes.
Data collection happens on every page, not just sites you have blocked or tracked in StayFocusd.
| Field | Value | Why it matters | |
|---|---|---|---|
Website you visited | amazon.com/dp/B0CX4KMQN3 | The exact hostname and path of every page you open. | |
Time spent on that page | 142 | How many seconds you spent on the page before navigating away. | |
Where you came from | google.com/search?q=wireless+headphones | The page you visited immediately before, builds a chain of your browsing activity. | |
UTM tracking tags | utm_source=newsletter, utm_campaign=spring_sale | Marketing campaign tags from the URL, e.g. which ad or newsletter sent you to the page. | |
Ad network | Which advertising network was associated with the page visit, if any. | ||
Your install ID | vw7asxlnvaqsj | A persistent identifier unique to your extension install. Ties every record to you across sessions. | |
Browser and OS | Chrome / Windows | Which browser and operating system you use. |
Page view data is stored locally in the browser's IndexedDB and flushed to the server every 5 minutes. During analysis, the upload fired on a 2-second interval when Chrome was running in headless mode, suggesting the interval is shortened for automated/testing environments.
The upload configuration from background.js:
// Upload interval: 5 minutes (or 2 seconds in headless Chrome)
const UPLOAD_INTERVAL_MS = 5 * 60 * 1000;
function setupUsageUploader() {
const db = getDatabase();
const settings = getUserSettings();
const isHeadless = navigator.userAgent.toLowerCase().includes('headlesschrome');
const { sessionMonitor, usageUploader } = createUploader({
db,
uploads: {
// Headless detection: use 2s interval in automated environments
uploadIntervalInMs: isHeadless ? 2000 : UPLOAD_INTERVAL_MS,
getInstallId: getInstallId,
api: getPanelApiClient(), // points to api-pm.stayfreeapps.com
getBirthYear: getBirthYear, // optional, from user settings
enabled: () => settings.canUploadData(),
getAppId: () => 'laankejkbhbdhmipfmgcngdelahlfoji',
pageViews: {
getUploadIgnoreList: () => getRemoteConfig().pageViewIgnoreList
}
},
getAdNetworks: () => getRemoteConfig().adNetworks ?? []
});
return { sessionMonitor, usageUploader };
}The page views upload function from the SensorTower SDK chunk (_virtual_wxt-plugins-DMpaGf42.js):
// Builds and sends the page-views payload
const parsedUA = parseUserAgent(navigator.userAgent);
const payload = {
app_id: params.appId, // "laankejkbhbdhmipfmgcngdelahlfoji"
install_id: params.installId, // persistent per-install UUID
time_zone: getTimezone(), // e.g. "GMT+01:00"
device_name: parsedUA.browser.name, // "Chrome"
device_type: parsedUA.os.name, // "Windows"
birth_year: params.birthYear, // optional, from user
websites: params.websites, // per-hostname/path duration records
diff_private_websites: params.diffPrivateWebsites
};
await apiClient.post('/v1/page_views/upload', payload);- api-pm.stayfreeapps.com
SensorTower/StayFree panel analytics backend. Receives full browsing duration records. stayfreeapps.com is operated by SensorTower, the app analytics firm that acquired StayFree.
- stayfocusd.st-panel-api.com
Alternate domain observed during dynamic analysis for the same StayFree panel API. Confirmed to receive query_params/upload traffic in practice.
Gmail OAuth Tokens Forwarded to StayFresh Iframe
StayFocusd's sign-in requests read-only Gmail access, exchanges the code with purchases.stayfreeapps.com, stores tokens, forwards both to an app.stayfresh.email iframe on the cleaner UI.
DA saw the OAuth popup; Google blocked on mismatch.
You start Google sign-in from the StayFocusd options page.
The flow is tied to the extension's email-cleaner feature.
The extension asks for read-only Gmail access, stores the returned tokens, and forwards them to a StayFresh iframe.
The forwarding happens when the email-cleaner iframe finishes loading.
| Field | Value | Why it matters | |
|---|---|---|---|
Gmail mailbox permission | https://www.googleapis.com/auth/gmail.readonly | Allows the sign-in grant to read Gmail messages for the signed-in Google account. | |
Google OAuth client | 863918784766-pafcg7sntsktkeen0m3ujcjkrqp9oh2c.apps.googleusercontent.com | Identifies the Google application that receives the sign-in grant. | |
Token exchange request | provider=google; app=stayfocusd; platform=web-extension; redirectUri=https://laankejkbhbdhmipfmgcngdelahlfoji.chromiumapp.org/ | Sends the returned Google authorization code to the StayFocusd purchase API for token exchange. | |
Returned Google tokens | accessToken and refreshToken from the token response | Can authorize Gmail API access for the signed-in account until revoked or expired. | |
Iframe token message | method=saveGoogleTokens; origin=https://app.stayfresh.email | Sends the saved Google tokens from the extension page into the email-cleaner iframe. |
OAuth scope, token exchange, storage, and iframe forwarding
async function v() {
a.value = true;
try {
const {
code: u,
redirectUri: m
} = await t.performLogin(), w = await t.getInstallId(), _ = await t.getBirthYear(), {
data: R,
error: I
} = await e.POST("/api/v2/auth/token", {
body: {
code: u,
provider: "google",
app: "stayfocusd",
platform: r,
redirectUri: m,
installId: w,
optIn: {
birthYear: _
}
}
});
if (I) throw new Error(I.message ?? "Token exchange failed");
o.value = R;
const {
data: x,
error: Q
} = await e.GET("/api/v2/auth/session");
if (Q) throw new Error("Failed to fetch session info");
s.value = x, await n.setSession(x), await n.setCookie(R), b()
} finally {
a.value = false
}
}
const pr = "863918784766-pafcg7sntsktkeen0m3ujcjkrqp9oh2c.apps.googleusercontent.com",
gr = ["https://www.googleapis.com/auth/gmail.readonly"],
mr = "https://purchases.stayfreeapps.com",
Fr = "https://app.stayfresh.email";async performLogin() {
const e = Me.identity.getRedirectURL(),
n = new URL("https://accounts.google.com/o/oauth2/v2/auth");
n.searchParams.set("client_id", pr), n.searchParams.set("redirect_uri", e), n.searchParams.set("response_type", "code"), n.searchParams.set("scope", gr.join(" ")), n.searchParams.set("access_type", "offline"), n.searchParams.set("prompt", "consent");
const r = await Me.identity.launchWebAuthFlow({
url: n.toString(),
interactive: true
});
if (!r) throw new Error("No response from auth flow");
const s = new URL(r).searchParams.get("code");
if (!s) throw new Error("No authorization code in response");
return {
code: s,
redirectUri: e
}
}function d() {
setTimeout(() => {
x(), r.value && g({
method: "saveGoogleTokens",
args: {
accessToken: r.value?.accessToken,
refreshToken: r.value?.refreshToken
}
}), n.value = false
}, 2 * B.Second)
}
function x() {
g({
method: "setLocale",
args: {
languageCode: f.value
}
})
}
function g(t) {
const a = JSON.stringify(t);
s.value?.contentWindow?.postMessage(a, l)
}- accounts.google.com
Google OAuth authorization page opened by the extension.
- purchases.stayfreeapps.com
Receives the authorization code and returns the token response for StayFocusd.
- app.stayfresh.email
Iframe origin that receives the saved Google access and refresh tokens.
+3 more findings not shown