Skip to content

seller daemon silently drops targeted offers while a job is delivered-but-unpaid (single-flight window) #15

Description

@gudnuf

Summary

The seller daemon (mobee sell, dev branch) silently drops targeted offers that arrive while a previous job is delivered but not yet paid. There is no buffer, no skip log line, and no replay on the live subscription — the offer is simply lost until the daemon restarts.

Observed live (production relay, 2026-07-16)

Seller c9e45147… (rate 21 sats) on wss://mobee-relay.orveth.dev:

  1. 17:10:38Z — offer 8cd20f5c… (targeted, 21 sats) → claimed, executed, delivered (6109 780bbc71…). Payment had not yet arrived.
  2. 17:32:56Z — offer ae29a0b7… (targeted, 21 sats, deadline +1 h) arrives from a second buyer.
  3. Daemon silently ignores it: job 1 is delivered-but-unpaid, so active is still Some, and on_offer_event hits the single-flight gate:
// crates/mobee-core/src/seller_daemon.rs
if Self::in_flight() || self.active.is_some() {
    return Ok(None); // single-flight: ignore while busy
}

active is only cleared in the payment-reconcile path (self.active = None after try_apply_payment), so the "busy" window extends across the entire buyer-side accept+pay latency — minutes to unbounded. Any targeted offer landing in that window is dropped with no log line (other skip paths like non-testnut mint do log; this one doesn't).

  1. Workaround that recovered the offer: restart the daemon. The 5109 subscription has no since filter, so the relay replays stored offers; the claim journal (journal.has_claim) made the already-completed job replay-safe, and the dropped offer was claimed and delivered within its deadline.

Why it matters

  • A seller earning at any real cadence will be "delivered-but-unpaid" most of the time, so this window is the common case, not the corner case.
  • Silent drop means the seller has no signal they lost work; the buyer just sees an offer nobody claims.
  • The recovery (restart) works but is accidental — it relies on the unfiltered subscription replay, which isn't documented as a contract.

Suggested direction

Any of these would close the gap (in rough order of preference):

  1. Buffer offers while busy — queue targeted 5109s that pass the rate gate and process them FIFO when active clears (mirrors what pending already does for early 1059 gift wraps).
  2. Re-scan on unpause — when activeNone, re-REQ stored 5109s (p-tagged, undelivered, within deadline) before returning to the live stream.
  3. At minimum, log the skip (seller skip offer <id>: busy with <job_id>) so operators can see the drop and act.

Environment

  • branch dev @ 1b6efe2, built with --features acp
  • relay wss://mobee-relay.orveth.dev, mint https://testnut.cashudevkit.org (testnut)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions