Event-driven CRM triage system. Hono middleware + React admin view + sequential pipeline (Ingest → Enrich → Draft → Review → Dispatch). Persistence in Google Sheets. Command & control via Telegram inline cards.
Status: code is deployed; you must provision env vars below before any pipeline call will succeed. The
/dashboard renders without secrets but shows an error banner untilGOOGLE_JSONis set.
Rendering mermaid diagram...
| status_id | label | written by |
|---|---|---|
0 | INGESTED | POST /api/lead |
1 | ENRICHED | (collapsed into DRAFTED) |
2 | DRAFTED | pipeline.enrichAndDraft |
3 | REVIEWED | Telegram ✅ EXECUTE button |
4 | DISPATCHED | pipeline.dispatch |
5 | ERROR | any failure path |
timestamp | source | status_id | uid_hash | contact_raw | context_metadata | ai_payload | priority_score
uid_hash = SHA-256("${contact_raw}|${source}") — used as the idempotency key.
index.ts ← HTTP routes + global error-boundary
config.ts ← SHEET_ID, niche rules, admin chat id, model
types.ts ← STATUS enum, COL enum, Lead/AIPayload/Callback types
services/
auth.ts ← Google service-account JWT (RS256, in-mem cache)
sheets.ts ← typed Sheets v4 wrapper (append/read/update)
idempotency.ts ← SHA-256 uid_hash
llm.ts ← GPT-4o "Elite Conversion" + niche map
telegram.ts ← bot client, C&C cards, callback parser
pipeline.ts ← lifecycle orchestrator (ingest/draft/dispatch)
cron/
sweep.ts ← 15-min reclamation task on A2:H35
frontend/
index.html ← shell (Twind)
index.tsx ← React entrypoint
components/
App.tsx ← layout + status strip + auto-refresh
LeadsTable.tsx ← last 50 rows table w/ expandable drafts
| Key | What |
|---|---|
GOOGLE_JSON | Full service-account JSON (paste the entire file content). Must have edit on the sheet. |
OPENAI_API_KEY | OpenAI key with access to gpt-4o. |
TELEGRAM_TOKEN | Bot token from @BotFather. Used for both webhook auth (secret = part after :) and API. |
- Share sheet
1YRlfO_L1bsyaPZPvenQfnpkZVcjUEYdIiQXo5BjfHcwwith the service-account email (Editor). - Sheet must have a header row 1 with the eight columns above (anything works
as labels — the code only reads
A2:H…).
-
Set the three env vars above.
-
Hit
GET /admin/initonce — performs:- Service-account handshake (verifies sheet access, returns sample row count)
setWebhookagainst/api/telegramwith the derived secret token
-
POST a test lead:
curl -X POST https://<your-val-url>/api/lead \ -H 'Content-Type: application/json' \ -d '{ "source": "manual", "contact_raw": "ada@example.com", "context_metadata": "Wants help with SEO for SaaS landing pages", "priority_score": 80 }'You should see:
- row 2 (or next) appended to the sheet with
status_id=0 - Telegram alert
📥 INGESTED …to admin
- row 2 (or next) appended to the sheet with
-
Wait ≤15 min for the sweep — or manually run
cron/sweep.tsfrom the editor's "Run" button. The row will advance tostatus_id=2(DRAFTED) and the admin gets a card with[✅ EXECUTE] [🔄 RE-GENERATE] [📊 VIEW RAW].
Defined in config.ts → NICHE_RULES. First-match-wins regex over
context_metadata:
| pattern | niche | injected CTA |
|---|---|---|
/seo/i | seo | https://your-expert-link.com/seo-audit |
/growth|scaling/i | growth | https://your-expert-link.com/vsl |
| (fallback) | general | https://your-expert-link.com/discovery |
The LLM is instructed to use {{CTA_LINK}} as a placeholder; we substitute
post-generation so the niche map is authoritative.
Every Hono route is wrapped by app.onError. Errors thrown from
services/sheets.ts carry provider="google" + status; services/llm.ts
carry provider="openai". The boundary fires tg.alertAdmin(...) for the
two spec-mandated codes:
429 + openai→ "OpenAI 429 (rate-limit) — …"403 + google→ "Google 403 (auth/quota) — …"
…plus a generic alert for any 5xx.
Callback data format: ${ACTION}:${rowIndex} where ACTION ∈
{EXECUTE, REGEN, VIEW}. Only chat_id === 466679766 is honored; any other
sender gets an "unauthorized" toast.
- Retarget the sheet → edit
SHEET_IDinconfig.ts. - Add a niche → push to
NICHE_RULES. - Wire a real outbound channel → fill in
pipeline.dispatch(). - Sweep window → edit
SWEEP_RANGEinconfig.tsand the interval delay oncron/sweep.ts(free tier minimum 15 min).