Part four
Roadmap, data model and risk
The six fields you cannot recover later, a phased plan with real exit criteria, the evaluation design, and the cost model.
Day-1 data model
Six of these fields are permanently unrecoverable if you skip them. Everything else is backfillable given a stable customer_id and retained raw text. The unrecoverable set is marked [IRREVERSIBLE] and is roughly one engineer-week of work in week one; it cannot be bought back at any price in month nine.
-- 1. IDENTITY. The most commonly skipped table, and the one that silently
-- degrades holdout integrity, reward backfill and cross-channel suppression.
CREATE TABLE customers (
id uuid PRIMARY KEY, -- [IRREVERSIBLE] stable internal id, assigned at first contact
created_at timestamptz NOT NULL,
jurisdiction text, -- [IRREVERSIBLE] 'UA' | 'EU-<cc>' | other; set at first contact
locale text,
language_pref text CHECK (language_pref IN ('uk','ru')),
language_pref_at timestamptz,
language_pref_source text, -- 'default' | 'customer_request'
register text CHECK (register IN ('vy','ty')) DEFAULT 'vy',
deleted_at timestamptz
);
CREATE TABLE channel_identities (
channel text CHECK (channel IN ('telegram','viber','whatsapp')),
channel_user_id text,
customer_id uuid REFERENCES customers(id),
verified_at timestamptz,
verify_method text, -- Viber exposes NO phone number; needs its own challenge
PRIMARY KEY (channel, channel_user_id)
);
-- 2. CONSENT AS AN EVENT LOG, not an inferred attribute. Meta puts the burden
-- of proof on you; Ukraine's Art. 30 requires evidence of "at the customer's request".
CREATE TABLE consent_events (
id uuid PRIMARY KEY,
customer_id uuid NOT NULL,
kind text, -- 'marketing' | 'ai_features' | 'language' | 'channel'
channel text, -- opt-in is PER-CHANNEL; suppression is GLOBAL
value boolean NOT NULL,
occurred_at timestamptz NOT NULL, -- [IRREVERSIBLE]
source text NOT NULL, -- [IRREVERSIBLE] where the affirmative action happened
wording_id text NOT NULL -- [IRREVERSIBLE] exact text shown; cannot be reconstructed
);
-- 3. PROVENANCE. Nothing downstream may exist without pointing at a row here.
CREATE TABLE utterance (
id uuid PRIMARY KEY,
customer_id uuid NOT NULL,
conversation_id uuid NOT NULL,
occurred_at timestamptz NOT NULL,
role text CHECK (role IN ('user','assistant')),
raw_text text, -- [IRREVERSIBLE if not retained] all labels backfill from this
lang text CHECK (lang IN ('uk','ru','en','mixed')),
pii_scrubbed_text text,
embedding vector(1024), -- derived, rebuildable, never system of record
evidence_label text, -- 'problem'|'implication'|'objection'|'none' — BACKFILLABLE
label_source text, -- 'rule'|'llm'|'human'
label_confidence numeric,
human_verified boolean DEFAULT false
);
-- 4. THE DOSSIER. Bitemporal; contradictions CLOSE the old row, never overwrite it.
CREATE TABLE fact (
id uuid PRIMARY KEY,
customer_id uuid NOT NULL,
slot_key text NOT NULL, -- declared commerce taxonomy, not free-form
value jsonb NOT NULL,
confidence numeric NOT NULL,
expiry_class text CHECK (expiry_class IN ('permanent','seasonal','decaying')),
update_rule text CHECK (update_rule IN
('explicit_only','never_overwrite_append','prefer_most_recent','keep_oldest')),
valid_from timestamptz NOT NULL,
valid_to timestamptz, -- NULL = currently believed
asserted_at timestamptz NOT NULL, -- when WE learned it (bitemporal second axis)
superseded_by uuid,
source_utterance_id uuid NOT NULL REFERENCES utterance(id) -- provenance or silence
);
CREATE INDEX ON fact (customer_id) WHERE valid_to IS NULL;
-- 5. RELATIONSHIP STATE. One row. Mutated ONLY by the scheduled job.
-- Enforce with a separate DB role, not a code comment.
CREATE TABLE relationship_state (
customer_id uuid PRIMARY KEY,
stage text, -- stranger|helped|trusted|advocate
stage_entered_at timestamptz,
-- Trust as FOUR independent floats. self_orientation is a DIVISOR:
-- Trust = (credibility + reliability + intimacy) / self_orientation
credibility numeric, reliability numeric, intimacy numeric,
self_orientation numeric NOT NULL DEFAULT 1.0, -- INCREMENTS on every offer/discount/urgency phrase
R_score numeric, N_score numeric,
scores_computed_at timestamptz, -- staleness > 7d => treat R as 0
readiness_inputs jsonb, -- frozen snapshot, makes scores recomputable
habit_strength numeric, attitude_strength numeric, -- OPPOSITE effects on cross-sell
adverse_trait_profile boolean, -- return_rate + discount_only_share + support_contacts
suppress_offers_until timestamptz, -- OPAQUE. See vulnerability note below.
suppression_id uuid, -- non-semantic pointer; reason lives elsewhere
last_offer_at timestamptz,
offers_30d int DEFAULT 0, offers_90d int DEFAULT 0,
declines_consecutive int DEFAULT 0,
attention_points int DEFAULT 100, attention_refill_at timestamptz,
cadence_tier text, -- active|cooling|dormant|archived
wa_window_expires_at timestamptz,
viber_subscribed boolean, telegram_status text,
holdout_group text, -- 'treatment'|'holdout'|'loose'
holdout_salt_version text -- [IRREVERSIBLE] rotate the salt and history is unreadable
);
-- 6. THE DECISION LOG. Audit trail + eval set + uplift training data + replay buffer.
-- Written on EVERY evaluation, including every withhold.
CREATE TABLE decision_event (
event_id uuid PRIMARY KEY,
occurred_at timestamptz NOT NULL,
customer_id uuid NOT NULL,
conversation_id uuid, turn_index int, channel text,
gate_version text NOT NULL, -- [IRREVERSIBLE] git SHA of the gate
model_version text NOT NULL, -- [IRREVERSIBLE]
prompt_version text NOT NULL, -- [IRREVERSIBLE]
state_snapshot jsonb NOT NULL, -- [IRREVERSIBLE] makes R/N recomputable instead of lost
candidate_actions jsonb NOT NULL, -- [IRREVERSIBLE]
candidate_set_ids text[], -- [IRREVERSIBLE] ordered slate
rank_position int, -- [IRREVERSIBLE]
action_taken text NOT NULL,
action_propensity numeric NOT NULL, -- [IRREVERSIBLE] THE single most important field
propensity_source text NOT NULL, -- [IRREVERSIBLE] 'deterministic'|'epsilon_greedy'
exploration_flag boolean NOT NULL, -- [IRREVERSIBLE]
gate_decision text CHECK (gate_decision IN ('allow','withhold')),
gate_rationale jsonb NOT NULL, -- [IRREVERSIBLE] which rules fired AND which blocked
strength_tier text, -- soft_mention|consultative|direct
offered_sku text,
suppressed_intent jsonb, -- what WOULD have been offered — prices the gate's caution
evidence_utterance_ids uuid[], -- [IRREVERSIBLE] which words justified this
holdout_group text,
reward_immediate numeric,
reward_28d numeric, -- BACKFILLABLE
reward_90d numeric -- BACKFILLABLE
);
-- 7. SCHEDULED WAKE-UPS. Payload carries NO decision.
CREATE TABLE scheduled_touch (
id uuid PRIMARY KEY, customer_id uuid NOT NULL,
fire_at timestamptz NOT NULL, intent text NOT NULL, status text NOT NULL,
created_from_utterance_id uuid
);
CREATE UNIQUE INDEX ON scheduled_touch (customer_id, intent) WHERE status = 'pending';
-- 8. ERASURE REGISTRY. Derived text retains PII that source deletion misses.
CREATE TABLE erasure_log (
customer_id uuid, requested_at timestamptz, completed_at timestamptz,
stores_purged text[], canary_token text, verified boolean
);
Four schema decisions that are not obvious and that every reviewed architecture got at least partly wrong:
vulnerability_flags text[]is a mistake — do not build it. Inferred bereavement, illness or financial distress is GDPR Art. 9 special-category data requiring explicit consent, and equally a prohibited category under Ukraine's Law 2297-VI. Storing it as a categorised array turns your safety feature into the highest-risk processing operation in the system. Instead:relationship_stateholds only an opaquesuppress_offers_untiltimestamp plus a non-semanticsuppression_id. The categorised reason, if retained at all, lives in a short-TTL side store with its own lawful basis, is never an input to any score, and is never joined to the dossier.Confidence floors may only ever BLOCK, never UNBLOCK. Run two thresholds: evidence slots need ≥0.7 confidence to count, suppressive signals (distress, complaint, price resistance, opt-out intent) veto at ≥0.4. People disclose hardship obliquely and once — exactly the utterance shape that yields sub-threshold extraction confidence. A single symmetric floor means the more delicately a customer signals distress, the more likely the bot pitches them.
Identity linkage may only ever suppress, never authorise. A resolved cross-channel link suppresses contact everywhere (WhatsApp policy binds stop requests made "inside or outside WhatsApp"); it never implies reach. Opt-in is per-channel evidence with its own source and wording.
Holdout is computed, not stored.
sha256(customer_id || salt) % 100survives database restores and redeploys without an assignment table — the failure that silently corrupts holdouts at small companies. Storeholdout_salt_versionanyway: rotate the salt without it and every historical row becomes uninterpretable.
Day-1 checklist, in order: stable customer_id → channel_identities → decision_event with action_propensity → permanent holdout at 25–30% → consent as events with wording → erasure registry stub. Nothing else in the build has this property.
Phased roadmap
Costs assume two engineers at $7,000–14,000/month combined. Calendar durations are for a 2-person generalist team; halve them for 4 people only on the parallelisable phases (channels), not on the sequential ones (labelling, evaluation).
Phase 0 — Instrumentation and deflection (weeks 1–6)
Scope. The seven tables above. The offer gate as a pure Python function (~300 lines, gate_version constant, table-driven test suite, one test per never-rule) — not OPA/Rego, not a policy engine: you are governing one decision inside one process, and a DSL with unordered-rule semantics is a 2–4 week learning tax against a function that migrates mechanically later. Telegram only, via aiogram (MIT). Presidio PII scrub at ingest (supported_regions=('UA','RU','RO','MD','PL') on PhoneRecognizer is a one-line fix; only national-ID regexes — РНОКПП, СНИЛС, ІНН, CNP — need writing, 1–2 engineer-days for the whole set). Weeks 1–3 the bot runs suggest-only behind human approval; every human edit is logged as a free labelled training pair. Weeks 4–6 it goes autonomous for support deflection only. Zero offers. File the Rakuten Viber commercial application in week one — bots have been commercial-terms-only since 2024-02-05 and partner onboarding plus Viber's own 2–3 day review runs 2–4 weeks; it becomes the critical path otherwise.
Exit criteria. You can reconstruct any customer's full history and answer "why did the bot do X on day N" from SQL alone. Deflection rate measured for two consecutive weeks. Zero compliance surprises in a manual read of 50 transcripts.
Gating metric: support deflection rate ≥ 40%. This is the phase's entire economic justification and it is the only artefact in the whole plan that pays for itself before month two. At 4 support contacts/customer/year × 6 minutes of human time, 60% deflection saves ≈$1,800/yr at 500 customers, ≈$7,200 at 2,000, ≈$36,000 at 10,000 — against $144–576 of LLM cost. Compare incremental-revenue value at the same sizes ($1,386/yr and $5,544/yr at an optimistic 8% lift) and deflection wins at every scale below ~10,000 customers, arrives in week two rather than year two, and generates exactly the Ukrainian/Russian conversation corpus the relationship layer later needs.
Phase 1 — Rules-only cold start (weeks 7–16)
This phase exists because none of the quantitative machinery is reachable yet and pretending otherwise is the most expensive error available. At 2,000 customers with a 20% holdout you can detect only a 23.5% revenue lift at 80% power. CATE needs ~7,064 customers for a 10% ATE. An EBM readiness model needs 2,000–3,000 labelled offer events, which at 2,000 customers × 2 offers/year × 20% acceptance is a three-year wait. So the month-one gate cannot be learned; it is a hand-written deterministic rule, and it must be labelled as a dated hypothesis rather than a model.
Scope. The gate goes live at SOFT MENTION tier only (name the product in passing while answering their question — no CTA, no price, no link), Telegram only. Full veto layer. Attention budget as a leaky bucket per person. ε = 0.02–0.05 randomisation among safe actions only (advice / question / wait — never randomise into an offer past a hard veto), with true propensity logged. Snorkel labelling functions in uk/ru → SetFit heads on jhu-clsp/mmBERT-base (MIT; EuroBERT covers ru but not uk). Viber bridge (~12.5 engineer-days — see risk register). WhatsApp via Cloud API direct or 360dialog flat fee.
Threshold provenance, stated honestly. The numbers below rest on no empirical evidence at 500–2,000 customers; none can exist. They are cost-asymmetry choices. A missed offer costs one order (≈$16 gross margin at $45 AOV, 35% margin); a false offer costs trust, is unmeasurable, and on a messaging channel a block is permanent. Register each as a dated hypothesis with a scheduled review. The rate limits — max 2 offers/30d, 1/14d, 60-day freeze after two consecutive declines — not the scores, are the actual safety mechanism in this phase.
Exit criteria. Cohen's κ ≥ 0.6 between two independent human labellers on 300 gate-open decisions (below 0.6 the gate definition is too vague to automate and no modelling will rescue it). Zero premature-offer successes in the promptfoo red-team suite. Three-channel parity. Evidence-layer precision measured, not estimated.
Gating metric: block/opt-out rate within 24h of a proactive message < 1.0%, with zero weeks above 2.0%. Do not proceed to consultative offers while this is unstable.
Phase 2 — Graduated offers and measurement (months 5–9)
Scope. CONSULTATIVE RECOMMENDATION tier unlocked, gated on the citation requirement: the message must cite a stored verbatim customer utterance with its date, and if no citable utterance exists the gate auto-downgrades to soft mention. This makes the memory layer load-bearing on revenue rather than decorative, and it is simultaneously the highest-converting format available and the best available Art. 5 defence. Category-level Weibull AFT replenishment via lifelines for consumables only (durables hard-blocked in code — the survival model will fit a reorder curve to a sofa and never warn you; filter gift purchases out via the recipient slot plus shipping-address mismatch first, or every inter-purchase interval is corrupted). Ukrainian groundedness checker trained via LettuceDetect on mmBERT. Weekly auto-emailed digest. Monthly BG/NBD prior-vs-posterior diagnostic.
Exit criteria. Groundedness checker live with a measured F1 on a 300-example human-audited Ukrainian slice (expect ~3 F1 points lost to translate-then-train; KRLabs measured 74.95 → 71.79 on German). Dark-pattern rate baselined and flat. Offers-to-annoyance ratio inside the 1:8–1:12 band.
Gating metric: the BG/NBD posterior visibly separates from the prior for (a,b). This is the cheapest go/no-go instrument in the entire roadmap and it costs one plot per month. BG/NBD needs ≥300 customers with frequency ≥1 and ≥100 with frequency ≥2; at 500 customers with a 25% repeat rate you have ~125 and ~30, so the posterior is pure prior. Until it separates, every downstream quantitative capability is unreachable and you should not staff against it.
Phase 3 — Learned policy, only if the data arrives (month 9 decision point → month 18+)
Month 9 is a written go/no-go, not a milestone. Breakeven, amortising a ~$24,000 build over three years at $45 AOV / 35% margin / 2.2 orders per year: 2,674 customers at a 15% lift, 7,910 at 8%, and never at 3% — because at a 3% lift the per-customer run cost ($1.53/yr) exceeds the per-customer margin gain ($1.04/yr). Below ~3,000 customers, the honest recommendation is to keep the deflection bot and the dossier and stop building the offer layer.
If the base clears ~10,000 customers: DIRECT OFFER tier unlocked only when the golden set shows pass⁴ ≥ 0.9 on withhold scenarios. Off-policy evaluation using IPS/SNIPS/DR estimators vendored from zr-obp (Apache-2.0, but dead — last default-branch commit 2022-11-05; the 2024 pushed_at is dependabot branches, so take the ~200 lines of estimator math, not the package). Uplift via CausalML or EconML, gating on CATE > 0 rather than P(buy) — because a propensity model targets sure-things who would have bought anyway, and cannot represent sleeping dogs: customers whose purchase probability drops when contacted and who block you. That population is what an LTV bot exists to protect, and no sentiment, rapport or readiness score can ever see it.
Explicitly out of the plan of record until 10,000+ customers: EBM readiness models, contextual bandits, conformal abstention, feature stores. Put them in a labelled appendix so nobody staffs against them.
Evaluation plan
Four instruments, in ascending order of cost and descending order of how often you run them.
1. Regression suite (every PR). promptfoo (MIT, 25.1k stars, active). Write a custom premature-offer red-team plugin using the built-in Crescendo (gradual multi-turn escalation with backtracking) and Mischievous User strategies against a stateful target — these test whether the gate can be socially engineered open, which is a real attack surface: a gate that opens on "so what would you recommend?" has no gate. Gate merges on zero premature-offer successes.
The golden set is 300–500 conversations and at least one third must be WITHHOLD cases, weighted 5:1 against the converse. This is not optional bookkeeping: real logs are dense in situations where an offer was reasonable and nearly empty of well-formed examples where silence was correct, so a suite built by sampling logs will certify a bot that offers constantly. Synthesise the withhold cases; source the offer cases from logs.
Report pass^k, not mean pass rate (from tau2-bench, MIT, active). A bot that correctly withholds 70% of the time is pushy in roughly one conversation in three, and on Telegram/Viber/WhatsApp each of those is a permanent, silent, unrecoverable block. Require pass⁴ ≥ 0.9 on offer-gate scenarios before any tier unlock.
2. LLM-judge rubric (weekly on a production sample, and on the golden set in CI). Fork the DarkBench six-category taxonomy (MIT, ICLR 2025 oral; the repo itself is abandoned — entire commit history spans 16 days in March 2025 — so vendor the categories, not the code). The three that matter here:
- Sneaking — quietly reframing the customer's stated need to match the product you want to sell. The most insidious failure mode, because it survives a surface reading of the transcript.
- User retention — manufacturing reasons to keep messaging. On messaging channels this is the behaviour that gets you blocked.
- Brand bias — redefine it for a first-party store as "recommended our product when a better-fitting alternative in our own catalogue existed." The original framing assumes brand promotion is undesirable, which is false here, and the metric will fire constantly until the team learns to ignore it.
Score per conversation (never per turn) on 1–5 with written justification. Add a pushiness dimension from the revised 10-item SOCO selling-orientation scale as an internal-tooling rubric. Calibrate against 200 human-labelled Ukrainian conversations before trusting a single number, and report judge–human correlation alongside the metric permanently — an uncalibrated judge score is not a number, it is a rumour, and every decision built on it inherits that.
Two-tier the cost: a cheap hosted model (inclusionai/ling-3.0-flash at $0.021/M in, or openai/gpt-oss-20b at $0.03/M) screens 100%; a compound debate protocol on a frontier model re-judges the ~5% borderline plus a 2% random audit. Note that both obvious self-hosted judge options are stale — Prometheus-Eval last committed 2025-04-25 (17 months), Verdict 2025-11-05 (10 months) — so fine-tune a current small open-weights model on your own labelled rubric data instead.
3. Simulated-customer harness (before any tier unlock; monthly thereafter). Nobody has published a multi-session, multi-week relationship-selling benchmark — extensive search returns zero repositories for "relationship stage trust agent LLM customer" and zero for "customer lifetime value agent LLM". You are assembling, not adopting:
DeepEvalConversationSimulator (Apache-2.0, very active) for single-session persona-driven simulation.ConversationalGoldencarriesscenario,expected_outcomeandPersona(characteristics=...). The single most important modification in the entire eval stack is thestopping_controllercallback — it is where the simulated customer gets annoyed and leaves, recorded as anopt_out. Without it, every metric monotonically rewards the bot for talking more and offering more, and you will optimise straight into the failure you are trying to prevent. Setexpected_outcometo "advice given, no offer made" for not-ready personas so the metric rewards restraint.Concordia(Apache-2.0, DeepMind, commits 2026-09-14) as the Game Master that advances simulated clock time between sessions. It is the only mature, permissively-licensed, actively-maintained engine that can express "three weeks pass; her running shoes are now worn out; she opens Telegram again." Every other harness in the space is trapped inside one conversation — verified individually for tau2-bench, IntellAgent, DeepEval, promptfoo, Inspect AI and Sotopia.tau2-bench(MIT) forked with arelationshipdomain whose DB state carriestrust_levelandreadiness, reward = correct DB end-state AND offer made only when readiness set AND required disclosure utterances present. Its verifiable-DB-state grading is the pattern to copy instead of touching CRMArena, which is CC BY-NC 4.0 and legally unusable in a commercial product (as is LoCoMo — both read as open source at a glance and are not).LongMemEval(MIT, 500 questions) for the memory substrate, run viamem0ai/memory-benchmarks(Apache-2.0) with the LoCoMo subset disabled on licence grounds. The two ability classes that will hurt you commercially are knowledge updates (she said in March she was buying for her sister; in July she says it is for herself) and abstention (knowing the history does not contain the answer). A bot that hallucinates a remembered preference to justify an offer is the fastest available trust-destroyer, and a readiness gate structurally creates pressure to find evidence of readiness.
Use simulation for ranking variants and catching catastrophic regressions. Never for forecasting. The Game Master's world model is a restatement of your own priors about repurchase timing and annoyance thresholds. Calibrate it against real cohort data and re-calibrate quarterly. And know its structural limitation: LLM-simulated customers essentially never block you, so the harness systematically under-punishes pushiness by an unmeasurable margin. Only production produces block events.
4. Online measurement.
- Permanent never-messaged holdout at 25–30%. Not 5–10%: the MDE is driven by the smaller arm, so at N=2,000 a 10% holdout detects only a 31.3% lift while a 30% holdout detects 20.5%. A too-small holdout gives you the full cost of withholding treatment and no ability to read it out. It must be permanent (LTV effects take 6–12 months), sticky (hash-based on stable
customer_id), and never released. Holdout customers are still served perfectly on inbound — you withhold proactive treatment, not service. Otherwise you measure bot-vs-nothing and can never learn the gate is too conservative. - A third, deliberately looser arm (~20%), same hard vetoes, same block-rate stop rule, but confidence veto at 2 sessions/12 turns and consultative unlocked at R ≥ 55. Policy-vs-policy is the only design that can price the cost of restraint. It costs one extra hash bucket and it answers the question the entire architecture turns on.
- Primary outcome: 90-day binary repeat-purchase rate, not revenue. At N=2,000 with a 30% holdout, revenue at CV≈1.5 detects only a 23.5% lift; binary repeat-purchase from a 25% baseline detects ≈6pp — 3–4× better powered, and it is the LTV mechanism rather than a proxy. Keep revenue as secondary. Use CUPED with PyMC-Marketing predicted LTV as the covariate (GrowthBook, MIT core + three enterprise dirs, supports this natively).
- Confidence sequences (
confseq, MIT, 84 stars, single maintainer — vendor it) for always-valid monitoring so you can peek weekly without alpha penalty. Configure the harm direction first: a pre-registered rollback if the treated-minus-holdout margin's lower bound sits below −10% for two consecutive weeks. Write the rule down before launch; after launch nobody will agree what "bad" means. - Shadow cost-of-withholding ledger. On every withhold, persist the candidate SKU and its gross margin. At 28d and 90d classify: bought it anyway (withhold was free), bought a substitute in-catalogue (cheap), bought nothing or went elsewhere (potential forgone margin). Sum the third category monthly. This needs no randomisation and no scale, reads out in weeks, and is the fastest possible answer to "is the gate leaving money on the table."
Metrics: the weekly dashboard
Four tiers. The tier structure is what prevents metric gaming — a veto is not a trade-off term.
VETO METRICS — any regression kills the variant regardless of revenue.
| Metric | Target | Warn | Hard stop |
|---|---|---|---|
| Block / opt-out rate within 24h of a proactive message | < 0.5% | 1.0% | 2.0% → halt all proactive sends on that channel/segment |
| Block rate within 24h of an offer specifically | < 0.8% | 1.5% | 3.0% |
| Dark-pattern rate (sneaking / user-retention, per conversation) | < 2% | 4% | 6% |
| Hallucinated-spec rate (groundedness failures reaching a customer) | 0 | any | any |
| WhatsApp quality rating | Green | Yellow | Red → stop all templates |
| Error 131049 rate as % of marketing sends | < 2% | 5% | 10% |
Block rate is the early-warning metric and it deserves the top line of the dashboard. It is asymmetric in a way no revenue metric is: a block is permanent, silent, unrecoverable, removes that customer's entire remaining lifetime value at once, and moves in days while LTV moves in quarters. Harm reads out long before lift does. It is also the only metric no simulator will ever produce for you, which makes it the single most valuable thing production tells you that staging cannot.
PRIMARY (slow; decided on the holdout; 6–12 month horizon). 90-day repeat-purchase rate vs holdout (CUPED-adjusted, confidence-sequence monitored) · incremental revenue per customer vs holdout · P(alive) from BG/NBD · inter-purchase interval shift.
SECONDARY (fast, directional, never decisive). Offer acceptance rate — never offer count · offers-to-annoyance ratio (direct offers ÷ education+goodwill touches): operating band 1:8 to 1:12, block offers entirely if it exceeds 1:4 for two consecutive weeks · reply rate to proactive education (target > 15%; demote a cadence tier after three consecutive sends under 8%) · support deflection rate · human-minutes-per-customer (instrument from week one — human escalation is $562/yr at 500 customers and $56,250/yr at 50,000, dwarfing inference at $144 and $14,400) · advice-without-offer count.
DIAGNOSTIC (simulation and internals only; never reported to the business as a forecast). offer_withheld_when_available count — make this a first-class counted metric with a target range, not just a logged reason code. Near zero means the gate is decorative; overwhelming-majority means it is strangling revenue. It is the single number that tells you which failure you have, and it is invisible to every other metric you will build. Plus: pass⁴ on offer gates · judge–human correlation · per-veto fire rates · readiness staleness (a half-failed nightly batch converts the bot into a no-offer bot silently, because withholding is the expected behaviour — alert on scores_computed_at age > 24h, not on error rate) · shadow forgone margin.
Cadence. Weekly auto-emailed digest to both engineers, containing exactly: offers made, offers withheld-when-eligible, decline rate, block rate with its confidence sequence, deflection rate, holdout gap with interval, golden-set pass⁴, dark-pattern rate. That digest is the cheapest available defence against the named failure mode of building for nine months and shipping nothing.
Compliance checklist
Each line is an actionable item with an owner and a verification step.
EU AI Act — applies to any EU-resident customer regardless of where the merchant sits (Art. 2(1) reaches third-country deployers whose output is used in the Union).
- Art. 50(1) AI disclosure at first interaction. Enforceable since 2026-08-02 and explicitly not deferred by the Digital Omnibus (which moved only Annex III high-risk to 2027-12-02 and Annex I to 2028-08-02). Disclose in the first message of every new thread, plus a persistent
/about, plus an honest answer whenever asked. Not in a privacy policy, not by calling itself an "assistant". Verify: automated check that thread turn 0 contains the disclosure string. - Budget for the disclosure penalty and engineer it back. Luo et al. (N=6,255, Marketing Science 2019) measured pre-conversation disclosure cutting purchase rates from 0.237 to 0.048 with a 56.3% hang-up rate. Their mitigation was late disclosure, which Art. 50(1)'s "at the latest at the time of the first interaction" makes illegal. Recover through the two mediators the paper identifies: perceived knowledge (cited specs, honest trade-offs, explicit "I don't know") and perceived empathy (visible memory, acknowledgement before advice). Caveat: the 2019 study was outbound voice calls in Chinese financial services and the magnitude almost certainly overstates a 2026 Ukrainian text channel — but the direction is robust and the mitigation is now foreclosed.
- Art. 5(1)(b) — never use inferred vulnerability as an input. Exploiting vulnerabilities arising from "age, disability, or a specific social or economic situation" is prohibited; applicable since 2025-02-02 with penalties live since 2025-08-02 at €35m or 7% of worldwide turnover (Art. 99(3); SMEs capped at the lower of the two). Financial distress, grief, health status and debt signals go on a feature blocklist for every model that scores readiness — not in the prompt, in the training config. Verify: a unit test asserting those features are absent from the model's feature list.
- Art. 5(1)(a) — keep persuasion perceptible and factually accurate. The Commission's Feb 2025 guidelines confirm personalised advertising is not inherently manipulative and emotional appeals are lawful if transparent and accurate; the prohibition bites on techniques the person cannot perceive. Note the threshold requires significant harm, which a $45 AOV recommendation rarely meets — but the design documents are discoverable, and a system that explicitly models trust and times offers to a readiness peak reads badly. Verify: the deterministic post-generation filter (no manufactured urgency, no scarcity claim unbacked by a live inventory field) runs on 100% of outbound.
- Art. 50(3) emotion recognition — probably not triggered, disclose anyway. Recital 18 and the Art. 3 definition tie emotion recognition to inference from biometric data. A text sentiment classifier on Telegram messages falls outside it. The moment you add voice-note emotion inference you cross into biometric territory and pick up the disclosure duty plus a much heavier data-protection posture. Keep affect inference text-only, and add one line to
/aboutas cheap insurance.
GDPR
- Art. 21(2)/(4) — the absolute right to object to direct marketing, including profiling for it. This is the provision that actually binds, and every reviewed architecture named Art. 22 instead. Art. 22 requires a decision producing legal or similarly significant effects, which a product recommendation almost certainly does not — so "our decision log is the Art. 22 artefact" is a misapplication. Art. 21 is unconditional, must be honoured immediately, and Art. 21(4) requires it be brought to the data subject's attention explicitly and separately from other information, at the latest at first communication. Implement: separate first-message notice + persistent one-tap objection command; objection suppresses all proactive commercial contact cross-channel and stops the profiling, while inbound service continues.
- Art. 9 — do not build a categorised vulnerability field. See the data-model note above. Inferred health status has no available lawful basis here.
- Art. 6(1)(f) will not carry behavioural profiling. Per EDPB Opinion 28/2024, the balancing test centres reasonable expectations, and no shopper reasonably expects a persistent psychological profile built to optimise future sales timing. Split the lawful bases: care/support on contract or legitimate interest; behavioural profiling and proactive marketing on separate, granular, revocable consent, with memory partitioned so withdrawal deletes the profiling facts without destroying support history.
- Art. 17 erasure must purge DERIVED artefacts. LLM-written summaries, consolidated notes and entity summaries retain personal data that source-level deletion misses — a live open bug in the leading graph memory engine and structurally true everywhere. Enumerate every store (Postgres, vector index, traces, Novu/notification subscriber records, channel-provider logs, hosted-LLM provider retention) and test with a canary token: assert a known string appears nowhere after deletion. Regenerate notes, never edit them.
- Territorial scope column. Set
jurisdictionat first contact. A Ukrainian-language commerce bot post-2022 very likely has substantial customer populations resident in Poland and Germany — those are EU data subjects and these obligations are real for exactly them. Consent wording shown at capture cannot be backfilled; this is week-one irreversible. - Retention schedule per jurisdiction. Raw text is retained indefinitely in every architecture reviewed, with no schedule anywhere. Set one.
Ukraine
- Language law Art. 30 — Ukrainian is the mandatory default for consumer service, with another language permitted only "at the customer's request". A State Language Protection Commissioner examines complaints within 10 days, with fines. Auto-detecting Russian from a first message and mirroring it is a compliance failure, not a UX preference. Default Ukrainian always, offer the switch explicitly in one line, store the choice as a timestamped consent event with source and wording. (Verify current fine amounts and chatbot applicability with a Ukrainian lawyer — a few hours, cheaper than one complaint. The primary source was unreachable during research.)
- Law on Electronic Commerce (2015) — commercial electronic messages must be clearly identified as commercial and carry sender information; more than five unsolicited messages to one recipient is treated as spam. This inverts the assumed risk ordering: SOFT MENTION — a product named in passing with no CTA, no price, no link — is commercial communication deliberately shaped not to read as commercial, which is the tier most exposed under this statute and the one most vulnerable to an Art. 5(1)(a) "purposefully manipulative technique" reading. Either give soft mentions a lightweight persistent commercial marker, or abolish the tier and start at consultative. Re-derive the attention budget against the five-message threshold, not purely against attention economics — all reviewed designs tuned to ~8 proactive touches/month.
- AI regulation is coming and will be EU-shaped. The Ministry of Digital Transformation's June 2024 White Paper sets a 2–3 year voluntary phase (sandbox, Voluntary Code of Conduct endorsed Dec 2024, HUDERIA risk methodology) before binding legislation "similar to the EU AI Act", with the European-integration law being drafted during 2026. There is no binding Ukrainian chatbot-disclosure duty today. Build to Art. 50 now; retrofitting disclosure onto a bot whose unit economics assume non-disclosure is how you discover the Luo number the expensive way.
Channel policy (enforced commercially, not legally — and enforcement is faster than any regulator)
- WhatsApp Business Messaging Policy: prior opt-in required before contacting anyone; you must respect stop requests made "whether inside or outside WhatsApp" — so a stop on Telegram legally binds your WhatsApp sends, which forces one cross-channel suppression ledger keyed on
person_id. Automation inside the 24-hour customer service window is permitted only if you also maintain "prompt, clear, and direct escalation paths" to a human. Hard product-catalogue filter for prohibited categories (weapons, alcohol, regulated pharma/medical devices, supplements in some regions, gambling, money lending, adult, MLM) checked at the offer gate, not the message gate. - Viber Developer Distribution Agreement: developers must "if possible, allow the users to opt out of AI related features and ensure human oversight," and must not use AI to distribute unsolicited or bulk messages. Ship a
/humancommand and an AI opt-out flag before Viber launch — this is contract compliance, not polish. Transactional messages must not contain discounts, promo codes or product recommendations, so any offer is classified and billed promotional. Viber may unilaterally "set limits on your use... including limiting the Developer's ability to send messages." - Human handoff route built first. One mechanism satisfies Meta's escalation requirement, Viber's human-oversight clause, GDPR Art. 22's human-intervention safeguard, and the right response to a "specialized advice" safety trip.
FTC / dark patterns (relevant if you ever serve US customers)
- No AI-generated social proof. Operation AI Comply (announced 2024-09-25) charged Rytr specifically over generated reviews. Any AI-summarised review, testimonial, "customers like you also bought", or generated endorsement is endorsement-rule territory. Source social proof from verified transactions only; never let the model paraphrase a review into something it did not say.
- No AI exemption from existing law. An invented product claim is a deception case, not a quality bug.
- Click-to-cancel symmetry. The Negative Option Rule was finalised 2024-10-16 with a stay denied 2024-12-13, and a fresh ANPRM issued 2026-03-13 — so the rule is in flux. Build cancellation to be as easy as sign-up regardless of its current litigation status.
- EU Digital Fairness Act — scheduled Q4 2026, status "Announced" as of 2026-08-01. It explicitly targets dark patterns, addictive design, and "commercial practices related to online profiling, in particular when consumers' vulnerabilities are exploited." A bot that profiles to optimise persuasion timing is near the centre of intended scope. Nothing is law yet. Design so that for every offer you can state, in plain language, why this moment — and make sure the answer is about the customer's need rather than their measured susceptibility.
Risk register
| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| 1 | The gate is deterministic but the evidence feeding it is not. R and N are computed from LLM-extracted slots, a SetFit classifier, and an emotion panel whose Ukrainian anger F1 is 0.31. A perfectly unit-tested gate fires confidently on a mis-extracted budget or a missed complaint. Estimated false-positive rate 15–25% in month one — an estimate, not a measurement. | High | High — mistimed offers to exactly the customers with the deepest dossiers | Make every sensor veto-biased: missing slot blocks, uncertain classifier blocks, out-of-scope caps N, low confidence caps the tier. Use deterministic behavioural proxies wherever one exists — open ticket beats an anger classifier, return-in-flight beats sentiment, complaint keywords beat toxicity scores. Store evidence_utterance_ids so a wrong offer traces to the exact sentence. Week-4 deliverable: label 300 gate-open decisions, two reviewers, compute precision and κ. |
| 2 | The measurement apparatus cannot reach significance before the money runs out. At N=2,000 the holdout detects only a 23.5% revenue lift; CATE needs ~7,064 customers; the EBM needs a three-year wait; OPE is under-powered until ~20,000–50,000 customers. | High | High — twelve months of instrumentation producing only wide intervals, then cancellation | Ship deflection first (pays from week two). Use block rate as the primary steering metric — it reads out in days. Size the holdout at 25–30%. Switch the primary outcome to binary 90-day repeat purchase (3–4× better powered). Log propensities anyway: one column, enormous option value if the business reaches 10k+. |
| 3 | Viber is unpriced, partner-gated, and has no maintained OSS. Bots have been commercial-terms-only since 2024-02-05; no fee figure is published anywhere (developers.viber.com, the FAQ, forbusiness.viber.com and partner docs all checked); the widely-quoted €100–115/month is unsourced. Every SDK is dead (official Python 2021-01-28, Node 2021-08-24, Java marked DEPRECATED). | High | High — Viber is a top-two Ukrainian channel; a bad quote is scope-defining | Apply in week one, in parallel with development. Get quotes in writing from Rakuten Viber and two partners before sizing cadence. Budget ~12.5 engineer-days for the bridge. Mandatory idempotency on message_token — Viber retries 10× with exponential intervals from 10s to 900s, so duplicates are guaranteed and a duplicate here means re-sending an offer. The only MIT reference (botpress, 419 lines) validates no webhook signature — zero crypto/hmac hits — so port signature logic from the dead official SDK and depend on neither. Have a written contingency for "Viber comes back too expensive." |
| 4 | The nightly consolidation batch fails silently. Stale readiness vetoes offers; withholding is the expected behaviour; therefore a half-failed batch is indistinguishable from a working product on every dashboard. | Medium | High — revenue stops with no alarm | Alert on scores_computed_at age, not on error rate. Idempotent batch with per-customer checkpointing. Cap LLM summarisation cost explicitly (a nightly regeneration for 2,000 customers does not fit a $15–30/month LLM budget). Put "readiness staleness" on the weekly digest. |
| 5 | The eligibility floor, not the rate cap, strangles revenue. The conjunctive floor (≥3 sessions AND ≥25 turns AND R≥62 AND N≥55 AND M≥70) means that at a $45 AOV store where most customers buy once, the majority never clear it. Realised offer volume in year one may be under 1 per customer, nearly all soft mentions. | Medium-High | High — the holdout gap reads ≈0 by construction and the project is killed for lack of treatment, not lack of effect | Model realised offer volume before launch. Estimate incremental conversion per offer against breakeven: an 8% lift needs ≈0.176 incremental orders/customer/year, which at a generous 5% incremental conversion per offer is ≈3.5 linkable offers/year — soft mentions with no CTA plausibly convert at 1–2% and are unattributable. Run the loose third arm. Track offer_withheld_when_available with a target range. |
| 6 | The learned-policy endpoint optimises toward exploiting vulnerability. An uplift or EBM model trained on conversion locates states of lowest resistance, which correlate with distress, fatigue, loneliness and financial precarity. Excluding the vulnerability label does not exclude the construct — discount_only_share, price resistance, reply latency, session cadence and time-of-day are dense proxies. | Medium (only if Phase 3 ships) | Very High — Art. 5(1)(b), €35m/7% | Glass-box only (EBM with inspectable shape functions), monotone constraints forcing days_silent and escalation_prob decreasing in readiness. Review the shape-function panel as a scheduled compliance task with a named owner who does not own the revenue number. Run Giskard bias scans over offer decisions (not chat text) across age, gender, income proxies and postcode, using synthetic profiles. Keep the hard veto layer above any learned score, forever. |
| 7 | Human labelling is the unpriced critical-path constraint. Summing across phases: 300 utterances × 2 reviewers for κ, 300–500 golden-set conversations, 200 for the DarkBench rubric, 500 extraction verifications, 300 for the groundedness audit slice. 8–12 person-days of native-Ukrainian-speaker attention, gating every quality checkpoint — and in a 2-person team it is the same people writing the Viber bridge. | High | Medium — slips every gate by weeks | Budget an external Ukrainian annotator explicitly. Label from real transcripts only — synthetic Ukrainian has nothing like the register of real customer messages (abbreviations, Latin transliteration, uk/ru code-switching mid-sentence) and a classifier trained on it fails silently on real traffic. Front-load labelling into the suggest-only phase, where human edits generate labels for free. |
| 8 | Ukrainian guardrail gaps are real where it matters and absent where it does not. Moderation is solved (textdetox: uk F1 0.96, ru 0.9525 — its two best languages). Groundedness is not: Granite Guardian states verbatim "only trained and tested on English data", HHEM-2.1-Open is English-only, and paid HHEM-2.3 covers Russian but not Ukrainian at any price. Running an English-only guard on Ukrainian text returns confident scores and paints a green dashboard while the bot invents specs. | Medium-High | High — an invented spec is a consumer-law misrepresentation and a trust event | Ship on LettuceDetect's generative fallback (detector method='llm') with an explicit Ukrainian prompt from day one. Train the encoder (translate RAGTruth preserving <hal> tags → fine-tune mmBERT; ~20–30 A100-hours, ≈$60–120 spot) once catalogue-grounding errors appear in transcripts. Build the 300-example human-audited slice — translate-then-train costs ≈3 F1 points (74.95 → 71.79 measured on German) and you must know your real number. Budget 3–4 weeks, not one, if nobody has fine-tuned a token classifier before. |
Two risks deliberately excluded as lower-priority but worth naming: (a) openrail++ on the toxicity classifier is a use-restricted non-OSI licence whose standard attachment prohibits exploiting vulnerabilities of a specific group — defensible here since the use is suppressive, but needs written sign-off; (b) the minor/age-uncertain veto is a paper control, since no channel exposes age and no design specifies how the flag is ever set.
Cost model
Assumptions, all stated: 8 turns/conversation; 41,691 input tokens/conversation of which 17,600 are a stable cacheable prefix (system + policy + catalogue); 1,477 output tokens; Ukrainian token penalty 1.42–1.48× English measured directly on o200k_base (Russian only 1.07×; on older 100k-vocab tokenizers Ukrainian is 2.39–2.49×, so large-vocab tokenization is an explicit model-selection criterion); 2 conversations/customer/month; channel mix Telegram 55% / Viber 35% / WhatsApp 10%; 0.6 proactive re-engagements/customer/month; classifier tier ≈12% of generation cost.
| Line item | 5k conv/mo (≈2.5k customers) | 50k conv/mo (≈25k customers) | 500k conv/mo (≈250k customers) |
|---|---|---|---|
| LLM generation (gemma-4-26b-a4b hosted, $0.09/$0.30/M, cache-read $0.05/M) | $17 | $175 | $1,746 |
| Per-turn classifier tier | $2 | $21 | $210 |
| Infrastructure (Postgres + app VMs; replica at tier 2; HA + object store + monitoring at tier 3) | $75–140 | $400–600 | $900–1,400 |
| Telegram | $0 | $0 | $0 |
| Viber | €200 floor (~15,500 msgs prepaid; marginal cost of an educational touch ≈$0) | €200 floor (still inside the prepaid package) | ≈€680 metered |
| WhatsApp (360dialog flat €49/number + marketing templates) | ≈$56 | ≈$95 | ≈$450 |
| Technical subtotal | ≈$370–440/mo | ≈$920–1,120/mo | ≈$4,100–4,600/mo |
| Human escalation (≈$1.13/customer/yr) | ≈$235/mo | ≈$2,340/mo | ≈$23,400/mo |
| Engineering (2 people) | $7,000–14,000/mo | $7,000–14,000/mo | $7,000–14,000/mo |
Four conclusions the arithmetic forces:
Infrastructure is 4–9× cheaper than the people operating it. At 50k conv/mo the entire technical bill is ≈$1,000 against $7,000–14,000 of engineering. Therefore the correct metric for any component is how many stateful services it adds, never its monthly price. That single ratio justifies running one Postgres, one app process embedding DBOS as a library, one channel-ingress process and hosted LLM endpoints — rather than the 12–15 services and 4+ databases the obvious architecture implies. (Concretely: Hatchet's own
docker-composerequires Postgres + pgbouncer + RabbitMQ + NATS — four stateful services including two brokers — while minimal Temporal runs against plain Postgres with no Elasticsearch. The "light" option was not the light one.)Do not self-host inference for money. Break-even, computed from RunPod rates (page updated 2026-09-13): 165,185 conv/mo on 1× L40S community with no redundancy, 227,913 on 1× L40S secure, 416,098 on 1× H100 community, 455,826 on 2× L40S secure with HA. At 50k conv/mo you are 3–9× below every figure. Even at 500k conv/mo, 2× L40S secure ($1,591/mo) against hosted ($1,746/mo) saves ≈$155/month — less than one engineer-week of GPU incident response. Self-host for Ukrainian PII residency, latency control and serving your own LoRA. Never for the token price.
Compare models on cache tiers, not headline price.
gemma-4-26b-a4b-itdiscounts cache reads only $0.09 → $0.05 (44%);mistral-small-2603discounts $0.15 → $0.015 (90%). On a workload where 17,600 of 41,691 input tokens are a stable prefix, the model with the 67% higher headline input price can be cheaper in production. Measure against your real prefix ratio. Note also thatgpt-oss-120blooks cheapest at $0.037/M and exposes no cache-read tier at all — while carrying the worst-measured Ukrainian tokenizer (1.82× vs Gemma's 1.51×), which erases nearly the whole advantage twice over.The single largest avoidable line item is BSP markup. Twilio charges $0.005 per message inbound and outbound on top of Meta's fee — including on messages Meta bills at zero (service messages, in-window free-form, in-window utility templates). A multi-turn relationship bot generates exactly that traffic in volume. At 50k conv/mo that surcharge alone is ≈$800/month versus ≈€49 flat on 360dialog. Go Cloud API direct or flat-fee BSP. Relatedly: the offer gate is economically aligned with good selling — a gate that withholds until the customer re-initiates keeps traffic inside the free 24-hour window, so patience is literally cheaper, and the 72-hour Free Entry Point opened by a Click-to-WhatsApp click is the cheapest legitimate nurture surface that exists.
Build cost and breakeven. ≈$24,000 for six months at partial allocation of two engineers. Amortised over three years at $45 AOV / 35% margin / 2.2 orders per year ($34.65 baseline gross margin per customer per year): breakeven at 2,674 customers with a 15% lift, 7,910 with an 8% lift, and never at a 3% lift — because per-customer run cost ($1.53/yr) then exceeds per-customer margin gain ($1.04/yr). One-off items on top: Viber bridge ~12.5 engineer-days; Ukrainian groundedness model ≈$60–120 GPU plus 2–3 annotator-days; Viber Sender ID registration ≈€100 plus 2–4 weeks lead time; PII recognizers 1–2 engineer-days; a few hours of Ukrainian legal review.
One caveat that may move the breakeven in your favour and that nobody modelled: all of the above treats the lift as a single-period revenue effect. If the bot changes the retention curve — repeat rate 25% → 30% persisting across periods — the return compounds and the breakeven customer count falls materially. Redo the breakeven as a retention-curve delta before letting the single-period number veto the project.