> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Next-best-product recommendations

> How affinity ranking scores a candidate catalog against a profile's recent purchase and browse signals, how to read the per-factor breakdown in the Recommendations tester, when to use it, and where it stops.

# Next-best-product recommendations

Orbit ships next-best-**action** (decisioning) and next-best-**channel**
(channel optimizer) — the recommender on this page closes the third gap:
next-best-**product**. Given one profile's purchase and browse history and
a candidate catalog, it ranks the items that profile is most likely to buy
next and returns each pick with a blended score and a per-factor affinity
breakdown. The [Recommendations tester](#the-recommendations-tester) in the
dashboard lets you exercise it interactively; the same ranking is exposed
programmatically on the CDP API.

## Section 1 — How affinity ranking works

The engine is pure content-affinity scoring. Every request carries its own
inputs — it derives the ranking from the request body and stores nothing:

* **Candidate catalog** — the products eligible for this ranking. Each item
  carries a product id and, optionally, a title, category, brand, unit
  price, free-form tags, a popularity prior in `[0,1]`, and an in-stock
  flag. A request ranks at most 2,000 candidates; items with no product id,
  out of stock, in `exclude_product_ids`, or already purchased by the
  profile (when suppression is on — the default) are excluded before
  scoring.
* **Signals** — the profile's recent interactions: purchases, cart adds,
  wishlist saves, product views, and clicks. Each signal carries the
  category, brand, or product id it touched, a monetary value for
  purchases, and an age in days.
* **Profile traits (optional)** — computed CDP affinities: a
  category-affinity map, a brand-affinity map, and a list of preferred
  tags. Traits merge with the behavioural signals, so an affinity the CDP
  already learned counts even when you do not restate it as an event.
* **Profile id (optional)** — a contact id (`cnt_…`) or an external CDP
  profile id. The rank does not *need* the profile id — it derives from
  the entered signals — but providing it ties the ranking to a stored
  profile and, in an experiment, makes A/B assignment reproducible per
  profile.

Signals are weighted by **intent** — a purchase outweighs a cart add,
which outweighs a view — and **decayed by recency** with a configurable
half-life (default 30 days, so a month-old interaction counts half as much
as a fresh one). Purchase values build the profile's observed average
spend, which the price-fit factor uses below.

### The five factors

Each surviving candidate is scored on up to five factors, each normalized
to `[0,1]`, blended with relative weights:

| Factor     | What it measures                                                                                                                                                                     | Default weight |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
| Category   | Affinity between the item's category and the profile's recency-decayed, intent-weighted category affinities (signals + traits, normalized across the profile's categories)           | 0.40           |
| Brand      | The same computation over the item's brand                                                                                                                                           | 0.20           |
| Tag        | Overlap of the item's tags with the profile's preferred tags                                                                                                                         | 0.15           |
| Price fit  | Proximity of the item's price to the profile's observed average purchase value, on a scale relative to that average — the factor works for a discount catalog and a luxury one alike | 0.15           |
| Popularity | The item's optional popularity prior — a tie-breaker for cold-start profiles with little history                                                                                     | 0.10           |

Two properties of the blend matter in practice:

* **Weights are relative.** They need not sum to 1; the final score is a
  weighted average over the applicable factors. Set only the weights you
  want to change — every unset one keeps its default.
* **Only applicable factors are blended.** A cold-start profile with no
  purchase history has no spend average and no tag preference. Instead of
  being penalized for absent evidence, its score is divided only by the
  factors that have any signal — so cold-start items rank fairly on what
  *is* known.

The result is deterministic: ties break by score, then popularity, then
product id, so the same request always returns the same ordering.

## Section 2 — The Recommendations tester

The tester (**Audience → Recommendations** in the dashboard) exercises the
ranker end to end — nothing you enter is sent to a customer or persisted
into the CDP; only your working draft is saved to your account so a
refresh, a new session, or another device restores the grid you left.

1. **Enter the candidate catalog.** Add one row per product: id, title,
   category, brand, price, comma-separated tags, and the in-stock switch.
2. **Enter the profile's signals.** One row per interaction: type
   (purchase, cart, wishlist, view, or click), the category/brand/product
   it touched, its age in days, and the purchase value where one applies.
3. **Optionally set the scope.** A profile id ties the run to a stored
   profile — the tester resolves the id as you type and warns when no
   profile matches, so a mistyped id never passes for real. Limit caps the
   number of returned picks (default 10, at most 100); half-life days set
   the recency decay; the suppress-purchased switch removes items the
   signals say the profile already bought.
4. **Optionally tune factor weights.** Leave a weight blank to keep the
   engine default; the others override it.
5. **Rank.** The result table lists the picks in rank order with the
   blended score, plus the run metadata — candidates considered, excluded,
   returned, and the half-life in effect.

### Reading the per-factor breakdown

Every row expands into the five factor scores that produced the blended
score. This is how you *explain* a ranking rather than merely read it:

* A pick with a high **category** but low **brand** score won on the
  profile's interest in the category overall, not on a favourite brand —
  the typical "what should the carousel's next slot be" answer.
* A weak **price fit** on an otherwise strong pick tells you the item is
  priced away from the profile's comfort zone; tune the price-fit weight
  and re-rank to see the sensitivity.
* When every factor reads low but one item still ranks first, the ranking
  was decided by exclusion — the stronger candidates were out of stock,
  excluded, or already purchased — not by affinity. The excluded count
  above the table confirms it.

## Section 3 — Using the ranker

The tester is the interactive front door; the same ranking is available
programmatically at `POST /api/v1/cdp/recommendations/rank` with the same
inputs and response shape (snake\_case field names, full parameter detail
in the [CDP API reference](/api-reference/endpoints/cdp)). It is role-gated
to owner, admin, or developer, like the other CDP compute surfaces.

Typical uses:

* **E-commerce recovery flows.** Rank a cart profile's recent signals
  against the current catalog and render the top picks into an RCS or
  WhatsApp in-thread carousel — personalization for the abandoned-cart
  win-back message rather than a static best-seller list.
* **Loyalty and loyalty-store merchandising.** Rank the reward or
  cross-sell catalog for a points-heavy member so the loyalty surface
  leads with items that match demonstrated affinities.
* **A/B measurement of ranking strategies.** Pass an `experiment` with
  named variants, each with its own factor weights. Every profile is
  deterministically bucketed by a stable hash of the experiment key and
  profile id — the same profile always lands in the same variant — and
  the assigned variant is echoed back so conversions attribute to a
  strategy. No RNG, fully replayable.

The ranker **only ranks — it never sends.** Rendering the picks into a
message and dispatching them belongs to the caller (the catalog send path
or a campaign orchestrator), so the ranking can be exercised and measured
without any outbound traffic.

## Section 4 — Limits: what this is not

Scope the recommender deliberately — it is a scoring primitive, not a
hosted recommendation service:

* **You bring the inputs.** The engine stores nothing and reads no tables
  at request time. Rolling events up into signals and traits, and keeping
  the candidate catalog current (including the in-stock flag), is the
  caller's job; the tester exists to make that visible.
* **Content-based affinity only.** It scores category, brand, tag, price,
  and popularity matches. It does not learn embeddings, run collaborative
  filtering ("customers who bought X bought Y"), or model sequence. If
  your use case needs those, treat this ranker as the fast, explainable
  first layer and a full recommendation engine as the deep layer behind
  it.
* **Per-profile, per-request.** One request ranks one profile against the
  supplied candidate set (60 requests per minute per tenant). It is not a
  batch pipeline that pre-computes a ranking for every profile on a
  schedule.
* **Deterministic, not trained.** Because the blend is a fixed, inspectable
  computation over the inputs you supply, the per-factor breakdown is the
  explanation. Nothing retrains between requests; a ranking changes only
  when its inputs change.

## Cross-references

* [The CDP event model](/concepts/cdp-event-model) — where the purchase and
  browse signals originate.
* [CDP segments](/guides/cdp-segments) — building the audience whose
  members you rank.
* [CDP API reference](/api-reference/endpoints/cdp) — full request and
  response detail for `recommendations/rank`.
