BUILD DOCS

PRAYER OFFER — BUILD SPEC v1

prayer/SPEC-PRAYER-ENGINE.md

PRAYER OFFER — BUILD SPEC v1

Directive doc for Fable/Claude runs. Written 2026-09-14 from the research in MASTER.md, rewritten the same day to match DECISIONS-2026-09-14.md, then rewritten a second time the same day to the FINAL ANSWERS in that file and the PrayerSong funnel shape (PRAYERSONG-MAPPING.md §2–§4). Everything below is build instruction — the mechanics only.

Every line that once carried a (PROPOSED — confirm) tag is now settled by the FINAL ANSWERS block in DECISIONS-2026-09-14.md. No open tags remain in this doc.


0. WHAT THIS IS

A prayer-delivery funnel engine. Buyer submits a prayer, pays, and receives a photo of their prayer card held at a famous holy site. Fulfilment is 100% image generation — two passes through an image model, nothing else in the path. No physical errand, no fulfiller, no logistics, no mailed goods. One core codebase, many brand fronts — each brand is a config file: domain, site, persona, location, copy, prices, pixels.

Part 1 = one brand live and converting. Part 2 = clone the engine across pages/identities by adding configs. Recurring layer = monthly prayer membership (new photo every month). Three one-time offers after the sale: second prayer at reduced cost, 24-hour rush delivery, and an audio recording of the prayer.

Delivery is a portal, not just an inbox. Every buyer gets a tokenised portal at /portal/<token>; every email links into it; email still carries the photo so a portal outage never blocks a delivery.


1. PRODUCT ARCHITECTURE

/brands/<slug>/          ← everything brand-specific lives here
    config.ts            ← copy, prices, persona, location, colors, pixels, domain
    prompts/             ← pass-1 scene prompts, pass-2 card-in-hand prompts, persona portraits
    assets/donor/        ← per-location DONOR reference image (sourced from Pinterest)
    assets/examples/     ← 7 example deliverable photos: 1 landing hero, 3 for /create/ready, 3 for /offer
    emails/              ← branded email templates
/apps
    web/                 ← Next.js funnel + portal (reads brand config at build/runtime)
    worker/              ← image gen + audio gen + reveal tick + email queue consumer
/packages
    engine/              ← shared: survey, checkout, session, upsell chain, subscriptions
    imaging/             ← image-model clients (GPT Image / Nano Banana Pro class), two-pass runner
    audio/               ← TTS + ambience mixing
    payments/            ← TagadaPay adapter + mock adapter

New brand = new folder + config + prompt pack + donor image + example photos + domain. Zero code changes. Target spin-up: one day.

Brand config schema (the modularity contract)

This block is byte-identical to OPS-RUNBOOK §1. Edit both or neither.

// brands/<slug>/config.ts
export type BrandConfig = {
  slug: string;                       // "st-peters" — used as brand_slug in DB
  domain: string;                     // "holycardsofsaintpeters.com" — middleware match key
  site: {
    name: string;                     // "Holy Cards of St. Peter's"
    tagline: string;                  // "Your prayer, placed at the heart of the Basilica."
    merchantName: string;             // legal name on receipts — never contains "prayer"
    supportEmail: string;             // "care@holycardsofsaintpeters.com" — the only support channel
    supportUrl: string;               // "holycardsofsaintpeters.com/support" — goes in the descriptor
    statementDescriptor: string;      // Stripe suffix after the fixed NUMINA* prefix, e.g. "HOLYCARDS"; <= 15 chars, no <>\'"* characters. Never the word "prayer".
    colors: { primary: string; accent: string; bg: string; text: string };
  };
  location: {
    key: string;                      // "st_peters_basilica" — keys donor refs + ambience presets
    displayName: string;              // "St. Peter's Basilica, Vatican City"
    storyBlock: string;               // long-form copy for /offer
    donorRef: string;                 // "assets/donor/st_peters_basilica.jpg" — pass-1 reference
    examplePhotos: string[];          // 7 paths under assets/examples — 1 landing hero, 3 on /create/ready, 3 on /offer
  };
  persona: {
    name: string;                     // "Fr. Marco"
    title: string;                    // "Pilgrim Chaplain"
    bio: string;                      // short bio for /offer + emails
    portraitSet: string;              // key into brands/<slug>/assets — consistent persona face
  };
  prayer: {
    maxChars: number;                 // 220 — hard cap so the text always fits the card
  };
  survey: {
    forWhom: string[];                // step 1 options: Myself, Mother, Father, Husband, Wife, Child, Friend, Someone who has passed, Other
    intentions: string[];             // step 2 options: Healing, Strength, Grief, Family, Provision, Protection, Gratitude, Other
  };
  products: {
    fe: { cents: number };            // 3490 — Western Wall front runs 2990
    anchorCents: number;              // 6900 — struck-through anchor above the FE price
    bumps: {
      candle: { cents: number };      // 499 — the only checkout bump, never pre-checked
    };
    oto1: { cents: number };          // 1400 — second prayer, saved card
    oto2: { rush: { cents: number; hours: number } };   // 1990 / 24 — rush delivery
    oto3: { audio: { cents: number } };                 // 990 — Voice of the Sanctuary MP3
    replacement: { cents: number };   // 1400 — anniversary re-placement, saved card
    monthly: { monthly: { cents: number }; annual: { cents: number } }; // 990 / 7900
  };
  delivery: {
    standardDays: number;             // 3 — fixed promise; delivery email fires on day 3
    rushHours: number;                // 24 — OTO2 rush SLA, queue-jumping
    queueHours: number;               // max hours from payment to asset generated (SLA: 24)
  };
  guarantee: {
    days: number;                     // 30 — satisfaction guarantee, stated on every sales surface
  };
  portal: {
    revealMinutes: number;            // 15 — reveal cron tick that flips a ready deliverable visible
  };
  audio: {
    voiceId: string;                  // TTS voice id for the persona
    ambiencePreset: string;           // ffmpeg mix preset keyed to location.key
    sampleUrl: string;                // static per-brand preview clip played on /oto/3
  };
  gift: {
    enabled: boolean;                 // true — optional recipient email at survey step 5
  };
  tracking: {
    pixelId: string;                  // Meta pixel — a fresh pixel per brand, never shared
    capiToken: string;                // Meta CAPI access token (server-side mirror)
  };
  locales: string[];                  // ["en-US"] — copy variants keyed by locale
};

2. FUNNEL FLOW

The PrayerSong spine, our mechanic. Twelve surfaces, in this order. Route names are the contract: OPS-RUNBOOK §2 carries the identical list.

  1. / — Landing. Testimonial headline, hero = example card-in-hand photo, trust line "Delivered in 3 days", then directly under the hero the live "prayers being carried in right now" ticker (city-rotated, from the COPY-CREATIVES §11 pool), then how-it-works, what-you-get, pricing block and FAQ. One CTA into the survey. No price is shown above the pricing block. No aggregate customer-count claim appears anywhere until real customers exist. Fires view_landing.
  2. /create — 5-step survey. Progress bar, 20% per step, Back/Next. No price on any step.
    1. Who is this prayer for?survey.forWhom: Myself, Mother, Father, Husband, Wife, Child, Friend, Someone who has passed, Other. Plus their first name.
    2. What is it for?survey.intentions: Healing, Strength, Grief, Family, Provision, Protection, Gratitude, Other. This answer names the discount on /offer and seeds the member's default monthly theme.
    3. The prayer — multiline, hard-capped at prayer.maxChars (220), live character counter, Next disabled over the cap, server re-validates.
    4. Anything else the Father should know? — optional free-text note. Feeds the persona's note, never printed on the card.
    5. Where should the photo go?your first name + email, plus an optional gift recipient email ("sending this for someone else? we deliver the photo to them"). Writes the lead row and keeps the abandonment sequence alive.
  3. /create/ready — transition. 3 example photos with customer quotes (stubs until real), the guarantee box, and one button: "Continue to your order". Fires transition_view on render. First route that requires the ph order handle.
  4. /offer — sales page. Headline "Almost There! Complete Your Order". In order: summary card (prayer for [name], placed by [date = today + delivery.standardDays]); "Limited Time [Intention] Discount" box named from the step-2 answer; price block $69 $34.90 with the subsidy line "our monthly members keep the door open for everyone else"; 3 example photos with quotes; the guarantee box; "what you get"; "why choose us"; and the persona letter as a lower section — a section, not the headline. Fires offer_view on render, and that is the only place offer_view fires.
  5. /checkout. Card only, tokenized by Stripe (Stripe.js Card Element creates a PaymentMethod in the browser — the server never sees a card number — then the server charges the vaulted instrument). Wallets and alternative rails are off for v1. One bump only: candle-lighting +$4.99, never pre-checked. Scarcity line under the order summary. Billing address required on every order; no phone collected anywhere. Statement descriptor: brand short name + support URL, never the word "prayer".
  6. /oto/1 — Second prayer, $14. Not a bare one-click. One textarea prefilled with the first prayer (same 220 cap, same live counter), one confirm checkbox, then Submit (charges the saved card) or Skip. 5-minute countdown on the page. Accept → a second prayers row on the same order → a second photo. Guarantee box on the page.
  7. /oto/2 — Rush delivery, $19.90. One tap on the saved card: your photo in 24 hours instead of 3 days. A banner at the top confirms the OTO1 decision before the pitch. Guarantee box on the page. Accept → orders.rush = true, every job on the order jumps the queue.
  8. /oto/3 — Voice of the Sanctuary, $9.90. One tap on the saved card: the prayer spoken aloud and delivered as a recording. A static per-brand sample clip (audio.sampleUrl) plays on the page — it is not the buyer's own prayer, which does not exist yet. Accept → audio job queued.
  9. /thanks. An order recap card per accepted item, each labelled with its SKU name from §3 (The Witnessed Carry, Votive Candle, Second Prayer, 24-Hour Placement, Voice of the Sanctuary). An "I need to correct my email" button posting to /thanks/correct-email. What happens next in 3 steps, with real dates. A spam-folder note. One big button: "Open your portal". A cross-sell: "place a prayer for someone else". The membership pitch at the bottom, not the top.
  10. /portal/<token> — the delivery portal. Magic-link entry, no passwords. Carries: the deliverable gallery (photos + audio, each held until the reveal tick flips it visible); the track-order timeline; the monthly intention field; membership manage (pause / resume / cancel, self-serve); email preferences at /portal/<token>/email-preferences; and the tokenised offer surface /offer?product=<id>&s=<token> for one-tap offers on the saved card. Every email links here.
  11. /membership. Monthly Prayer Membership, $9.90/mo or annual $79, theme preview carousel. Also the remarketing landing page. A visitor who arrives here with no prior order is a standalone member signup: the route creates a subscriptions row carrying its own email and first_name, mints a portal_sessions token against that subscription, and sends the Member welcome email (§7 template 17).
  12. /replace. Anniversary Re-Placement, $14, one tap on the saved card. Reached from the day-365 email. An accepted re-placement writes a new prayers row on the original order (new variant_seed, same or edited text) and produces a new photo, because the invariant is one row per prayer, one photo per prayer.

Supporting routes. Eight surfaces exist outside the funnel order. This list is identical in OPS-RUNBOOK §2.

  • /support — contact form posting to site.supportEmail. Target of site.supportUrl.
  • /terms, /privacy, /refunds — the three footer pages linked from every page.
  • /create/resume — GET only. The target of all three abandoned emails. Verifies the signed t token, re-mints ph for that order, and lands on /create/ready with the draft intact. 30-day TTL, matching draft retention. OPS §2 carries the token spec.
  • /thanks/correct-email — POST only. Rewrites orders.email and re-sends the confirmation.
  • /admin — the orders dashboard (§9 / OPS §9), gated by ADMIN_DASH_TOKEN.
  • /portal/<token>/email-preferences — sets orders.marketing_opt_out / subscriptions.marketing_opt_out. Every marketing send checks it; transactional ignores it.

Guarantee. A 30-day satisfaction guarantee, in this exact frame, verbatim on every surface that carries it: "100% Money Back Guarantee. Not satisfied? Get a full refund. No questions asked, no hassle. 30-day guarantee. Risk-free purchase." It is always tied to the price anchor. It appears on /, /create/ready, /offer, /oto/1, /oto/2, /membership, /replace and in all three abandoned emails.

Delivery promise. Standard = day 3, fixed. Rush = inside 24 hours. Both dates are printed on /offer, /thanks, in the confirmation email, and on the portal timeline. Every date on every surface is computed from orders.paid_at, never from orders.created_at:

Token Formula
[DELIVERY_DATE] paid_at + delivery.standardDays (3 days), or paid_at + delivery.rushHours (24 h) on a rush order
[PLACEMENT_DATE] paid_at + 2 days standard, paid_at + 12 hours on rush — the moment prayers.placed_at is written. Always this order's own placement, never a prior year's.
[ORIGINAL_PLACEMENT_DATE] the placed_at of the first prayer on the order — last year's actual date. Used on /replace and in the day-365 email, which both name the original placement and the new one on one surface.
[RUSH_DATE] paid_at + delivery.rushHours
[NEXT_DATE] the member's next monthly delivery date = subscriptions.member_since + N months, independent of billing interval

orders.created_at is lead-creation time. It keys only the three abandoned-lead emails.

Post-purchase: confirmation email within 1 minute (order id, promise date, portal link, what happens next). Delivery email on day 3 (or inside 24 h on rush) with the photo — and the audio inside the same email if OTO3 was taken.

State/session — the ph order handle. Survey step 5 writes the lead: the server creates the orders row (status lead) and sets a signed, httpOnly cookie ph carrying the order id plus an HMAC over it, 7-day TTL. That cookie is the whole of funnel identity.

  • Every route from /create/ready onward requires a valid ph. No ph, no page.
  • /oto/1, /oto/2, /oto/3, /thanks and /replace additionally require orders.status = 'paid' on the handle's order.
  • Ownership pre-check: never re-sell an owned SKU. If the upsells table already carries that kind for the order, the OTO page skips straight to the next step in the chain.
  • Per-handle rate limit: 5 charge attempts per hour. A saved-card one-tap route with a guessable identifier is an abuse surface; the handle plus this limit is the defence.
  • The portal does not use ph. /portal/<token> authenticates on its own portal_sessions.token magic link, and so does the tokenised offer surface /offer?product=<id>&s=<token>. The two identities never substitute for each other.

Payment path: /checkout charges, and the synchronous settle path is authoritative — it sets orders.status = 'paid' and orders.paid_at, mints the portal_sessions token, fires purchase, and enqueues fulfilment. The webhook only reconciles. Upsells attach as upsells rows. One row per prayer, one photo per prayer, one parent order. orders, prayers, deliverables, portal_sessions, subscriptions, upsells, charge_intents, disputes tables.


3. PRODUCT CATALOG + PRICING

SKU names are a cross-doc invariant. The name in the middle column is the only name this product ever has: on the receipt, on the /thanks recap card, in the email that delivers it, and as the take-rate label on the orders dashboard. OPS-RUNBOOK §4/§9 and COPY-CREATIVES use these exact strings.

# SKU name Price Notes
FE The Witnessed Carry $34.90 prayer placed at [location] + photo; anchored against $69; Western Wall front runs $29.90 (§11)
Bump Votive Candle +$4.99 the only checkout bump, opt-in, never pre-checked; a second lit candle appears in the photograph
OTO1 Second Prayer $14.00 typed on /oto/1, saved card, 5-minute countdown
OTO2 24-Hour Placement $19.90 one tap on /oto/2, saved card, queue-jumps every job on the order
OTO3 Voice of the Sanctuary $9.90 one tap on /oto/3, saved card; see §5
Post Anniversary Re-Placement $14.00 one tap at /replace, saved card, day-365 email
Monthly Monthly Prayer Membership $9.90/mo (annual $79) new photo monthly, themed; cancel anytime

No physical SKU exists anywhere in the ladder. Everything delivered is a generated image, an MP3, or an email.

Max stacked order: 34.90 + 4.99 + 14 + 19.90 + 9.90 = $83.69, across up to five separate charges on the saved card.

Annual is "save $39" against 12 × $9.90, not "two months free". OTO1 is "less than half" of $34.90, not "half".

Economics (Stripe rail, recomputed 2026-09-16 on published Stripe Canada card pricing)

Processing is 5.7% + $0.22 per approved transaction (2.9% base + 0.8% international-card

  • 2% currency conversion), flat on every charge regardless of type. Stripe has no fixed monthly platform fee.

Take rates below are assumptions, not observations. They are the ECONOMICS.md §2 model: candle 25%, OTO1 20%, OTO2 rush 12%, OTO3 audio 15%. Recompute every one of them from real data once ≥100 orders exist.

Item Price Take rate Revenue per buyer
The Witnessed Carry $34.90 100% $34.90
Votive Candle $4.99 25% $1.25
Second Prayer $14.00 20% $2.80
24-Hour Placement $19.90 12% $2.39
Voice of the Sanctuary $9.90 15% $1.49
AOV ≈ $42.83

Average charges per order = 1.47 (the candle rides inside the FE charge; each OTO is its own charge).

Cost per order Amount
Processing 5.7% of $42.83 (2.9% + 0.8% intl-card + 2% FX) $2.44
Per-charge US$0.22 × 1.47 (Stripe's C$0.30 at 1.36 CAD/USD) $0.32
Image generation (2 passes + retries) $0.30
Card-text QA read-back (vision model, §4) $0.02
Audio, email, storage $0.10
Refunds + disputes allowance (4% of AOV, under a 30-day guarantee) $1.71
Net before ads ≈ $37.94

Membership layer: 6% of buyers join at $9.90/mo, average life 4 months, fee ≈7.9% → ≈ $36.46 net per member → +$2.19 per buyer. Contribution per buyer before ads ≈ $40.13. Break-even CPA ≈ $40.

Rail cash terms, not in the per-order table above. This reserve-and-chargeback-fee note was TagadaPay-specific and does not carry over: the Numina House Stripe account has charges and payouts enabled with no reserve noted, and Stripe's exact dispute fee for this account is not yet confirmed. Disputes now arrive as webhooks (OPS §4), landing in the disputes table directly, on top of the existing manual daily entry — one more reason the 0.6% dispute kill-switch (§9, OPS §9) exists.

Per-ticket net:

Ticket Fee Net
FE $34.90 $2.21 ≈ $32.52 after generation cost
FE + candle $39.89 $2.49 ≈ $37.00 after generation cost
OTO1 $14.00 $1.02 ≈ $12.98
OTO2 rush $19.90 $1.35 ≈ $18.55
OTO3 audio $9.90 $0.78 ≈ $9.12
Replacement $14.00 $1.02 ≈ $12.98
Monthly $9.90 rebill $0.78 ≈ $9.12
Annual $79 $4.72 ≈ $74.28

Generation is near-zero marginal cost; the margin lives entirely in media CPA. Target CPA ≤ $19 → ≈ $21.13 net per buyer at the modelled take rates (contribution ≈$40.13 minus CPA $19). Stripe has no fixed monthly platform fee, so there is no order-count threshold needed to cover one.

Scale and kill live in one place only: the day-10 read in §9. No other threshold is stated in this doc, in OPS-RUNBOOK or in ECONOMICS.


4. IMAGE PIPELINE (the fulfilment core)

Image models only — GPT Image class and Nano Banana Pro class. Nothing else in the path. This section is engineering. None of it appears on any customer surface.

Pass 1 — scene. Every location has one DONOR reference image (sourced from Pinterest, stored at brands/<slug>/assets/donor/<location_key>.jpg) showing exactly what the site and the scene look like. Pass 1 conditions on that donor and regenerates the scene, randomizing time of day (morning / afternoon / evening / night), light and camera angle per order so every photo reads as a real, different moment. Variation is seeded from variant_seed.

Pass 2 — card in hand. An edit pass on the Pass-1 output renders a hand holding the prayer card, with the buyer's first name and prayer text on it. Card-in-hand is the default composition for every brand. Pass 2 always lights one votive beside the card, because a candle lit beside the card is promised on every order; the Votive Candle bump makes it two. The prayer input is capped at prayer.maxChars so the text always fits at legible size — there is no long-prayer path. The card is THE product: the name and the words must read as real. Legibility rules, the name-mangle recovery trick, the donor references and the hand-held recipe live in PROMPT-PACKS.

Prompt pack per location (brands/<slug>/prompts/): donor reference + pass-1 scene prompt with the time/light/angle axes; pass-2 hand-held card edit prompt with {FIRST_NAME} / {PRAYER_TEXT} slots; monthly theme variants so recurring members never repeat a photo.

Card-text QA is a pipeline stage, not a hope. Between Pass 2 and upload the worker runs one vision call over both frames — the Pass-1 scene and the Pass-2 output — which reads the card back against the source strings, counts the lit flames in each frame and compares the delta (one new votive on every order, two when the Votive Candle bump was taken; absolute counts are never used, because most of these sites are full of flames already), and returns four scene booleans: scene unchanged, no extra text, card not occluded, hand plausible. All seven checks gate the upload. The thresholds, the retry budget and the failure state are specified in OPS-RUNBOOK §5. A job that cannot clear QA in three Pass-2 attempts dead-letters and sets prayers.status = 'failed'.

Example photos. Seven finished deliverables per brand — 1 landing hero, 3 for /create/ready, 3 for /offer — generated once with placeholder names and committed to brands/<slug>/assets/examples/. They are a launch prerequisite (§13), not a nice-to-have.

Pipeline:

settle(paid) → enqueue one job PER PRAYER row (order_id, prayer_id, location_key, variant_seed, rush, candle)
worker → load prompt pack + donor ref → pass 1 scene → pass 2 card-in-hand
      → card-text QA read-back (pass or re-run pass 2, max 3) → S3/B2 upload
      → insert deliverable (hidden) with reveal_at → reveal tick flips it visible in the portal
      → schedule delivery email (day 3 standard, ≤24 h rush)

Queue: Postgres table read with FOR UPDATE SKIP LOCKED — same DB, no extra infra. Rush jobs are pulled first. Worker: single droplet, headless. Store every generation; emails carry signed URLs (7-day expiry); the portal always serves fresh ones.

Persona portraits: generate once per brand — 6–10 consistent portraits (same face across the set, reference-image conditioning) for the offer page, emails, and creative frames.


5. AUDIO PRODUCT — "Voice of the Sanctuary" (OTO3)

The prayer, read aloud inside the space, delivered as an MP3:

  1. Script build: prayer text → polished spoken version (light cleanup, breath points).
  2. TTS: persona voice from audio.voiceId (Spanish/Italian voice variants per locale).
  3. Mix: TTS track layered over location ambience (bells, distant chant, echo/reverb matching the site interior, candle-room tone) — ffmpeg, one preset per audio.ambiencePreset.
  4. 60–90 s master. The preview on the /oto/3 page is a static per-brand sample file (audio.sampleUrl) — not the buyer's own prayer, which does not exist yet at that point.
  5. Delivery: inside the photo delivery email, and in the portal gallery. There is no separate audio email.

Marginal cost ~$0.02–0.06. Placement as the third OTO is deliberate: the buyer has already made two decisions, the card is on file, and the one-tap is frictionless. Expect 15% take.


6. MONTHLY RECURRING ENGINE

Monthly Prayer Membership — the recurring layer:

  • Signup: /thanks (bottom of the page) + the dedicated /membership landing for remarketing. A /thanks signup attaches to the buyer's order. A standalone /membership signup has no order: it writes its own email and first_name onto the subscriptions row, gets its own portal_sessions token keyed to the subscription, and receives the Member welcome email. Member deliverables carry subscription_id instead of order_id.
  • The first member photograph is enqueued on the createSubscription call (app-side, not a webhook), on the same path the monthly cron uses. The initial payment is not a rebill, so nothing else would produce it, and the Member welcome email names the date it lands.
  • Every member job reads its two card strings off the subscriptions row — the name from subscriptions.first_name, the text from subscriptions.current_intention or the month's generic themed intention. A member job never dereferences an order, because a standalone member does not have one.
  • Subscription on Stripe's native Subscriptions engine, inline price_data per brand (monthly $9.90, annual $79 = save $39).
  • Monthly intention is opt-in and optional. Capture surface: the intention field in the portal, plus one reminder email 5 days before the member's monthly delivery date. If the member submits nothing, the engine places the generic themed intention for that month and delivers on that. Nobody ever misses a delivery for not filling in a form. A new member's default theme is seeded from their survey step-2 intention.
  • Rotation: theme = the calendar month (12 themes: "January — Renewal", "March — St. Joseph", "November — All Souls" …). Treatment = (member_no + year_offset) mod 4, where member_no is a serial assigned at signup and year_offset is years since member_since. 12 × 4 = 48 combinations; no member repeats a photo inside four years.
  • Cron (daily, fires on each member's monthly delivery date = member_since + N months): generate a NEW photo on that month's theme and treatment → member delivery email + the portal gallery. The delivery date is independent of the billing interval. An annual member at $79 rebills once a year and still receives twelve photographs a year, on the same schedule as a $9.90 monthly member — the copy sells twelve walks and twelve photographs to both. A rebill never enqueues a delivery; only this cron does, after the first one (OPS §4, §5).
  • Every rebill email states what the member is receiving, by name, in the subject line and the first line of the body. The Divine Rev chargeback audit traced ~75% of dispute volume to one recurring charge that delivered nothing recognisable; 85% of those were reason-coded "fraudulent" and never contacted support first. The recurring deliverable must be unmissable, every single month.
  • Dunning: Stripe retries a failed invoice on its own adaptive Smart Retries schedule (dashboard-configurable, not a fixed hour ladder), then marks it uncollectible and the subscription cancels. Our three dunning emails ride Stripe's real webhook events, not a calendar of our own: dunning 1 on the first invoice.payment_failed, dunning 2 on a later invoice.payment_failed before the final scheduled retry, dunning 3 on the last scheduled retry's invoice.payment_failed. Dunning 3 says one more attempt is coming, update your card in the portal to keep it. No email ever says charges have stopped while the rail can still charge. The "billing has stopped" statement lives in one place only: the Membership ended email, fired on customer.subscription.deleted and nowhere else.
  • Retention hooks: 3-month milestone = a bonus generated keepsake image; 6-month = annual audio library ("every prayer you've sent us, voiced").
  • Skip / pause / cancel from the portal (self-serve, one click) — the single biggest churn reducer. There is no chargeback-as-cancel path: cancelling is always easier than disputing.

Membership includes: monthly photo + monthly themed card. Audio is NOT included — it stays a paid add (protect OTO3's slot).


7. EMAIL SYSTEM

Transactional + lifecycle (Resend, per-brand sending domain, DKIM/SPF/DMARC). Seventeen templates. This list is mirrored one-for-one in OPS-RUNBOOK §6, and COPY-CREATIVES §9 must carry finished copy for all seventeen. Every one of them links into /portal/<token>. Every post-purchase schedule below counts from orders.paid_at; only templates 8, 9 and 10 count from orders.created_at.

# Template Trigger
1 Confirmation within 60 s of payment — order id, promise date, prayer echoed back, portal link
2 In-progress paid_at + 24 h, standard orders not yet delivered (rush orders skip it)
3 Delivery — photo + audio paid_at + 3 days standard, ≤24 h on rush. Audio rides inside this email if OTO3 was taken.
4 Day-5 re-send paid_at + 5 days — "here is your photo again", for buyers who never opened email 3
5 Day-7 membership invite paid_at + 7 days, non-members only
6 Day-30 win-back (buyer) paid_at + 30 days, no second purchase
7 Day-365 re-placement paid_at + 365 days → /replace
8 Abandoned #1 night 1 after a lead row that never paid — social proof, 3 example photos
9 Abandoned #2 night 2 — "you started a prayer" + what you get
10 Abandoned #3 night 3 — one example + the guarantee as urgency
11 Monthly intention reminder 5 days before the member's monthly delivery date (member_since + N months) → the portal intention field
12 Member delivery each monthly delivery date, themed, the photograph named in the subject line. Twelve a year for monthly and annual members alike.
13 Dunning #1 first invoice.payment_failed — "we could not renew, we will try again"
14 Dunning #2 the rail's 96 h attempt fails
15 Dunning #3 the rail's first 168 h attempt fails — "one more attempt in 7 days, update your card in the portal to keep it"
16 Membership ended customer.subscription.deleted only — the one email that says billing has stopped, plus the win-back ask
17 Member welcome standalone /membership signup with no prior order — portal link, what arrives and when

Rules:

  • Delivery email: one clickable CTA only, and it is the Monthly Prayer Membership. No audio pitch, no second-prayer pitch, no seasonal block. The review ask is a reply request ("reply to this email with 1–5 stars"), not a link — reviews are collected by reply only.
  • Abandoned emails fire on status = 'lead' only, re-checked at send time, and each one carries the guarantee. All three link to /create/resume?t=<token>, which re-mints ph for that order and lands on /create/ready with the draft intact — that is the mechanism behind "exactly where you left it", and its 30-day TTL matches draft retention.
  • Every marketing send checks the opt-out (orders.marketing_opt_out / subscriptions.marketing_opt_out, set from /portal/<token>/email-preferences). Transactional templates ignore it. OPS-RUNBOOK §6 carries the class per template.
  • No psychic/manifestation cross-promo anywhere — keeps the list on-brand.

8. TECH STACK

Layer Choice Why
Funnel/app Next.js 14 (app router) the curse-funnel pattern already exists; port it
Payments Stripe — own checkout pages, Stripe.js Card Element tokenization, server-side charge merchant of record, subscriptions, saved-card one-tap
Upsell chain self-hosted one-tap (vaulted instrument) Upsell Checkout v2 pattern, fresh instance
Portal tokenised magic-link portal Divine Rev client-portal-worker pattern: portal_sessions, reveal tick, offer surface
DB Postgres + Drizzle orders, prayers, deliverables, portal sessions, subs, charge intents
Queue Postgres table, FOR UPDATE SKIP LOCKED no extra infra; matches OPS §5
Image gen GPT Image class + Nano Banana Pro class two-pass donor → card-in-hand
TTS/audio TTS voice per persona + ffmpeg voice + ambience mix
Email Resend transactional + lifecycle templates
Storage B2/S3 + signed URLs deliverables
Hosting droplet (isolated from every other brand) one box runs web + worker initially
Support email only — Resend inbound or a shared mailbox Divine Rev reply-engine v2 pattern ported later
Escalations Telegram digest Divine Rev check-escalations pattern: investigate → present → approve → execute
Analytics Meta Pixel + CAPI + PostHog full funnel events

Deploy isolation: this stack shares zero infra with any existing brand. Own domain, own payment store, own BM, own sending domain, own droplet.


9. ANALYTICS + DECISION RULES

Events, in funnel order:

view_landing → survey_step → lead_submit → transition_view → offer_view → initiate_checkout → purchase → bump_take → oto1_take → oto2_take → oto3_take → monthly_signup → deliverable_sent → portal_open → replacement_take → email_open → email_click

  • survey_step fires once per completed step, with meta { step: 1–5 } — this is the drop-off map for the survey and the single most important new number in the funnel.
  • transition_view fires on /create/ready render. offer_view fires on /offer render, and nowhere else. One event, one firing point: two firing points on one name double-counts the step and corrupts the drop-off read.
  • purchase fires from the synchronous settle path, server-side, with event_id = order_id so the browser pixel and the CAPI mirror dedupe against each other. The webhook never fires it a second time.
  • portal_open fires on every successful /portal/<token> magic-link entry.
  • oto3_take fires on an accepted audio one-tap; oto2_take is now the rush upsell.
  • replacement_take fires on an accepted /replace charge.

Meta will classify this domain as Religion. Divine Rev's religion-flagged pixel got an EU block and a slow appeal. Build for it from day one:

  • Attribution must not rely on URL parameters. Assume click ids get stripped.
  • Every CAPI event carries event_source_url plus the full match-key set, with event_id dedupe against the browser pixel.
  • A fresh pixel per brand. Never reuse a pixel across brand fronts.
  • New-ads-per-day cap ≤ 4 to keep a restricted account out of review pileup.

Read order: CPA is the only day-3–13 metric. Then survey step drop-off. Then bump/OTO take rates (needs ≥100 orders). Then monthly signup rate and portal open rate.

The scale-and-kill rule. This is the only version of it that exists. OPS-RUNBOOK §10 and ECONOMICS quote these four sentences verbatim and state no threshold of their own:

Contribution per buyer before ads ≈ $40.13. Day-10 read: CPA ≤ $31.40 → scale +20–30% every 3 days. CPA $31.40–$33.00 → hold, rotate creative. CPA > $33.00 → kill the ad set.

  • Creative rotation always comes before any offer change.
  • Dispute watch: daily manual read from the payment dashboard; >0.6% in any month → pause ads, fix billing clarity first.

10. LAUNCH SEQUENCE

Day 0 — entity + accounts: new entity/numbered co., domain(s), Stripe account live for this brand (Numina House already live) + PSP_ADAPTER=stripe set + brand Stripe env keys populated + webhook endpoint registered once the public URL exists and its signing secret captured, Meta BM + page, Resend sending domain, image-model + TTS accounts, droplet provisioned. Donor reference image approved for the launch location. Seven example photos generated and approved. Day 1–2 — build: funnel + survey + portal, wire Stripe + webhooks, image pipeline end-to-end with a test order, all 17 email templates, brand config v1 filled (site, persona pack, prompt pack, donor image, example photos, copy). Day 2one live purchase and one live refund on production before a single ad runs. This gate is not skippable. Day 3 — creative + live: 3 ad creatives — (a) 15–30 s silent B-roll-style generated footage

  • on-screen text (the proven format), (b) "what you receive" walkthrough of the delivery email, (c) persona-voiced honest framing. Launch broad US+CA, women 40+, $50/day, ≤4 new ads/day. Day 3–13 — read CPA only, then survey step drop-off. Rotate creative at day 6. Day 14+ — if scaling: push the membership, then the anniversary re-placement to the day-365 cohort. Part 2 — second brand config (new location + persona + donor image + example photos + domain): clone day.

11. PART 2 — BRAND EXPANSION PLAYBOOK

Each new brand = one config + one prompt pack + one donor image + seven example photos + one domain:

Brand slot Location key FE Angle
v1 st_peters_basilica $34.90 Catholic, Vatican authority framing
v2 holy_sepulchre $34.90 Jerusalem terms; proven largest demand
v3 lourdes $34.90 healing angle; Marian devotion
v4 western_wall $29.90 Jewish audience; note-in-wall mechanic; Wave 1
v5 guadalupe $29.90 Spanish-language twin, LATAM+US Hispanic
v6 fatima $34.90 expansion inside the Christian brand — zero incumbent ads

The Western Wall front runs a $29.90 front-end. The $34.90 floor was a v1 rule, not a system rule; products.fe.cents is per brand.

Location keys are snake_case everywhere: BrandConfig.location.key, the donor filename, the ambience preset key, and the PROMPT-PACKS location pack headings must all match exactly.

Rules: one persona per brand (never shared faces across brands), one ad account and one fresh pixel per brand, copy contrast stays categorical ("this is a service fee") — never comparative against named competitors. Shared engine, zero shared identity surface.


12. BACKLOG (post-launch ideas)

  • Video OTO — Ken Burns pan over the generated photo + the audio product + music → 15 s MP4 ("your prayer at the altar, filmed"). $19. Trivial pipeline on top of §4+§5.
  • Prayer wall — free tier: opt-in public listing of (first name + intention) for SEO + lead capture.
  • Seasonal campaigns — All Souls (Nov), Advent, Lent, St. Joseph (Mar 19): themed creatives + member offers, one email each.
  • SEO prayer library — ~90 pages per brand (the durable-traffic layer the incumbent uses).
  • Localized twins — ES first (the incumbent runs ES ads successfully), then PT.
  • Price-ladder test — $13.90 / $27 / $55 three-tier variant against single-price $34.90. Run only after the single price has a stable CPA.

Gift flow and anniversary re-placement moved out of the backlog: both ship in v1 (§2, §3).


13. WHAT MUST EXIST BEFORE FIRST DOLLAR (checklist)

  • Entity + bank + Stripe account live under Numina House (acct_1UGPHEKGASr0FGk4) — DONE 2026-09-16
  • Brand Stripe env keys populated (<SLUG>_STRIPE_SECRET_KEY / _WEBHOOK_SECRET / _PUBLISHABLE_KEY); webhook endpoint registered via scripts/stripe-register-webhook.ts once the public URL exists, its signing secret captured at creation
  • Statement descriptor set: NUMINA* HOLYCARDSDONE 2026-09-16, no phone, never "prayer"
  • Live Stripe keys swapped in for test keys (pass companies/9566-0866/stripe-api-live) before the live-purchase-and-refund gate below
  • Domain + Cloudflare + deployed Next.js funnel (brand config v1 filled)
  • Donor reference image approved for the launch location and committed to brands/<slug>/assets/donor/
  • Seven example photos generated, approved and committed to brands/<slug>/assets/examples/ — 1 landing hero, 3 on /create/ready, 3 on /offer
  • 5-step survey live end-to-end: 220-char cap enforced client and server, survey_step firing with step meta, lead row written at step 5
  • Image pipeline: pass 1 donor scene → pass 2 card-in-hand → card-text QA read-back → S3 → delivery email (one test order end-to-end, one photo per prayer row)
  • Audio pipeline mixed (one test MP3 per locale) + the static OTO3 sample clip live
  • Portal live: magic-link entry at /portal/<token>, deliverable gallery, 15-minute reveal tick flipping a ready deliverable visible, track-order timeline, intention field, membership pause/resume/cancel, email preferences, tokenised offer surface
  • Supporting routes live: /support, /terms, /privacy, /refunds, /create/resume, /thanks/correct-email, /admin
  • Stripe webhooks verified: all 12 subscribed event types reaching the endpoint, Stripe-Signature verified inside the 5-minute replay window, handler returning 2xx
  • 48 h reconciliation sweep running against the orders/payments list API
  • Resend domain verified; all 17 email templates live (§7), every one linking into the portal
  • Support mailbox live and answered — email only
  • Meta BM + page + fresh pixel + CAPI verified (Events Manager check post-pixel, event_source_url on every server event)
  • 3 creatives cut and uploaded
  • Orders dashboard: revenue, CPA, take rates, portal open rate, disputes (manual daily entry) — one screen
  • One live purchase and one live refund completed on production before any ad spend
  • Pre-mortem doc: what a bad dispute month does to any adjacent brand — then confirm zero shared infra