← All posts

2026-07-08

How ScrapeKey Scores and Ranks Affiliate Matches for a Keyword

Affiliate marketingRankingFastAPIMonetizationScrapeKey

ScrapeKey affiliate match program cards on mobile

Part of Building ScrapeKey — Product Engineering Notes. See also the ScrapeKey case study.

Introduction

The hook

A creator opens an affiliate network, types “coffee maker,” and gets three thousand programs — none ranked for their keyword.

Spreadsheets appear. Gut feel appears. Three weekends disappear.

ScrapeKey’s promise is narrower and harder: given keywords from a project, return a short list of programs ranked by how well they fit to monetize that phrase — with scores you can explain.

The objective

By the end of this post you’ll understand ScrapeKey’s matching pipeline:

  1. Load an active affiliate catalog.
  2. Score each keyword × program candidate on several dimensions.
  3. Cut low-relevance noise, keep top-N.
  4. Layer a separate monetization score (0–100) for UI badges.
  5. Persist matches and show Est. revenue signals without pretending to be Google Keyword Planner.

Prerequisites

  • Comfortable with weighted sums and 0–1 normalized scores
  • Basic CRUD with a document DB
  • Optional: prior post on plan limits (Pro unlocks deeper matching / caps)

Body

Step 1: Frame the pipeline before the math

ScrapeKey scoring pipeline diagram

[ Project keywords ]
        │
        ▼
[ Active catalog programs ]  ← policy: only matchable programs
        │
        ▼
for each keyword × program:
   relevance, commission, competition, trend, performance
        │
        ▼
 overall = Σ (component × weight)
        │
        ▼
 filter relevance ≥ threshold → sort → top N per keyword
        │
        ▼
[ MatchedAffiliate rows + monetization_score for UI ]

Product callout: Creators do not want “200 matches.” They want five they might join this week. Default max_matches_per_keyword in ScrapeKey is small (e.g. 5). Ranking quality beats dump size.


Step 2: Weighted overall score — make tradeoffs visible

AffiliateMatcher owns explicit weights (easy to tune and to defend in design review):

class AffiliateMatcher:
    def __init__(self):
        self.weight_relevance = 0.3
        self.weight_commission = 0.25
        self.weight_competition = 0.2
        self.weight_trend = 0.15
        self.weight_performance = 0.1

They sum to 1.0. That is intentional: every component is a 0–1-ish score, and overall stays comparable across programs.

Core loop (simplified from production):

for keyword in keywords:
    candidates = []
    for program in affiliate_programs:
        relevance_score = self._calculate_relevance_score(keyword, program)
        if relevance_score < 0.03:   # hard cut — noise floor
            continue
        commission_score = self._calculate_commission_score(program)
        competition_score = self._calculate_competition_score(keyword, program)
        trend_score = self._calculate_trend_score(keyword, program)
        performance_score = self._calculate_performance_score(program)
        overall_score = (
            relevance_score * self.weight_relevance
            + commission_score * self.weight_commission
            + competition_score * self.weight_competition
            + trend_score * self.weight_trend
            + performance_score * self.weight_performance
        )
        candidates.append({...})
    candidates.sort(key=lambda x: x["overall_score"], reverse=True)
    matches.extend(candidates[:max_matches_per_keyword])

Why a relevance floor? Without it, high commission alone can surface junk matches that barely share a token with the keyword. Commission is valuable after fit.


Step 3: Relevance is text fit — not magic ML (for v1)

v1 relevance in ScrapeKey is classic IR-ish work: tokenize the keyword, compare against program name, categories, description, and related fields (sequence similarity, overlapping tokens, category hits). The point of the blog-level lesson:

  • Start with explainable features.
  • Store match_data.relevance_factors for debugging and UI transparency.
  • Raise the floor threshold when the catalog grows dirty.

Product callout: When a user asks “why this program?”, you need a story better than “the model said so.” Breakdown fields earn trust in beta feedback.


Step 4: Commission as a normalized score

Programs store commission as a dict (type, value) — sometimes percentage, sometimes fixed. ScrapeKey normalizes:

  • Percentagemin(value / 100, 1.0)
  • Fixed → approximate against an average order value assumption

Malformed legacy rows are read through a safe helper so one bad document doesn’t kill a match job:

def _commission_field(commission, key: str, default=0):
    if not isinstance(commission, dict):
        return default
    ...

Hiring managers care that you planned for dirty catalog data. Affiliate feeds are never clean.


Step 5: Catalog policy before brute force

Matching every keyword against every program forever does not scale. ScrapeKey loads programs through active_programs_for_matching() (policy layer) and falls back to the full collection only if needed.

Tutorial takeaway: separate “what is in the catalog” from “how we score.” Ingestion, network status, and niche coverage belong in catalog health — not inside the scorer.


Step 6: Monetization score is a second lens (0–100)

Match overall_score answers: “How good is this keyword↔program pairing?”
Monetization score answers: “How money-shaped is this keyword for a solo affiliate creator?”

They share signals but are not the same. Tools and keyword rows use:

FIT_BASE = {"high": 42, "medium": 28, "low": 12}
INTENT_BONUS = {
    "transactional": 18,
    "commercial": 14,
    "informational": 6,
    "navigational": 2,
}

def compute_monetization_score(*, affiliate_fit=None, intent=None,
                               top_match_relevance=None, top_commission_pct=None,
                               search_volume=None, difficulty=None,
                               volume_is_live=False, difficulty_is_live=False):
    ...
    # Live SEO metrics only boost when explicitly marked live

Tiers for UI badges:

ScoreTier
≥ 70high
≥ 45medium
elselow

Product callout: ScrapeKey refuses to mix estimated volume into the score as if it were live SEM rush. Estimated metrics can decorate the card; they do not quietly inflate monetization until volume_is_live / difficulty_is_live is true. That honesty is a product decision encoded in a boolean.

Serialization for the UI may rescale overall for display (e.g. 0–10 cards) while keeping raw fields for debugging — see match_serialization.py.


Step 7: Persist, notify, don’t block the human loop

After scoring:

  1. Upsert MatchedAffiliate rows (update scores if the pair already exists).
  2. Optionally notify (“Found N matches for your project”).
  3. Return cards sorted for the matches page / mobile view.

Estimated clicks/revenue helpers exist for storytelling on cards. Treat them as illustrative unless backed by real analytics — label them the way you label Est. volume.

Product callout: Matching is Pro-shaped work: unlimited projects, richer catalog, and “what do I join?” CTA. Free users get tools vocabulary; Pro users get the ranked shortlist.


Step 8: How you’d extend this without rewriting everything

Good next steps (roadmap-honest):

  • Tighten relevance with embeddings behind the same weight interface.
  • Pull live commission from network APIs into _commission_field.
  • Add per-niche catalog coverage metrics (already sketched in catalog health).
  • A/B weight sets by vertical (fitness vs SaaS).

The contract stays: components → weights → overall → top-N → UI score.

Step 9: What good “match QA” looks like

Before you trust weights in production, run a thin eval set:

KeywordExpect to seeExpect to suppress
best coffee makerskitchen / appliance programsrandom B2B SaaS
project management softwareSaaS / productivityoutdoor gear
yoga matfitness / wellnessenterprise cloud

Score a golden list of “should rank in top 5” vs “must not appear.” When you change weights, re-run the table. That is engineering maturity without a full ML platform.

Product callout: Beta testers are your first eval set. When a beta tester says “this match makes no sense,” capture the keyword + program + score breakdown. That ticket is more valuable than another UI polish pass.


Worked example (narrative)

Seed keyword: best coffee makers.

  1. Intent → commercial / transactional lean.
  2. Affiliate fit → high (buying language).
  3. Programs in “home / kitchen” categories get stronger relevance.
  4. High % commission boosts commission_score.
  5. Low relevance espresso-machine B2B SaaS programs fall under 0.03 and never appear.
  6. Top five overall_score rows surface as cards; monetization_score paints the badge.

That is the “weekend of research” compressed into one request.


Conclusion and next steps

Summary

  • Match ranking is a weighted multi-factor score, not a single vibe number.
  • A relevance floor protects the list from high-commission spam.
  • Monetization score is a second, product-facing 0–100 lens with honest Est./live rules.
  • Dirty catalogs and SSRF-free tools (prior post) matter as much as clever weights.
  • Persist top-N; optimize explainability for beta users.

Call to action

If you ship recommendations (jobs, courses, offers, programs): write down your weights. If you can’t, you don’t have a ranking system — you have a sort by created_at.

This closes the three-part set: plan limitsfree tools funnel → match scoring. See the ScrapeKey case study for the product narrative.

References

  • Classical ranking / linear feature models (IR textbooks; BM25 as contrast for pure text)
  • ScrapeKey: api/services/affiliate_matcher.py, api/services/monetization_score.py, api/services/match_serialization.py, api/services/keyword_quality.py
  • Portfolio diagrams: public/images/projects/scrapekey-diagram-scoring.png, scrapekey-diagram-funnel.png (SVG masters under docs/case_studies/extracted/assets/diagrams/)