Garbage collector for stale products from deleted/inactive merchants #3

Open
opened 2026-05-03 13:56:46 +00:00 by padreug · 0 comments
Owner

Problem

When an LNbits user deletes their account, the corresponding merchant record and their stalls/products may be cleaned up locally, but the kind 30017 (stall) and kind 30018 (product) events they previously published remain on Nostr relays indefinitely. Since these are parameterized replaceable events (NIP-33), they don't expire and their authoring pubkey doesn't get notified to remove them.

Customers browsing the marketplace continue to see products that:

  • No longer have a backing merchant
  • Will never receive an order DM (no one is listening on that pubkey)
  • Would fail at invoice generation if a customer tried to order

The system isn't unsafe — invoices won't be issued for nonexistent merchants — but it's a poor UX. Customers click "buy", get a confusing error or silent timeout, and lose trust in the marketplace.

Examples of stale-product scenarios

  1. Account deletion: user deletes their LNbits account; merchant + stalls/products gone locally, events stay on relays.
  2. Long-inactive merchant: merchant flag set to active: false (or restore_in_progress stuck), no relay subscription is active, orders won't be processed.
  3. Keypair rotation without migration: a user rotates their account keypair (the new feature in this branch) but doesn't run "Migrate Keys". Old kind 30017/30018 events remain under the old pubkey; new orders to that pubkey go nowhere.
  4. Test/demo merchants: merchants created during testing whose servers are torn down.

Proposed approach

A periodic garbage-collection task that publishes kind 5 deletion events (NIP-09) for stalls/products that should no longer be advertised. Two layers:

1. Local-merchant GC (cheap, definitive)

  • On merchant DELETE /api/v1/merchant/{id}: publish kind 5 deletion events for every stall and product that merchant owned before wiping the local DB rows.
  • This already partially exists for update_merchant_to_nostr with delete=True — verify the merchant deletion endpoint at views_api.py:204–230 actually broadcasts deletes; if not, add it.
  • This handles the "user explicitly deletes" case cleanly.

2. Periodic stale-event reaper (best-effort)

For events where the merchant was lost without a graceful delete (account deletion at the LNbits level, server reset, etc.), publish kind 5 deletes from a cron-style task:

  • Run hourly (or daily — frequency tunable).
  • For each merchant in the DB with config.active == True:
    • Touch time to refresh liveness.
  • For each merchant where:
    • config.active == False for > N days, or
    • The LNbits account no longer exists (the user_id JOIN returns nothing), or
    • time hasn't been refreshed in > N days (suggests background tasks aren't running for this user)
  • Publish kind 5 deletes for all their stalls/products and either soft-delete locally or flag them as zombie.

The merchant can still be revived if the account is restored — keep merchant rows but prune the published events.

3. Customer-side defensive UI (cheap, complementary)

In the customer-facing market, when a stall/product event is fetched but no kind 0 metadata or recent activity exists for the author pubkey, dim or hide the listing with a "Merchant unavailable" badge. This mitigates the case where deletion events haven't propagated yet.

Implementation notes

  • NIP-09 deletion: the existing to_nostr_delete_event(pubkey) on Stall/Product (see models.py) already builds kind 5 events. The hook is there.
  • Cadence: a daily background task added alongside subscription_health_monitor in tasks.py would be the natural home.
  • Determining "deleted account": cross-check the merchants.user_id against accounts table. If no row → account is gone.
  • Migrate-Keys interaction: the migration feature in this branch republishes stalls/products under the new pubkey but doesn't delete the old ones from relays. The GC task should also delete events under the old pubkey when a merchant has rotated keys (track previous pubkeys?). Or, better: when migrating keys, immediately publish deletes for the old key's events as part of _auto_create_merchant / migration flow.

Out of scope

  • Relay-level pruning policies (each relay has its own).
  • Cross-relay reconciliation if an event is on a relay we don't subscribe to.
  • "Confirm merchant heartbeat" via a periodic kind 0 republish — different problem (presence, not staleness).

References

## Problem When an LNbits user deletes their account, the corresponding merchant record and their stalls/products may be cleaned up locally, but **the kind 30017 (stall) and kind 30018 (product) events they previously published remain on Nostr relays indefinitely**. Since these are parameterized replaceable events (NIP-33), they don't expire and their authoring pubkey doesn't get notified to remove them. Customers browsing the marketplace continue to see products that: - No longer have a backing merchant - Will never receive an order DM (no one is listening on that pubkey) - Would fail at invoice generation if a customer tried to order The system isn't *unsafe* — invoices won't be issued for nonexistent merchants — but it's a poor UX. Customers click "buy", get a confusing error or silent timeout, and lose trust in the marketplace. ## Examples of stale-product scenarios 1. **Account deletion**: user deletes their LNbits account; merchant + stalls/products gone locally, events stay on relays. 2. **Long-inactive merchant**: merchant flag set to `active: false` (or `restore_in_progress` stuck), no relay subscription is active, orders won't be processed. 3. **Keypair rotation without migration**: a user rotates their account keypair (the new feature in this branch) but doesn't run "Migrate Keys". Old kind 30017/30018 events remain under the old pubkey; new orders to that pubkey go nowhere. 4. **Test/demo merchants**: merchants created during testing whose servers are torn down. ## Proposed approach A periodic garbage-collection task that publishes **kind 5 deletion events** (NIP-09) for stalls/products that should no longer be advertised. Two layers: ### 1. Local-merchant GC (cheap, definitive) - On merchant `DELETE /api/v1/merchant/{id}`: publish kind 5 deletion events for every stall and product that merchant owned **before** wiping the local DB rows. - This already partially exists for `update_merchant_to_nostr` with `delete=True` — verify the merchant deletion endpoint at `views_api.py:204–230` actually broadcasts deletes; if not, add it. - This handles the "user explicitly deletes" case cleanly. ### 2. Periodic stale-event reaper (best-effort) For events where the merchant was lost without a graceful delete (account deletion at the LNbits level, server reset, etc.), publish kind 5 deletes from a cron-style task: - Run hourly (or daily — frequency tunable). - For each merchant in the DB with `config.active == True`: - Touch `time` to refresh liveness. - For each merchant where: - `config.active == False` for > N days, **or** - The LNbits account no longer exists (the `user_id` JOIN returns nothing), **or** - `time` hasn't been refreshed in > N days (suggests background tasks aren't running for this user) - Publish kind 5 deletes for all their stalls/products and either soft-delete locally or flag them as zombie. The merchant *can* still be revived if the account is restored — keep merchant rows but prune the published events. ### 3. Customer-side defensive UI (cheap, complementary) In the customer-facing market, when a stall/product event is fetched but no kind 0 metadata or recent activity exists for the author pubkey, dim or hide the listing with a "Merchant unavailable" badge. This mitigates the case where deletion events haven't propagated yet. ## Implementation notes - **NIP-09 deletion**: the existing `to_nostr_delete_event(pubkey)` on `Stall`/`Product` (see `models.py`) already builds kind 5 events. The hook is there. - **Cadence**: a daily background task added alongside `subscription_health_monitor` in `tasks.py` would be the natural home. - **Determining "deleted account"**: cross-check the `merchants.user_id` against `accounts` table. If no row → account is gone. - **Migrate-Keys interaction**: the migration feature in this branch republishes stalls/products under the new pubkey but doesn't delete the old ones from relays. The GC task should also delete events under the old pubkey when a merchant has rotated keys (track previous pubkeys?). Or, better: when migrating keys, immediately publish deletes for the old key's events as part of `_auto_create_merchant` / migration flow. ## Out of scope - Relay-level pruning policies (each relay has its own). - Cross-relay reconciliation if an event is on a relay we don't subscribe to. - "Confirm merchant heartbeat" via a periodic kind 0 republish — different problem (presence, not staleness). ## References - NIP-09 (event deletion): https://github.com/nostr-protocol/nips/blob/master/09.md - NIP-33 (parameterized replaceable events): https://github.com/nostr-protocol/nips/blob/master/33.md - Existing delete pathway: `services.py:sign_and_send_to_nostr(merchant, n, delete=True)` and `views_api.py:api_delete_merchant`
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/nostrmarket#3
No description provided.