Attribution audit — spec
Table of Contents
1. Purpose
The attribution-audit sits on top of the six-model attribution engine and adds the four things a rules-based engine structurally cannot supply on its own:
- Provenance — is this touchpoint
observed(user did it) orasserted(system claimed it)? Recorded at write time. - Credit-conservation — an inflation ratio that quantifies how much stuffing has bent the credit vector.
- Incrementality — a holdout arm that suppresses ads and measures the true lift. The only falsifying measurement in the whole stack.
- A same-origin
/go/:tokenaffiliate-link resolver with a static destination allowlist — merchant networks are structurally unreachable, which is the demonstration.
Refutation condition (from the source docstring): if all five attribution models agree with each other and with the incrementality number across a real holdout, the six-model vector has no information and R19 (per-model credit) is not earning its place.
ORACLE: src/wal_sh/adtech/attribution_audit/core.cljc (pure),
browser.cljs (window._srAttribution).
2. Requirements
- R18
- Provenance is a closed set:
#{"observed" "asserted"}. Enforced byvalid-provenance?; C3 says the touchpoint constructor throws on any other value. - R19
attributereturns a MAP of model-name → per-touchpoint credit vector, never a single scalar. Every reader sees all five models simultaneously.- R20
claimed-credit-ratiocomputes each claimant's most flattering model, sums across claimants. Honest ledger sums to 1.0; anything above is the inflation stuffing exploits.- R21
incrementalityis defined only forholdout > 0. Without a holdout it returnsnil— R22 makes this explicit: no holdout, no incrementality claim.- R22
- Default holdout is 0.10; the arm assignment is deterministic
via
arm=djb2(user-id) mod 10000 / 10000 < holdout ? "holdout" : "exposed". Thedjb2is the same hash the ab-engine uses so aholdoutuser is consistent across the whole surface. - R23
- The
/go/:tokenresolver appends anobservedOnClicktouchpoint on every hit. Never on miss (fail-closed under C1). - C1
/go/destinations are a static allowlistdestinationsincore.cljc. No dynamic destination is possible; the resolver's return value is nil for anything unknown.- C3
touchpointconstructor throws on unlisted:provenance— the ledger cannot end up with unlabelled asserted touches.
3. Contract signature
Pure (core.cljc):
(valid-provenance? p) ; -> bool
(touchpoint {:keys [id t channel source medium campaign claimant provenance]})
;; -> map; throws on invalid provenance
(last-click ts) ; -> credit vector
(first-click ts) ; -> credit vector
(linear ts) ; -> credit vector
(time-decay ts) ; -> half-life 7d, normalised
(position-based ts) ; -> 40/40/20
(attribute ts) ; -> {model-name [{:claimant :provenance :credit}]}
(claimed-credit-ratio ts) ; -> {:ratio n :claims [...]}
(incrementality {:keys [holdout exposed-conversions ...]})
;; -> {:exposed-rate :holdout-rate :lift :relative-lift :incremental-conversions} or nil
(djb2 s) ; -> uint32
(arm user-id holdout) ; -> "exposed" | "holdout"
(resolve-token token) ; -> {:dest :token} or nil
(hops-to-content ts) ; -> count of sponsored/affiliate hops
Browser surface:
window._srAttribution = {
Ledger: {
add: (fn [touchpoint] -> nil)
entries: (fn [] -> [touchpoint...])
clear: (fn [] -> nil)
}
Audit: {
arm: (fn [] -> "exposed" | "holdout")
attribute: (fn [] -> {model -> credit-vec})
ratio: (fn [] -> {:ratio :claims})
lift: (fn [] -> incrementality-map or nil)
}
Go: {
resolve: (fn [token] -> {:dest :token} or nil)
}
Demo: {
explain: (fn [] -> string)
stuff: (fn [claimant] -> nil) ; asserts a phantom touchpoint
}
}
4. The demonstration
Demo.stuff("bad-actor") asserts a phantom touchpoint labelled
:provenance "asserted" with :claimant "bad-actor". Two things
then observably change:
claimed-credit-ratioinflates above 1.0.incrementalitystays flat — the holdout arm never saw the ad, so a phantom touchpoint on the exposed arm doesn't move the lift.
The asymmetry is the demonstration. A rules-based engine bends; a holdout-anchored measurement doesn't.
5. Related literature
- Cook, Nithyanand & Shafiq on tracker-advertiser topology (Cook, Nithyanand, and Shafiq 2020) shows the structural opacity that makes provenance recording at write time the only reliable defence.
- ANA 2023/2024 MFA transparency studies (Association of National Advertisers 2023), (Association of National Advertisers 2024) document the industry-scale version of this failure: 15 % of impressions on "made-for-advertising" sites, credited by rules-based attribution the same as premium publisher inventory.
- Zeng, Kohno & Roesner on bad ads (Zeng, Kohno, and Roesner 2020) is the observational analogue — deceptive ads measured at scale, no assertion of provenance possible from the outside.
- Bashir et al. on retargeted-ad tracing (Bashir et al. 2016) documents how attribution across 20+ touchpoints, most of which the user never observed, is the norm not the exception.
- Ali et al. on discrimination through delivery (Ali et al. 2019) — if the audit surface is opaque, the bias is undetectable. Our audit surface is deliberately visible.
- Google's Attribution Reporting API
(Google 2022) is the privacy-sandbox
proposal that pushes attribution to a browser-mediated aggregation;
our
/go/resolver is the same-origin analogue at a much smaller scale. - Srinivasan on Google's advertising dominance (Srinivasan 2020) frames why the audit layer is structurally unavailable to publishers: the buyer, seller, and auctioneer are the same firm, and the ledger is proprietary.
- Hwang on ad-market fragility (Hwang 2020) — an unfalsifiable metric fed to a bid-optimizer is the definition of a subprime signal.
6. Cross-references
- attribution-engine/spec.org — the v1 six-model layer this namespace sits on top of.
- stuffing-detectors/spec.org — v5 addendum; four distributional
detectors that consume the same ledger, without reading
:provenance. - intent-signals/spec.org — v6; every signal enters this
ledger as
observed(R24). - exit-intent/spec.org — reads
arm; suppressed inholdout(R31), which is what makes incrementality measurable. - ORACLE:
src/wal_sh/adtech/attribution_audit/core.cljc(pure)src/wal_sh/adtech/attribution_audit/browser.cljs(window._srAttribution)
- web-history — post-cookie improvisation on Google's Attribution Reporting API and the sandbox context.
- CPRR Methodology — the Conjecture-Proof-Refutation-Refinement framing this audit's refutation gates borrow (holdout re-randomisation, credit conservation are structurally the same shape).
7. Open questions
- Should the
/go/resolver support signed destinations? The static allowlist (C1) is a structural guard against dynamic redirect chains — the class of exploit documented in the OpenRTB ecosystem (IAB Tech Lab 2016). A signed-destination scheme would let us cover more of the surface without giving up the guard, at the cost of a key-rotation runbook. - Holdout re-randomisation. The current arm assignment is stable per
user-id; a user inholdoutstays there. Long-lived holdouts bias the sample toward returning visitors. A time-boxed re-randomisation would fix this at the cost of per-arm sample independence.