Every team I've seen chase exactly-once deposit notifications went looking in the queue. The answer was in the watcher's database.
Here is the shape of the problem. A watcher polls the chain, sees a deposit, publishes a notification. Then it polls again and sees the same deposit. Then it restarts mid-batch and re-scans the block range it already processed. Then a reorg rewrites the block it read. None of that is the queue's business — by the time a message exists, the duplicate has already been created upstream.
So the decision moves earlier. Before publishing anything, the watcher writes a row keyed by the thing that is actually unique on-chain, and lets the database reject the second attempt. Publish only if that write is new. Restart-safe, poll-safe, and it survives a consumer that resubscribes from an older offset.
My take: delivery guarantees move messages, they do not define identity. Identity is yours to persist. Once the watcher owns it, the consumer's queue semantics stop mattering — at-least-once delivery becomes acceptable, because the duplicate never entered the stream in the first place.
I wrote up the full reasoning here: https://polycratia.com/c/09828aa7
If you run a chain watcher: what is your dedup key — transaction hash, hash plus output index, or something you derived yourself?