// 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" / "" // 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" / // "" is 11–29 where every " 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.'], ];