An exit-interview Mind that remembers why each member left โ and autonomously wins them back the moment their reason is actually fixed
Porchlight is built on Minds by Animoca Brands โ the sponsor platform for the Creative Minds Jam. The Minds agent is the engine, not a wrapper: per-member long-term memory, semantic reasoning, and native-channel outreach all come from the Mind via @animocabrands/minds-client-lib. Builder console: build.hellominds.ai.
Track: Audience growth & community engagement.
For any membership creator, churn is the direct revenue wound. When a member cancels, the reason vanishes โ nobody records it. And when the creator later fixes the very thing that drove people away (resumes a series, drops the price, ends the drama), the people who left for exactly that reason are never told. Today's "win-back" is a generic "we miss you" blast that ignores why each person actually left.
- Exit interview (memory). A member cancels โ the Mind runs a short, warm interview and files a structured return-condition per member โ the reason, in their own words.
- Churn board (continuity). Every departure compounds into a live board: "37% left over the pivot away from deep-dives."
- Conditional win-back (autonomous follow-up). The creator announces what changed โ the Mind semantically matches it against every stored reason and, unprompted, messages only the members whose reason is now resolved โ quoting their own words from weeks ago. Do-not-contact is always honored. Recovered MRR is tracked.
The money moment โ the Mind wins a member back, unprompted, quoting their own words. The creator posts "deep-dives are back"; the Mind semantically matches it to everyone who left over the pivot and reaches out to only them.
![]() The dashboard โ every departure, remembered with its reason. |
![]() The churn board โ "37% left over the pivot away from deep-dives." |
![]() Recovered MRR โ every conditional win-back, tracked to the dollar. |
|
- The exit interview is an adaptive conversation, not a form.
- Condition matching is semantic judgement โ "the pivot to short clips" is resolved by "deep-dives are back" even with zero shared keywords. That is the Mind reasoning, not a filter.
- The win-back recalls the member's own words from months earlier โ the Mind's long-term memory.
- Remove the Mind and there is neither the memory nor the trigger. A stateless tool can send "we miss you"; it cannot know this person left for that reason and that reason is now fixed.
Uses @animocabrands/minds-client-lib across 7 methods: listMinds, getMind, ensureConversation, sendMessage, waitForReply, getHistory, getLatestHistoryFingerprint.
try.porchlight.edycu.dev โ no signup, loads with data.
- Announce a curated change โ instant. Those verdicts are real Mind output, captured ahead of
time (
npm run precompute) because judging one change against all 18 departures takes minutes of live Mind time. Each is stamped with when it was captured. - Put the live Mind on the spot โ pick someone who left, describe a change in your own words, and a real call runs there and then, with its reasoning, confidence, and what a keyword rule would have scored. Rate-limited to 3 per 15 minutes.
- The interesting test is trying to fool it. Describe a fix that should not win someone back. It is supposed to stay quiet.
There is no keyword fallback in the deployed app. Without credentials it returns
503and says so, rather than quietly answering with the very mechanism this project exists to beat.
Recovered-MRR figures are per run, not constants. Mind verdicts are non-deterministic โ the same departure judged against the same change can resolve on one run and not the next, and we have the traces both ways (FEEDBACK.md ยง7). The demo video shows a 19-departure arc that recovered $105/mo vs $35/mo for the keyword baseline; the deployed app starts from the 18-departure seed and shows whatever its own dated capture produced. Both are real; neither is a fixed marketing number. The shape is the claim: a large slice of recovered revenue that keyword matching provably cannot reach.
npm install
npm run demo # the full arc, headless (works offline in MOCK mode)
npm start # dashboard at http://localhost:5173 (serves the pinned src/demoSeed.json)
npm run precompute # capture REAL Mind verdicts + latency into src/liveCache.json (needs a key)
npm test # 58 tests (interview parse, win-back, semantic edge, do-not-contact, live SDK, sessions, edges)With no API key, Porchlight runs in MOCK mode so the whole flow is demoable offline. To drive a real Mind:
- Create a Mind at hellominds.ai.
- Author the Porchlight Skill by describing it to your Mind:
"When I forward a cancellation, run a short warm exit interview and reply with a JSON return-condition
{reasonCategory, detail, verbatimQuote, doNotContact}. Later, when I tell you something changed, tell me which past cancellers that resolves and draft a personal win-back quoting their own words." - Create a Builder API key in the Builder console.
export MINDS_BUILDER_API_KEY=โฆ(optionallyMIND_ID=โฆ), thennpm run spike.
flowchart TD
subgraph client["๐ฅ๏ธ Browser"]
direction LR
SF["Membership storefront<br/>cancel / rejoin"]
DASH["Dashboard<br/>churn board ยท recovered MRR ยท audit"]
end
subgraph backend["โ๏ธ Porchlight โ Node/TS ยท Express"]
SRV["server.ts<br/>REST API ยท per-visitor sessions"]
subgraph logic["engine/ โ the three behaviours that need a Mind"]
direction LR
EI["exitInterview.ts<br/>runs the interview,<br/>files the condition"]
CM["conditionMatcher.ts<br/>does this change<br/>resolve that reason?"]
TE["triggerEngine.ts<br/>wins back only<br/>the resolved"]
end
MW["minds.ts ยท client-lib wrapper"]
KB["keywordBaseline.ts<br/>the control we measure against"]
DB[("db.ts โ JSON store<br/>reason ยท verbatim quote ยท do-not-contact")]
end
subgraph minds["๐ง Minds ยท @animocabrands/minds-client-lib"]
direction LR
MIND["The Mind<br/>long-term memory ยท semantic reasoning ยท Skill"]
CH["Native channel<br/>email"]
end
SF -- "cancel" --> SRV
SRV --> EI
SRV -- "creator posts what changed" --> TE
TE --> CM
TE -- "same change, keyword rule" --> KB
EI -- "interview" --> MW
CM -- "semantic judgement" --> MW
TE -- "resolved ยท not do-not-contact" --> MW
MW <--> MIND
MIND --> CH
EI -- "condition + quote" --> DB
TE -- "recovered MRR" --> DB
KB -- "what keywords would have caught" --> DB
DB --> SRV
MW -- "getHistory โ audit" --> SRV
SRV --> DASH
%% Amber = the Mind, and the code that cannot work without it.
%% Every amber edge is a real round-trip to the agent.
classDef agent fill:#ffcf6b,stroke:#b8761a,stroke-width:2px,color:#20160a
classDef brainwork fill:#fff1cc,stroke:#e89f45,stroke-width:1.5px,color:#20160a
classDef plumbing fill:#e7ebf8,stroke:#6b76a3,stroke-width:1.5px,color:#171b2e
classDef store fill:#2a3050,stroke:#575f8a,stroke-width:1.5px,color:#e9ecff
classDef ui fill:#dcf3e5,stroke:#3fa96a,stroke-width:1.5px,color:#0b1a10
classDef control fill:#f2e3e3,stroke:#a87878,stroke-width:1.5px,color:#3a2020
class MIND,CH agent
class EI,CM,TE brainwork
class SRV,MW plumbing
class DB store
class SF,DASH ui
class KB control
style client fill:#f4fbf7,stroke:#bfe3cd,color:#0b1a10
style backend fill:#f7f8fd,stroke:#ccd3ea,color:#171b2e
style logic fill:#fffaf0,stroke:#edc98a,color:#20160a
style minds fill:#fff8e8,stroke:#e2b666,color:#20160a
linkStyle 5,6,7,8,9 stroke:#e89f45,stroke-width:2.5px
src/minds.tsโ the Minds integration (LIVE client-lib + MOCK brain).src/engine/โ exit interview, condition matcher, trigger engine.src/board.tsโ the churn-intelligence board.src/server.ts+public/โ dashboard.
Node 22+ ยท TypeScript ยท Express ยท zod. A JSON store mirrors the Mind's memory for the board and audit trail; the Mind remains the semantic brain.
A production-grade harness runs on every push and PR โ a 7-stage GitHub Actions pipeline (ci.yml): Quality โ Security โ Build & Smoke โ E2E โ Performance โ Deploy Gate โ Fly.io deploy (main only, app files only, and it fails the build if the deployed app comes up in MOCK mode). Releases run in a dedicated release.yml workflow (see below).
npm run ci # audit + strict typecheck + unit tests
npm test # 58 unit tests (node:test)
npm run test:coverage # 100% lines / branches / functions on every source file
npm run e2e # Playwright E2E against the live dashboard (demo mode, no key)
npm run lighthouse # Lighthouse CI (performance / a11y / SEO)
make security-scan # npm audit + license checkVersion numbers are never bumped by hand. On every push to main, the release.yml workflow runs semantic-release: it reads the Conventional Commits since the last tag, computes the next SemVer, then โ with no manual release PR โ writes CHANGELOG.md, bumps package.json, tags the commit, and publishes a GitHub Release automatically.
| Commit prefix | Release |
|---|---|
fix: |
patch โ 0.1.0 โ 0.1.1 |
feat: |
minor โ 0.1.0 โ 0.2.0 |
feat!: / BREAKING CHANGE: footer |
major โ 0.1.0 โ 1.0.0 |
chore: docs: test: ci: refactor: |
no release |
Commit conventionally and the version takes care of itself.
| Layer | Tool | Status |
|---|---|---|
| Code Quality | TypeScript strict (tsc --noEmit) |
โ |
| Unit Testing | node:test โ core Mind flow + engine, DB, server, live-SDK & edge cases |
โ (51 ยท 100% coverage) |
| E2E Testing | Playwright โ demo-mode smoke, win-back flow, responsive | โ (3 specs ร 2 browsers) |
| Security (SAST) | CodeQL | โ |
| Security (SCA) | Dependabot + npm audit |
โ |
| Secret Scanning | TruffleHog (CI) | โ |
| Performance | Lighthouse CI | โ (advisory) |
| Versioning | semantic-release (Conventional Commits โ SemVer) | โ |
MIT ยฉ 2026 Edy Cu
Built for the Creative Minds Jam (Minds by Animoca Brands). Thank you to the Minds team for the @animocabrands/minds-client-lib builder surface.



