{
 "id": "5.6",
 "title": "Plays engine",
 "department": "Engineering",
 "department_slug": "engineering",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "Mon Oct 26",
 "holds": "Specs, the first ten, rewrites, caps, cadence families",
 "urls": {
  "html": "https://handbook.joinsquareup.com/engineering/plays-engine",
  "markdown": "https://handbook.joinsquareup.com/engineering/plays-engine.md",
  "json": "https://handbook.joinsquareup.com/engineering/plays-engine.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Plays are written from the brief and ledger, then fact checked",
  "points": [
   {
    "icon": "book",
    "text": "66 specs: the index, 50 core, and 15 that wait for listening evidence."
   },
   {
    "icon": "spark",
    "text": "Each Play gets the house prompt, a skill file, the brief and an Arata exemplar."
   },
   {
    "icon": "check",
    "text": "A verify step scans numbers against the ledger and runs a claims check."
   },
   {
    "icon": "money",
    "text": "Plays run on Gemini 3.1 Pro on a capped weekly budget."
   },
   {
    "icon": "gear",
    "text": "Weekly cadence Plays start at 1001 and are never reused."
   }
  ],
  "stat": {
   "value": "66",
   "label": "Play specs"
  },
  "decide": "Which model writes Plays at launch? Proposal: Opus if credit exists, after a fair test.",
  "mood": "building"
 },
 "sources": [
  "brew origin/main: moves-engine.js:1-30, 473-492, 553-626, 652-674; data/move-specs.json (66 specs); prompts/move-house.md; sources/moves/ (51 files)",
  "brew origin/main: build.js:58, 110-116, 356-430, 1068-1080, 1156, 1547-1600, 1745, 1777; rewrite-pot.js:1-60; refresh.js:1-6; moves-cadence.js:1-10, 274, 409, 501",
  "brew origin/main: deploy.sh:39 (model and cap settings); tools/prod-env.sh:9",
  "memory brew-lean-pipeline, brew-move-freshness, brew-connect-refresh, brew-pro-switch-and-flush, brew-gemini-provider, brew-model-compare-2026-10-20, keep-all-moves, brew-arata-reload-2026-10-20, brew-qa-round2-2026-10-24"
 ],
 "open_questions": [
  "**Which model writes Plays at launch?** Proposal: Opus when Anthropic credit exists, since the 20 Oct comparison favoured it; run the fair 5-Play test first (about $1).",
  "**Should an unverified Play show to the brand?** Proposal: yes, with \"N claims to review\" as today, never hidden."
 ],
 "parts": {
  "Answer": "The Plays engine writes every applicable Play for a brand from its brief and ledger, checks each number and claim, turns each Play into cards as it lands, and keeps Plays current on a capped weekly budget; this page settles the specs, the order, the model, the caps and the rewrite rules.",
  "Today": "In code, files, routes and ids a Play is still called a move. People see Plays. [src: memory brew-qa-round2-2026-10-24]\n\n### Specs\n\n- 66 specs in `data/move-specs.json`: 0 the index, 1 to 50 core, 51 to 65 listening-era (written once their evidence exists). Groups: reach 16, convert 12, know 11, found 11, keep 9, expand 6, index 1. [src: brew/data/move-specs.json]\n- Each spec names its group, skill, kicker, discipline, title and stat hints, dependencies, evidence, lookups, an exemplar and a timing. [src: brew/data/move-specs.json]\n- Dynamic Plays 100 to 999 (a festival at its plan point, a research gap, replies that reached a threshold) and weekly cadence Plays from 1001 (listening and automation, never reused). [src: brew/moves-engine.js:1-3] [src: brew/moves-cadence.js:9, 274]\n- `status.total` is the applicable playbook (1 to 999, not the index, not cadence). [src: memory brew-lean-pipeline]\n\n### Writing one Play\n\n`writeMove(n)` = the house prompt + evidence contract + the discipline's skill file + brand brief + ledger slice (plus listening, pulse, video and calendar lines) + dependency digests + one Arata Play as depth exemplar. [src: brew/moves-engine.js:4-7]\n\n| Setting | Value | Source |\n|---|---|---|\n| Model | `GEMINI_MOVES_MODEL` (`gemini-3.1-pro-preview`) on Gemini; Opus on Claude | [src: brew/moves-engine.js:17] [src: brew/deploy.sh:39] |\n| Effort | `MOVES_EFFORT`, default medium; a max-tokens hit retries at low | [src: brew/build.js:398] [src: brew/moves-engine.js:473-479] |\n| Cache | System and brand block cached for an hour | [src: brew/moves-engine.js:473] |\n| Search | `MOVES_SEARCH`, default 1 | [src: brew/build.js:398] |\n| Concurrency | `MOVES_CONCURRENCY` 8 on staging and production; a second pass at 4 for failures | [src: brew/deploy.sh:39] [src: brew/build.js:404-406] |\n| Order | Wave A first: 1, 2, 3, 9, 10, 4, 5, 6, 7, 8; then dependency order | [src: brew/build.js:58] |\n\n### Checking one Play\n\n`verify()` = a deterministic number scan against the ledger, a fast-model claims check on the lines the Play rests on, one targeted fix. `verified` is true only when the claims check ran with no issues, no stat is unbacked, no number is unresolved and no ledger key is unknown. [src: brew/moves-engine.js:553-626]\n\nCover numbers are checked again at load (`stat-check.js`): an entry price that does not match the catalog, a \"cited in reviews\" figure not in a review, or an unbacked stat becomes \"(to confirm)\". [src: brew/docs/audit-2026-10-24-design-partners.md]\n\n### As each Play lands\n\nIt is saved as `moves/NN.json` and in `moves.json`, turned into 1 or 2 cards, Play 21 starts the static ads, progress and pace are saved, and the team is emailed once wave A is done. PDFs are drawn when someone opens one (`pdfOn`), versioned by `PDF_V` 12. [src: brew/build.js:378-395, 1745, 1777]\n\n### Caps\n\n| Cap | Value | Source |\n|---|---|---|\n| Build per brand | `BUILD_CAP_USD` 45 | [src: brew/deploy.sh:39] [src: brew/tools/prod-env.sh:9] |\n| All builds per day | `DAILY_BUILD_CAP_USD` 250 | same |\n| Pro brand build | max(150, 2.5 × cap) | [src: brew/build.js:115] |\n| Failed Plays | retried up to `BUILD_RETRY_MAX` 6 times, then the brand is Ready with them named | [src: brew/build.js:110, 418-421] |\n\n### Keeping Plays current\n\nReading is cheap and frequent; rewriting is batched, needs a real change, and is capped. [src: memory brew-move-freshness]\n\n| Trigger | What it does | Cap | Source |\n|---|---|---|---|\n| A fresh read or fact change | `refresh.js` finds material changes (price, product, rival, claim, policy, live ads, 10%+), a fast model judges which Plays change, flags them | | [src: brew/refresh.js:1-6] |\n| Out of stock or removed product | flags every Play naming it, high priority, no AI | | [src: brew/build.js:1600-1640] |\n| A rival's 10%+ price move or launch | rewrites the Play it hits hardest, at most one a day | | [src: brew/server.js:510-514] |\n| First connected account | rewrites every Play privately after a 30-minute settle and the first read | `CONNECT_CAP_USD` 40 | [src: brew/build.js:1070-1072] [src: memory brew-connect-refresh] |\n| Connector numbers move 10% from the base | flag, then the weekly run | `LIVE_CAP_USD` 30 a week | [src: brew/build.js:1076] |\n| Learnings | high and medium impact now; low waits for the week | `LEARN_CAP_USD` 30 | [src: brew/build.js:1075] |\n| Hourly stale sweep | queues flagged Plays within the weekly pot, best kind first; never Plays written in the last 24 hours | the pot | [src: brew/build.js:1570-1595] |\n\nThe weekly pot per brand is $10 on Flash, $40 on Pro and $50 on Claude; each kind may spend only down to its share (learnings 1.0, events 0.9, check 0.8, connected 0.7, low 0.6, new 0.5). [src: brew/rewrite-pot.js:14-21]\n\nRewrite priority in the queue: learnings, then events, manual and connected, then stale, weekly and live, then new. [src: brew/build.js:1156]\n\n### Weekly cadence Plays\n\nListening Plays (2 to 4 from what was heard) and automation Plays (2 to 4 routines written as plans the team runs; Brew never runs them). One run per brand across instances by a 30-minute mark. [src: brew/moves-cadence.js:1-10, 501]\n\n### Quality and cost\n\nOn 20 Oct, 54 Arata Plays compared: Gemini Pro median $0.18, 2.4 minutes, about 2,250 words; Opus median $0.83, 5.7 minutes, about 5,450 words with far more facts and sources. Opus read clearly better, but it also had the team's uploaded audit. [src: memory brew-model-compare-2026-10-20]",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| Every applicable Play is kept; cost cuts make each Play cheaper, never fewer | v1 | Stays [src: memory keep-all-moves] |\n| Plays on Gemini Pro, the rest on Flash | v1 | Changes once Param picks the launch model |\n| \"Verified\" reached on most Plays | v2 | New (needs the claims check and fix to converge) |\n| Dedupe of the same ask across Plays at write time (today only at serve time) | v2 | New |\n| One weekly rewrite budget across every source | v1 | Stays (built as the pot) |",
  "How we do it": "1. Never propose fewer Plays to save money; cut tokens, caching and repeat evidence calls. [src: memory keep-all-moves]\n2. New flag sources only flag; the stale sweep and the weekly run rewrite. [src: memory brew-move-freshness]\n3. Check a matcher against real brand data (variants) before deploying; one 15 ml variant once flagged 54 Arata Plays. [src: memory brew-move-freshness]\n4. One-off rewrites use the ops switch, then confirm `meta.rewrittenAt` changed:\n\n```bash\ngcloud run services update shopos-feed --region=asia-south1 --update-env-vars=OPS_REWRITE=<slug>:<n,n>\n```\n\n[src: memory brew-qa-audit-2026-10-20]\n\n5. Before a long paid run, check credit and expect other sessions to deploy; runs resume, but plan for it. [src: memory brew-arata-rewrite-2026-10-15]",
  "Numbers": "| Metric | Definition | Where measured |\n|---|---|---|\n| Cost per Play | `meta.usd` × 1.5 overhead | `moves/NN.json` meta; `rewrite-pot.js` OVERHEAD [src: brew/rewrite-pot.js:24] |\n| Build cost per brand | Total for a full build | `cost.json` |\n| Verified share | Plays with `meta.verified` true | `moves.json` (Proposed to show on the ops page) |\n| Stale Plays | Flagged and not yet rewritten | `updates.json` stale |",
  "Open": "1. **Which model writes Plays at launch?** Proposal: Opus when Anthropic credit exists, since the 20 Oct comparison favoured it; run the fair 5-Play test first (about $1).\n2. **Should an unverified Play show to the brand?** Proposal: yes, with \"N claims to review\" as today, never hidden."
 }
}