---
id: "5.7"
title: "Cards and the feed"
dept: engineering
status: draft
owner: Param
branded: false
last_verified: 2026-10-26
sources:
  - "brew origin/main: cards-gen.js:1-30; feed-service.js:1-30, 535-568 (score), 696-725 (feed layout), 1312 (stories); value.js:1-6; plain.js:1-5, 18; evidence.js:1-6; proof-logic.js; card-nums.js; same-ask.js; member-tuning.js:1-6; social-feed.js; community-gate.js; research.js:1-5"
  - "brew origin/main: SCOPE.md (Feed, Your feed rows)"
  - "brew origin/main: docs/handover/engineering/02-feature-audit.md section 3, 12-ui2-service-api.md section 2"
  - "memory brew-stories-and-cards, brew-community-seat, brew-proof-plain-make, brew-card-fit-and-counts, brew-one-version, brew-drop-learnings, brew-qa-round2-2026-10-24"
---

# Cards and the feed

## Answer

Cards are made from Plays, the read, listening, ideas, stories, watchers and statics, healed as they are read, and ranked per person by one score with fixed first-screen seats; this page settles how a card is made, how the feed is ordered and which knobs tune it.

## Today

### Where cards come from

| Source | Maker | Source |
|---|---|---|
| Plays | `cards-gen.js`: 1 or 2 cards per Play; the title is the point, the body two sentences (25 to 35 words), every number in the fact sheet | [src: brew/cards-gen.js:1-3] |
| The read | instant cards at read time | [src: brew/build.js:601] |
| Listening and ideas | `social-feed.js` `heardCards`, `ideaCards`; threads must pass `community-gate.js` | [src: brew/social-feed.js] [src: memory brew-community-seat] |
| Stories | `research.js` (ads, trends, news, how-to, viral) | [src: brew/research.js:1-5] |
| Watchers | catalog every 3 hours, AI answers weekly (`live-watch.js`) | [src: brew/live-watch.js:1-5] |
| Statics | Play 21's static ads (`statics.js`) | [src: brew/statics.js:1-4] |
| Connected accounts | `live-*` cards, members only | [src: brew/connectors/index.js:324] |

### Rules every card passes

- **Fact gate:** every money amount and percent must be in the fact sheet; strict mode also gates multipliers and large counts. [src: brew/fact-gate.js:1-5]
- **Claims:** a card says only what the brand itself says (`claims.js`). [src: brew/cards-gen.js:6-7]
- **Today's counts:** a count row naming one fact takes today's value, never a rival's, never to or from 0, never a jump over 5×. [src: memory brew-card-fit-and-counts]
- **The right button:** `fitCta` and `fitMedium` heal the button and format as the card is read (a Reddit post never gets an Instagram format). [src: brew/feed-service.js:9] [src: memory brew-read-stream-backfill]
- **One take:** no card promises "3 ads"; one version, then Redo it. [src: memory brew-one-version]
- **Plain words:** one background pass rewrites title, body, reasons and steps in everyday words, numbers checked in code, cached in `plain.json` (`PLAIN_V` 3); `PLAIN_OFF=1` stops it. [src: brew/plain.js:1-5, 18] [src: brew/SCOPE.md]
- **Proof:** a shield with a count opens "Why this, and the proof", built with no AI from facts, accounts, quotes, rival ads and the pages read; a Play is never evidence; the proof must prove the claim (`proof-logic.js`). [src: brew/evidence.js:1-6] [src: memory brew-proof-plain-make]
- **One card per ask:** six Plays saying "fix the pack prices" show once (`same-ask.js`). [src: brew/feed-service.js:27]

### The score

For each card and viewer, `score()` adds: [src: brew/feed-service.js:539-568]

| Part | What it is |
|---|---|
| value | the Play's grade (impact 1 to 5, confidence, effort) from `value.js`, one fast call per 20 Plays [src: brew/value.js:1-6] |
| seat | fit to the viewer's role, lane and stated focus words |
| affinity | hearts and clicks by agent, campaign, format, product; minus "Not for me", with near titles sinking by 4 |
| fresh | streaming cards decay over 24 hours, others over 72 |
| fatigue | days seen, the agent's share of today's screen, quiet jams |
| feedback | "Fewer" minus 2 per step |
| tune | the member's own weights (less −4, more +4 per agent) and topics (+2 each, two at most) [src: brew/docs/handover/engineering/12-ui2-service-api.md] |
| told and lift | a Play rewritten on the team's learnings +6; team cards +10, learned up +8 |
| contra | the team said it does not work: −40 until "bring back" |
| extra | urgency (sold out, days left), a first draft to look at (+3), statics and ads (+2), live account cards |
| behind | its Play is being rewritten: −8 |
| chan, pace | channels the viewer runs and their ad pace |

### The layout

- Pins from the "new cards" pill sit on top for 30 minutes. [src: brew/feed-service.js:700]
- Two picture cards for every text card, unless the other kind is more than 2 points stronger. [src: brew/feed-service.js:703-706]
- Social seats: position 3 the best community reply or post, 7 the best picture post, 9 another text card. [src: brew/feed-service.js:708-715]
- Make seats: positions 2 and 6 hold an ad or post the brand can make, from different groups, never a rival's ad; a finished static takes one. [src: brew/feed-service.js:716-722]
- No two cards of the same sort in a row. [src: brew/feed-service.js:723]
- Storefront cards are hidden in v1 (`?storefront=1` shows them). [src: brew/SCOPE.md]

### Stories

Five rings always show: Brand today, Trends, Popular ads, Industry news, How to. Each story lives 24 hours from first seen; an expired one can still rank as a card. A ring whose stories expire reads its source again, at most every 2 hours. Story score is 0.4 freshness plus 0.6 strength. [src: memory brew-stories-and-cards] [src: brew/feed-service.js:1312]

## Tomorrow

| Item | Version | Tag |
|---|---|---|
| One score, fixed seats, plain words and proof on every card | v1 | Stays |
| Ranking weights checked on fixtures from several brands | v2 | New |
| Cards in "we" voice across every writer | v1 | Changes (open from QA 25 Oct) [src: memory brew-qa-round2-2026-10-24] |
| Storefront cards back in the feed | v2 | Changes |
| Ranking tests for `score`, `rank` and `feed` | v2 | New |

## How we do it

1. When "X cards never show", check where the ranker puts them before blaming the source. [src: memory brew-community-seat]
2. Before shipping a card-rule change, compare old and new code on every saved card:

```bash
node tools/audit-card-fit.mjs DIR --before
```

[src: memory brew-card-fit-and-counts]

3. Heal wherever text is read (pack load, `pub`, jam brief), not only at load; shared jams and workspace copies outlive pack heals. [src: memory brew-one-version]
4. New brand routes must be added to the three path regexes in `feed-service.js`, or they return "not found". [src: memory brew-drop-learnings]

## Numbers

| Metric | Definition | Where measured |
|---|---|---|
| Cards served per brand | Visible cards in a member's feed | `GET /api/tenants/:t/feed` |
| Wrong-medium and empty-asset cards | Cards failing the fit audit | `tools/audit-card-fit.mjs` (0 of 9,540 on 22 Oct) [src: memory brew-card-fit-and-counts] |
| Proof coverage | Share of cards whose numbers all match a source | `evidence.js` counts (Proposed to aggregate) |
| Feed load time | Server time for the feed answer | `Server-Timing`, `[slow]` log over 1.5 s |

## Open

1. **Are the ranking weights right?** Proposal: build fixtures from the five partners and freeze the top 20 per brand as a test, then tune.
2. **Should Storefront come back for v2?** Proposal: yes for Shopify brands with a connected store only.
