<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinosv3.git/nwcpush, branch fdroid-v1.6</title>
<subtitle>coinos v3 — bitcoin, Ark, Lightning and nostr wallet (web + Android)</subtitle>
<id>https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.6</id>
<link rel='self' href='https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.6'/>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/'/>
<updated>2026-09-15T00:42:04Z</updated>
<entry>
<title>A reply or a mention on nostr can buzz your phone.</title>
<updated>2026-09-15T00:42:04Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-15T00:42:04Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=5a0f70fcedd7a02a159de1819082981a6fd6244f'/>
<id>urn:sha1:5a0f70fcedd7a02a159de1819082981a6fd6244f</id>
<content type='text'>
The push notifier now watches kind 1 for the user's pubkey beside DMs and
zap receipts, and pushes who, what, and an excerpt. The worker shows
"New reply to your post" or "You were mentioned" with the text, and a tap
lands in the Notifications list — as ?open=notifs when a window has to
be opened, or as a worker message when one already is. It has its own
switch, "Replies and mentions on nostr", in Settings → Notifications,
on by default like payments and DMs.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
</content>
</entry>
<entry>
<title>One device pays a zap, whichever of them heard it first.</title>
<updated>2026-09-14T16:06:02Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-14T16:06:02Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=671d2a96474f59ca9234c876ce6604d03de95727'/>
<id>urn:sha1:671d2a96474f59ca9234c876ce6604d03de95727</id>
<content type='text'>
Every awake device of a wallet hears a wallet-connect pay request within
the same second, and the per-tab lock only reaches one browser profile:
two profiles on one laptop each locked an HTLC for the same 21-sat zap.
The push notifier now hands each pay request to the first device that
claims it; a device that loses the claim stands down silently, and a
device that cannot reach the notifier answers as before so nobody goes
quiet over a hiccup. The worker path claims the same way.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
</content>
</entry>
<entry>
<title>UnifiedPush: reach a phone that has no Google in it.</title>
<updated>2026-09-12T17:05:10Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-12T17:05:10Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=a846211262f37238c001e5210938749294f8db6a'/>
<id>urn:sha1:a846211262f37238c001e5210938749294f8db6a</id>
<content type='text'>
Web push on Android is Firebase's, so a phone without Google Play Services —
GrapheneOS, CalyxOS, any de-Googled Android — cannot be woken by a web app at
all. UnifiedPush is the way that doesn't involve Google: the user runs a
distributor (ntfy and friends), apps ask it for an endpoint, and whoever
wants to reach the device POSTs to that endpoint.

THE WRAPPER speaks the protocol directly — four broadcasts each way, so it
pulls in no library; an APK this small has no business growing a Kotlin
runtime for eight intents. It asks the distributor on every launch (the call
is idempotent, and it's how a distributor installed later gets noticed),
raises a notification when a message arrives, and acknowledges it so the
distributor stops resending.

THE ENDPOINT reaches the web app on the launch URL as ?up=, the same road
payment intents already travel — because the distributor talks to the app and
only the web app knows which pubkeys are worth watching. It's registered with
the notifier in place of a browser subscription, for notifications and for
NWC alike, and needs neither permission nor push service.

THE NOTIFIER sends to either kind: one deliver() that POSTs the payload to a
UnifiedPush endpoint, or hands it to web-push as before. Same payloads, same
404/410 pruning. The endpoint is the only secret there, so it's never logged
in full.

Verified: a POST to a live ntfy endpoint delivers the payload byte for byte,
so the app parses exactly what the notifier sent; the APK builds, carries the
receiver, and is signed with the certificate assetlinks already pins (v1.6).
tools/unifiedpush-test.js drives a browser with no push service and checks
the endpoint is taken off the URL, kept, and registered.

Honest about the limit: a UnifiedPush message wakes the ANDROID app, not
Chrome's service worker, and the wallet's keys live in the browser's storage
where nothing here can reach them. So a zap request arrives as a notification
you tap, and the wallet answers when it opens — one tap instead of none.
Automatic answering while closed still needs a browser with a push service.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Heal dead push subscriptions from a VAPID key rotation — the reason notifications went silent. registerPush reused whatever getSubscription() returned and only ever subscribed fresh when there was none; a subscription made against an OLD VAPID key can never receive a push (the notifier's send fails with a permanent 403, which was never pruned, and the client kept re-registering the same dead endpoint). Now the client fetches the current VAPID key first and, when an existing subscription's applicationServerKey doesn't match, unsubscribes and re-subscribes against the current key — so a device self-heals on next open. And the notifier prunes 403 endpoints alongside 404/410. Deployed nwcpush on cs</title>
<updated>2026-09-06T04:47:30Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-06T04:47:30Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=7e96dc4d4292b29156ec6b9b9c7009f8c60106f1'/>
<id>urn:sha1:7e96dc4d4292b29156ec6b9b9c7009f8c60106f1</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
</content>
</entry>
<entry>
<title>Payment push notifications are scoped to their network, so a mainnet payment stops opening the staging PWA. The same seed derives the same nostr pubkey on mainnet and staging, so both PWAs registered push under one pubkey and the notifier — matching only by pubkey — fanned every mainnet payment out to the staging subscription too; tapping it opened staging.coinos.io. Now the client stamps its network on the push registration (pushWatch net: getNetwork()), the registrar declares its own network on the /notify call (CFG.forwarder.network), and nwcpush delivers a payment only to registrations whose net matches — with a rollout-safe fallback: a device that hasn't re-registered yet (net=null) still gets the push, and a caller that omits net still reaches everyone, so nobody goes dark mid-deploy. Deployed names + nwcpush on cs</title>
<updated>2026-09-06T04:38:09Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-06T04:38:09Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=ec1d6b8c6da0f9e4e169f4e8f95b88133207b741'/>
<id>urn:sha1:ec1d6b8c6da0f9e4e169f4e8f95b88133207b741</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
</content>
</entry>
<entry>
<title>Settings grows a Notifications page — per-category switches the notifier honours server-side — and the status bar gets a real coinos badge instead of a black blob</title>
<updated>2026-08-14T08:19:44Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-14T08:19:44Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=7a7b2c61fe8394844fa306debb84917454ade7b0'/>
<id>urn:sha1:7a7b2c61fe8394844fa306debb84917454ade7b0</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
</content>
</entry>
<entry>
<title>Only people you know get to buzz your phone</title>
<updated>2026-08-04T05:07:13Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-04T05:07:13Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3'/>
<id>urn:sha1:1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3</id>
<content type='text'>
A NIP-17 wrap is signed by a throwaway key with the sender sealed under
two layers of nip44, so no server can filter DMs by who sent them. The
notifier now ships the wrap inside the push payload and the service
worker opens it locally: a message from someone you follow arrives with
their name on it, a stranger's is dropped, and your own sent-copy — every
DM is wrapped to the sender too — no longer notifies you about what you
just typed.

That needs the nostr identity key in the worker's IndexedDB, which
dm-inbox.js states plainly: for a seed-derived identity it is a second
copy of a key localStorage's seed already yields, and it exists only to
keep strangers out. A remote signer never hands its key over, so those
wallets store no secret and keep getting unfiltered notifications rather
than silence.

Silence is always the riskier failure here, so the rule leans the other
way whenever it isn't sure: a wrap we can't open notifies, an account
with no contact list published notifies (otherwise a new user's welcome
message would vanish), and anyone you already have a thread with counts
as known whether or not you follow them. The server-side DM cooldown
drops to a second — with the device discarding pushes, a throttle there
would let a discarded one swallow the message behind it.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
</content>
</entry>
<entry>
<title>Push notifications for payments and messages; offline Spending notice</title>
<updated>2026-08-03T05:41:39Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-03T05:41:39Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=d60697c8d1cc0b862d7488be4cf8ee21cafdfd1d'/>
<id>urn:sha1:d60697c8d1cc0b862d7488be4cf8ee21cafdfd1d</id>
<content type='text'>
The nwcpush notifier grows a second registration kind alongside NWC:
a notify watch of what it can see without keys — kind-1059 wraps
p-tagged at the user (DMs, direct invites), zap receipts (9735/9737),
and wraps authored by the user's community channel keys (chat). It
pushes a typed nudge, throttled per device (chat bursts collapse); the
service worker shows a generic notification unless a window is visible,
and tapping opens the app. The names registrar reports settled
lightning-address receives server-to-server (token-gated /notify), so
the common payment path notifies even with no nostr event in sight.

The app registers silently whenever permission is already granted and
keeps the watch current as communities change; a dismissible card on
the chat home offers to enable it otherwise. Ark zap receipts (9737)
join the relay whitelist so the payment watch can see them.

Also: the offline Spending receive pane now says you're offline and
offers a one-tap switch to Savings instead of spinning forever.

Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
</content>
</entry>
<entry>
<title>Zappable offer codes and per-name Lightning offers</title>
<updated>2026-08-02T03:46:12Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-02T03:46:12Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=ed1d996f9dbd6cedc18ec293daba0cfcd745ae2b'/>
<id>urn:sha1:ed1d996f9dbd6cedc18ec293daba0cfcd745ae2b</id>
<content type='text'>
CLINK offers (noffer): every wallet gets a static offer keypair (synced
across devices, first-minted wins) answering kind-21001 invoice
requests — from open tabs and from the service worker while closed,
which mints against the mirrored state (the record now carries chain-4
and chain-5 key windows) and raises an open-to-receive notification.
Zap requests ride along and their kind-9735 receipts publish once the
payment is claimed. The code is shown in the payment-address card for
pasting into a Nostr profile; the notifier watches and relays kind
21001 alongside NWC.

Payment names also gain a Lightning offer: the registrar creates a
BOLT 12 offer per name on the node and appends lno= to the DNS record;
a settle-forwarder pushes each received payment on to the name's ark
destination from a small float wallet (custody window: seconds between
Lightning settle and the ark send).

Name resolution now runs full client-side DNSSEC validation
(src/dnssec.js): binary DoH, root-anchor chain walk, RSA + ECDSA via
WebCrypto — the resolver is untrusted transport, verified against live
signed zones including the canonical BIP-353 test name.

Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Background NWC: the worker now answers — and pays — from the pouch</title>
<updated>2026-08-02T02:02:25Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-02T02:02:25Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=b73bb9ee51b9be33bc1859fd56d3e11905c3d456'/>
<id>urn:sha1:b73bb9ee51b9be33bc1859fd56d3e11905c3d456</id>
<content type='text'>
The no-tap half of background wallet-connect. The push payload carries
the (encrypted, client-signed) request event; a bundled service-worker
responder (src/sw-nwc.js -&gt; nwc-respond.js) finds the matching pouch in
IndexedDB, decrypts with the connection's service key, and answers
get_info/get_balance/list_transactions from pouch state. pay_invoice
pays through the swap bridge with an ArkManager running over the
pouch's chain-6 key window (one HTLC lock, one HTTP swap), enforcing
per-payment and daily budgets from the pouch's own spend log, checking
the notifier's /answered endpoint so it never double-pays or shouts
over an open device, and persisting the swap before handing it off so
the app can refund a crashed flight. Anything it can't handle falls
back to the wake-the-user notification.

The notifier grows /publish (verified 23195s only, rate-limited) as the
worker's reply pipe and /answered for race avoidance. The app gains
pouch funding (mailbox-less arkoor self-send whose recipient bytes we
keep), emptying, spend/budget reconciliation on open, and refund
recovery with the pouch's own keys.

Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
