What Music Recognition Can Teach Anti-Money Laundering
An audio fingerprinting algorithm from 2003 turns out to be an unreasonably good fit for transaction monitoring.
What Music Recognition Can Teach Anti-Money Laundering

An audio fingerprinting algorithm from 2003 turns out to be an unreasonably good fit for transaction monitoring.
In 2003, Avery Wang published a paper that explained how Shazam recognizes a song from a noisy 5-second clip recorded on a cellphone in a bar. The algorithm has aged extraordinarily well. It runs in milliseconds against a database of millions of tracks, survives compression and background chatter, and uses no neural networks. The trick is a particular kind of locality-sensitive hash over a sparse 2-D representation of the signal.
Re-read that sentence with a different ear. A locality-sensitive hash over a sparse 2-D representation of a signal. That is what you want when you are trying to detect money-laundering typologies in a stream of transactions: most of the bytes are uninteresting, the bad pattern is sparse, and you need to recognize it under noise and partial observation.
This article argues that Shazam’s pipeline ports almost component-by-component into transaction monitoring, with a graph-shaped twist at the end. We will walk through both versions in code.
The Shazam pipeline in 200 words
Shazam turns a song into a constellation map: take the magnitude spectrogram (an STFT with a Hann window, ~2048-sample frames, 50–75% overlap), and keep only the local maxima — the loudest peak in each small (time, frequency) neighborhood, above an adaptive threshold. A 3-minute song collapses from millions of spectrogram cells to ~30 peaks per second.
Peaks alone are not selective enough to identify a track. So Shazam pairs them. For each peak A, look at a target zone a few seconds to its right and pair A with every peak B inside it. Each pair becomes a hash:
H = pack(f_A, f_B, Δt = t_B - t_A)
The reference database is just an inverted index H → (track_id, t_A). To recognize a query clip, hash it the same way, look up every hash, and for every hit, emit a vote (track_id, t_A_ref - t_A_query). The correct match is the only one whose votes pile up at a single time offset — the song plays in a fixed temporal order, so its hashes all agree on Δt. The wrong tracks scatter votes uniformly. Find the tallest spike. Done.
Here is the audio version compressed to its skeleton:
def fingerprint(audio):
spec = stft(audio, win=2048, hop=512, window=hann)
peaks = local_maxima(np.abs(spec), nbhd=(5, 21), threshold_db=-40)
hashes = []
for i, (t_a, f_a) in enumerate(peaks):
for (t_b, f_b) in peaks_in_target_zone(peaks, i,
dt_max=5.0, df_max=200, fan_out=5):
h = pack(f_a, f_b, t_b - t_a)
hashes.append((h, t_a))
return hashes
def identify(query_audio, index):
votes = collections.Counter()
for h, t_q in fingerprint(query_audio):
for track_id, t_r in index.get(h, []):
votes[(track_id, t_r - t_q)] += 1 # offset-bucket vote
(track_id, _), score = votes.most_common(1)[0]
return track_id, score
That is it. No model, no training. The whole intelligence is in what counts as a peak and how pairs are encoded.
Now do it for transactions
A transaction stream is also a signal. It just has different axes. Pretend each account is a “track” and each window of its activity is a “snippet of audio.” Replace the spectrogram with a multi-channel feature stream: counts, amounts, in/out degree, cash share, virtual-asset share, cross-border share, dormancy flags, counterparty risk. These are your “frequency bins.”
A peak in this domain is an event where one or more channels are unusually loud relative to the account’s local baseline. Concretely: a rolling z-score above some τ, with non-maximum suppression so a single anomalous day doesn’t fire ten times. Each peak gets one of a small set of categorical labels — LARGE_CASH_IN, STRUCTURED_DEPOSIT_CLUSTER, BURST_FANOUT, VA_INFLOW, DORMANT_WAKE, and so on — perhaps 15–20 labels total. The label represents the frequency f.
Hash pairs of peaks exactly the way Shazam does, with Δt bucketed logarithmically (minutes/hours /days/weeks) so it survives jitter:
def behavioural_fingerprint(account_events):
series = build_multichannel_features(account_events) # T x C
z_scores = rolling_zscore(series, window="30d")
peaks = local_maxima(z_scores, nbhd=(7, 1), threshold=3.0)
peaks = [(t, label_for(c, z_scores[t, c])) for t, c in peaks]
hashes = []
for i, (t_a, lab_a) in enumerate(peaks):
for (t_b, lab_b) in peaks_in_target_zone(peaks, i,
dt_max="14d", fan_out=5):
h = pack(lab_a, lab_b, log_bucket(t_b - t_a))
hashes.append((h, t_a))
return hashes
You now need a reference library to match against. In music, it’s a catalog of tracks. Here it’s a catalog of typologies: Unusual Transactions, Savings Account Patterns, Cash-Intensive Behavior, and Money Laundering Through Virtual Assets. Each typology fingerprint is built by aggregating hashes from labeled cases, or — more realistically, given how rare labels are — hand-authored by an investigator from a few canonical templates. Cash-intensive structuring, for example, has a textbook pair signature: many STRUCTURED_DEPOSIT_CLUSTER → LARGE_CASH_OUT hashes at Δt ≈ days.
Matching against the library is the same vote you ran on audio:
def match_typologies(account_events, library_index):
votes = collections.Counter()
for h, t_q in behavioural_fingerprint(account_events):
for typology_id, t_ref in library_index.get(h, []):
votes[(typology_id, log_bucket(t_ref - t_q))] += 1
return votes.most_common(5) # ranked typologies for this account
A tall (typology_id, δ) spike says: this account's recent window matches that typology, and δ tells you when in its lifecycle it currently is. Multiple spikes in different typologies are fine — Wang noticed the same property in audio: his algorithm has a transparency property where two songs mixed together can both be identified. Multiple typologies on a single account are a feature, not a bug.
The library is open. Adding a new typology is hash insertion. No retraining, no model migration. This is what you actually want for a regulator-driven environment where typologies are constantly being added.
The graph twist
Music has a single time axis. Transactions have time and topology. The constellation generalizes.
In the graph version, a “peak” is a node — or a small motif — whose local structural-anomaly score is unusual. The role of the frequency bin is played by a role tag: HUB, BRIDGE, MIXER_LIKE, MULE_LIKE, EXCHANGE_LIKE, MERCHANT_LIKE, etc. The role tags can come straight out of an existing graph service — motif counts, GNN embeddings binned, or a small classifier.
The target zone is no longer a rectangle in (t, f) but a k-hop temporal neighborhood of the anchor. A pair-hash now also encodes the relative graph position of B:
def graph_fingerprint(subgraph, t_now):
peaks = anomalous_nodes(subgraph) # (node, role)
hashes = []
for a in peaks:
for b in peaks_in_khop(subgraph, a, k=2,
dt_max="14d", fan_out=8):
h = pack(a.role, b.role,
hop_distance(a, b),
log_bucket(b.t - a.t),
edge_pattern_signature(a, b))
hashes.append((h, a.t))
return hashes
edge_pattern_signature is a fixed-length sketch of the path between A and B — directionality, count of cash vs VA hops, presence of an exchange-like intermediate. It's the structural counterpart of (f_B, Δt) in audio: where, relative to A, did B appear?
The matching vote is now over (typology_id, Δt, orientation) rather than just (track_id, Δt), but the shape is identical: a tall spike indicates a match; scattered votes indicate noise. This is also where the technique meshes most cleanly with an existing graph service — role tags, motif counts, k-hop neighborhoods are already what such a service produces. You're adding an indexing and voting layer, not a new pipeline.
Why this is worth taking seriously
Three properties make Shazam-style fingerprinting attractive for AML specifically:
- No labels required to bootstrap. Typologies are templated, not learned. You can author a useful library with an investigator on day one. Labels, when they arrive, refine the library by reweighing hashes — they don’t need to retrain a model.
- Explanations are free. Every spike is backed by the specific hashes that voted for it, which point back to specific transactions. The investigator gets, at no extra cost: “this account matches Cash-Intensive because we saw a
STRUCTURED_DEPOSIT_CLUSTER → LARGE_CASH_OUTpair at Δt ≈ 3 days, plus 14 similar pairs in the same window." Most ML methods have to retrofit explanations; this one starts with them. - Insertion-time scaling. A new typology is
O(#hashes)to insert andO(1)per hash to look up. The same store can hold per-account, typology, and graph-side fingerprints, distinguished by a type tag.
It is not a panacea. The two real risks are adversarial drift — launderers iterate, songs don’t — and magnitude erasure — peak picking discards “loudness,” but in AML, the amount often is the identity. Both have known fixes: per-hash decay weights for drift, and amount-bucket bits in the hash (a trick borrowed from Panako, a Shazam descendant designed for pitch- and tempo-invariance) for magnitude.
The deeper point is that audio fingerprinting solved a problem the AML world is still solving: how to find a known sparse pattern within a noisy, partially observed signal at scale, with explainable matches. Borrowing across domains is unfashionable in an era when the default move is to throw a transformer at the problem. But Shazam’s 2003 algorithm is, by every operational metric — latency, scale, explainability, low label requirements — a better fit for a regulated transaction-monitoring stack than any model I have seen pitched for the same job.
A short pilot is enough to find out: pick one typology, define a 15-label peak alphabet, hand-author three reference fingerprints, backfill a year of accounts, and measure precision and false-positive rate against the rule engine you currently run. If the histogram spikes look like the ones in Wang’s paper, you have something.
Built on top of work in our Transaction Monitoring project on graph-based AML. A longer technical companion note covering parameter choices will follow, along with the typology-by-typology peak alphabets. Please stay tuned.
메타데이터
- post_id
- d1cd23efb41a
- slug
- what-music-recognition-can-teach-anti-money-laundering-d1cd23efb41a
- url
- https://medium.com/graph-praxis/what-music-recognition-can-teach-anti-money-laundering-d1cd23efb41a
- canonical_url
- https://medium.com/graph-praxis/what-music-recognition-can-teach-anti-money-laundering-d1cd23efb41a
- author_url
- https://medium.com/@shereshevsky
- status
- ok
- fetched_at
- 2026-06-11 22:20:54