gridscoot
data

Affiliate attribution

How a click on a Gridscoot search result becomes an attributable retailer commission — with full disclosure of where the money flows.

Overview

Gridscoot earns affiliate commission on outbound clicks. The model is pay-on-click— no placement fees, no “ranked higher because they pay more.” The retailer pays the affiliate network on a successful purchase; the network pays us. We never see the customer’s payment.

The click lifecycle

Every outbound click follows the same five-step lifecycle:

  • 1. Search request — agent (or browser) calls search_products or GET /api/v1/search. The server returns offers with product_url(raw retailer URL) plus internal references the search response doesn’t expose directly.
  • 2. Click mint — when the user (or agent) selects an offer, they hit /go/[click_id] on Gridscoot. The server mints a unique click_id (BIGSERIAL), logs the click in affiliate_clicks with a hashed IP + user-agent fingerprint, and 302-redirects to the retailer.
  • 3. URL building — the redirect URL is the raw product_url augmented with affiliate-network parameters via packages/attribution/src/url-builder.ts. The exact param set depends on the network (eBay Partner Network, Commission Factory, Amazon Associates AU).
  • 4. Retailer purchase— the user completes a purchase on the retailer’s site. The retailer’s checkout reads our affiliate cookie / URL parameters and records the attribution.
  • 5. Network reporting — the affiliate network reconciles the purchase against our click_id(echoed back via the network’s tracking param) and credits the commission. We receive the report via the network’s API or CSV export.

URL builder — the per-network detail

Each affiliate network expects different URL parameters. We maintain a central packages/attribution/src/url-builder.ts with one builder per network. Below is the EPN (eBay Partner Network AU) example — same shape applies to the others, different params.

packages/ingestion/ebay-au/src/affiliate-url.ts
// eBay Partner Network (EPN) URL format
export function buildAffiliateUrl(
  productUrl: string,
  clickId: string,
  campaignId: string = process.env.EBAY_EPN_CAMPAIGN_ID ?? 'DEV',
): string {
  if (!isEbayAuUrl(productUrl)) return productUrl;  // pass-through non-eBay

  const url = new URL(productUrl);
  url.searchParams.set('mkrid', '705-53470-19255-0');  // AU rotation
  url.searchParams.set('siteid', '15');                 // eBay AU site ID
  url.searchParams.set('campid', campaignId);           // our EPN campaign
  url.searchParams.set('customid', clickId);            // our click_id (round-trip)
  url.searchParams.set('toolid', '10001');              // deeplink tool ID
  return url.toString();
}

The customid parameter is the round-trip token — we set it to our internal click_id, EPN echoes it back in their commission report, and we reconcile the commission to the click that generated it.

Reconciliation

Reports come back from each network on different cadences (EPN: daily CSV; CF: API poll; Amazon: monthly batch). Reconciliation runs in packages/attribution:

  • For each line in the report, parse the click_id (echoed via network-specific param)
  • Look up the affiliate_clicks row by click_id
  • Set finalised_at + commission_aud + order_id
  • If click_id not found (rare; typically click-id mismatch or impression-without-click), log a structured warn and route to manual_review_queue

We never see the customer’s payment, shipping address, or PII. The reconciliation report contains only: our click_id, the retailer’s order id, the order subtotal in AUD, the commission rate, and the commission amount. That’s it.

Ranking impact: zero

Affiliate commission rates are stored in retailers.commission_band on the server. They are never read at search-rank time. The full ranking function lives in apps/web/src/lib/search/run-search.ts— search the file for “commission” and you’ll find zero hits.

The two-stage ranking:

  • Product ranking — by hybrid_search SQL RPC (pg_trgm + word_similarity + pgvector embedding distance + brand similarity). Affiliate-rate-independent.
  • Offer ranking — within a product, offers sort cheapest-total-first (price + shipping). Ties broken by in-stock first, then feed freshness. Retailer name is a stable tiebreaker for display, not a rank signal.

This is auditable: every search response includes a query_meta.search_method field naming the algorithm by name. The full disclosure is at /rankings.

Disclosure obligations we comply with

AU + global rules we hold ourselves to (verified row-by-row at /compliance):

  • ACL § 18 (Misleading or deceptive conduct)— Gridscoot’s ranking algorithm is fully disclosed; no paid placement; data-source advisory shipped on every search response
  • ACCC Advertising on Digital Platforms — disclosure of paid relationships /partners + /about disclose the affiliate model in plain English; outbound click URLs flagged rel="nofollow sponsored" in markup
  • ACCC: ranking must not misrepresent merit-based ordering — two-stage sort fully named on /rankings; affiliate commission % explicitly never read at rank-time; this doc is the auditable engineering claim