Trackily Docs

A/B testing de prix au niveau landing

price_overrides est un champ JSONB sur landing_pages qui mappe native_product_variant.id → prix override. Le checkout natif utilise ce prix au lieu du variant.price pour les visiteurs qui ont vu cette landing. Combiné au flow weighted, c'est le pattern "4 tiers de prix simultanés".

Concept

Pour tester si MemoMind se vend mieux à $24.99 ou $44.99, l'approche classique force à :

  • Créer 4 produits ou 4 variants (un par prix) → casse le reporting unifié.
  • OU changer le prix manuellement et split les périodes → biaisé par la saisonnalité.

Trackily prend une autre route : un seul produit + un seul variant + N landings qui override le prix au render. Le checkout détecte la landing source, applique le bon override, et la conversion arrive dans la même métrique de produit (juste avec un effective_price différent capturé sur l'order).

Schema price_overrides

{
  "id": 31,
  "name": "MemoMind — $24.99 tier",
  "slug": "memomind-2499",
  "type": "ai",
  "price_overrides": {
    "42": 24.99,        // native_product_variants.id 42 → $24.99
    "43": 24.99         // si plusieurs variants partagent ce tier
  }
}

La clé est l'ID du native_product_variants row (en string JSON). La valeur est le prix override en dollars (float). Pour clear, passe {}.

Comment faire — via MCP

Tool : update_landing.

{
  "name": "update_landing",
  "arguments": {
    "id": 31,
    "price_overrides": { "42": 24.99 }
  }
}

Trackily écrit la map en JSONB sur la landing. Le checkout natif lit ces overrides au moment de pricer le cart (/native/checkout/pricing).

Pour effacer :

{
  "name": "update_landing",
  "arguments": { "id": 31, "price_overrides": {} }
}

Le setup MemoMind (campaign #8 — live sur Trackily)

Sur l'instance prod trackily.online, voici le setup A/B price test live :

Step 1 — Un seul produit natif

  • native_products row : "MemoMind 30-day pack".
  • native_product_variants row : id 42, price $34.99 (base price, le "ne sera jamais affiché").

Step 2 — 4 landings, chacune avec son override

{ "id": 31, "slug": "memomind-tier-1", "price_overrides": { "42": 24.99 } }
{ "id": 32, "slug": "memomind-tier-2", "price_overrides": { "42": 34.99 } }
{ "id": 33, "slug": "memomind-tier-3", "price_overrides": { "42": 44.99 } }
{ "id": 34, "slug": "memomind-tier-4", "price_overrides": { "42": 54.99 } }

Les 4 landings sont générées avec le MÊME HTML (archetype wellness, framework pas, image provider openai_gpt_image). Seul le prix affiché dans le bloc pricing.single_box est différent — il est interpolé depuis price_overrides au render.

Step 3 — Flow 25/25/25/25

{
  "name": "create_flow",
  "arguments": {
    "campaign_id": 8,
    "type": "default",
    "binding": "cookie",
    "binding_hours": 168,
    "schema": {
      "landings": [
        { "id": 31, "weight": 25 },
        { "id": 32, "weight": 25 },
        { "id": 33, "weight": 25 },
        { "id": 34, "weight": 25 }
      ],
      "offers": []
    }
  }
}

binding: cookie + 168h : le visiteur voit TOUJOURS la même tier au cours d'une fenêtre 7 jours — sinon il pourrait voir $24.99 le lundi puis $54.99 le mercredi en revenant via retargeting, ce qui ruine l'A/B et la confiance.

offers: [] car le checkout est natif (pas un postback affiliate). C'est l'order native qui capture la conversion + l'effective_price en fonction de la landing source.

Step 4 — Métrique decisive : revenue per 1000 clicks (RPM)

Tu ne mesures PAS la conversion rate par tier — elle décroît mécaniquement avec le prix. Tu mesures revenue par 1000 clics :

Tier Prix CR estimée RPM (CR × prix × 10)
1 $24.99 3.5 % $874
2 $34.99 2.4 % $840
3 $44.99 1.8 % $810
4 $54.99 1.3 % $715

(Chiffres illustratifs.) Le winner est le tier qui maximise RPM, pas le tier au CR le plus haut.

Step 5 — Kill switch automizer

{
  "name": "create_automizer_rule",
  "arguments": {
    "name": "Kill MemoMind tier 4 si RPM < $500 après 1000 clics",
    "campaign_id": 8,
    "conditions": [
      { "metric": "clicks",  "operator": ">=", "value": 1000, "window": "7d" },
      { "metric": "revenue", "operator": "<",  "value": 500,  "window": "7d" }
    ],
    "actions": [{ "type": "pause_landing", "landing_id": 34 }],
    "check_interval_minutes": 60
  }
}

(Comme la condition mesure la campagne entière, en production tu raffines avec une condition par-landing — voir automizer/conditions.)

Variations du pattern

A/B simple : 2 prix

{
  "schema": {
    "landings": [
      { "id": 31, "weight": 50 },
      { "id": 32, "weight": 50 }
    ]
  }
}

3 tiers (low / mid / premium)

{
  "schema": {
    "landings": [
      { "id": 31, "weight": 33 },
      { "id": 32, "weight": 34 },
      { "id": 33, "weight": 33 }
    ]
  }
}

Override discount targeted geo (forced flow)

Si tu veux que la landing #31 (prix bas) ne soit servie qu'aux pays low-income :

{
  "name": "create_flow",
  "arguments": {
    "campaign_id": 8,
    "type": "forced",
    "name": "Low-income geo cheaper",
    "filters": [{ "key": "country", "op": "in", "value": ["IN", "PH", "VN", "BR"] }],
    "schema": { "landings": [{ "id": 31, "weight": 100 }] }
  }
}

Puis un default qui sert les autres tiers aux pays high-income.

Différences avec un A/B test de landing classique

Aspect A/B landing classique A/B price overrides
HTML Différent par variante Identique
Variable testée Copy, design, structure Prix uniquement
Bias possible Plus de variables = bruit Une variable seulement = clean
Reporting Conv rate par landing RPM par landing
Setup time Long (créer N landings) Très court (un genai + N updates)

Le price overrides est strictement plus propre quand tu veux isoler la sensibilité prix.

Génération efficace des N landings

Tu peux utiliser generate_landing_variants pour cloner la landing originale 3 fois, puis update chaque variante avec son override de prix :

{ "name": "generate_landing_variants", "arguments": { "ai_landing_id": 31, "count": 3 } }

Réponse retourne [32, 33, 34] (les ids des nouveaux siblings). Puis :

{ "name": "update_landing", "arguments": { "id": 32, "price_overrides": { "42": 34.99 } } }
{ "name": "update_landing", "arguments": { "id": 33, "price_overrides": { "42": 44.99 } } }
{ "name": "update_landing", "arguments": { "id": 34, "price_overrides": { "42": 54.99 } } }

Toutes les variantes partagent le variant_group_id — bonus pour le reporting.

Le checkout natif et effective_price

Quand le visiteur passe au checkout depuis landing #31 (/native/checkout?landing_id=31), Trackily :

  1. Lit landing_pages.price_overrides pour id=31.
  2. Pour chaque variant dans le cart, override le prix si la clé existe dans la map.
  3. Stocke orders.effective_unit_price (snapshot — l'order garde le prix même si tu changes les overrides plus tard).
  4. Inscrit orders.source_landing_id = 31 pour pouvoir grouper par landing dans les reports.

Le report "Revenue by landing" est ensuite trivial : GROUP BY source_landing_id puis SUM(effective_unit_price * quantity).

Erreurs courantes

  • Override sur un variant.id qui n'existe pas : silent, l'override est ignoré (le prix base s'applique). Vérifie les IDs.
  • Clé en number vs string : { 42: 24.99 } vs { "42": 24.99 }. JSONB normalise en string — toujours utiliser des string keys côté JS.
  • Binding off : sans binding: cookie, le visiteur revient sur une tier différente, voit un autre prix, se sent floué. Toujours cookie sur du price testing.
  • Override > price base : si tu mets $74.99 quand le base est $34.99, OK c'est un upsell — mais vérifie que c'est intentional.
  • Mixer price_overrides + Stripe Coupon : double discount possible. Choisis un mécanisme à la fois.
  • Pas de revenue tracking : si la landing ne route pas vers un produit natif (type: redirect_tracker vers offer affiliate), les price_overrides ne servent à rien — le revenu vient du postback affiliate qui ignore les overrides.
  • Pas assez de volume par tier : 100 clics × 4 tiers = 25 clics chacun. Statistiquement nul. Attends 500-1000 clics par tier minimum.

Voir aussi