Is StayFocusd – Website Blocker & Focus Timer & Shorts Blocker safe?

High risk

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.…

ST Pulsev4.6.14Chrome Web Store
75Risk
Who publishes it

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.

Publisher
ST Pulse
Declared legal entity
Sensor Tower
Registered address
275 Battery St #800, San Francisco, CA 94111-3364, US
Registered contact
Sensor Tower, Inc.

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.

app.stayfresh.email
Also called by 3 other listings, including StayFree - Web Analytics & Time Tracker
stayfreeapps.com
Also called by 3 other listings, including StayFree - Web Analytics & Time Tracker
purchases.stayfreeapps.com
Also called by 4 other listings, including StayFree - Web Analytics & Time Tracker
sensortower.com
Also called by 5 other listings, including Luna Adblock for Youtube & Websites

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You browse the web with StayFocusd installed and accept its terms of service.

The extension did this

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.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://api-pm.stayfreeapps.com/Ajax0001/IPD
200 OK (14 such requests observed across 2 dynamic analysis runs after ToS acceptance)
Headers
User-AgentMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Typeapplication/octet-stream
Body
<binary protobuf payload, ~28-33 bytes — not human-readable>
03EvidenceFIELD TABLE
Data known to be included in session uploads (from source code and SDK internals):
FieldValueWhy it matters
Your install ID
vw7asxlnvaqsjA persistent identifier unique to your extension install. Ties every session upload to you across browser restarts.
Session duration
847 secondsHow long your browsing session lasted.
Session timestamp
1744640000 (Unix epoch)When the browsing session started and ended.
Browser and OS
Chrome / LinuxWhich browser (Chrome) and operating system you are using.
Birth year (if provided)
1992If you entered your birth year in StayFocusd settings, it is included in every upload.
Panel partner ID
33Hardcoded identifier (33) marking these uploads as belonging to the SensorTower/StayFree panel research programme.
04EvidenceCODE COMPARE
The code that does this

The endpoint constants and Ko() ad-finder setup from background.js:

What it actually does
// 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));
}
05EvidenceCODE COMPARE
The code that does this

The SDK upload payload assembly (from ad-finder.js):

What it actually does
// 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
  };
}
06EvidenceARTIFACT
Reproduce it yourself

Intercepts StayFocusd's Ajax0001/IPD upload in Chrome DevTools and decodes the LZ-String compressed payload so you can read what was sent.

RequiresNode.js 16+npm install lz-string
decode-ipd-payload.js · js
// 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);
}
How to run it
  1. 1
    Capture a POST to api-pm.stayfreeapps.com/Ajax0001/IPD in Chrome DevTools.
  2. 2
    Copy the request body: right-click → Copy → Copy as base
  3. 3
    6
  4. 4
  5. 5
    Run: node decode-ipd-payload.js <paste-base64-here>
07EvidenceTHIRD PARTY LIST
Where session data is sent:
  • 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.

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You visit any web page while StayFocusd is installed.

The extension did this

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.

02EvidenceFIELD TABLE
What the extension collects per page visit:
FieldValueWhy it matters
Website you visited
amazon.com/dp/B0CX4KMQN3The exact hostname and path of every page you open.
Time spent on that page
142How many seconds you spent on the page before navigating away.
Where you came from
google.com/search?q=wireless+headphonesThe page you visited immediately before, builds a chain of your browsing activity.
UTM tracking tags
utm_source=newsletter, utm_campaign=spring_saleMarketing campaign tags from the URL, e.g. which ad or newsletter sent you to the page.
Ad network
googleWhich advertising network was associated with the page visit, if any.
Your install ID
vw7asxlnvaqsjA persistent identifier unique to your extension install. Ties every record to you across sessions.
Browser and OS
Chrome / WindowsWhich browser and operating system you use.
03EvidenceTEMPORAL PATTERN
When this fires
Every 5 minutes

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.

04EvidenceCODE COMPARE
The code that does this

The upload configuration from background.js:

What it actually does
// 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 };
}
05EvidenceCODE COMPARE
The code that does this

The page views upload function from the SensorTower SDK chunk (_virtual_wxt-plugins-DMpaGf42.js):

What it actually does
// 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);
06EvidenceTHIRD PARTY LIST
Where your browsing history ends up:
  • 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.

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-522
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You start Google sign-in from the StayFocusd options page.

The flow is tied to the extension's email-cleaner feature.

The extension did this

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.

02EvidenceFIELD TABLE
OAuth and token fields handled by the extension
FieldValueWhy it matters
Gmail mailbox permission
https://www.googleapis.com/auth/gmail.readonlyAllows the sign-in grant to read Gmail messages for the signed-in Google account.
Google OAuth client
863918784766-pafcg7sntsktkeen0m3ujcjkrqp9oh2c.apps.googleusercontent.comIdentifies 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 responseCan authorize Gmail API access for the signed-in account until revoked or expired.
Iframe token message
method=saveGoogleTokens; origin=https://app.stayfresh.emailSends the saved Google tokens from the extension page into the email-cleaner iframe.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://purchases.stayfreeapps.com/api/v2/auth/token
04EvidenceCODE COMPARE
The code that does this

OAuth scope, token exchange, storage, and iframe forwarding

What it actually does
Readable token exchange and local storagechunks/create-base-app-BB3ZXQy4.js
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";
Readable Google OAuth launchchunks/create-base-app-BB3ZXQy4.js
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
  }
}
Readable iframe token messagechunks/email-cleaner-D7ynmxUX.js
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)
}
05EvidenceTHIRD PARTY LIST
External services receiving the OAuth flow or token message
  • 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

Our write-ups

Updated 30 September 2026laankejkbhbdhmipfmgcngdelahlfoji