diff options
| author | Adam Soltys <[email protected]> | 2026-11-03 18:19:14 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-11-03 18:19:14 -0700 |
| commit | 08821882bc323c3411940f6150b4a731c1833c49 (patch) | |
| tree | 61244060215f248f24f7ef020d277cf9c4c2f3bc /src/notify-text.js | |
| parent | 7e8f2b375aebd3809b6fe9c474c6a36b392163f2 (diff) | |
| download | coinosv3-08821882bc323c3411940f6150b4a731c1833c49.tar.gz coinosv3-08821882bc323c3411940f6150b4a731c1833c49.zip | |
The wallet's push notifications are worded so Chrome's spam model lets them through. "Any way to stop classifying v3 notifications as spam? I've seen this a few times now" (Adam, 2026-11-03, phone: "Possible spam — Chrome detected possible spam from v3.coinos.io", Unsubscribe / Show notification): Chrome on Android scores every web notification's title and body with an on-device model before showing it, hides it behind that warning above 90 of 100, and — nine warnings in a week on a phone whose engagement with the site is under 50 — revokes the site's notification permission outright (Chromium kAutoRevokeSuspiciousNotification, enabled by default). The model is a text classifier trained on Gemini-made examples of scam notifications, and our lines were their very shape: "Payment received" 95, "Incoming payment / Someone is trying to send you money…" 99, "Payment request / An app is asking your wallet to pay…" 99, "alice is calling / Voice call. Tap to answer." 99, "New message / You have a new private message." 92, "alice / sent you a message." 95, "New reply to your post / <text>" 98, "alice reacted ❤️ / to your post…" 97. Desktop Chrome runs the same model (without the warning) and reports each score to chrome://histograms, so the lines were measured rather than guessed — ~200 candidates on Chrome 156's model: "sats" reads as money without reading as a payment scam, "payment" doesn't; a sender's name in the body of a message or reaction line tips it over, "New chat" / "<name>" doesn't; any "reply" title is 96–99; a reply's own text is out of our hands (an @name or a link in it scores 97+), so it no longer rides along; "Calling" / "<name>" is 11–29 where every "<name> is calling" is 80–99. src/notify-text.js holds every line, with the measurements; the worker, the background responder and the renewal warning draw from it. The new lines score 8–46 (the shortcode reaction 67). tools/notif-spam-score.js re-measures them in a throwaway Chrome (SystemNotifications off, so nothing reaches the desktop) and fails at 90 — run it before changing any of them. On the phone that already warned, "Show notification" then "Always allow" marks the site trusted and stops both the warning and the revocation countHEADmaster
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
Diffstat (limited to 'src/notify-text.js')
| -rw-r--r-- | src/notify-text.js | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/src/notify-text.js b/src/notify-text.js new file mode 100644 index 0000000..a88ab14 --- /dev/null +++ b/src/notify-text.js @@ -0,0 +1,79 @@ +// The words of every notification the worker shows while the app is closed. +// +// Chrome on Android (and, quietly, on desktop) scores each web notification's +// title and body with an on-device "unwanted notification" model before it +// shows it, and above 90 of 100 the notification is replaced by a "Possible +// spam" warning — hidden until the user taps "Show notification", and nine +// such in a week revoke the site's permission outright on a phone that +// doesn't visit much (Chromium kAutoRevokeSuspiciousNotification). Our old +// wording scored 92–99 across the board: "Payment received", "Incoming +// payment", "Payment request", "alice is calling", "New message", "alice +// sent you a message" — the exact shape of the scam notifications the model +// was trained (on synthetic data) to catch. What it lets through is less a +// rule than a measurement; the scores here come from Chrome 156's model via +// tools/notif-spam-score.js, which re-scores SAMPLES in a throwaway Chrome. +// Measured 2026-11-03: "sats" reads as money without reading as a payment +// scam (9–14), "payment" does not (95–97); a person's NAME in the body of a +// message or reaction line pushes it over (80–99), "New chat" / "<name>" +// does not (8–32); "reply" in any title is 96–99, "Someone replied" / +// "Open coinos to see it." is 13; a reply's TEXT is uncontrollable (an +// "@name" or a link in it scores 97+), so none rides along; "Calling" / +// "<name>" is 11–29 where every "<name> is calling" shape is 80–99. +// Re-run the scorer before changing any of these. + +export const OPEN = 'Open coinos to see it.'; + +function sats(n) { + const v = Number(n); + return v > 0 ? `+${v.toLocaleString()} sats. ${OPEN}` : OPEN; +} + +// A user-facing notification by reason. `d` is the push payload (amountSat, +// emoji) plus what the device adds (a sender's name). +export function notifyText(reason, d = {}) { + switch (reason) { + case 'payment': return ['Sats received', sats(d.amountSat)]; + case 'zap': return ['Zap received', sats(d.amountSat)]; + case 'dm': return ['New chat', d.name || 'Someone wrote to you']; + case 'chat': return ['New chat activity', 'There are new messages in your communities.']; + case 'mention': return [d.reply ? 'Someone replied' : 'You were mentioned', OPEN]; + case 'reaction': { + // a unicode emoji in the title is fine (17); a :shortcode: is not (85) + const e = d.emoji && !/^:[\w-]+:$/.test(d.emoji) ? ' ' + d.emoji : ''; + return ['New reaction' + e, `to your post. ${OPEN}`]; + } + case 'call': return ['Calling', d.name || 'Someone']; + // the wallet was asked for an invoice while closed (someone paying its name) + case 'incoming': return ['Someone is sending sats', 'Open coinos to receive them.']; + // a connected app asked the closed wallet to pay + case 'spend': return ['Spend request', 'A connected app asked to spend sats. Open coinos to approve.']; + default: return notifyText('chat'); + } +} + +// What tools/notif-spam-score.js scores: one line per shape a user can see. +export const SAMPLES = [ + notifyText('payment', { amountSat: 1000 }), + notifyText('payment', { amountSat: 21 }), + notifyText('payment'), + notifyText('zap', { amountSat: 1000 }), + notifyText('zap'), + notifyText('dm', { name: 'alice' }), + notifyText('dm', { name: 'Bitcoin Bob' }), + notifyText('dm', { name: 'Jarryd' }), + notifyText('dm'), + notifyText('chat'), + notifyText('mention'), + notifyText('mention', { reply: true }), + notifyText('reaction', { emoji: '❤️' }), + notifyText('reaction', { emoji: '🤙' }), + notifyText('reaction', { emoji: ':pepe:' }), + notifyText('call', { name: 'alice' }), + notifyText('call', { name: 'Bitcoin Bob' }), + notifyText('call', { name: 'Alice Caller' }), + notifyText('call'), + notifyText('incoming'), + notifyText('spend'), + ['Payment sent', '-1,000 sats via Damus'], + ['Spending balance', 'Receive or spend sats before 11/20/2026 to keep it alive.'], +]; |
