{
 "id": "5.7",
 "title": "Cards and the feed",
 "department": "Engineering",
 "department_slug": "engineering",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "Mon Oct 26",
 "holds": "Generation, ranking, rings, tuning",
 "urls": {
  "html": "https://handbook.joinsquareup.com/engineering/cards-and-the-feed",
  "markdown": "https://handbook.joinsquareup.com/engineering/cards-and-the-feed.md",
  "json": "https://handbook.joinsquareup.com/engineering/cards-and-the-feed.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Cards come from seven sources and are fact checked first",
  "points": [
   {
    "icon": "box",
    "text": "Sources: Plays, the read, listening, ideas, stories, watchers, statics and accounts."
   },
   {
    "icon": "check",
    "text": "Fact gate: every money amount and percent must be in the fact sheet."
   },
   {
    "icon": "eye",
    "text": "A shield opens Why this, with proof built from facts, no AI."
   },
   {
    "icon": "heart",
    "text": "One take per card, then Redo it; six Plays saying one thing show once."
   },
   {
    "icon": "chart",
    "text": "Audit result on 22 Oct: no wrong medium or empty asset cards."
   }
  ],
  "stat": null,
  "decide": "Are the ranking weights right? Proposal: freeze each partner's top 20 as a test, then tune.",
  "mood": "solid"
 },
 "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"
 ],
 "open_questions": [
  "**Are the ranking weights right?** Proposal: build fixtures from the five partners and freeze the top 20 per brand as a test, then tune.",
  "**Should Storefront come back for v2?** Proposal: yes for Shopify brands with a connected store only."
 ],
 "parts": {
  "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\n\n| Source | Maker | Source |\n|---|---|---|\n| 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] |\n| The read | instant cards at read time | [src: brew/build.js:601] |\n| Listening and ideas | `social-feed.js` `heardCards`, `ideaCards`; threads must pass `community-gate.js` | [src: brew/social-feed.js] [src: memory brew-community-seat] |\n| Stories | `research.js` (ads, trends, news, how-to, viral) | [src: brew/research.js:1-5] |\n| Watchers | catalog every 3 hours, AI answers weekly (`live-watch.js`) | [src: brew/live-watch.js:1-5] |\n| Statics | Play 21's static ads (`statics.js`) | [src: brew/statics.js:1-4] |\n| Connected accounts | `live-*` cards, members only | [src: brew/connectors/index.js:324] |\n\n### Rules every card passes\n\n- **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]\n- **Claims:** a card says only what the brand itself says (`claims.js`). [src: brew/cards-gen.js:6-7]\n- **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]\n- **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]\n- **One take:** no card promises \"3 ads\"; one version, then Redo it. [src: memory brew-one-version]\n- **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]\n- **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]\n- **One card per ask:** six Plays saying \"fix the pack prices\" show once (`same-ask.js`). [src: brew/feed-service.js:27]\n\n### The score\n\nFor each card and viewer, `score()` adds: [src: brew/feed-service.js:539-568]\n\n| Part | What it is |\n|---|---|\n| 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] |\n| seat | fit to the viewer's role, lane and stated focus words |\n| affinity | hearts and clicks by agent, campaign, format, product; minus \"Not for me\", with near titles sinking by 4 |\n| fresh | streaming cards decay over 24 hours, others over 72 |\n| fatigue | days seen, the agent's share of today's screen, quiet jams |\n| feedback | \"Fewer\" minus 2 per step |\n| 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] |\n| told and lift | a Play rewritten on the team's learnings +6; team cards +10, learned up +8 |\n| contra | the team said it does not work: −40 until \"bring back\" |\n| extra | urgency (sold out, days left), a first draft to look at (+3), statics and ads (+2), live account cards |\n| behind | its Play is being rewritten: −8 |\n| chan, pace | channels the viewer runs and their ad pace |\n\n### The layout\n\n- Pins from the \"new cards\" pill sit on top for 30 minutes. [src: brew/feed-service.js:700]\n- Two picture cards for every text card, unless the other kind is more than 2 points stronger. [src: brew/feed-service.js:703-706]\n- 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]\n- 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]\n- No two cards of the same sort in a row. [src: brew/feed-service.js:723]\n- Storefront cards are hidden in v1 (`?storefront=1` shows them). [src: brew/SCOPE.md]\n\n### Stories\n\nFive 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 |\n|---|---|---|\n| One score, fixed seats, plain words and proof on every card | v1 | Stays |\n| Ranking weights checked on fixtures from several brands | v2 | New |\n| Cards in \"we\" voice across every writer | v1 | Changes (open from QA 25 Oct) [src: memory brew-qa-round2-2026-10-24] |\n| Storefront cards back in the feed | v2 | Changes |\n| 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]\n2. Before shipping a card-rule change, compare old and new code on every saved card:\n\n```bash\nnode tools/audit-card-fit.mjs DIR --before\n```\n\n[src: memory brew-card-fit-and-counts]\n\n3. 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]\n4. 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 |\n|---|---|---|\n| Cards served per brand | Visible cards in a member's feed | `GET /api/tenants/:t/feed` |\n| 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] |\n| Proof coverage | Share of cards whose numbers all match a source | `evidence.js` counts (Proposed to aggregate) |\n| 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.\n2. **Should Storefront come back for v2?** Proposal: yes for Shopify brands with a connected store only."
 }
}