{"id":50559221,"url":"https://github.com/chatman-media/lead-engine","last_synced_at":"2026-06-04T10:30:42.632Z","repository":{"id":359601578,"uuid":"1241870653","full_name":"chatman-media/lead-engine","owner":"chatman-media","description":"AI sales-funnel engine — Telegram, WhatsApp \u0026 web widget, RAG, visa pipeline, admin UI, Postgres (monorepo)","archived":false,"fork":false,"pushed_at":"2026-06-03T04:20:00.000Z","size":21615,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T04:22:30.565Z","etag":null,"topics":["ai","bun","chatbot","monorepo","multichannel","postgresql","rag","recruitment","sales-funnel","telegram","typescript","whatsapp"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chatman-media.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-17T23:09:10.000Z","updated_at":"2026-06-03T04:20:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chatman-media/lead-engine","commit_stats":null,"previous_names":["chatman-media/lead-engine"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/chatman-media/lead-engine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chatman-media%2Flead-engine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chatman-media%2Flead-engine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chatman-media%2Flead-engine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chatman-media%2Flead-engine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chatman-media","download_url":"https://codeload.github.com/chatman-media/lead-engine/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chatman-media%2Flead-engine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33901305,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","bun","chatbot","monorepo","multichannel","postgresql","rag","recruitment","sales-funnel","telegram","typescript","whatsapp"],"created_at":"2026-06-04T10:30:41.475Z","updated_at":"2026-06-04T10:30:42.616Z","avatar_url":"https://github.com/chatman-media.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003ca name=\"top\"\u003e\u003c/a\u003e\n\n# Lead Engine\n\n**Multichannel AI Sales Closer — Telegram · WhatsApp · Web Widget**\n\n[![CI](https://github.com/chatman-media/lead-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/chatman-media/lead-engine/actions/workflows/ci.yml)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![Bun](https://img.shields.io/badge/Bun-1.3-fbf0df?logo=bun\u0026logoColor=black)](https://bun.sh/)\n[![PostgreSQL + RLS](https://img.shields.io/badge/PostgreSQL-RLS%20%2B%20pgvector-336791?logo=postgresql\u0026logoColor=white)](https://github.com/pgvector/pgvector)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Telegram](https://img.shields.io/badge/Telegram-bot%20%2B%20userbot-26A5E4?logo=telegram\u0026logoColor=white)](https://core.telegram.org/bots/api)\n[![WhatsApp](https://img.shields.io/badge/WhatsApp-Cloud%20API-25D366?logo=whatsapp\u0026logoColor=white)](https://developers.facebook.com/docs/whatsapp)\n[![Stripe](https://img.shields.io/badge/Stripe-billing-635BFF?logo=stripe\u0026logoColor=white)](https://stripe.com/)\n\nMulti-tenant SaaS · BYOK LLM · per-tenant RAG · sales methodologies (SPIN / NEPQ / AIDA) · operator takeover\n\n---\n\n🌐 **Language / Язык / 语言**\n\n🇬🇧 **English** \u0026nbsp;·\u0026nbsp; [🇷🇺 Русский](README.ru.md) \u0026nbsp;·\u0026nbsp; [🇨🇳 中文](README.zh.md)\n\n\u003c/div\u003e\n\n---\n\n**Multichannel AI Sales Closer for recruitment agencies.**\nA multi-tenant SaaS platform. Replies to inbound leads in 30 seconds\nacross Telegram, WhatsApp, and a web widget — walks a candidate from\n\"just curious\" to a submitted application, and hands hot leads off to a\nrecruiter. Driven by sales methodologies (SPIN, NEPQ, AIDA) — not a FAQ bot.\n\n**Phase 1 ICP:** recruitment agencies in RU/CIS/MENA, Telegram-first,\nARPU $99–199/mo. [Phase 2: real estate. Phase 3: horizontal.]\n\n**How it works:** a business signs up → connects its own Telegram bot\n(auto-`setWebhook` in 60s) → configures its own OpenAI / Anthropic key\n(BYOK) → uploads documents into the KB → the AI replies and drives the\nfunnel. An operator can take over any conversation at any time from the\ninbox.\n\nEach customer is an independent `tenant` with its own channels, LLM\nconfig, knowledge base, and data isolation enforced at the Postgres RLS\nlevel.\n\n\u003e The product is technically universal for any customer-facing business\n\u003e with a messenger funnel. Phase 1 focuses on recruitment for a\n\u003e laser-precision go-to-market. More in [`docs/COMPETITORS.md §0`](docs/COMPETITORS.md).\n\nExtracted from a legacy Telegram bot through a series of architectural\nPRs (see `docs/ROADMAP.md` and the git log).\n\n📖 **See also:**\n- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — data flow, RLS, hot-reload details\n- [`docs/ONBOARDING.md`](docs/ONBOARDING.md) — a new tenant's path (UI + curl)\n- [`docs/ROADMAP.md`](docs/ROADMAP.md) — what's done, in progress, and next\n- [`docs/COMPETITORS.md`](docs/COMPETITORS.md) — competitor analysis and positioning\n\n---\n\n## Features at a glance\n\n| Channels | AI Engine | Operator Tools |\n|---|---|---|\n| Telegram Bot | RAG: pgvector + BM25 + RRF fusion | Inbox + conversation takeover |\n| Telegram Userbot | Multi-query expansion (parallel search) | Lead pipeline (Kanban) |\n| WhatsApp Cloud API | MMR diversification + distance threshold | Drag-and-drop funnel builder |\n| Web Widget (WebSocket) | Jina / Cohere cross-encoder reranking | A/B experiments + ELO ranking |\n| Auto-setWebhook (60s) | BYOK LLM (OpenAI / Anthropic / Ollama) | Outreach broadcasts (bulk message) |\n| Per-tenant RLS isolation | SPIN / NEPQ / AIDA methodologies | Message templates CRUD |\n| — | Passport OCR + photo vision | Superadmin panel (tenant list + plan mgmt) |\n| — | Hallucination guard + semantic cache | Forgot/reset password flow |\n| — | Per-purpose LLM routing | Admin invite flow + roles |\n\n\u003e **Screenshot / GIF coming soon** — admin UI preview (inbox · lead pipeline · funnel builder)\n\n---\n\n## Self-service tenant flow\n\nThe full onboarding cycle — **no env vars, no restarts**:\n\n```\n1. /signup       → email + password → JWT + tenant created (free plan) →\n                   redirect to /onboarding (guided wizard: channel → keys → KB → done)\n2. /channels     → Telegram tab: paste @BotFather token → auto setWebhook + encrypt + reload\n                   Personal tab: phone → code → 2FA (MTProto userbot, superadmin)\n                   WhatsApp tab: paste { phoneNumberId, accessToken } → Meta Graph\n                   validate → encrypt + webhook-setup hint for the Meta dashboard\n                   ✓ Channels accept inbound immediately (Worker reload ≤30s)\n3. /settings     → save OpenAI / Anthropic / Ollama key → encrypted AES-256-GCM,\n                   InMemoryLlmRouter hot-reload → ✓ AI ready to answer\n4. /dashboard    → upload .txt / .md / .json → ingest + embed → kb_chunks\n                   ✓ RAG answers grounded in the business's knowledge\n5. /conversations → inbox with auto-poll 5s. \"Take over\" → mode='human' →\n                   AI goes silent on that conversation. \"Return to AI\" → back\n6. /audit        → which admin changed what (every PUT/POST/DELETE)\n7. /diagnostics  → health check of the whole setup with one button\n8. /dashboard    → PlanWidget: usage bars + \"Upgrade Starter $99 / Pro $199\"\n                   → Stripe Checkout (14-day trial) → webhook bumps the plan →\n                   quota increases instantly\n```\n\n**Quota by tier** (see `apps/api/src/lib/plans.ts`):\n\n| Plan | Channels | KB docs | Rate/min | Price |\n|---|---|---|---|---|\n| `free` | 1 | 50 | 30 | $0 |\n| `starter` | 3 | 500 | 60 | $99/mo |\n| `pro` | 10 | 10000 | 120 | $199/mo |\n| `enterprise` | 100 | 100000 | 600 | custom (self-host) |\n\nExceeding a channel/KB POST limit → `402 Payment Required` with a structured\nresponse (`{ reason, limit, current, plan, upgradeHint }`) — the UI shows an\n\"Upgrade\" CTA.\n\nChanges apply **live** through an in-process bus (`apps/api`) plus a\n30-second polling reload (`apps/worker`). Details in\n[`docs/ARCHITECTURE.md#hot-reload`](docs/ARCHITECTURE.md).\n\n---\n\n## Architecture\n\n### Apps (deployable processes)\n\n| App | What it is | Deploy |\n|---|---|---|\n| `apps/api` | HTTP server: webhook handlers (telegram/whatsapp/stripe), `/ws/:slug` (web), admin API (auth + KB + LLM config + channels + conversations + leads + funnel builder + skills + styles + experiments + audit + diagnostics + tenant pause), `/metrics`, `/healthz` | Fly app / Node hosting |\n| `apps/worker` | Outbound dispatcher (`SKIP LOCKED` queue), polling channel-reload, cron jobs | Fly app process group |\n| `apps/admin-ui` | React 19 + Vite SPA on **Tailwind v4 + shadcn/ui** (Linear theme, left sidebar, light/dark) — full SaaS UI: guided onboarding wizard + dashboard / channels / settings / conversations / leads / funnel builder / skills / styles / experiments / team / audit / diagnostics | Static / CDN |\n| `apps/vertical-recruitment-uae` | Vertical template (KB seed + funnel stages + style prompts) — NOT deployed, loaded via `packages/verticals` | — |\n\n### Packages (domain modules)\n\n```\n@chatman-media/storage             — Drizzle schema + migrations, integration helpers\n@chatman-media/observability       — JsonLogger, Counter/Histogram, PlatformMetrics\n@chatman-media/channel-core        — ChannelAdapter contract, Inbound, OutboundEnvelope\n@chatman-media/channel-telegram    — BotAPI + MTProto userbot\n@chatman-media/channel-whatsapp    — Meta Graph API\n@chatman-media/channel-web         — WebSocket-based chat-widget channel\n@chatman-media/llm-router          — LLM I/O (chat/embed/providers/router). Per-tenant config\n@chatman-media/kb                  — RAG (ingest, answer, hybrid search, ABRouter, photo classification + passport OCR)\n@chatman-media/sales               — sales domain (CoachAnalyzer, StageClassifier, ELO)\n@chatman-media/conversation-engine — pipeline contracts + DAL + persistence\n@chatman-media/verticals           — VerticalTemplate registry (recruitment_uae_v1)\n```\n\nAll `packages/*` are published to npm under the `@chatman-media` scope. See\n[Releasing packages](#releasing-packages).\n\n**Dependency direction** (acyclic):\n\n```\nconversation-engine ── llm-router\n                  ├── kb ── llm-router\n                  ├── sales ── kb, llm-router\n                  └── storage\nchannel-* ── channel-core\napps/api ── conversation-engine, channel-*, sales, kb, llm-router\napps/worker ── conversation-engine, channel-telegram\n```\n\n---\n\n## Quick start (local dev)\n\n### Requirements\n\n- [Bun](https://bun.sh) 1.3.14+\n- Docker (for Postgres with pgvector)\n\n### Setup\n\n```bash\ngit clone git@github.com:chatman-media/lead-engine.git\ncd lead-engine\nbun install\n\ncp .env.example .env\n# Minimum: PLATFORM_MASTER_KEY (openssl rand -hex 32),\n#          TELEGRAM_WEBHOOK_SECRET (any string),\n#          PLATFORM_PUBLIC_URL=http://localhost:3000 (for auto-setWebhook)\n\nbun db:up                                                    # postgres@5434\nbun run apps/api/scripts/reset-and-migrate.ts                # apply migrations\n\nbun run dev          # apps/api on PORT 3000\nbun run dev:worker   # apps/worker (outbound + reload polling)\ncd apps/admin-ui \u0026\u0026 bun run dev   # admin-ui on http://localhost:5173\n```\n\nOpen `http://localhost:5173/signup` → create a tenant → guided onboarding\nwizard (`/onboarding`): channel → API keys → knowledge base → done.\n\nServer update / production runbook: [docs/SERVER_RUNBOOK.md](docs/SERVER_RUNBOOK.md).\n\n### Bun shortcuts\n\n```bash\nbun db:up          # start the Postgres container\nbun db:down        # stop it\nbun db:reset       # drop + re-migrate (clean DB)\nbun db:psql        # psql shell in the container\nbun run typecheck  # tsc across all 15 packages\nbun run test       # bun test across the whole monorepo (700+ tests)\n```\n\n---\n\n## Multi-tenant model\n\nEach customer is a `tenant` row with a unique `slug`. All domain data is\nscoped by `tenant_id`:\n\n```\ntenants ─┬─ admins (multi-admin per tenant + invite flow) ─ admin_invites\n         ├─ channels (telegram_bot / telegram_userbot / whatsapp / web)\n         ├─ contacts ─ channel_identities (channel-agnostic person ↔ messenger)\n         ├─ conversations ─ messages\n         ├─ leads ─ lead_events ─ lead_notes\n         ├─ kb_documents ─ kb_chunks (per-tenant RAG)\n         ├─ styles, experiments, skills, ...\n         ├─ outbound_queue (SKIP LOCKED)\n         ├─ tenant_secrets (AES-256-GCM encrypted)\n         ├─ llm_provider_configs (per-purpose: chat | embed | vision | judge)\n         └─ audit_log\n```\n\n### RLS — Row-Level Security\n\n`FORCE ROW LEVEL SECURITY` on 34 tenant-scoped tables with the policy:\n\n```sql\nUSING (tenant_id = current_setting('app.tenant_id', true)::int)\nWITH CHECK (tenant_id = current_setting('app.tenant_id', true)::int)\n```\n\nAll production code paths wrap repo calls in `withTenant(db, tenantId, fn)`\n— it opens a transaction and runs `SET LOCAL app.tenant_id = X`.\n\n**Production critical:** `apps/api` / `apps/worker` MUST connect under a\n`NOSUPERUSER NOBYPASSRLS` Postgres role. Otherwise RLS is bypassed. On\nboot both processes log `info \"RLS enforced\"` or `warn \"RLS not enforced\"`\nwith a remediation hint.\n\nValidated in `packages/storage/src/rls.integration.test.ts` (8 tests) and\n`apps/api/src/multi-tenant.integration.test.ts` (10 E2E tests).\n\n---\n\n## Channels\n\n| Channel | Inbound | Outbound | Adapter location |\n|---|---|---|---|\n| `telegram_bot` | webhook `POST /webhook/telegram/:slug` (X-Telegram-Bot-Api-Secret-Token) | `apps/worker` → BotAPI HTTPS | apps/api + apps/worker |\n| `telegram_userbot` | `apps/api` MTProto receive loop (pinned connection) | `apps/api` in-process via `UserbotOutboundDispatcher` | apps/api only |\n| `whatsapp` | webhook `POST /webhook/whatsapp/:slug` (X-Hub-Signature-256) | `apps/worker` → Meta Graph | apps/api + apps/worker |\n| `web` | WebSocket `/ws/:slug?user=X\u0026auth=Y` | `apps/api` in-process via `WebOutboundDispatcher` (pinned WS) | apps/api only |\n\n**Auto-setWebhook**: after insert, `POST /api/admin/channels/telegram`\nautomatically calls Telegram `setWebhook(url=\u003cPLATFORM_PUBLIC_URL\u003e/webhook/telegram/\u003cslug\u003e,\nsecret_token=\u003cTELEGRAM_WEBHOOK_SECRET\u003e)`. The channel works immediately, with\nno manual curl command.\n\n### Signature verification\n\n- **Telegram**: `X-Telegram-Bot-Api-Secret-Token` = `TELEGRAM_WEBHOOK_SECRET`\n- **WhatsApp**: `X-Hub-Signature-256` HMAC-SHA256 of the raw body with `WHATSAPP_APP_SECRET`. Checked **before** tenant lookup (anti-enumeration).\n- **Web**: optional shared secret via `WEB_WS_AUTH_SECRET`. JWT is the next iteration.\n- **Stripe**: HMAC-SHA256 with `STRIPE_WEBHOOK_SECRET`.\n\n---\n\n## Pipeline (inbound → outbound)\n\n```mermaid\nflowchart LR\n  W([Webhook]) --\u003e S{Signature OK?}\n  S -- ❌ --\u003e E1([401])\n  S -- ✅ --\u003e R{Rate limit OK?}\n  R -- ❌ --\u003e E2([429])\n  R -- ✅ --\u003e TX1\n\n  subgraph TX1 [tx1 — persist]\n    C[resolveContact] --\u003e CV[resolveConversation]\n    CV --\u003e M[persist Message]\n    M --\u003e SC[stageClassifier ~300ms]\n    SC --\u003e ME[memoryExtractor ~500ms]\n  end\n\n  TX1 --\u003e LLM[RAG · reply.generate ~1-2s]\n  LLM --\u003e TX2[tx2 — enqueue outbound]\n  TX2 --\u003e ACK([200 ack \u003c 100ms])\n\n  TX2 --\u003e W2[apps/worker]\n  W2 --\u003e Out1[Telegram Bot API]\n  W2 --\u003e Out2[WhatsApp Graph API]\n  TX2 --\u003e Out3[WebSocket / Userbot in-process]\n\n  M -.-\u003e|async fire-forget| OCR[passport OCR · vision]\n```\n\n**Detailed steps:**\n\n```\n1. Webhook handler receives the HTTP POST                       (apps/api)\n2. Validate signature → 401 if bad\n3. Lookup tenant + channel via ChannelRegistry (in-memory)\n4. Rate-limit check per tenant (60/min, 600/hour default) → 429 if over\n5. adapter.pushUpdate(payload) → adapter inbox\n6. ┌─ Phase 1 (tx1, withTenant): persist inside Postgres ──────┐\n   │  - resolveContact (lookup or create Contact + ChannelIdentity)\n   │  - resolveConversation (per channel)\n   │  - persist Message (unique dedup by external_message_id)\n   │  - vertical-template extractFields hook\n   │  - stageClassifier (~300ms LLM) → applyClassifiedStage\n   │  - memoryExtractor (~500ms LLM) → mergeAttributes\n   └────────────────────────────────────────────────────────────┘\n7. Phase 2 (NOT in tx): reply.generate(...) — ~1-2s LLM. Pool connection\n   released.\n8. Phase 3 (tx2, withTenant): enqueue OutboundEnvelope[] into outbound_queue.\n9. Phase 4 (async, NOT in tx): if inbound has photo parts and tenant has a\n   `vision` LLM configured → classifyPhoto() → if \"passport\" →\n   extractPassportIdentity() → merge into contact.attributes_json.\n   Fire-and-forget; never blocks the webhook response.\n10. Webhook → 200 ack (\u003c 100ms typical).\n11. apps/worker (TG-bot / WA) or apps/api (web / TG userbot) drain\n    outbound_queue via SKIP LOCKED → adapter.send → mark sent.\n```\n\n\n---\n\n## Hot-reload (no app restarts)\n\n| Change | Effect | Latency |\n|---|---|---|\n| `PUT /api/admin/llm-configs/:purpose` | `InMemoryLlmRouter.invalidate(tenantId)` + setConfig + mutate `LoadedRef.current` | instant |\n| `POST /api/admin/channels/telegram` | `ChannelRegistry.reloadTenant(tenantId)` in `apps/api` instant; `apps/worker` picks it up via polling | instant in api, ≤30s in worker |\n| `POST /api/admin/channels/whatsapp` | same; Meta webhook setup in the Meta dashboard is manual | instant in api, ≤30s in worker |\n| `PUT /api/admin/tenant/status` (pause/resume) | reloadChannels — evict on pause, restore on resume | instant in api |\n| `PUT /api/admin/conversations/:id/mode` | mutate `conversations.mode`, the pipeline respects it immediately | instant |\n| Stripe webhook `customer.subscription.*` | `tenants.plan` mutates based on the priceId map | instant (after Stripe delivery) |\n| KB upload | DrizzleKbStore reads live from the DB | instant |\n\nDetails in [`docs/ARCHITECTURE.md#hot-reload`](docs/ARCHITECTURE.md).\n\n---\n\n## Admin API endpoints (SaaS flow)\n\nAll under `/api/admin/*`, requiring `Authorization: Bearer \u003cjwt\u003e` (from `/api/auth/signup` or `/login`).\n\n```\nGET    /api/auth/me                              — admin + tenant info\nPOST   /api/auth/signup                          — create tenant + admin\nPOST   /api/auth/login                           — issue JWT\nPOST   /api/auth/logout                          — invalidate (client-side)\nPOST   /api/auth/forgot-password                 — send reset email (Resend)\nPOST   /api/auth/reset-password                  — consume token + set new password\nPOST   /api/auth/change-password                 — change password (authenticated)\nPOST   /api/auth/accept-invite                   — accept team invite token\n\nGET    /api/admin/onboarding-status              — checklist (channel/llm/kb)\nGET    /api/admin/tenant                         — { id, slug, plan, status, ... }\nPUT    /api/admin/tenant/status                  — { paused: boolean } → pause/resume\nGET    /api/admin/diagnostics                    — health check (channel + LLM + KB)\n\nPOST   /api/admin/channels/telegram              — { botToken } → auto-setWebhook\nPOST   /api/admin/channels/whatsapp              — { phoneNumberId, accessToken } → Meta Graph validate + webhook-setup hint\nPOST   /api/admin/channels/userbot/start         — { phone } → MTProto sendCode (superadmin)\nPOST   /api/admin/channels/userbot/verify        — { loginId, code } → signIn (or needs2fa)\nPOST   /api/admin/channels/userbot/2fa           — { loginId, password } → SRP → channel created\nGET    /api/admin/channels                       — list (without credentials)\nDELETE /api/admin/channels/:id\n\nGET    /api/admin/admins                         — list admins\nGET    /api/admin/admins/invites                 — list pending invites\nPOST   /api/admin/admins/invite                  — { email, role } → magic-link (superadmin)\nDELETE /api/admin/admins/invites/:id             — revoke invite\n\nPUT    /api/admin/llm-configs/:purpose           — { provider, model, apiKey?, ... }\nGET    /api/admin/llm-configs                    — list (without secret values)\nDELETE /api/admin/llm-configs/:purpose\n\nPOST   /api/admin/kb/documents                   — multipart file OR { title, body, topic? }\nGET    /api/admin/kb/documents                   — list\nDELETE /api/admin/kb/documents/:id\n\nGET    /api/admin/conversations                  — paginated list (cursor)\nGET    /api/admin/conversations/:id              — thread + messages\nPOST   /api/admin/conversations/:id/reply        — operator reply (mode=human)\nPUT    /api/admin/conversations/:id/mode         — { mode: 'ai'|'human' } toggle takeover\n\nGET    /api/admin/leads                          — list leads (kanban data, fill-rate stats)\nPOST   /api/admin/leads                          — create lead\nGET    /api/admin/leads/:id                      — lead detail (stage, fields, events, notes, contact)\nPATCH  /api/admin/leads/:id/stage               — move lead to a different stage\nPUT    /api/admin/leads/:id/field-values         — bulk upsert stage field values\nPOST   /api/admin/leads/:id/notes               — add operator note\n\nGET    /api/admin/funnel                         — funnel + stage_definitions + stage_fields\nPOST   /api/admin/funnel/seed                    — seed from template (visa/real_estate/modeling/recruitment)\nPOST   /api/admin/funnel/stages                  — create stage\nPATCH  /api/admin/funnel/stages/:id              — update stage config (incl. supportMode)\nDELETE /api/admin/funnel/stages/:id              — delete stage\nPATCH  /api/admin/funnel/stages/reorder          — bulk position update\nPOST   /api/admin/funnel/stages/:id/fields       — add field to stage\nPATCH  /api/admin/funnel/stages/:id/fields/:fid  — update field\nDELETE /api/admin/funnel/stages/:id/fields/:fid  — delete field\n\nGET    /api/admin/skills                         — list skills with ELO scores\nGET    /api/admin/styles                         — list styles (versions, deletedAt filter)\nGET    /api/admin/experiments                    — list A/B experiments\n\nGET    /api/admin/audit-log                      — cursor-paginated audit history\n\nGET    /api/admin/billing/plan                   — current plan + usage + status\nGET    /api/admin/billing/plans                  — list 4 tiers + stripeEnabled bool\nPOST   /api/admin/billing/checkout               — { plan: 'starter'|'pro' } → Stripe Checkout URL\nPOST   /api/admin/billing/portal                 — Stripe Customer Portal URL\n\nGET    /api/admin/message-templates              — list message templates\nPOST   /api/admin/message-templates              — create template { name, body }\nPATCH  /api/admin/message-templates/:id          — update name and/or body\nDELETE /api/admin/message-templates/:id          — delete template\n\nPOST   /api/admin/outreach                       — bulk message: { text, leadIds|stageSlug, scheduledAt? }\n\nGET    /api/superadmin/tenants                   — list all tenants with stats (superadmin only)\nPATCH  /api/superadmin/tenants/:id/plan          — change tenant plan (superadmin only)\n```\n\n---\n\n## Testing\n\n```bash\nDATABASE_URL=postgres://lead:lead@localhost:5434/lead_engine bun test\n```\n\n**950+ tests** across 15 packages. Highlights:\n\n- **Multi-tenant E2E** (`apps/api/src/multi-tenant.integration.test.ts`): tenant isolation through the real webhook handler + admin API\n- **RLS contract** (`packages/storage/src/rls.integration.test.ts`): non-bypass role validation\n- **Auth integration** (`auth.integration.test.ts`): signup/login/me + forgot-password/reset-password + change-password (mock mailer)\n- **Superadmin integration** (`superadmin.integration.test.ts`): tenant list + plan change, role guards (403 for manager)\n- **Message templates** (`admin-message-templates.integration.test.ts`): CRUD + cross-tenant isolation\n- **Outreach** (`admin-outreach.integration.test.ts`): by leadIds + by stageSlug, enqueued vs skipped, scheduledAt\n- **RAG pipeline** (`packages/kb/test/`): 180 tests — MMR, RRF merge, dynamic threshold, multi-query expansion, reranker, semantic cache, topic classifier, rewrite-query, tool-loop, structured output\n- **SaaS routes** (KB, LLM configs, channels, conversations, onboarding, audit, diagnostics, tenant pause): ~250 integration tests\n- **Exchange e2e mocks**:\n  - `apps/api/src/lib/exchange/tools.forsanya.integration.test.ts` — 10 redacted Forsanya exchange workflows over real DB/tools: quote, KYC gate, order, requisites, receipt proof, payout code\n  - `apps/api/src/routes/admin-exchange.integration.test.ts` — admin rate-card approval, requisites, order CRM, operator patch, turnover and tenant isolation\n- **Rate limiter**: 6 unit + 3 webhook integration tests\n- **Hot-reload**: 6 tenant-reloader tests (LLM + channels)\n\nFocused exchange check:\n\n```bash\nDATABASE_URL=postgres://lead:lead@localhost:5434/lead_engine \\\n  bun test apps/api/src/routes/admin-exchange.integration.test.ts \\\n           apps/api/src/lib/exchange/tools.forsanya.integration.test.ts\n```\n\nRun with coverage:\n\n```bash\nbun test --coverage\n```\n\n---\n\n## Releasing packages\n\nThe `@chatman-media/*` packages in `packages/*` are published to npm via\n[semantic-release](https://semantic-release.gitbook.io/) on every push to\n`main`. Versioning is independent per package and derived from\n[Conventional Commits](https://www.conventionalcommits.org/) scoped to each\npackage's directory ([semantic-release-monorepo](https://github.com/pmowrer/semantic-release-monorepo)).\n\n- A `feat:` commit touching `packages/kb` cuts a `minor` for `@chatman-media/kb`.\n- A `fix:` cuts a `patch`; `feat!:` / `BREAKING CHANGE:` cuts a `major`.\n- Each release tags `@chatman-media/\u003cpkg\u003e-vX.Y.Z`, updates the package\n  `CHANGELOG.md`, publishes to npm, and creates a GitHub Release.\n- Workspace (`workspace:*`) dependencies are resolved to concrete versions\n  at publish time via `bun publish`.\n\nCI publishes packages in dependency order so interdependent packages always\nresolve. Requires an `NPM_TOKEN` repository secret with publish rights to the\n`@chatman-media` scope.\n\n---\n\n## Deployment\n\n### Env vars (see `.env.example`)\n\n| Var | Required | Description |\n|---|---|---|\n| `DATABASE_URL` | ✅ | Postgres connection. **NOSUPERUSER NOBYPASSRLS role in prod** |\n| `PLATFORM_MASTER_KEY` | ✅ | 32-byte hex for AES-256-GCM (tenant_secrets) |\n| `PLATFORM_AUTH_SECRET` | opt | HMAC secret for JWT-like auth tokens (falls back to MASTER_KEY) |\n| `TELEGRAM_WEBHOOK_SECRET` | ✅ | X-Telegram-Bot-Api-Secret-Token header |\n| `TELEGRAM_API_ID` / `TELEGRAM_API_HASH` | opt | MTProto app credentials (my.telegram.org). Required for userbot onboarding. Empty → Personal tab hidden, routes return 503 |\n| `PLATFORM_PUBLIC_URL` | opt | Base URL of apps/api for auto-setWebhook (`https://api.example.com`) |\n| `WHATSAPP_VERIFY_TOKEN` / `WHATSAPP_APP_SECRET` | opt | Meta webhook setup |\n| `WEB_WS_AUTH_SECRET` | opt | Shared secret for `/ws/:slug?auth=...` |\n| `STRIPE_SECRET_KEY` | opt | `sk_test_xxx` / `sk_live_xxx`. Empty → `/checkout` and `/portal` return 503 |\n| `STRIPE_PRICE_STARTER` / `STRIPE_PRICE_PRO` | opt | Price IDs from the Stripe dashboard. The webhook handler maps priceId → plan |\n| `STRIPE_CHECKOUT_SUCCESS_URL` / `STRIPE_CHECKOUT_CANCEL_URL` | opt | Redirect URLs (support a `{TENANT}` placeholder) |\n| `STRIPE_WEBHOOK_SECRET` | opt | Stripe webhook HMAC |\n| `LLM_*` / `LLM_EMBED_*` | opt | Env fallback if a tenant has no DB config |\n| `RATE_LIMIT_PER_MIN` / `RATE_LIMIT_PER_HOUR` | opt | Default 60 / 600. `0` = disabled |\n| `WORKER_CHANNEL_RELOAD_MS` | opt | Worker polling interval. Default 30000. `0` = disabled |\n\n### Production checklist\n\n- [ ] Postgres role `NOSUPERUSER NOBYPASSRLS` for the apps (NOT owner / NOT superuser)\n- [ ] Migrations run under a separate BYPASSRLS role (owner / superuser)\n- [ ] `WHATSAPP_APP_SECRET` set if WhatsApp is active\n- [ ] `WEB_WS_AUTH_SECRET` set if web channels are active (or JWT auth)\n- [ ] Rotate `PLATFORM_MASTER_KEY` via the `rotate-master-key.ts` script\n- [ ] `PLATFORM_PUBLIC_URL` set for the auto-setWebhook UX (Telegram channel onboarding)\n- [ ] `RATE_LIMIT_*` set (do not leave disabled in prod — runaway-cost protection)\n- [ ] Stripe: `STRIPE_SECRET_KEY` + `STRIPE_PRICE_STARTER` + `STRIPE_PRICE_PRO` +\n      `STRIPE_WEBHOOK_SECRET` + success/cancel URLs. In the Stripe dashboard,\n      register a webhook on `\u003cPLATFORM_PUBLIC_URL\u003e/webhook/stripe`\n- [ ] Boot log check: `\"RLS enforced\"` at info; `\"RLS not enforced\"` warn = misconfigured\n\n---\n\n## Positioning\n\n| | **Lead Engine** | Intercom Fin | Chatbase | CustomGPT | Decagon |\n|---|:---:|:---:|:---:|:---:|:---:|\n| Telegram-native | ✅ | ❌ | ❌ | ❌ | ❌ |\n| WhatsApp | ✅ | ✅ | ❌ | ❌ | ❌ |\n| Web Widget | ✅ | ✅ | ✅ | ✅ | ✅ |\n| BYOK LLM | ✅ | ❌ | partial | ❌ | ❌ |\n| Operator takeover | ✅ | ✅ | ❌ | ❌ | ✅ |\n| Lead pipeline (Kanban) | ✅ | ❌ | ❌ | ❌ | ❌ |\n| Funnel builder | ✅ | ❌ | ❌ | ❌ | ❌ |\n| Multi-tenant SaaS | ✅ | ✅ | ✅ | ✅ | ✅ |\n| Self-host | ✅ enterprise | ❌ | ❌ | ❌ | ❌ |\n| Open source | ✅ MIT | ❌ | ❌ | ❌ | ❌ |\n\nFull competitive analysis: [`docs/COMPETITORS.md`](docs/COMPETITORS.md)\n\n---\n\n## Roadmap \u0026 competitors\n\n- **Done / in progress / next** — see [`docs/ROADMAP.md`](docs/ROADMAP.md)\n- **Market analysis and positioning** — see [`docs/COMPETITORS.md`](docs/COMPETITORS.md)\n\nTL;DR product niche: **AI-first customer service for messenger-centric\nmarkets** (Telegram / WhatsApp). Competitors like Intercom Fin / Sierra /\nDecagon are enterprise + web-chat-first. Chatbase / CustomGPT are simple\nknowledge bots without operator takeover or channels-as-a-service. Our\nposition: open architecture + BYOK + Telegram-first + a full operator\nworkflow (inbox + reply + audit + diagnostics).\n\n---\n\n## Contributing\n\nPRs are welcome. A few guidelines:\n\n- **Branches**: `feat/\u003cname\u003e` for features, `fix/\u003cname\u003e` for bug fixes\n- **Commits**: follow [Conventional Commits](https://www.conventionalcommits.org/) — `feat:`, `fix:`, `chore:`, etc. Semantic-release derives versions and changelogs from them\n- **Before submitting**: run `bun run typecheck \u0026\u0026 bun test` across the monorepo. CI is the same check\n- **Architecture context**: read [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) before touching `apps/api` or the packages — the RLS / withTenant contract and the split-tx pipeline are critical invariants\n- **New packages**: add to `packages/`, export from `@chatman-media/\u003cname\u003e`, wire up `bun.lockb` + tsconfig paths\n\n---\n\n## License\n\n[MIT](LICENSE) — Alexander Kireev / [chatman-media](https://github.com/chatman-media)\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n🇬🇧 **English** \u0026nbsp;·\u0026nbsp; [🇷🇺 Русский](README.ru.md) \u0026nbsp;·\u0026nbsp; [🇨🇳 中文](README.zh.md)\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchatman-media%2Flead-engine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchatman-media%2Flead-engine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchatman-media%2Flead-engine/lists"}