SemiLayerDocs

Feeds — Explain

"Why did this item rank here?" The explain endpoint returns the per-scorer math for a single record — raw value, weight, weighted contribution, final composite score, and page rank.

Because it leaks the exact ranking arithmetic, it's sk-only. Public keys get a 403. Put it behind your backend and expose a redacted form to end users if you want.

The response shape

interface FeedExplanation<M> {
  recordId:    string
  finalScore:  number
  rank:        number | null             // position in the page, 1-indexed; null if excluded
  contributions: FeedScorerContribution[]
  metadata:    M
}

interface FeedScorerContribution {
  scorer:        'similarity' | 'recency' | 'engagement' | 'diversify' | 'boost'
  weight:        number                  // from config
  rawValue:      number                  // scorer output before weighting
  weightedValue: number                  // weight × rawValue (or `add` for boosts)
}

finalScore = Σ weightedValue + Σ boosts. diversify appears with weightedValue: 0 — it reorders, it doesn't score.

Call it

import { beam } from './beam'

const explanation = await beam.recipes.feed.discover.explain({
  recordId: 'r_104',
  context:  { liked_titles: ['Tom Yum Goong', 'Laksa'] },
})

// explanation.contributions
// [
//   { scorer: 'similarity', weight: 0.6, rawValue: 0.91, weightedValue: 0.546 },
//   { scorer: 'recency',    weight: 0.1, rawValue: 0.72, weightedValue: 0.072 },
//   { scorer: 'engagement', weight: 0.3, rawValue: 0.88, weightedValue: 0.264 },
//   { scorer: 'boost',      weight: 1,   rawValue: 0.15, weightedValue: 0.15  },
// ]
// explanation.finalScore  === 1.032
// explanation.rank        === 1

CLI

Fast iteration loop:

semilayer feed explain recipes.discover \
  --record r_104 \
  --context '{"liked_titles":["Tom Yum Goong","Laksa"]}' \
  --api-key sk_production_...

Output:

record      r_104
rank        #1 of 12
finalScore  1.032

scorer       weight    raw   weighted
────────── ──────── ──────── ──────────
similarity    0.60    0.91     0.546
recency       0.10    0.72     0.072
engagement    0.30    0.88     0.264
boost         1.00    0.15     0.150

Use --json for a machine-readable form. --api-key must start with sk_ — public keys are rejected before the request hits the wire.

In-app "why?" overlay

The explain endpoint is sk-only, so the public-facing "why is this at the top?" widget needs a small proxy on your backend. Shape:

// your backend
app.post('/api/why/:recordId', async (req, reply) => {
  const { recordId } = req.params
  const claims = await verifyUserJwt(req)

  const explanation = await beam.recipes.feed.discover.explain({
    recordId,
    context: buildUserContext(claims),
  })

  // Redact the exact weights if you don't want users reverse-engineering
  return {
    topContributor: explanation.contributions.reduce((a, b) =>
      b.weightedValue > a.weightedValue ? b : a
    ).scorer,
    rank: explanation.rank,
  }
})
// your component
async function showWhy(recordId: string) {
  const why = await fetch(`/api/why/${recordId}`).then((r) => r.json())
  toast(`Ranked #${why.rank} — boosted by ${why.topContributor}`)
}

What explain doesn't tell you

  • It doesn't explain candidate selection. If a row never made it into the candidates.limit pool, explain doesn't know why. That's a different question ("show me everything that matched the pre-filter"), served by the upcoming candidates debug endpoint.
  • It doesn't explain dedup. A record excluded by within: 'session' or excludeIds returns rank: null. The page didn't drop it for scoring reasons.
  • Returned values are pre-clamp. Individual scorers clamp value to [0, 1], but rawValue shows the computed number before clamping — useful for tuning similarity thresholds.

Explain is the feedback loop for your weights. When a feed underperforms, run it on a few example records and watch where the contributions land.