Deduplikacja prowizji afiliacyjnych w GTM — jedna prowizja
Sklep pracuje z kilkoma partnerami rozliczanymi od sprzedaży: trzema sieciami afiliacyjnymi, porównywarką cen i retargetingiem płatnym od zamówienia. Klient przed zakupem klika w trzy z nich. Każdy partner ma na stronie podziękowania swój pixel zakupu, każdy widzi „swoje” zamówienie i każdy wystawia fakturę. Sklep płaci trzy prowizje za jedną sprzedaż — i nikt tu nie oszukuje, bo każdy liczy uczciwie według własnych danych.
W panelu żadnej sieci tego nie widać. Problem wychodzi dopiero wtedy, gdy ktoś zsumuje prowizje ze wszystkich faktur i porówna je z liczbą zamówień.
Dlatego projektuję mechanizm, w którym spór sprowadza się do sprawdzenia reguły. Regułę, kiedy pixel trafia do partnera, ustalam z góry — w tagu i w umowie. Partner, który wygrał, dostaje 100% prowizji. Pozostali nie dostają informacji o sprzedaży wcale. Sama technika jest prosta. Trudne są przypadki brzegowe i zamówienia klientów, którzy nie zgodzili się na cookies.
Ten mechanizm wdrażałem w sklepach internetowych. Tu opisuję go ogólnie, bez odwołań do konkretnych projektów — nazwy partnerów w przykładach są fikcyjne.
Jak działa deduplikacja prowizji afiliacyjnych w GTM
Całość mieści się w kontenerze GTM, po stronie przeglądarki, w trzech krokach:
utm_source → gclid → referrer_aff_pathpurchase)aff_decision w dataLayeraffiliate_attributionZapis ścieżki. Przy każdym wejściu z zewnątrz tag dopisuje źródło do ciasteczka _aff_path, ale tylko wtedy, gdy klient zgodził się na ad_storage. Na stronach koszyka i płatności nie zapisuje nic. Na pozostałych ustala źródło w tej kolejności: najpierw utm_source, potem identyfikator kliknięcia (gclid → google-ads), na końcu referrer. Ciasteczko trzyma najwyżej 10 wejść. Gdy to samo źródło powtarza się pod rząd, tag odświeża tylko jego czas:
partner_a~tq4k2o|organic.google~tq5b8c|partner_b~tq6x1d
Równie ważne jest to, czego tag nie zapisuje. Wejście bezpośrednie (wpisany adres) nie nadpisuje partnera. Nawigacja wewnątrz sklepu nie jest źródłem. Bramki płatności, banki i poczta są przezroczyste — bez tego powrót z płatności zapisałby się jako nowe wejście z zewnątrz. Gdy klient trafia po płatności na stronę pośrednią i dopiero z niej na podziękowanie, zamówienie przypadłoby bramce albo bankowi. Gdy decyzja zapada wcześniej, wpis z banku zostaje w ścieżce i odbiera partnerowi kolejny zakup — chyba że klient wróci z jego linku.
Decyzja przy zakupie. Na zdarzeniu purchase tag czyta ścieżkę od końca i pomija wyszukiwarkę organiczną. Klient często klika w partnera, a dwa dni później wraca, wpisując nazwę sklepu w Google. To powrót, nie nowe źródło.
Pixel tylko dla zwycięzcy. Tag decyzyjny niczego nie wysyła do sieci. Wypycha do dataLayer zdarzenie aff_decision, a pixel każdej sieci ma trigger „zwycięzca = ta sieć”. Jeśli purchase dla tego samego zamówienia przyjdzie drugi raz (odświeżona strona podziękowania), wynik to duplicate_transaction (tag pamięta ostatnie zamówienie w ciasteczku _aff_tx) i nie odpala żaden pixel.
Atrybucja last click w afiliacji — dlaczego nie model współdzielony
Prowizję trzeba rozliczyć co do zamówienia i obronić przed partnerem, który zobaczy fakturę. Reguła musi być więc binarna i sprawdzalna: albo ostatnie kliknięcie było twoje i mieściło się w oknie z umowy, albo nie. Modele, które dzielą zasługę między kanały, przydają się przy decyzjach budżetowych. Tak też podchodzę do atrybucji reklam w marketplace. Ale ułamek prowizji wyliczony przez algorytm to zaproszenie do kłótni o algorytm.
Sedno logiki to jedna funkcja:
function decideWinner(path, now, cfg) {
if (!path.length) {
return {
winner: null, reason: 'no_touchpoints', touch: ''
};
}
for (var i = path.length - 1; i >= 0; i--) {
var tp = path[i];
// wyszukiwarka organiczna to powrót, nie nowe źródło
if (isNavigationalToken(tp.t, cfg)) continue;
var partnerId = partnerForToken(tp.t, cfg);
if (!partnerId) {
return {
winner: null, reason: 'non_partner_last_click',
touch: tp.t
};
}
var windowDays = cfg.partners[partnerId].windowDays || 30;
if (now - tp.ts > windowDays * DAY) {
return {
winner: null, reason: 'partner_window_expired',
touch: tp.t
};
}
return {
winner: partnerId, reason: 'partner_last_click',
touch: tp.t
};
}
return {
winner: null, reason: 'only_navigational', touch: ''
};
}
Dwie decyzje siedzą w tym kodzie i obie trzeba wpisać do umowy. Po pierwsze, partner po swoim oknie atrybucji nie wygrywa, a funkcja nie cofa się do starszego partnera — zakup zostaje bez prowizji. Po drugie, jeśli ostatnie wejście z zewnątrz przyszło z Google Ads, newslettera albo cudzego bloga, sprzedaż należy do tego kanału i nikt z CPS jej nie dostaje. Blog sklepu w jego domenie to nawigacja wewnętrzna — ścieżki partnera nie przerywa. Trzecia decyzja siedzi w konfiguracji: czy drugi zakup w oknie znów należy do partnera, czy wymaga nowego kliknięcia (clearPathOnPurchase).
Okna i źródła partnerów są w jednej zmiennej GTM:
partners: {
partner_a: { sources: ['partner_a'], windowDays: 30 },
partner_b: { sources: ['partner_b'], windowDays: 30 },
partner_c: { sources: ['partner_c'], windowDays: 14 },
// porównywarka: wejście z UTM albo z samej domeny
partner_d: {
sources: ['partner_d', 'comparison-d.example'],
windowDays: 7
},
// retargeting CPS post-click konkuruje jak sieć
retargeting_a: { sources: ['retargeting_a'], windowDays: 7 }
// retargeting_b poza listą: CPC/CPM, pixel przy każdym
// zakupie (przy zgodzie)
}
Kto wygrywa — osiem ścieżek, które uczą najwięcej
Partner A i B mają okno 30 dni, porównywarka D — 7 dni.
| Ścieżka klienta | Pixel zakupu | Dlaczego |
|---|---|---|
| Partner A → Partner B → zakup | Partner B | ostatni klik |
| Partner A → Google organic → zakup | Partner A | organic pomijany |
| Partner B → PayU (powrót) → zakup | Partner B | bramka przezroczysta |
| Partner A → Google Ads → zakup | nikt | ostatni klik płatny |
| Partner A → Retargeting B (klik) → zakup | nikt z CPS; pixel B jak zawsze | B płatny za klik, nie za sprzedaż |
| Partner D → 10 dni → wpisany adres → zakup | nikt | okno 7 dni minęło |
| Partner A → 3DS banku spoza listy → podziękowanie | Partner A | checkout nic nie zapisuje |
| Partner A → zakup → odświeżenie podziękowania | Partner A raz | duplicate_transaction |
Wiersz z retargetingiem pokazuje rozróżnienie, które w umowach często się zlewa. Retargeting „płatny od sprzedaży” i retargeting „płatny za kliknięcie” wymagają w tagach zupełnie innego traktowania:
| A: CPS post-click | B: CPC / CPM | |
|---|---|---|
| Udział w deduplikacji | tak | nie |
| Pixel zakupu | tylko gdy wygra | przy każdym zakupie, przy zgodzie (optymalizacja kampanii) |
| Ryzyko bez deduplikacji | płacisz za cudzą sprzedaż | brak |
Pułapki z checkoutu i zgody
Trzy pierwsze wyszły w przeglądzie kodu, nie w dokumentacji sieci.
Bramka płatności spoza listy zabiera sprzedaż. Klient płaci kartą, bank wymaga 3DS, a powrót ze strony banku na stronę podziękowania zapisuje się jako referral. Ten referral staje się ostatnim wejściem i partner traci zakup. Listy bramek i banków nigdy nie będą pełne, za to ścieżki checkoutu sklep zna. Dlatego na stronach koszyka, płatności i podziękowania tag nie zapisuje niczego (noCapturePaths), a lista bramek zostaje tylko drugą linią obrony.
Referrer android-app://. Link z aplikacji Gmail na Androidzie nie przychodzi jako mail.google.com, tylko jako android-app://com.google.android.gm. Bez osobnego mapowania klient, który wraca z maila od sklepu, odebrałby zakup partnerowi. To samo dotyczy aplikacji Google: jej referrer trzeba traktować jak wyszukiwarkę organiczną.
Wycofana zgoda, zostawione ciasteczko. Tag ustawiony w GTM „na zgodę ad_storage” po wycofaniu zgody po prostu przestaje się odpalać, a ciasteczko zostaje na urządzeniu. Potrzebny jest osobny tag bez wymogu zgody, który kasuje _aff_path i _aff_tx. Zakładam, że kasowanie nie jest przechowywaniem informacji i zgody nie wymaga, a ciasteczko zostawione po wycofaniu zgody już tak. Tag czyta stan z komend gtag('consent', 'update') — jeśli CMP ustawia zgodę szablonem GTM, sprawdź w podglądzie, że naprawdę kasuje.
Zgoda na tej samej stronie. Klient wchodzi z linku partnera i klika „Akceptuję” na pierwszej stronie. GTM nie odpali wstecz tagu zablokowanego na DOM Ready, więc tag zapisu potrzebuje drugiego triggera — zdarzenia CMP po decyzji.
Safari skraca życie ciasteczka. ITP ogranicza ciasteczka ustawiane z JavaScriptu do 7 dni, a po wejściu z domeny, którą Safari uznał za śledzącą, z parametrami w linku — do 24 godzin. Przy oknie 30 dni część zakupów z Safari wypadnie. Wyjście to ciasteczko z nagłówka HTTP, ustawiane (wciąż tylko przy zgodzie) przez serwer pod tym samym adresem co sklep — ten sam mechanizm, który opisałem przy server-side GTM i adblockerach. Jeden warunek: od Safari 16.4 ciasteczko z nagłówka też dostaje 7 dni, jeśli serwer, który je ustawia, stoi pod innym adresem IP niż strona sklepu — a przy sGTM na osobnej infrastrukturze zwykle tak jest. Pełne okno daje dopiero serwer pod tym samym IP co sklep, np. przez proxy na jego hostingu.
Afiliacja bez zgody na cookies — co wpisać w umowę
Zapis ścieżki w ciasteczku to przechowywanie informacji na urządzeniu użytkownika (art. 399 Prawa komunikacji elektronicznej). Rozliczenie prowizji nie jest niezbędne do świadczenia usługi, o którą prosi klient, a Wytyczne EDPB 2/2023 obejmują art. 5 ust. 3 dyrektywy ePrivacy także piksele i linki śledzące. Traktuję to jako założenie projektowe, nie jako opinię prawną. Wniosek: bez zgody ciasteczka nie ma i sieci nie widzą nic. A umowę wciąż trzeba rozliczyć.
Domyślnie proponuję najprostszy zapis: bez zgody nie ma prowizji, w zamian partner dostaje wyższą stawkę podstawową. Projektuję pod odsetek zgód powyżej 90%. Przy takim pokryciu partner traci kilka procent zamówień, zyskuje na stawce, a obie strony nie muszą co miesiąc dyskutować o modelu.
Gdy zgód jest mało, ten zapis przestaje być uczciwy wobec partnera. Wtedy wchodzi pula modelowana. Zakłada, że klienci bez zgody wybierają partnerów w tych samych proporcjach co klienci ze zgodą:
udział partnera = sprzedaż partnera ze zgodą
/ cała sprzedaż ze zgodą
prowizja z puli = MIN(udział × sprzedaż bez zgody
× stawka modelowana, limit)
W mianowniku muszą być także zamówienia ze zgodą, w których nie wygrał nikt. Inaczej udziały sumują się do 100% i pula w całości trafia do sieci, łącznie ze sprzedażą z Google Ads i newslettera. Stawka modelowana to zwykle 50–70% podstawowej, do tego miesięczny limit i comiesięczny raport udziału zgód.
Niezależnie od wariantu do umowy trafia:
- okno atrybucji w dniach i model: last click, tylko post-click,
- pixele sieci tylko przy zgodzie,
- podstawa prowizji bez kosztu dostawy, pomniejszana o zwroty.
Post-view to osobna sprawa. Sieć, która liczy sprzedaż po samym wyświetleniu reklamy, przypisze sobie zamówienia, w których mechanizm nie widzi jej śladu. Tego nie rozwiąże żaden tag. Rozwiązuje to zapis, że rozliczenie idzie wyłącznie z raportu sklepu, post-click.
Rozliczenie partnerów CPS w BigQuery
GA4 nie wystarczy do liczenia prowizji, bo nie zna zwrotów ani anulacji. Decyzja z tagu trafia do GA4 jako zdarzenie affiliate_attribution (numer zamówienia, zwycięzca, powód, ścieżka). Dopiero w BigQuery łączę ją z tabelą zamówień z backendu, po tym samym numerze zamówienia. Kolejne zapytania liczą podstawę po zwrotach, prowizje per partner (i pulę, jeśli umowa ją przewiduje) i sprawdzają jakość: pokrycie zamówień decyzją, zamówienia bez pary i duplikaty. Po publikacji kontenera najważniejsze jest pierwsze z nich — pokrycie zamówień decyzją per dzień. Nagły spadek pokaże zepsuty tag albo zmieniony baner zgód, zanim zrobi to jakikolwiek raport.
Wnioski
- Reguła w tagu bez reguły w umowie to połowa roboty. Okna, model last click, post-view i hierarchia bez zgody muszą być spisane, zanim pierwszy pixel pójdzie do sieci.
- Checkout to strefa zakazu zapisu. Lista bramek płatności zawsze będzie niepełna, a ścieżki checkoutu sklep zna.
- Zgoda działa w obie strony. Tag zapisu potrzebuje triggera na zdarzenie CMP, a wycofanie zgody — osobnego tagu, który kasuje ciasteczka.
- Pula modelowana to narzędzie na niski odsetek zgód. Przy wysokim zwykle prostsza i uczciwsza jest wyższa stawka bez puli.
Ograniczenia: wejście sprzed zgody na wcześniejszej stronie przepada, adblockery zatrzymują pixele, a ścieżka żyje w jednej przeglądarce. Klik i zakup na tym samym telefonie działają normalnie, ale klik na laptopie i zakup na telefonie — albo klik w aplikacji Instagrama i zakup w Chrome — to dla mechanizmu dwóch różnych klientów. Zamówienia w aplikacji sklepu i przez infolinię omijają przeglądarkę całkowicie. Następna iteracja to server-side GTM: ciasteczko z nagłówka HTTP, decyzja i wysyłka do sieci po stronie serwera. Serwer nie zwalnia jednak ze zgody — ciasteczko ustawione nagłówkiem HTTP to nadal zapis na urządzeniu użytkownika.