Context
_is_likely_change() (in cryptotrace/core.py) folds a lone fresh output back into the spender's cluster only when a tx has exactly one previously-unseen, non-input output. This is deliberately conservative, but it means a very common real-world pattern is missed:
- A payment tx with two fresh outputs (e.g. a batched payout that also creates change to a fresh address) is treated as fully ambiguous → the change output is never clustered.
- CoinJoin-adjacent or wallet-batching txs that legitimately produce multiple fresh outputs get no change attribution at all.
Why it matters
Change detection is one of the two clustering heuristics that drive sanctions inheritance. Under-clustering here means a co-owned change wallet can escape a cluster's sanctions tag, which is a false negative in a compliance context.
Possible directions (non-breaking)
- Add an optional value-based change heuristic: when tx value is available, the output whose value is not a "round" amount (or the residual) is the likely change even among multiple fresh outputs.
- Expose a
change_strategy parameter on cluster_addresses() defaulting to the current conservative behaviour, so existing output is unchanged.
Reproducer (current behaviour):
from cryptotrace.core import cluster_addresses, Transaction
# tx with two fresh outputs -> neither is folded as change
txs = [Transaction("t0", ["A"], ["FRESH1", "FRESH2"], "BTC", 1.0)]
print([c.addresses for c in cluster_addresses(txs)]) # -> [] (no cluster)
Context
_is_likely_change()(incryptotrace/core.py) folds a lone fresh output back into the spender's cluster only when a tx has exactly one previously-unseen, non-input output. This is deliberately conservative, but it means a very common real-world pattern is missed:Why it matters
Change detection is one of the two clustering heuristics that drive sanctions inheritance. Under-clustering here means a co-owned change wallet can escape a cluster's sanctions tag, which is a false negative in a compliance context.
Possible directions (non-breaking)
change_strategyparameter oncluster_addresses()defaulting to the current conservative behaviour, so existing output is unchanged.Reproducer (current behaviour):