Methodology
How every score is computed.
Recipe Pub publishes interpreted signal, not raw rates. This page is the open methodology: every formula, every band threshold, every sample gate, every fallback. If a number on the wire seems wrong, this is the page that lets you check our work.
The thesis
Other food-trend APIs ship search volume. Some ship press counts. None ship first-party engagement from the people actually publishing recipes — because none of them have it. Recipe Pub has it through Recipe Kit's network of specialty CPG brands.
The thesis is simple. Raw rates are sensitive. A brand's exact print rate is the kind of number you don't put on a public wire without a contract. But the interpreted version — "tofu's reader engagement is strong, running 1.83x the entity-type median, with a high sample base" — is what a buyer actually needs and exposes nothing about any individual shop.
So the API publishes scores, bands, and indices. Raw rates stay in the database. The transformations are deterministic, documented here, and locked to a versioned score_default field so historical comparisons stay coherent.
Four signal sources
Every Recipe Pub score derives from at most four sources, all refreshed weekly on Sunday in the recipekit-insights cron.
- Search demand. DataForSEO Google Trends, ~300 curated food keywords across 9 category buckets. Exposed as volume bands and rising queries.
- Press coverage. RSS from ~25 food-press outlets plus GDELT, every headline since November 2024, OpenAI-tagged for primary topic, entity type, sentiment, mentioned brands and ingredients, audience fit, time anchor.
- First-party engagement. Aggregated weekly cohorts from Recipe Kit's network of specialty CPG brands. Print rate, share rate, ATC rate, rating average, and attributed revenue per view, all gated by sample size before publication.
- Risk signals. openFDA Food Enforcement + USDA FSIS recall feeds, joined to the trend topics via OpenAI ingredient tagging.
Composite trend score
The composite trend.score on /v1/topics/{topic} is a weighted sum of three normalised signals: search, press, and network. Default weights are 0.40 / 0.30 / 0.30 when network data is present, 0.55 / 0.45 / 0 when it is not. Each contribution is on the same 0 to 100 scale so a single composite number is interpretable without per-source context.
Pro and Enterprise customers can reweight at request time by passing ?weights=search:0.6,press:0.2,network:0.2. The custom score appears in score and the canonical default appears in score_default for alerting that needs a stable signal.
Network interpretation layer
This is the part most buyers care most about and that most APIs do not have. Recipe Pub exposes three interpreted blocks on the network side: reader engagement, recipe quality, and conversion strength. Each is anchored to the median of the topic's peer set (same entity_type, same week), computed in memory at request time.
Score formula
Every per-constituent index is the topic's rate divided by the entity-type median. The block score is the weighted geometric mean of present constituents, mapped to a 0–100 scale via a log-base-2 transform anchored at 50:
index = topic_rate / median_rate
weighted_index = 2 ^ ( sum_i(w_i * log2(index_i)) / sum_i(w_i) )
score = clamp(50 + 20 * log2(weighted_index), 0, 100) So parity with the entity-type median is 50, twice the median is 70, four times is 90, eight times pegs at 100. The downside is symmetric: half the median is 30, a quarter is 10. Geometric mean is the right aggregator for ratios — averaging 2x and 0.5x produces parity (1x), which matches intuition.
Source: api/src/lib/signal-interpretation.ts, indexToScore() and weightedLogMean().
Reader engagement
Two constituents, equally weighted: print_rate_pct and share_rate_pct. Both populated by Migration 006 in recipekit-insights, both per recipe view, both expressed as percentages. The block returns null when the engagement sample gate fails.
Recipe quality
Quality maps directly off the rating average on a fixed 0–5 scale rather than against a category-relative index. A loved rating is a loved rating regardless of category context, so the absolute scale is the honest one.
score = clamp(100 * (rating_avg - 3.0) / 2.0, 0, 100) So 3.0 stars maps to 0, 4.0 to 50, 5.0 to 100. Bands: loved at 75+, liked at 50–74, mixed at 25–49, panned below.
Rating band is reported separately on the actual star average: 4.5_plus, 4.0_plus, 3.5_plus, below_3.5.
Conversion strength
The only constituent is atc_rate_pct. Revenue per view is intentionally not in this score.
Why: attribution coverage is opt-in per shop. At the time this page was written, roughly 12 percent of publishable cuisines and 43 percent of title shapes had meaningful revenue per view above $0.01. Sorting or weighting on a sparse signal would tank scores for the 88 percent of topics that have universal print, share, and ATC data but no attribution. So ATC is the score, revenue is supplementary.
This will be revisited when attribution coverage crosses ~50 percent across publishable rows. The decision is pinned in api/src/lib/signal-interpretation.ts at the top of the file and in migrations/insights/006_engagement_columns.sql.
Revenue per view (supplementary)
When present, revenue surfaces in the network block as a separate field: revenue_per_view: { usd, vs_median_band, coverage }. The coverage flag is "high" when the entity-type median is at or above $0.01 per view, "low" when below, "unknown" when the median itself isn't computable.
When coverage is low or unknown, vs_median_band is null. We do not produce a comparison band against a noisy median — that would be confidently wrong, which is the one mode we avoid.
Sample-size gates
Every interpreted block has a sample gate. Below the gate, the block returns null and the row's sample_below_threshold flag is true. Above the gate, the block reports a sample_size_band: HIGH, MED, or LOW. These match the gates in recipekit-insights/src/lib/engagement-gates.js exactly.
| Block | Minimum sample for publication | HIGH band threshold |
|---|---|---|
| Reader engagement | n_brands ≥ 10 AND n_recipes ≥ 30 | n_brands ≥ 30 AND n_recipes ≥ 100 |
| Recipe quality | n_ratings ≥ 50 | n_ratings ≥ 200 |
| Conversion strength | n_brands ≥ 10 AND n_recipes ≥ 30 | n_brands ≥ 30 AND n_recipes ≥ 100 |
The gates were chosen to balance two failure modes. Set them too low and noisy single-recipe outliers leak into the score. Set them too high and most of the catalog is gated out. The current numbers reflect the smallest cohorts where the team has confidence the published rate is stable across re-randomisations of the underlying recipe set.
Band thresholds
Three band sets are in the response shape. The score bands and quality bands map from a 0–100 score; the rating bands map from the raw star average.
Score bands (engagement and conversion)
| Score | Band |
|---|---|
| 90 – 100 | exceptional |
| 75 – 89 | strong |
| 50 – 74 | moderate |
| 25 – 49 | weak |
| 0 – 24 | insufficient_signal |
Quality bands
| Score | Band |
|---|---|
| 75 – 100 | loved |
| 50 – 74 | liked |
| 25 – 49 | mixed |
| 0 – 24 | panned |
Rating bands
| Rating average | Band |
|---|---|
| 4.5 – 5.0 | 4.5_plus |
| 4.0 – 4.49 | 4.0_plus |
| 3.5 – 3.99 | 3.5_plus |
| 0 – 3.49 | below_3.5 |
Verdict templating
The verdict block on /v1/topics/{topic} is templated, not LLM-generated. We do not call an LLM on every request. There are eight templates keyed on (persistence label × press peak status × strongest network signal). The full table:
- accelerating + press rising_pre_peak → "Publish this week. Search is leading press — the window is open."
- accelerating + press at_peak → "Accelerating, press now at peak."
- accelerating + press past_peak → "Riding the tail. Worth a single recipe, not a series."
- cooling → "Pass unless this is evergreen for your shelf. Better trends below."
- plateau + strong conversion → "Safe content choice — converts above category median consistently."
- plateau (otherwise) → "Steady performer. Useful filler between trend bets, not the trend bet itself."
- breakout (no persistence yet, ≥5 new brands in 6w) → "Watch closely. Publish only if this fits your shelf — too early to chase."
- no signal → "Quiet. No play here this week."
Verdicts are deterministic from the underlying signal. If the same topic, same week, same data produced a different verdict twice, that is a bug. Source: api/src/lib/topic-composer.ts, composeVerdict().
Trajectory projection method
The /v1/topics/{topic}/trajectory endpoint returns history plus a forward projection. The projection method is selected automatically based on the topic's network history depth.
| Network history weeks | Method used |
|---|---|
| ≥ 52 | network_led_with_search_lead_lag |
| 26 – 51 | ensemble_search_plus_network |
| < 26 | search_led_with_recent_network |
Projections blend a year-over-year same-week anchor (when ≥ 52 weeks of history exist) with a recent-slope adjustment from the last 8 weeks. Confidence intervals come from baseline residual variance and widen with horizon. Confidence itself decays linearly with horizon, capping at 0.9 in week 1 and floor 0.2 at the far edge. Source: api/src/lib/trajectory.ts.
What Recipe Pub never publishes
Three rules govern what stays in the database and never crosses the API boundary.
- Per-shop metrics. No row in any API response is keyed on a single Recipe Kit shop. Even the network movers endpoint aggregates across at least 10 brands per row.
- Per-recipe metrics. No individual recipe's print rate, share rate, ATC rate, or revenue per view is exposed. Aggregated topic-level versions are exposed under sample gates.
- Raw rates. Even at the topic level, the rate values that drive the engagement scores are not in any public response shape. The score and band are the public surface.
The single exception is the Enterprise tier, under a non-redistribution contract, for which Recipe Pub can negotiate raw-rate access for analyst use cases. Pro tier does not include raw access. That is the moat and the privacy promise to the Shopify merchants in Recipe Kit's network.
Where to verify
Every formula on this page maps to a function in the public source. If you want to read the code:
api/src/lib/signal-interpretation.ts— score formulas, gates, bands, indicesapi/src/lib/topic-composer.ts— verdict templates, response compositionapi/src/lib/trajectory.ts— projection method selection and CI mathapi/tests/signal-interpretation.test.ts— 31 tests pinning every score boundary, gate, and bandmigrations/insights/006_engagement_columns.sql(inrecipekit-insights) — schema for the engagement rate columns and the framing decision around revenue coverage
If a score on the wire seems wrong, email patrick@recipekit.com with the topic and the week. I read everything and respond personally.