Skip to content

Spike: notify when a dispenser sells or an order matches - #299

Draft
droplister wants to merge 1 commit into
mainfrom
spike/notifications
Draft

Spike: notify when a dispenser sells or an order matches#299
droplister wants to merge 1 commit into
mainfrom
spike/notifications

Conversation

@droplister

Copy link
Copy Markdown
Member

A spike, not a proposal to ship. Off by default, behind an optional permission, so it can be lived with locally before anyone decides whether it belongs.

To try it: load the unpacked build, Settings → Advanced → Notifications. Chrome asks once. Turning it off deletes the stored addresses.

Optional permission, deliberately

A new required permission disables the extension on update until every existing user re-consents — an unreasonable toll for a feature most of them will never switch on. Declared as optional_permissions it costs nothing until someone enables it, at which point chrome.permissions.request() asks once. That call needs a user gesture, so it happens on the toggle click rather than in the poller.

I could not test the Chrome Web Store's review treatment of this from here; that is worth confirming before it goes anywhere near a release.

What keeps it off the node's back

This was the part worth getting right — a background poller is exactly the thing that quietly becomes a nuisance.

One request per poll, all addresses /v2/addresses/events takes a comma-separated list and filters by event name server-side. Cost is flat in address count, not a call per address per event type.
Zero traffic when off No request at all if the feature is off, the permission is missing, or there are no addresses. Most installs never make one.
Exponential backoff A failing node skips 1, 2, 4… ticks rather than being retried every time.
Capped address list The list travels in the query string; an unbounded wallet would build an unbounded URL.
Capped bursts Five notifications and a summary. A wallet closed over a busy weekend should not produce ninety alerts.
Slow tick Two minutes. Blocks are ~10, so this is already several redundant polls per block; faster buys latency nobody asked for at a multiple of the traffic.

The first draft did two calls per address per block and gated on block height. Finding the plural-address events route made it one call regardless of address count, and made the block-height check pointless — it was itself a request, so dropping it removed one.

The rules that make it bearable rather than noisy

Adopt silently on first sight. Enabling this must not replay last year. Same when the address set changes: a watermark taken against one set says nothing about an address newly added to it, so it is dropped and re-adopted rather than skipping that address's history forever.

Direction matters. A dispense names both sides — source is the seller, destination the buyer. Reading it the wrong way round would tell a buyer their dispenser sold something they had just bought.

The trade I want you to look at

Addresses are mirrored into extension-local storage, unencrypted. Settings live in the keychain and read as defaults while locked, and the wallet auto-locks after five minutes — a poller that could only read settings would be asleep almost exactly when notifications are worth having.

Addresses are public data, so this is not a key leak, but it does write them somewhere they were not before. The toggle's copy says so plainly. If you would rather not, the alternative is notifications that only work while unlocked, which is close to not having them.

Structure

Deciding what is worth announcing is pure and tested without a browser (core/notifications/detect.ts, 14 tests). The service owns storage, backoff and the Chrome calls (services/notificationService.ts, 14 tests). The layering invariant caught two of my comments mentioning chrome. inside core/ — reworded, since both modules are genuinely runtime-free.

Verification

4414 unit tests pass; tsc, biome, oxlint clean. Build produces "optional_permissions":["notifications"] in the manifest. Not manually exercised in a browser — that is what this branch is for.

Known gaps

  • Only DISPENSE and ORDER_MATCH. The BTC-leg match with a payment deadline is the one with real money at stake and deserves its own treatment.
  • Click routing is in-memory, so a click after the worker restarts opens the wallet without a deep link.
  • Notifications appear as native macOS notifications there, which changes how priority and richer templates behave; basic is used throughout to avoid depending on any of it.

https://claude.ai/code/session_01QJS9Bj6uAMoYPATvfr6GZ1

A spike, behind a setting that is off by default and an optional permission, so
it can be lived with locally before anyone decides whether it belongs.

The permission is optional rather than required on purpose. A new *required*
permission disables the extension on update until every existing user
re-consents, which is an unreasonable toll for a feature most will never switch
on. Declared as optional it costs nothing until someone turns it on, at which
point chrome.permissions.request() asks once -- from the click, since that call
needs a user gesture.

Addresses are mirrored into extension-local storage because settings live in the
keychain and read as defaults while locked, and the wallet auto-locks after five
minutes. A poller that could only read settings would be asleep exactly when
notifications are worth having. That is a real trade and the toggle's copy says
so: addresses are public data, but this writes them somewhere unencrypted they
were not before. Switching it off deletes them.

What keeps this off the node's back, since a background poller is the part that
could quietly become a nuisance:

  - One request per poll for every watched address at once. Core's
    /v2/addresses/events takes a comma-separated list and filters by event name
    server-side, so cost is flat in the number of addresses rather than a call
    per address per event type.
  - Nothing is requested at all when the feature is off, the permission is
    missing, or there are no addresses. For an install that never enables it the
    steady-state traffic is zero.
  - A failing node backs the poller off exponentially instead of retrying every
    tick.
  - The address list is capped, so one enormous wallet cannot turn one request
    into a huge one.
  - A burst is capped at five notifications and a summary. A wallet closed over a
    busy weekend should not produce ninety alerts.

The watermark rule is what makes it bearable rather than noisy: on first sight,
adopt the current position silently. Enabling this should not replay last year.
The same applies when the address set changes -- a watermark taken against one
set says nothing about an address newly added to it, so it is dropped and
re-adopted rather than skipping that address's history forever.

Direction matters for a dispense, which names both sides: source is the seller
and destination is the buyer. Reading it the wrong way round would tell a buyer
their dispenser sold something they had in fact just bought.

The deciding is pure and tested without a browser (core/notifications/detect.ts);
the service owns storage, backoff and the Chrome calls.

Claude-Session: https://claude.ai/code/session_01QJS9Bj6uAMoYPATvfr6GZ1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant