Probabilistic attribution: building a device fingerprint scorer that doesn't lie
How probabilistic matching works: the five device signals, the weights, the precision-recall harness — and the consent requirement most guides skip.
Fingerprint attribution sounds like dark magic. It is not. It is a handful of fields, a weighted similarity score, and a confidence threshold. The hard part is the discipline: choosing your weights, holding the line on the threshold, and writing the tests that keep both honest. (Where probabilistic matching sits in the wider waterfall is covered in the post-IDFA attribution playbook.)
One thing to say up front, because this is our blog: this post is about how the technique works in general, if you are building it. It is not a description of LinkTrail. Our own probabilistic layer is deliberately narrower — it compares the IP address and platform only, the outcome is binary rather than scored, and it is off until a workspace switches it on. Our methodology page sets out what we actually run.
What is probabilistic attribution?
Probabilistic attribution matches an install to a click by comparing device and network characteristics that both events happened to carry, and scoring how similar they are. There is no shared identifier — nothing is passed through the store. You are asking whether two observations, minutes apart, plausibly came from the same device, and answering with a number rather than a yes.
It exists because deterministic paths cover only part of the traffic. Android's Play Install Referrer carries a token straight through the store and is exact when it fires. iOS has no equivalent. Where a deferred click token isn't available either, probabilistic matching is what remains — and it is the layer where most attribution products quietly overstate themselves.
The five fields that earn their keep
- IP address — strongest single signal, but mobile-network shared NAT is real. Don't weight it >0.5.
- User-Agent — fine-grained on browsers, coarse on apps. Useful but noisy.
- Screen dimensions — device-class signal. Strong when paired with model hints.
- Timezone — narrows geography without revealing it.
- Locale — language + region. Cheap to compare, surprisingly discriminating.
export function score(click: Signals, install: Signals): number {
const ip = click.ip === install.ip ? 1 : 0;
const ua = uaSimilarity(click.ua, install.ua); // 0..1
const sc = click.screen === install.screen ? 1 : 0;
const tz = click.timezone === install.timezone ? 1 : 0;
const lo = click.locale === install.locale ? 1 : 0;
return 0.40 * ip + 0.25 * ua + 0.15 * sc + 0.10 * tz + 0.10 * lo;
}Why does the time window matter so much?
Because every signal above degrades with time, and none of them degrade at the same rate. An IP address on a mobile network can change within minutes of the click. A timezone effectively never changes. So a match at 90 seconds and a match at 6 days are not the same claim, even at an identical score.
The practical consequence is that the window is a precision lever, not a coverage setting — and it should be tuned per campaign type rather than set once globally:
| Window | What it suits | Trade-off |
|---|---|---|
| Under 1 hour | Direct-response ads, QR at point of sale | Highest precision; misses considered installs |
| 24 hours | Most paid social and search | The usual default; good balance |
| 7 days | Long consideration cycles, high-value apps | Coverage rises, false positives rise faster |
| Over 7 days | Rarely justifiable probabilistically | Signal decay makes the score close to meaningless |
How do you know the scorer is any good?
You measure it against installs whose true source you already know, which is the one part of this most teams skip. Keep two reference datasets: known-attributed installs where a deterministic match exists and the fingerprint was computed anyway, and known-organic installs with no preceding click at all. The first set tells you recall; the second tells you your false positive rate.
Compute precision and recall on every release and refuse to ship a scoring change that regresses either. Without this harness, weight tuning is superstition — someone nudges the IP weight because a channel looked low last week, and nobody can prove whether it helped.
Why a threshold above 0.5?
Because the two error types cost wildly different amounts. A false negative under-credits a paid campaign, and you notice — the channel looks weak and someone investigates. A false positive credits paid for an install that was organic, which inflates ROAS, looks like success, and gets more budget. Nobody investigates a number that is going the right way.
So the threshold should be set where a false positive is more expensive than a missed match, which for almost every business means comfortably above 0.5. Start at 0.6, measure, and move it based on your harness rather than on how a dashboard looks this quarter.
The consent question
One thing the code above can't handle: legality. Comparing device signals like these is device fingerprinting under Article 5(3) of the ePrivacy Directive, and the EDPB's Guidelines 2/2023 confirm it requires end-user consent — legitimate interests doesn't cover it, and Apple's App Store guidelines prohibit fingerprinting regardless of ATT status. So build the gate in from day one: collect nothing until the host app signals consent, and make matching switchable off entirely. That's how the LinkTrail SDK ships by default — our DPA sets out the full analysis, in public.
“If your fingerprint scorer doesn't have a precision-recall harness, you don't have a scorer — you have a vibe.”
Ahsan
LinkTrail Engineering
Ahsan is on the LinkTrail engineering team and the engineer behind its SDKs for iOS, Android, React Native, and Flutter. He started the company after the Firebase Dynamic Links shutdown left teams with links that opened the store and forgot where the user was going — and after too many vendor calls that ended without a price. He writes here about deferred deep linking, install attribution after ATT, and the parts of the mobile growth stack the category tends to leave vague.
All posts by Ahsan