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:

Każde wejście — tylko przy zgodzie
Wejście na stronę
Klasyfikacja źródła: utm_source → gclid → referrer
Dopisanie do ciasteczka _aff_path
Zakup
Zakup (purchase)
Decyzja: kto był ostatni?
aff_decision w dataLayer
Pixel zwycięskiej sieci
GA4: affiliate_attribution
Zapis ścieżki działa na każdej stronie poza checkoutem, decyzja zapada raz — przy zakupie.

Zapis ś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 klientaPixel zakupuDlaczego
Partner A → Partner B → zakupPartner Bostatni klik
Partner A → Google organic → zakupPartner Aorganic pomijany
Partner B → PayU (powrót) → zakupPartner Bbramka przezroczysta
Partner A → Google Ads → zakupniktostatni klik płatny
Partner A → Retargeting B (klik) → zakupnikt z CPS; pixel B jak zawszeB płatny za klik, nie za sprzedaż
Partner D → 10 dni → wpisany adres → zakupniktokno 7 dni minęło
Partner A → 3DS banku spoza listy → podziękowaniePartner Acheckout nic nie zapisuje
Partner A → zakup → odświeżenie podziękowaniaPartner A razduplicate_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-clickB: CPC / CPM
Udział w deduplikacjitaknie
Pixel zakuputylko gdy wygraprzy każdym zakupie, przy zgodzie (optymalizacja kampanii)
Ryzyko bez deduplikacjipł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.

Powiązane wpisy