Skip to content

Epic 44 — Paid Messaging & One-Message-Then-Wait Rule

✅ SHIPPED 2026-06-18 (backend 07ccb5a, frontend 06dc4d9). Recipient-typed cost (PRODUCER 2500 / talent·pet-owner·agency 2000) from the recipient's producerProfile; non-rejected Match/PetMatch (either axis) waives cost + turn rule; one-message-then-wait enforced server-side (AWAITING_REPLY); charged per outbound turn via deductCasTars before the message is created; messagingPolicy(recipientId, conversationId) drives the FE pre-send disclosure + composer lock (ConversationPage handles draft + existing; NewMessagePage routes into the draft). Gated by the existing DIRECT_MESSAGE_CREDIT_COST flag. The flat DIRECT_MESSAGE_CREDIT FEATURE_COSTS entry is now dead-but-retained. BE 1404 + FE 564 tests green.

Reworks: Epic 17 (Messaging) + the current DIRECT_MESSAGE_CREDIT (50 CasTars cold-outreach) model. Decision (user, 2026-06-16): Messaging costs CasTars by recipient type2,000 to message a Talent, 2,500 to message a Producer. A user may send only one message and must wait for a reply before sending again, and this must be made clear before the first message.

Why: Today opening a cold conversation costs a flat 50 CasTars (waived when users share a job context) with no rate limit. The stakeholders want a much higher, recipient-typed cost and a strict turn-taking rule to keep outreach high-intent and reduce spam. Remember talent = Talent + Pet Owner for the 2,000 cost.

Architecture: Replace FEATURE_COSTS.DIRECT_MESSAGE_CREDIT with a recipient-type cost resolved from the recipient's profile: TALENT/PET_OWNER/AGENCY → 2000, PRODUCER → 2500 (decision 2026-06-16: Agency = talent cost). Charge on each outbound message that opens a new turn. Enforce the one-message rule server-side: a sender may have at most one unanswered outbound message in a conversation; the next send is blocked until the recipient replies. Decision (user, 2026-06-16): messaging inside a mutual Match (Epic 43) is EXEMPT (free) — a match is mutual opt-in, so it replaces the old shared-job-context waiver; the cost + one-message rule apply only to non-matched (cold) outreach.

BE-MSG-COST-001 — Recipient-typed message cost

Files:

  • Edit: castyou-backend/src/services/messaging/index.ts + src/services/castars/features.ts — remove the flat DIRECT_MESSAGE_CREDIT (50); replace the usersShareJobContext waiver with a match waiver (a conversation between two users who share an active Match is free); resolve cost from recipient profile type (2000 talent/pet-owner/agency, 2500 producer); deduct via the CasTars ledger on send; throw INSUFFICIENT_BALANCE when short.

Acceptance criteria: Sending to a talent/agency costs 2000, to a producer 2500; a matched conversation is free; insufficient balance blocks the send with a clear error.

BE-MSG-COST-002 — One-message-then-wait enforcement

Files:

  • Edit: castyou-backend/src/models/mongo/Conversation.ts / Message.ts + src/services/messaging/index.ts — track the last outbound sender; reject a second outbound message from the same sender until the counterpart replies (code: 'AWAITING_REPLY').

Acceptance criteria: A sender's 2nd consecutive message (no reply yet) is rejected; after a reply, they can send again (and are charged again).

FE-MSG-COST-001 — Pre-send disclosure + turn-aware composer

Files:

  • Edit: castyou-frontend/apps/app/src/pages/messages/{ConversationPage,NewMessagePage}.tsx — before the first message, a clear notice/modal: the exact cost (by recipient type) and the one-message-then-wait rule; disable the composer while awaiting a reply or when balance is insufficient; show the cost on the send button. DS + i18n en/pt/es.

Acceptance criteria: The cost + one-message rule are shown before the first send; the composer disables while awaiting a reply or underfunded.

TEST-MSG-COST-001 — Messaging cost coverage

Acceptance criteria: Recipient-typed cost, insufficient-balance block, and the awaiting-reply lock are tested; the disclosure renders.